Logo

Adding Decap CMS to an Astro Site

I wanted a way to edit blog content without opening a code editor every time. Decap CMS looked like the obvious answer — free, git-based, no database, content lands in my repo as markdown. Simple enough on paper.

It wasn’t simple. Not because Decap is bad, but because of one detail I didn’t think about until I was already in it: my site is on Cloudflare Pages, and Decap’s auth story is built around Netlify.

I branched the site before touching any of this. Not because I expected disaster — because I didn’t know yet what I didn’t know, and a CMS integration touches auth, deployment, and content structure all at once. If something broke, I wanted it to break somewhere that wasn’t my live site.

It broke. Here’s what I learned.

The config isn’t the hard part

Getting config.yml right is mechanical. Collections, fields, a backend block. The error messages are vague — a schema anyOf failure just means something doesn’t match Decap’s expected shape, could be a missing key, could be indentation. But once the file validates, that part’s done and it stays done.

The hard part is auth. Decap doesn’t manage its own login system. It hands that off to whatever backend you configure — and that’s where the Cloudflare/Netlify split actually matters.

Why my CMS doesn’t live where my site lives

Decap’s github backend needs an OAuth proxy to talk to GitHub. Netlify provides one for free if your site is hosted there. Cloudflare Pages doesn’t have an equivalent baked in — you either self-host a small OAuth worker, or you lean on Netlify’s git-gateway backend and Netlify Identity instead.

I went with Netlify Identity. Which means: my site deploys on Cloudflare, but the editor’s authentication runs through a Netlify project that isn’t even serving the site — just sitting there managing who’s allowed to commit content. Two platforms, two jobs, one repo in the middle.

That’s a real dependency, not a footnote. If Netlify has an outage, or I ever migrate away from it, the CMS login breaks even though the site itself is untouched. Worth knowing going in, not discovering later.

Netlify Identity invites always link to your bare root domain — yoursite.com/#invite_token=... — never straight to /admin/. If the Identity widget script isn’t loaded on your actual homepage, that token just sits in the URL doing nothing. You land on your homepage, no modal, no password prompt, nothing.

The fix is two lines of script on your main layout, not just the admin page:

<script src="https://identity.netlify.com/v1/netlify-identity-widget.js"></script>
<script>
  if (window.netlifyIdentity) {
    window.netlifyIdentity.on("init", user => {
      if (!user) {
        window.netlifyIdentity.on("login", () => {
          document.location.href = "/admin/";
        });
      }
    });
  }
</script>

The Astro gotcha that cost me the most time

Astro processes and bundles inline <script> tags by default. That’s usually a good thing — except the Identity widget needs to read the invite_token straight out of the URL hash the moment the page loads, and Astro’s bundling can delay that past the point where it matters. No error, no console warning. It just quietly doesn’t work.

The fix is is:inline:

<script is:inline src="https://identity.netlify.com/v1/netlify-identity-widget.js"></script>
<script is:inline>
  if (window.netlifyIdentity) {
    window.netlifyIdentity.on("init", user => {
      if (!user) {
        window.netlifyIdentity.on("login", () => {
          document.location.href = "/admin/";
        });
      }
    });
  }
</script>

Once that was on, invites worked instantly. Everything before it was a symptom of the script running at the wrong time.

The short version

I tested all of this on a branch before merging anything into the live site. A CMS touches too many moving parts — auth, deploy config, content structure — to debug live. Slower to set up, but I’d rather lose an afternoon to a branch than an evening to a broken production site.