Adding Keystatic to an Astro project

This guide assumes you are trying to add Keystatic to an existing Astro v2 project.

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

npm create astro@latest

Now, let's add Keystatic to our project!

Installing dependencies

We're going to need to install a few packages to get Keystatic going.

Keystatic outputs Markdoc and has a dependency on React, so first we need to add Astro's first-party integrations for both:

npx astro add react markdoc

Let's also install two Keystatic packages:

npm install @keystatic/core @keystatic/astro

Creating a Keystatic config file

Keystatic needs a config file. This is where you can connect a project with a specific GitHub repository and define a content schema.

Let's create a file called keystatic.config.ts (or .js if not using TypeScript) in the root of the project:

// 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.document({
          label: 'Content',
          formatting: true,
          dividers: true,
          links: true,
          images: true,
        }),
      },
    }),
  },
});

We export a config object wrapped in the config function imported from @keystatic/core.

For now, we set the storage strategy to local, and we create a “posts” collection.

This is all Keystatic needs to start managing content, configuration-wise.

Now, we need to display the Keystatic Admin UI on our site!


Keystatic Admin UI pages

In our pages directory, we want every route within the /keystatic segment to become a Keystatic Admin UI route.

We can leverage Astro's Rest parameters to match file paths of any depth.

Let's create a new page called src/pages/keystatic/[...params].astro:

---
// src/pages/keystatic/[...params].astro

// TODO: Display Keystatic pages
---

The Keystatic Admin UI is built with React. So that we can import and mount this app here with React, let's create a new file outside of the pages directory.

I'll put it in the project root, alongside our Keystatic config file.

// keystatic.page.ts
import { makePage } from '@keystatic/astro/ui'
import keystaticConfig from './keystatic.config'

export const Keystatic = makePage(keystaticConfig)

Now, we can import that file in our keystatic/[...params].astro page, and mount the component on the clientside only:

---
// src/pages/keystatic/[...params].astro
import { Keystatic } from '../../../keystatic.page'
---

<Keystatic client:only />

Next, start up Astro's integrated server:

npm run dev

Visiting the Keystatic Admin UI

Great! So now, we should be able to visit the /keystatic router in the browser.

Try it!

Oh no.

We're getting this error:

getStaticPaths() function is required for dynamic routes. Make sure that you export a getStaticPaths function from your dynamic route.

Two lines further, we get this nugget of information:

Alternatively, set output: "server" in your Astro config file to switch to a non-static server build.

We could do this, but Astro actually has a new hybrid rendering mode, which works even better for our needs.

Astro's SSR hybrid mode

In the astro.config.mjs, add the output: 'hybrid' option:

export default defineConfig({
+ output: 'hybrid',
  integrations: [react()],
})

Hybrid rendering will still try to pre-render every page by default, with a mechanism to opt out of it.

We don't want prerendering on Keystatic routes. Let's export a const prerender = false in our Keystatic pages file:

---
// src/pages/keystatic/[...params].astro
import { Keystatic } from '../../../keystatic.page'

+ export const prerender = false
---

<Keystatic client:only />

Now, try visit the /keystatic route once again. You should see the Keystatic Admin UI! 🎉

But before we can read and write data, we also need to create some API routes for Keystatic


Keystatic API Routes

Create a new file called src/pages/api/keystatic/[...params].ts

Once again, we use Astro's Rest parameters here.

// src/pages/api/keystatic/[...params].ts
import { makeHandler } from '@keystatic/astro/api'
import keystaticConfig from '../../../../keystatic.config'

export const all = makeHandler({
  config: keystaticConfig,
})

export const prerender = false

Notice we also opt out of prerendering for those routes.

We should be all set to use Keystatic now.

Creating a new post

Try and visit the /keystatic page in the browser one more time, and click on the “Posts” collection.

Go ahead, and create 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!

If everything worked correctly, you'll find your new post inside the src/content/posts directory:

src/
  content/
    posts/
      my-first-post.mdoc   

That Markdoc file should look something like this:

---
title: My First Post
---

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

Niiiice ✨

Let's try to display that post on the frontend now.


Rendering Keystatic content

💡

Keystatic provides its own Reader and Renderer APIs to pull data from the file system into your frontend.

However, you can also leverage Astro's built-in content collections if you prefer.

Using the Keystatic Reader API

To display content in your frontend with Keystatic's Reader API, please refer to the Reader API documentation.

Using Astro's content collections

The TL;DR; here: Astro provides its own built-in mechanism to pull-in content from the file system into JSON data.

Paired with the @astrojs/markdoc integration, it works perfectly with Keystatic-generated content!

Check out the Astro documentation on content collections and the Markdoc integration for more details.

Displaying a collection list

In any .astro page, you can get all posts collection entries like so:

---
import { getCollection } from 'astro:content'

const posts = await getCollection('posts')
---

<main>
  <pre>{JSON.stringify(posts, null, 2)}</pre>
</main>

Since we told Keystatic to manage posts in src/content/posts, they'll be available as Astro's content collections as the posts collection slug.

Pretty cool, eh?

Displaying a single collection entry

You can also get a single collection entry by its slug:

---
import { getEntry } from 'astro:content'

const post = await getEntry('posts', 'my-first-post')
---

<main>
  <pre>{JSON.stringify(post, null, 2)}</pre>
</main>

Rendering document field content

If you look at the output from our post entry from above, you'll notice that the body field – the content of our post – is displaying raw Markdoc.

But you probably want to render HTML instead on your page.

Well, good news: Astro provides a <Content /> component that can do the heavy lifting of converting rich text data into properly formatted markup:

---
import { getEntry } from 'astro:content'

const post = await getEntry('posts', 'my-first-post')
const { Content } = await post.render()
---

<main>
  <h1>{post.data.title}</h1>
  <Content />
</main>

Sweet!


Deploying Keystatic + Astro

Because Keystatic needs to run serverside code and use Node.js APIs, you'll need to add an Astro adapter to deploy your project.

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