How to Use Keystatic with Astro: Complete Guide
Step-by-step guide to integrating Keystatic with your Astro website.
Keystatic is a git-based CMS that stores your content directly in your repository as Markdown, Markdoc, JSON, or YAML files. There is no external database, no third-party API, and no monthly bill. You get a clean admin UI for editing content, and everything commits straight to your repo. For solo developers and small teams that already use Git, this is the most straightforward CMS setup you can get with Astro.
The Astro team documents Keystatic as a first-class CMS option, and Keystatic ships an official @keystatic/astro integration, so the setup is well supported on both sides.
This guide is written against Astro 6 and the current Keystatic packages. If you are still on an older Astro version, note that a few APIs changed in Astro 5 and 6 (covered in the Common Errors section below).
Prerequisites
- Node.js 22.12.0 or higher (Astro 6 dropped support for Node 18 and 20)
- An Astro project (
npm create astro@latest) - A GitHub account (for the GitHub storage mode, optional for local development)
Installation
Keystatic needs three things in your Astro project: the React integration (the admin UI is a React app), the Markdoc integration (Keystatic stores rich content as .mdoc files), and the two Keystatic packages themselves.
Add the Astro integrations with the CLI, then install the Keystatic packages:
npx astro add react markdoc
npm install @keystatic/core @keystatic/astro
This pulls in current versions at the time of writing:
@keystatic/astro5.1.0@keystatic/core0.5.50@astrojs/react5.0.6@astrojs/markdoc1.0.6astro6.4.2
The @keystatic/astro package declares peer support for Astro 2 || 3 || 4 || 5 || 6 and React 18 or 19, so it works across recent Astro majors.
Adapter for the admin UI
Keystatic's admin dashboard runs server-side code and uses Node.js APIs, so the route that serves it has to be rendered on demand. That means you need an Astro server adapter installed even though your blog pages stay static. For a self-hosted or Node deployment, add the Node adapter:
npx astro add node
This installs @astrojs/node (10.1.2 at the time of writing, which lists astro ^6.3.0 as a peer dependency). If you deploy somewhere else, use that platform's adapter instead (for example @astrojs/netlify, @astrojs/vercel, or @astrojs/cloudflare).
Configuration
Create a keystatic.config.ts file in your project root:
// keystatic.config.ts
import { config, fields, collection } from "@keystatic/core";
export default config({
storage: {
kind: "local", // Use "github" for production with GitHub
},
collections: {
posts: collection({
label: "Blog Posts",
slugField: "title",
path: "src/content/posts/*",
format: { contentField: "content" },
schema: {
title: fields.slug({ name: { label: "Title" } }),
description: fields.text({ label: "Description" }),
publishDate: fields.date({ label: "Publish Date" }),
draft: fields.checkbox({ label: "Draft", defaultValue: false }),
content: fields.markdoc({
label: "Content",
}),
},
}),
},
});
Update your astro.config.mjs to register the integrations and the adapter:
// astro.config.mjs
import { defineConfig } from "astro/config";
import react from "@astrojs/react";
import markdoc from "@astrojs/markdoc";
import keystatic from "@keystatic/astro";
import node from "@astrojs/node";
export default defineConfig({
integrations: [react(), markdoc(), keystatic()],
output: "static",
adapter: node({ mode: "standalone" }),
});
A note on output mode. Older Keystatic and Astro tutorials told you to set output: "hybrid". That option was removed in Astro 5, which merged hybrid into static. In Astro 5 and 6, static is the default, and any page that needs to run on demand opts in with export const prerender = false. The Keystatic integration handles that for its own /keystatic route, so you keep output: "static" and let the adapter serve the admin UI. Your blog pages stay fully static.
Defining the content collection
In current Astro, content collections use the Content Layer API and must declare a loader. The legacy type: "content" collections were removed in Astro 6. Create a src/content.config.ts file (note: this lives at src/content.config.ts, not the old src/content/config.ts path) and point a glob loader at the folder Keystatic writes to:
// src/content.config.ts
import { defineCollection, z } from "astro:content";
import { glob } from "astro/loaders";
const posts = defineCollection({
loader: glob({ pattern: "**/*.mdoc", base: "./src/content/posts" }),
schema: z.object({
title: z.string(),
description: z.string().optional(),
publishDate: z.coerce.date(),
draft: z.boolean().default(false),
}),
});
export const collections = { posts };
Keystatic stores Markdoc content as .mdoc files, which is why the glob pattern targets **/*.mdoc.
Basic Usage
Once configured, start your dev server and visit http://127.0.0.1:4321/keystatic to access the admin panel. You can create and edit posts through the UI, and Keystatic saves them as files in your src/content/posts/ directory.
To display posts on your site, read them with Astro's content collections. In Astro 5 and 6, entries expose id (the old slug property was removed), and you render an entry by importing the standalone render function from astro:content:
---
// src/pages/blog/index.astro
import { getCollection } from "astro:content";
import BaseLayout from "../../layouts/BaseLayout.astro";
const posts = (await getCollection("posts"))
.filter((post) => !post.data.draft)
.sort((a, b) => new Date(b.data.publishDate).getTime() - new Date(a.data.publishDate).getTime());
---
<BaseLayout title="Blog">
<h1>Blog</h1>
<ul>
{posts.map((post) => (
<li>
<a href={`/blog/${post.id}`}>
<h2>{post.data.title}</h2>
<time>{new Date(post.data.publishDate).toLocaleDateString()}</time>
</a>
</li>
))}
</ul>
</BaseLayout>
For individual post pages:
---
// src/pages/blog/[slug].astro
import { getCollection, render } from "astro:content";
import BaseLayout from "../../layouts/BaseLayout.astro";
export async function getStaticPaths() {
const posts = await getCollection("posts");
return posts.map((post) => ({
params: { slug: post.id },
props: { post },
}));
}
const { post } = Astro.props;
const { Content } = await render(post);
---
<BaseLayout title={post.data.title}>
<article>
<h1>{post.data.title}</h1>
<Content />
</article>
</BaseLayout>
Production Tips
Switch to GitHub storage for production. Change
storage.kindto"github"and add your repo details. This way, edits made through the Keystatic admin create commits directly to your repository, triggering automated builds.Protect the admin route. In production, the
/keystaticroute should only be accessible to authenticated users. Keystatic supports GitHub mode for this, so only collaborators on your repo can sign in and edit.Use Markdoc for content. Keystatic stores rich content as Markdoc (
.mdoc). Markdoc gives you custom components and variables inside your content, which is more powerful than plain Markdown for structured content.Leverage Astro content collections. Since Keystatic writes standard files, Astro's content collections handle validation, typing, and querying automatically through the Content Layer API. No extra data fetching layer needed.
Version your content with Git. Every edit is a commit. You get full history, diffs, rollbacks, and branch-based workflows for free. This is a real advantage over database-backed CMS platforms.
Common Errors and Fixes
output: "hybrid" is not a valid config option. Hybrid was removed in Astro 5 and merged into static. Use output: "static" and let individual on-demand routes opt out of prerendering with export const prerender = false. The Keystatic integration manages this for its admin route.
Admin UI 404s or fails to render after deploy. The /keystatic dashboard needs server-side rendering. If you have no adapter configured, the route cannot run. Install an adapter (@astrojs/node for self-hosting, or your platform adapter) and set it in astro.config.mjs.
post.slug is undefined. Astro 5 replaced the reserved slug property with id on collection entries. Use post.id for params and links. You can still define an explicit slug field in frontmatter if you want a custom value, but the built-in property is now id.
post.render is not a function. The per-entry render() method was removed. Import the standalone function instead: import { render } from "astro:content", then call await render(post).
Collection must define a loader. The legacy type: "content" and type: "data" collections were removed in Astro 6, and the legacy.collections flag no longer exists. Every collection in src/content.config.ts must declare a loader, such as glob({ pattern: "**/*.mdoc", base: "./src/content/posts" }).
Content config file is ignored. In current Astro the file is src/content.config.ts, not the older src/content/config.ts. If your schema is not being applied, confirm the file path.
React or Markdoc integration missing. Keystatic's admin UI is React, and its content is Markdoc, so both @astrojs/react and @astrojs/markdoc must be installed and present in the integrations array. Run npx astro add react markdoc if either is missing.
Node version too old. Astro 6 requires Node.js 22.12.0 or higher. Older Node versions will fail at install or build time. Upgrade Node if you see engine or syntax errors during npm run build.
Alternatives to Consider
- TinaCMS if you want a similar git-based approach but with visual inline editing directly on the page.
- Strapi if you need a more traditional CMS with user roles, permissions, and a REST/GraphQL API for multiple consumers.
- Sanity if your project requires real-time collaboration and you prefer a hosted backend.
Official Docs and Examples
- Astro CMS guide for Keystatic: https://docs.astro.build/en/guides/cms/keystatic/
- Keystatic Astro installation docs: https://keystatic.com/docs/installation-astro
- Keystatic homepage and concepts: https://keystatic.com/
- Astro Content Collections guide (Content Layer API, loaders, render, id): https://docs.astro.build/en/guides/content-collections/
- Astro v5 upgrade notes (hybrid removed, render and id changes): https://docs.astro.build/en/guides/upgrade-to/v5/
- Astro v6 upgrade notes (Node 22, legacy collections removed): https://docs.astro.build/en/guides/upgrade-to/v6/
- Keystatic source and examples (monorepo, including Astro examples): https://github.com/Thinkmill/keystatic
Wrapping Up
Keystatic removes the complexity of external CMS services entirely. Your content lives in your repo, your admin UI runs alongside your Astro site, and there is no API to manage or billing to track. For developers who want a clean editing experience without the overhead of a hosted CMS, Keystatic and Astro is a hard combination to beat.
Sources
Checked on 2026-05-29:
- Astro Docs: Keystatic and Astro
- Keystatic Docs: Adding Keystatic to an Astro project
- Astro Docs: Content Collections (Content Layer API, loaders, render, id)
- Astro Docs: Upgrade to Astro v5 (hybrid removed, render and id changes)
- Astro Docs: Upgrade to Astro v6 (Node 22.12.0, legacy collections removed)
- npm: @keystatic/astro (5.1.0, peer deps)
- npm: @keystatic/core (0.5.50)
- npm: @astrojs/react (5.0.6)
- npm: @astrojs/markdoc (1.0.6)
- npm: @astrojs/node (10.1.2, astro ^6.3.0 peer)
- npm: astro (6.4.2)
- Keystatic source and examples (GitHub)
Related Articles
How to Use Algolia with Astro: Complete Guide
Step-by-step guide to integrating Algolia with your Astro website.
How to Integrate Auth0 with Astro: Complete Guide
Step-by-step guide to integrating Auth0 with your Astro website. Setup, configuration, and best practices.
How to Use AWS Amplify with Astro: Complete Guide
Step-by-step guide to integrating AWS Amplify with your Astro website.