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.