How-to

How to Build a Static Astro Site with a Headless CMS

How to build a static Astro site with a headless CMS: build-time fetch, a route per entry, publish webhooks, and a draft preview route that actually works.

7 min read

Building a static Astro site with a headless CMS takes three moving parts. A build-time fetch, a route per entry, and a webhook that rebuilds when someone publishes. Astro handles the first two natively, so most of the work is wiring.

The result is a site with no CMS in the request path. Visitors get files from a CDN. Editors still work in a normal admin. This guide uses Draftbase for the code samples, because its delivery API is a plain cached HTTP endpoint.

What Do You Need Before You Start?

Three things, and a missing one will stop you later.

A CMS with a server-side HTTP API. Astro fetches during the build, so a browser-only SDK cannot work. You need a key you can keep out of the bundle.

A webhook on publish. Without it, someone has to trigger a deploy by hand every time a post goes live.

A host that runs builds. Netlify, Vercel, Cloudflare Pages, or your own CI all qualify.

Store the API key as an environment variable. Astro exposes it to build-time code through import.meta.env, and it never reaches the browser as long as you only read it above the dashes.

How Do You Fetch CMS Content in an Astro Page?

The frontmatter script at the top of an .astro file runs at build time, on the server. That is where the fetch goes.

---
const res = await fetch('https://api.draftbase.co/delivery/entries?templateId=blogPost', {
  headers: { Authorization: `Bearer ${import.meta.env.DRAFTBASE_KEY}` },
});
const { items } = await res.json();
---
<h1>Blog</h1>
<ul>
  {items.map((post) => (
    <li><a href={`/blog/${post.fields.slug}`}>{post.fields.title}</a></li>
  ))}
</ul>

That is the whole index page. The fetch happens once during the build, and the output is plain HTML.

Nothing here reaches the browser. No key, no API call, no loading spinner.

For a bigger site, move this into a content loader instead. Astro's Content Layer lets a collection pull from an API and gives you typed access to it in every page. (Astro Docs)

How Do You Generate a Page Per Entry?

Astro uses file-based routing with dynamic segments. A file at src/pages/blog/[slug].astro handles every post.

The getStaticPaths function tells Astro which slugs exist. It runs at build time and returns one entry per page.

---
export async function getStaticPaths() {
  const res = await fetch('https://api.draftbase.co/delivery/entries?templateId=blogPost&limit=100', {
    headers: { Authorization: `Bearer ${import.meta.env.DRAFTBASE_KEY}` },
  });
  const { items } = await res.json();

  return items.map((post) => ({
    params: { slug: post.fields.slug },
    props: { post },
  }));
}

const { post } = Astro.props;
---
<article>
  <h1>{post.fields.title}</h1>
</article>

Two details matter here. The params object supplies the URL. The props object passes the already-fetched entry into the page, so you do not fetch twice.

Watch the pagination limit. Delivery APIs cap page size, so a site past a hundred entries needs to follow the cursor until the list runs out. Skipping that silently truncates your site.

How Do You Render the Body Content?

The body comes back as a string, not as HTML. What you do next depends on the format.

Draftbase stores rich text as plain MDX. That is convenient here, because Astro treats MDX as a first-class format rather than something to convert.

You have two options. Compile the MDX during the build and output static HTML, which keeps the zero-JavaScript default intact. Or mount a renderer as a client island when the content includes interactive components.

Prefer the first for ordinary posts. Reach for the second only when a specific entry needs live components on the page.

If your CMS stores a proprietary rich-text tree instead, this step is where the cost shows up. You write and maintain a converter from that tree to HTML, and you rewrite it if you change front ends.

How Do You Rebuild When Content Changes?

A static site shows what existed at build time. Publishing must trigger a new build, or nothing changes.

The wiring is short:

  1. Create a build hook on your host. It gives you a URL that starts a deploy.
  2. Add a webhook in the CMS pointing at that URL.
  3. Fire it on publish, update, and delete events.

Draftbase sends webhooks on every entry event, so this is configuration rather than code.

Deletes are the one people forget. If a webhook only fires on publish, a removed post keeps its page until the next unrelated deploy.

Builds take time, so set expectations with your editors. A minute or two between publish and live is normal. If that is too slow, the answer is usually an on-demand route for that one page, not abandoning static.

The Part Most Tutorials Skip: Previewing Drafts

Here is the problem nobody covers. A delivery API serves published content only. Your static build cannot see a draft, so editors have no way to preview work in progress.

It is worth understanding precisely how strict this is. Draftbase's delivery API returns entries with a status of published or updated. When an entry has been edited after publishing, the API still serves the last published revision, not the pending edit.

That behaviour is correct for production. It is exactly what blocks preview.

There are two workable answers.

The first is a preview route that renders on demand. Add an adapter, mark one route with export const prerender = false, and have it read from the management API using a server-side token. Editors hit /preview/[id] and see the draft. The rest of the site stays static.

The second is a preview deploy. Build a separate branch that pulls drafts too, and give editors that URL. It is simpler to reason about and costs an extra build.

Pick the first when editors preview constantly. Pick the second when preview is occasional.

Either way, keep the management token server-side. It can write, so it must never ship to a browser.

Astro or Next.js for a CMS-Backed Site?

Both work. The difference shows up in what you pay for by default.

Astro ships no JavaScript unless a component asks for it, so a content page stays lean without tuning. Next.js gives you a React runtime on every page, which is worth it when the page is genuinely an app.

For a blog, docs site, or marketing site backed by a CMS, Astro is usually the leaner fit. For a signed-in product with content bolted on, Next.js is the better base.

The CMS should not care either way. If your content layer works with only one of them, that is a warning about the CMS, not a reason to pick a framework. Draftbase serves the same MDX entries over REST and GraphQL to both.

What Goes Wrong in Production?

Four failures show up again and again on these builds.

Silent truncation. The first fetch returns one page of results and nobody follows the cursor. The site looks fine at forty entries and quietly drops posts at a hundred and one. Log the count you fetched and compare it to the count in the CMS.

A broken build takes the site down. If the CMS is unreachable mid-build, the deploy fails. Keep the last good deploy live rather than publishing a half-built site. Most hosts do this by default, but confirm it.

Images served from the CMS origin. Pulling every image straight from the API undoes the CDN benefit. Serve media from a CDN-backed URL, and let Astro process the assets it can.

Stale pages after a delete. Covered above, and still the most common one. Fire the webhook on delete, not just publish.

None of these are Astro problems. They are integration problems, and they surface the week after launch rather than during the build.

Conclusion

A static Astro site backed by a headless CMS is three pieces: fetch at build, one route per entry, and a webhook to rebuild. Add a preview route once editors ask for it, which they will.

The CMS decides how pleasant this is. Look for a server-callable API, webhooks on every entry event, and a content format you can render without a converter.

Draftbase covers all three, and stores rich text as plain MDX that Astro renders natively. Hobby is free and Startup runs $49/mo. See the pricing page, or read what Astro is first if the framework is new to you.

How to

  1. 1
    Store the API key as an environment variable

    Add the CMS delivery key to your environment. Read it only in build-time code, so it never ships to the browser.

  2. 2
    Fetch entries in the frontmatter script

    Call the delivery API from the script block at the top of an .astro file. That code runs once, on the server, during the build.

  3. 3
    Generate one route per entry

    Add a dynamic route file and return one object per entry from getStaticPaths. Pass the fetched entry through props so the page does not fetch twice.

  4. 4
    Follow the pagination cursor

    Delivery APIs cap page size. Loop until the list is exhausted, or the build silently drops entries past the first page.

  5. 5
    Render the body content

    Compile the MDX at build time for static output. Mount a renderer as a client island only when an entry needs live components.

  6. 6
    Wire a publish webhook to a build hook

    Create a build hook on your host, then point a CMS webhook at it. Fire on publish, update, and delete so removed pages disappear.

  7. 7
    Add an on-demand preview route

    Mark one route with prerender false and read drafts from the management API with a server-side token. Keep the rest of the site static.

Ship content that's built to be found

Draftbase generates schema, structured data, and a fast MDX editor for every post.

Frequently asked questions

Can Astro fetch content from a headless CMS at build time?

Yes. The frontmatter script in an .astro file runs on the server during the build, so it can call a delivery API with a server-side key.

How do I create one Astro page per CMS entry?

Use a dynamic route file and return a params object per entry from getStaticPaths. Pass the entry through props to avoid a second fetch.

How does an Astro site update when content is published?

A CMS webhook calls your host's build hook, which triggers a redeploy. Fire it on publish, update, and delete events, not publish alone.

How do editors preview drafts on a static Astro site?

Add one on-demand route that reads drafts from the management API with a server token. Delivery APIs serve published content only.

Should I use Astro or Next.js with a headless CMS?

Astro for content sites, since it ships no JavaScript by default. Next.js when the site is really an app with content attached.

Related reading

Go deeper on Frameworks