Add a CMS to Astro with Keystatic and Cloudflare Pages
Most CMS options for Astro fall into two camps: hosted services with monthly fees and API rate limits (Contentful, Sanity, Storyblok), or self-hosted databases that require a separate server. Keystatic is neither.
Keystatic is a Git-based CMS that runs inside your existing Astro site. It stores content as Markdown and YAML files directly in your GitHub repository. No separate database, no separate server, no per-seat pricing. The editor UI is served from a /keystatic route on your own domain, authenticated via GitHub OAuth. In development it uses local file storage — no auth needed, works immediately on localhost.
It’s a good fit for content-heavy sites where a client needs to edit text and images without touching code: blog posts, news articles, product descriptions, team pages. The tradeoff is a 30–90 second rebuild delay between saving content and seeing it live — Keystatic commits to GitHub, then Cloudflare Pages rebuilds automatically. If instant updates are required, a different architecture is needed.
Step 1 — GitHub OAuth App
Keystatic authenticates production users via GitHub OAuth. You need to register an OAuth App:
- GitHub → Settings → Developer settings → OAuth Apps → New OAuth App
- Fill in:
- Application name:
Your Site CMS(or any label) - Homepage URL:
https://yourdomain.com - Authorization callback URL:
https://yourdomain.com/api/keystatic/github/oauth/callback
- Application name:
- Register → note the Client ID
- Generate a client secret → copy it immediately (only shown once)
Step 2 — Cloudflare KV namespace for sessions
Keystatic stores session tokens in Cloudflare KV. Create one:
- Cloudflare dashboard → Workers & Pages → KV
- Create a namespace — name it something like
your-site-sessions - Note the Namespace ID
Step 3 — Install Keystatic
pnpm add @keystatic/core @keystatic/astro
Keystatic’s admin UI requires React, so you also need @astrojs/react if not already installed.
In astro.config.mjs:
import keystatic from '@keystatic/astro';
import react from '@astrojs/react';
export default defineConfig({
integrations: [react(), keystatic()],
output: 'hybrid', // Keystatic needs SSR for the /keystatic route
adapter: cloudflare(),
});
Note: Keystatic requires output: 'hybrid' or output: 'server'. It cannot be used with a fully static build.
Create keystatic.config.ts at the project root:
import { config, fields, collection } from '@keystatic/core';
export default config({
storage: process.env.NODE_ENV === 'production'
? { kind: 'github', repo: 'your-github-username/your-repo-name' }
: { kind: 'local' },
collections: {
posts: collection({
label: 'Blog Posts',
slugField: 'title',
path: 'src/content/blog/*',
format: { contentField: 'content' },
schema: {
title: fields.slug({ name: { label: 'Title' } }),
publishedDate: fields.date({ label: 'Published Date' }),
description: fields.text({ label: 'Description', multiline: true }),
content: fields.markdoc({ label: 'Content' }),
},
}),
},
});
Add the Keystatic API route at src/pages/api/keystatic/[...params].ts:
import { makeRouteHandler } from '@keystatic/astro/api';
import config from '../../../../keystatic.config';
export const { GET, POST } = makeRouteHandler({ config });
Step 4 — Environment variables and KV binding
In Cloudflare Pages → Settings → Environment variables (Production):
| Variable | Value |
|---|---|
NODE_ENV | production |
GITHUB_REPO_OWNER | your GitHub username |
GITHUB_REPO_NAME | your repo name |
KEYSTATIC_GITHUB_CLIENT_ID | from Step 1 |
KEYSTATIC_GITHUB_CLIENT_SECRET | from Step 1 |
In Pages → Settings → Functions → KV namespace bindings:
| Variable name | KV namespace |
|---|---|
SESSION | the namespace from Step 2 |
And in wrangler.toml:
[[kv_namespaces]]
binding = "SESSION"
id = "your-kv-namespace-id"
Step 5 — Update the OAuth callback URL
After first deploy, go back to your GitHub OAuth App and update both fields with the real deployed domain:
- Homepage URL:
https://yourdomain.com - Authorization callback URL:
https://yourdomain.com/api/keystatic/github/oauth/callback
If you want the CMS accessible on Cloudflare Pages preview URLs too, add a second callback URL: https://yoursitename.pages.dev/api/keystatic/github/oauth/callback.
Troubleshooting
“Invalid binding SESSION” — The KV namespace binding variable must be exactly SESSION. Check Pages → Settings → Functions.
GitHub OAuth not working — Verify the callback URL in the GitHub OAuth App exactly matches your deployed domain, including https://. Also check that env vars are set under Production, not just Preview.
Content not saving — GITHUB_REPO_OWNER and GITHUB_REPO_NAME must match the repo exactly, including case. The GitHub account used must have write access to the repo.
Build errors after adding Keystatic — Ensure output: 'hybrid' is set and @astrojs/react is installed.
Local development
pnpm dev
Visit http://localhost:4321/keystatic — no authentication needed in development. Keystatic uses local file storage and saves directly to your filesystem.
Notes for client projects
Keystatic is free and open source. Clients need a GitHub account to use the editor, or you create one for them. Content lives in the repository as files, which makes it easy to version, audit, and migrate away from if needed. The /keystatic route is only accessible to authenticated GitHub users — you can restrict it further with Cloudflare Access (Zero Trust) if the client requires it.
Want to learn more?
See what a modern Astro website can do for your business, transparently priced.
Learn more