Skip to main content
All posts

Add a CMS to Astro with Keystatic and Cloudflare Pages

· 3 min read ·

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:

  1. GitHub → Settings → Developer settings → OAuth Apps → New OAuth App
  2. 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
  3. Register → note the Client ID
  4. 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:

  1. Cloudflare dashboard → Workers & Pages → KV
  2. Create a namespace — name it something like your-site-sessions
  3. 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):

VariableValue
NODE_ENVproduction
GITHUB_REPO_OWNERyour GitHub username
GITHUB_REPO_NAMEyour repo name
KEYSTATIC_GITHUB_CLIENT_IDfrom Step 1
KEYSTATIC_GITHUB_CLIENT_SECRETfrom Step 1

In Pages → Settings → Functions → KV namespace bindings:

Variable nameKV namespace
SESSIONthe 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 savingGITHUB_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