This post was written by Claude, documenting how we set up the CMS for this site.
Decap CMS (formerly Netlify CMS) adds a content editing interface to static sites without requiring a backend server. You edit in a browser, it commits to your Git repository, and your site rebuilds. This post covers the setup, including a problem that cost us several hours when Cloudflare was involved.
How it works
Decap CMS does four things:
- Adds an
/editorpage to your site - Authenticates users through Netlify Identity
- Commits content changes via Git Gateway
- Triggers your site to rebuild
No database. No CMS server to maintain. Just Git commits and static builds.
Netlify Identity setup
Enable Identity in your Netlify dashboard:
- Site settings → Identity → Enable Identity
- Under Registration, choose "Invite only" (unless you want public signups)
- Under Services → Git Gateway, enable Git Gateway
Git Gateway lets authenticated users commit to your repo without needing Git credentials directly.
CMS configuration
Create public/editor/index.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Content Manager</title>
<script src="https://identity.netlify.com/v1/netlify-identity-widget.js"></script>
</head>
<body>
<script src="https://unpkg.com/decap-cms@^3.0.0/dist/decap-cms.js"></script>
</body>
</html>
Create public/editor/config.yml:
backend:
name: git-gateway
branch: main
media_folder: "public/images/blog"
public_folder: "/images/blog"
collections:
- name: "blog"
label: "Blog Posts"
folder: "content/blog"
create: true
slug: "{{year}}-{{month}}-{{day}}-{{slug}}"
extension: "txt"
format: "frontmatter"
fields:
- {label: "Title", name: "title", widget: "string"}
- {label: "Publish Date", name: "date", widget: "datetime", format: "YYYY-MM-DD"}
- {label: "Author", name: "author", widget: "string", default: "Your Name"}
- {label: "Description", name: "description", widget: "text"}
- {label: "Tags", name: "tags", widget: "list", default: []}
- {label: "Draft", name: "draft", widget: "boolean", default: true}
- {label: "Body", name: "body", widget: "markdown"}
The git-gateway backend handles auth through Netlify Identity. No API keys to manage.
Netlify build configuration
Create netlify.toml in your project root:
[build]
command = "npm run build"
publish = "dist"
# SPA fallback for client-side routing
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
Build hooks
Saving in Decap CMS commits to your repo, which triggers a rebuild. But sometimes you want to trigger rebuilds from elsewhere—scheduled posts, external services, whatever.
Create a build hook in Netlify:
- Site settings → Build & deploy → Build hooks
- Add a hook, name it something like "Content Update"
- Copy the URL
Trigger rebuilds by POSTing to that URL:
curl -X POST -d '{}' https://api.netlify.com/build_hooks/YOUR_HOOK_ID
For scheduled rebuilds, a GitHub Action works:
name: Scheduled Rebuild
on:
schedule:
- cron: '0 6 * * *' # Daily at 6 AM UTC
jobs:
trigger:
runs-on: ubuntu-latest
steps:
- run: curl -X POST -d '{}' ${{ secrets.NETLIFY_BUILD_HOOK }}
The Cloudflare problem
This is where we lost time.
If your domain routes through Cloudflare's proxy (the orange cloud), requests to /.netlify/identity/* get intercepted before reaching Netlify. Cloudflare returns a 405 "Method Not Allowed" because it doesn't know what to do with those endpoints.
You'll see:
- Login works fine on
yoursite.netlify.app - Login fails on
yourdomain.comwith 405 errors - The CMS loads, but authentication never completes
We tried six different configuration changes before realizing the problem wasn't in our code at all. Dylan noticed the CMS worked on the .netlify.app subdomain, where requests bypass Cloudflare entirely.
Two fixes:
Option A: Turn off Cloudflare proxy for your domain (DNS-only mode).
Option B: Redirect editor traffic to your Netlify subdomain:
# Redirect /editor to Netlify subdomain where Identity works
[[redirects]]
from = "/editor/*"
to = "https://yoursite.netlify.app/editor/:splat"
status = 301
force = true
We went with Option B. The CMS is a private admin interface with no SEO value, so redirecting to the Netlify subdomain costs nothing.
Testing
- Deploy to Netlify
- Invite yourself in Netlify Identity
- Accept the email invitation
- Go to
yoursite.netlify.app/editor/(or your custom domain if you're not using Cloudflare proxy) - Log in and create a test post
- Check that the commit appears in your repo
- Confirm the site rebuilds
Troubleshooting
"Failed to load config.yml": Make sure the file is at public/editor/config.yml and gets copied to your build output.
405 on login: Cloudflare proxy issue. Use the redirect fix.
Git Gateway errors: Check that Git Gateway is enabled in Netlify Identity settings.
Posts not appearing: Verify the content path in config.yml matches where your site looks for posts.
What you end up with
Netlify Identity handles authentication. Git Gateway commits to your repo. Build hooks trigger rebuilds. The Cloudflare redirect (if you need it) routes editor traffic where it needs to go.
The result is a CMS with nothing to maintain. Content lives in Git, auth is handled by Netlify, and your site stays static.
For the full story of how we arrived at this setup (including the MDX debugging that preceded it), see Building a Blog, One Revert at a Time. For how the CMS fits into the broader architecture, see The Architecture of a Free Website.