How to Set Up a JAMstack Blog with Astro and Contentful: A Complete Walkthrough

If you’re a developer looking to launch a blazing-fast blog in 2026, the JAMstack blog headless CMS approach is still the gold standard for performance, SEO, and developer experience. In this walkthrough, we’ll build a real production-ready blog using Astro as our static site generator and Contentful as our headless CMS, then deploy it to Netlify.

Unlike other tutorials that stop at theory, this guide gives you working code, a solid content model, and a clear deployment path. Let’s get building.

Why JAMstack + Headless CMS Still Wins in 2026

The JAMstack architecture (JavaScript, APIs, Markup) has evolved, but the core benefits remain unbeatable for content sites:

  • Speed: Pre-rendered HTML served from a CDN loads in milliseconds.
  • Security: No database or server means far fewer attack vectors.
  • Scalability: Static files scale infinitely without infrastructure headaches.
  • SEO: Fully rendered HTML at build time is exactly what Google wants.
  • Developer freedom: Decouple your content from your presentation layer.

A headless CMS like Contentful separates content management from delivery. Editors work in a friendly UI while your frontend consumes clean JSON from an API. This is a much better workflow than committing markdown files or wrestling with WordPress themes.

Why Astro Over Next.js or Gatsby?

Astro has become the go-to choice for content-driven sites because:

  • It ships zero JavaScript by default.
  • It supports React, Vue, Svelte components if you need interactivity.
  • Its content collections and image optimization are built-in.
  • Build times are significantly faster than Gatsby.
laptop code developer

Stack Comparison at a Glance

Layer Tool Role
Static Site Generator Astro 5.x Builds static HTML pages
Headless CMS Contentful Stores and serves content via API
Hosting Netlify CDN, builds, deploy previews
Styling Tailwind CSS Utility-first CSS

Step 1: Set Up Your Contentful Space

Create a free Contentful account and set up a new space. Then define your content model.

Blog Post Content Model

Create a content type called blogPost with these fields:

  1. title (Short text, required)
  2. slug (Short text, unique, required)
  3. excerpt (Short text, 200 chars)
  4. coverImage (Media, single image)
  5. body (Rich text)
  6. publishedDate (Date & time)
  7. tags (Short text, list)
  8. author (Reference to Author content type)

Create a couple of test posts so you have data to query.

Get Your API Keys

In Contentful, go to Settings > API keys and grab:

  • Space ID
  • Content Delivery API access token
  • Content Preview API access token (for drafts)

Step 2: Bootstrap Your Astro Project

Open your terminal and run:

npm create astro@latest my-jamstack-blog
cd my-jamstack-blog
npm install contentful
npx astro add tailwind

Create a .env file in the root:

CONTENTFUL_SPACE_ID=your_space_id
CONTENTFUL_DELIVERY_TOKEN=your_delivery_token
CONTENTFUL_PREVIEW_TOKEN=your_preview_token
laptop code developer

Step 3: Create the Contentful Client

Create src/lib/contentful.ts:

import contentful from 'contentful';

export const contentfulClient = contentful.createClient({
  space: import.meta.env.CONTENTFUL_SPACE_ID,
  accessToken: import.meta.env.DEV
    ? import.meta.env.CONTENTFUL_PREVIEW_TOKEN
    : import.meta.env.CONTENTFUL_DELIVERY_TOKEN,
  host: import.meta.env.DEV ? 'preview.contentful.com' : 'cdn.contentful.com',
});

export interface BlogPost {
  contentTypeId: 'blogPost';
  fields: {
    title: string;
    slug: string;
    excerpt: string;
    body: any;
    publishedDate: string;
    tags?: string[];
    coverImage?: any;
  };
}

Step 4: Build the Blog Index Page

Create src/pages/blog/index.astro:

---
import { contentfulClient } from '../../lib/contentful';
import type { BlogPost } from '../../lib/contentful';
import Layout from '../../layouts/Layout.astro';

const entries = await contentfulClient.getEntries<BlogPost>({
  content_type: 'blogPost',
  order: ['-fields.publishedDate'],
});

const posts = entries.items.map((item) => ({
  title: item.fields.title,
  slug: item.fields.slug,
  excerpt: item.fields.excerpt,
  date: new Date(item.fields.publishedDate).toLocaleDateString('en-US'),
}));
---

<Layout title="Blog">
  <section class="max-w-4xl mx-auto p-8">
    <h1 class="text-4xl font-bold mb-8">Latest Posts</h1>
    <ul class="space-y-6">
      {posts.map((post) => (
        <li class="border-b pb-4">
          <a href={`/blog/${post.slug}`} class="text-2xl font-semibold hover:underline">
            {post.title}
          </a>
          <p class="text-gray-500 text-sm">{post.date}</p>
          <p class="mt-2">{post.excerpt}</p>
        </li>
      ))}
    </ul>
  </section>
</Layout>

Step 5: Generate Dynamic Post Pages

Create src/pages/blog/[slug].astro:

---
import { contentfulClient } from '../../lib/contentful';
import { documentToHtmlString } from '@contentful/rich-text-html-renderer';
import Layout from '../../layouts/Layout.astro';

export async function getStaticPaths() {
  const entries = await contentfulClient.getEntries({ content_type: 'blogPost' });
  return entries.items.map((item) => ({
    params: { slug: item.fields.slug },
    props: {
      title: item.fields.title,
      body: documentToHtmlString(item.fields.body),
      date: item.fields.publishedDate,
    },
  }));
}

const { title, body, date } = Astro.props;
---

<Layout title={title}>
  <article class="max-w-3xl mx-auto p-8 prose">
    <h1>{title}</h1>
    <p class="text-gray-500">{new Date(date).toLocaleDateString('en-US')}</p>
    <div set:html={body} />
  </article>
</Layout>

Install the rich text renderer:

npm install @contentful/rich-text-html-renderer

Step 6: Add SEO Essentials

Update your layout to include proper meta tags, Open Graph, and structured data:

---
const { title, description } = Astro.props;
---
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>{title}</title>
    <meta name="description" content={description} />
    <meta property="og:title" content={title} />
    <meta property="og:description" content={description} />
    <meta property="og:type" content="article" />
  </head>
  <body>
    <slot />
  </body>
</html>

Don’t forget to add a sitemap and RSS feed using the official Astro integrations:

npx astro add sitemap
npm install @astrojs/rss
laptop code developer

Step 7: Deploy to Netlify

  1. Push your project to GitHub.
  2. Log into Netlify and click Add new site > Import an existing project.
  3. Connect your repo. Netlify auto-detects Astro.
  4. Add your environment variables under Site settings > Environment variables.
  5. Deploy.

Trigger Rebuilds on Content Changes

In Netlify, create a Build hook. Then in Contentful, go to Settings > Webhooks and add a webhook that pings the Netlify URL whenever content is published or unpublished. Your site rebuilds automatically.

Performance Checklist Before Going Live

  • Run Lighthouse and aim for scores above 95 across the board.
  • Use Astro’s <Image /> component for automatic optimization.
  • Enable Netlify’s asset optimization and Brotli compression.
  • Add a robots.txt and submit your sitemap to Google Search Console.
  • Set up canonical URLs on every post.

Common Pitfalls to Avoid

  • Forgetting rate limits: Contentful’s free tier has API call limits. Cache aggressively at build time.
  • Not modeling references properly: Use Contentful references for authors and categories, don’t duplicate strings.
  • Ignoring draft workflows: Use the Preview API in a staging environment.
  • Skipping image alt text: Make it a required field in your Contentful model.

FAQ

Is Astro better than Next.js for a JAMstack blog?

For a content-focused blog with minimal interactivity, yes. Astro ships less JavaScript, has faster build times, and its content collections are purpose-built for blogs. Next.js is a better choice when you need heavy React interactivity or server-side rendering everywhere.

Can I use a free headless CMS instead of Contentful?

Absolutely. Alternatives include Sanity, Strapi (self-hosted), Storyblok, and Directus. The Astro integration pattern remains nearly identical, only the SDK and query syntax change.

How often will my blog rebuild?

Only when you publish content in Contentful (thanks to the webhook) or push code to your Git repo. Typical Astro builds complete in under 60 seconds for blogs with fewer than 500 posts.

Does this setup work with a custom domain?

Yes. Netlify handles custom domains and free SSL automatically. Just point your DNS to Netlify’s nameservers or add a CNAME record.

What about comments and search?

For comments, plug in Giscus (GitHub Discussions) or Cusdis. For search, use Pagefind, which generates a static search index at build time and requires no backend.

Wrapping Up

You now have a complete JAMstack blog with a headless CMS workflow: Astro renders lightning-fast static pages, Contentful gives your editors a clean UI, and Netlify handles global delivery. This stack scales from a personal blog to enterprise content hubs without changing the architecture.

The best part? You own your content, you own your code, and your site will still load fast years from now, no matter how the JavaScript ecosystem shifts.