Adding Keystatic to a Next.js project

☝️

This guide assumes you have an existing Next.js 14 project, and are using the app directory.

If you don't have an existing Next.js project, you can create a new one with the following command:

npx create-next-app@latest

Installing dependencies

Install two Keystatic packages and @markdoc/markdoc:

npm install @keystatic/core @keystatic/next @markdoc/markdoc

Creating a Keystatic config file

A Keystatic config file is required to define your content schema. This file will also allow you to connect a project to a specific GitHub repository (if you decide to do so).

Create a file called keystatic.config.ts in the root of the project and add the following code to define both your storage type (local) and a single content collection (posts):

// keystatic.config.ts
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' }),
      },
    }),
  },
});

Keystatic is now configured to manage your content based on your schema.


Setting up the Keystatic Admin UI

First, create a src/app/keystatic/keystatic.ts file:

// src/app/keystatic/keystatic.ts
"use client";

import { makePage } from "@keystatic/next/ui/app";
import config from "../../../keystatic.config";

export default makePage(config);

Next, create a layout file called src/app/keystatic/layout.tsx:

// src/app/keystatic/layout.tsx
import KeystaticApp from "./keystatic";

export default function Layout() {
  return (
    <KeystaticApp />
  );
}

Next, create a page called src/app/keystatic/[[...params]]/page.tsx:

// src/app/keystatic/[[...params]]/page.tsx

export default function Page() {
  return null;
}

Finally, create an API route called src/app/api/keystatic/[...params]/route.ts

// 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 launch the Keystatic Admin UI. Start the Next dev server:

npm run dev

Visit http://127.0.0.1:3000/keystatic to see the Keystatic Admin UI running.


Creating a new post

☝️

In our Keystatic config file, we've set the path property for our posts collection to src/content/posts/*.

As a result, creating a new post from the Keystatic Admin UI should create a new content directory in the src directory, with the new post .mdoc file inside!

Go ahead — create a new post from the Admin UI, and hit save.

You will find your new post inside the src/content/posts directory:

src
└── content
    └── posts
        └── my-first-post.mdoc

Navigate to that file in your code editor and verify that you can see the Markdown content you entered. For example:

---
title: My First Post
---

This is my very first post. I am **super** excited.

Rendering Keystatic content

💡

Keystatic provides a Reader API to bring content to the front end. As it is a Node API it must be run server-side.

Displaying a collection list

The following example displays a list of each post title, with a link to an individual post page:

// src/app/posts/page.tsx
import { createReader } from '@keystatic/core/reader';
import keystaticConfig from '../../../keystatic.config';

import Link from 'next/link';

// 1. Create a reader
const reader = createReader(process.cwd(), keystaticConfig);

export default async function Page() {
  
  // 2. Read the "Posts" collection
  const posts = await reader.collections.posts.all();
  return (
    <ul>
      {posts.map(post => (
        <li key={post.slug}>
          <Link href={`/posts/${post.slug}`}>{post.entry.title}</Link>
        </li>
      ))}
    </ul>
  );
}

Displaying a single collection entry

To display content from an individual post, you can use Markdoc's transform and renderers.react functions:

// src/app/posts/[slug]/page.tsx
import { createReader } from "@keystatic/core/reader";
import React from "react";
import Markdoc from "@markdoc/markdoc";

import keystaticConfig from "../../../../keystatic.config";

const reader = createReader(process.cwd(), keystaticConfig);

export default async function Post({ params }: { params: { slug: string } }) {
  const post = await reader.collections.posts.read(params.slug);
  if (!post) {
    return <div>No Post Found</div>;
  }
  const { node } = await post.content();
  const errors = Markdoc.validate(node);
  if (errors.length) {
    console.error(errors);
    throw new Error('Invalid content');
  }
  const renderable = Markdoc.transform(node);
  return (
    <>
      <h1>{post.title}</h1>
      {Markdoc.renderers.react(renderable, React)}
      <hr />
      <a href={`/posts`}>Back to Posts</a>
    </>
  );
}

Deploying Keystatic + Next.js

Because Keystatic needs to run serverside code and Next.js API routes, you will need to ensure that your hosting provider supports Node.js.

You will also probably want to connect Keystatic to GitHub so you can manage content on the deployed instance of the project.