/ astro-integrations / How to Use Keystatic with Astro: Complete Guide
astro-integrations 9 min read

How to Use Keystatic with Astro: Complete Guide

Step-by-step guide to integrating Keystatic with your Astro website.

How to Use Keystatic with Astro: Complete Guide

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/astro 5.1.0
  • @keystatic/core 0.5.50
  • @astrojs/react 5.0.6
  • @astrojs/markdoc 1.0.6
  • astro 6.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

  1. Switch to GitHub storage for production. Change storage.kind to "github" and add your repo details. This way, edits made through the Keystatic admin create commits directly to your repository, triggering automated builds.

  2. Protect the admin route. In production, the /keystatic route should only be accessible to authenticated users. Keystatic supports GitHub mode for this, so only collaborators on your repo can sign in and edit.

  3. 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.

  4. 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

    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: