A CMS for Astro that keeps the site static

How to let a client publish on an Astro site without switching to server rendering, without handing over the repository, and without the CMS becoming a single point of failure.

An Astro site that ships as static output deploys to a CDN, costs almost nothing to host and absorbs a traffic spike without anyone doing anything. Then the client wants to change a price, and the whole thing gets awkward: either you hand them the Git repository, or you switch the site to server rendering, or you get the “can you change this for me?” email for the third time this month.

None of those three is a good answer. Here is what we settled on.

Why the usual options get stuck

Git-based (Decap, Tina). The editor commits to the repository. Technically elegant, but the client inherits a GitHub account, a workflow they do not understand, and any merge mistake becomes their problem. More importantly, content and code share one history, so restoring a piece of content means touching the repository.

Classic headless (Strapi, Sanity, Prismic). Good products, but the paid floor sits between $10 and $18 per month per project. Across twenty brochure sites billed at 40 € of monthly maintenance, the margin is gone. Extra locales are almost always another pricing tier.

Server rendering. You lose the exact thing Astro was picked for: the site becomes an application to watch, with a response time, a cache to reason about and a hosting bill that follows traffic.

What we do instead

The principle fits in one sentence: the CMS never serves the site, it serves the build.

  1. The editor works in a web interface, on their phone if they want to.
  2. When they hit Publish, the API freezes the content into an immutable JSON file: a snapshot, pushed to a CDN.
  3. The API then triggers the build at the site’s host (Vercel, Netlify, Cloudflare, GitHub Actions or your own server).
  4. The build reads the snapshot and produces the static site, as usual.

Content therefore travels from the CMS to the site at build time, never on a visitor’s request. The finished site has no dependency on the CMS: it does not call it, does not know about it, and does not need it to be running.

The consequence that matters

If the CMS goes down, published sites stay up. They are static, they are on a CDN, there is nothing to go down.

Better: because the snapshot lives on a CDN rather than inside the API, a build started during a CMS outage still succeeds. We test this deliberately, by switching the API off and checking that a deployment still goes through.

That is the difference between a CMS the site depends on, and a CMS the site draws from.

For developers: the schema stays in code

The content model is declared in a TypeScript file in the project, not clicked together in a UI:

import { defineCms, fields } from '@menestrel/fields';

export default defineCms({
  collections: {
    services: {
      label: { fr: 'Prestations', en: 'Services' },
      fields: {
        title: fields.text({ required: true, localized: true }),
        excerpt: fields.textarea({ localized: true }),
        price: fields.number(),
        cover: fields.image(),
      },
    },
  },
});

One command syncs that schema to the server, which generates the matching editing forms. The schema therefore lives in code review, in branches and in Git history, like the rest of the project. What does not live there is the content: a client’s copy has no business in a repository.

On the rendering side, content arrives through a standard Content Layer loader, so getCollection() and typing behave exactly as with any other Astro source.

What it does not do

For the sake of honesty, the limits of the model:

Those three limits are choices, not missing features. They are what keeps the site static, the hosting at a few cents and the mental model simple.

Trying it

The documentation covers installation, and the starter puts a project on its feet in one command:

npm create menestrel@latest

The free plan covers a full site, two languages included, with no card.

Back to the blog