A Markdown-based CMS sounds great until it is time to set it up. The good news is that with Keystatic and Next.js the process is short: by the end you will have an editing panel at /keystatic that writes .md files inside your repository, with no database in between.
This is the step-by-step guide. If you are coming in cold, it helps to be clear first about what a Markdown-based CMS is and why it fits a headless approach with Next.js. The steps below follow the official Keystatic documentation.
What we are going to build
- A Keystatic admin panel served at the
/keystaticroute of your own app. - A posts collection stored as Markdown files in
src/content/posts. - Reading that content from your pages with the Keystatic reader.
Before you start
- A Next.js project with the App Router (a new one from
npx create-next-appworks). - Node.js installed and the project versioned in a Git repository.
- Basic terminal knowledge; you need nothing else.
Step 1: Install the dependencies
npm install @keystatic/core @keystatic/next @markdoc/markdoc@keystatic/core is the core, @keystatic/next connects the panel to Next.js and @markdoc/markdoc processes the rich Markdown in the body of entries.
Step 2: Create the configuration file
In the project root create keystatic.config.ts. Here you define where content is stored (storage) and which collections exist with their field schema.
import { config, fields, collection } from '@keystatic/core';
export default config({
storage: {
kind: 'local',
},
collections: {
posts: collection({
label: 'Posts',
slugField: 'title',
path: 'src/content/posts/*',
format: { contentField: 'content' },
schema: {
title: fields.slug({ name: { label: 'Title' } }),
content: fields.markdoc({ label: 'Content' }),
},
}),
},
});storage: { kind: 'local' } saves the files on your disk while you develop. Further down we switch it to GitHub for production.
Step 3: Mount the admin panel
Keystatic serves its own interface inside your app. Create these three files under src/app/keystatic to render the panel at the /keystatic route.
// src/app/keystatic/keystatic.ts
'use client';
import { makePage } from '@keystatic/next/ui/app';
import config from '../../../keystatic.config';
export default makePage(config);
// src/app/keystatic/layout.tsx
import KeystaticApp from './keystatic';
export default function Layout() {
return <KeystaticApp />;
}
// src/app/keystatic/[[...params]]/page.tsx
export default function Page() {
return null;
}Step 4: Add the API route handler
The panel needs an endpoint to read and write. Create src/app/api/keystatic/[...params]/route.ts.
import { makeRouteHandler } from '@keystatic/next/route-handler';
import config from '../../../../../keystatic.config';
export const { POST, GET } = makeRouteHandler({
config,
});You can now start the project with npm run dev and open http://127.0.0.1:3000/keystatic: there is your panel to create entries.
Step 5: Read and display the content
To render the entries on your site, Keystatic offers the reader. With it you access the whole collection or a specific entry by its slug.
import { createReader } from '@keystatic/core/reader';
import keystaticConfig from '../../keystatic.config';
const reader = createReader(process.cwd(), keystaticConfig);
// All entries in the collection
const posts = await reader.collections.posts.all();
// A specific entry by its slug
const post = await reader.collections.posts.read('my-first-post');The body of each entry is obtained with post.content() and rendered with Markdoc (Markdoc.transform and Markdoc.renderers.react). You have the details in the reader documentation.
From local mode to GitHub
Local mode is perfect for developing, but in production you will want editors to save changes without touching your machine. That is what GitHub mode is for: Keystatic makes commits directly to your repository. You only change the storage.
storage: {
kind: 'github',
repo: { owner: 'your-user', name: 'your-repo' },
},The first time you enter /keystatic in GitHub mode, Keystatic guides you to create a GitHub App and connect the repo; that process generates the variables KEYSTATIC_GITHUB_CLIENT_ID, KEYSTATIC_GITHUB_CLIENT_SECRET and KEYSTATIC_SECRET in your .env. From then on, anyone with write access to the repository can edit from the panel. The full steps are in the GitHub mode documentation.
The order that avoids getting stuck
Most Keystatic problems show up when moving to production, not when installing it. That is why it pays to work in this order: first local mode with two or three test entries; then check that the reader reads them and renders them in your pages; and only then move to GitHub.
A frequent trap: local mode writes to disk, so it works on your machine but not on hosting without a persistent file system such as Vercel. In production the panel needs GitHub mode. When you enable it, check that the variables Keystatic generates in your .env (KEYSTATIC_GITHUB_CLIENT_ID, KEYSTATIC_GITHUB_CLIENT_SECRET and KEYSTATIC_SECRET) are also loaded in the production environment.
And make clear who can edit: anyone with write permission on the repository will be able to do it from the panel. If the content grows toward thousands of entries or many editors, compare it against a headless with a database such as Directus or Strapi.
Want to set up Keystatic (or the CMS that fits best) in your Next.js project without fighting the configuration? At Codegea we do it for you: see the CMS integration service or write to us from the contact page.
Written by
Derian Pinto
Founder & Full Stack Developer
Founder of Codegea. Builds custom websites, stores and platforms, and integrates and supports Arc XP for media companies.
