From 677c6a285970c6ce798097ce74131ac20b813bf3 Mon Sep 17 00:00:00 2001 From: Edouard Duplessis Date: Fri, 30 Oct 2026 15:27:12 -0400 Subject: [PATCH] Revise Keystatic installation guide for Astro 5 ++ Updated installation guide for Keystatic with Astro, including changes for Astro 5 and later. Added details on defining content collections and adjustments to code examples. --- .../src/content/pages/installation-astro.mdoc | 100 ++++++++++++++++-- 1 file changed, 91 insertions(+), 9 deletions(-) diff --git a/docs/src/content/pages/installation-astro.mdoc b/docs/src/content/pages/installation-astro.mdoc index 6cfd27df5..98cee333b 100644 --- a/docs/src/content/pages/installation-astro.mdoc +++ b/docs/src/content/pages/installation-astro.mdoc @@ -1,6 +1,6 @@ --- title: Adding Keystatic to an Astro project -summary: Integrating Keystatic with an existing Astro v2 project. +summary: Integrating Keystatic with an existing Astro project (content layer, Astro 5 and later). --- {% aside icon="☝️" %} This guide assumes you have an existing Astro project. @@ -76,6 +76,51 @@ export default config({ Keystatic is now configured to manage your content based on your schema. +{% aside icon="☝️" %} +This config only tells Keystatic how to read and write files. Astro needs its own +collection definition before `getCollection('posts')` can return anything. The +next section covers it. +{% /aside %} + +## Defining the content collection + +Since Astro 5, collections are defined in `src/content.config.ts` with an +explicit `loader`. Astro no longer discovers collections automatically from the +folders in `src/content/`, so the `.mdoc` files written by Keystatic stay +invisible until you declare them: + +```ts +// src/content.config.ts +import { defineCollection } from 'astro:content'; +import { glob } from 'astro/loaders'; +import { z } from 'astro/zod'; + +const posts = defineCollection({ + loader: glob({ base: './src/content/posts', pattern: '**/*.mdoc' }), + schema: z.object({ + title: z.string(), + }), +}); + +export const collections = { posts }; +``` + +{% aside icon="💡" %} +Only `title` is declared in the schema, because `fields.markdoc()` stores the +body of the file, not frontmatter. If you add fields to your Keystatic schema, +add the matching properties to the Astro schema too. +{% /aside %} + +{% aside icon="☝️" %} +Use `src/content.config.ts` (singular `content.config`), not the old +`src/content/config.ts`. Restart the dev server after creating or renaming this +file. Without it, `getCollection('posts')` returns `[]` and Astro logs a +`The collection "posts" does not exist or is empty` warning in the terminal — the +page itself renders without an error. +{% /aside %} + +--- + ## Running Keystatic You can now launch the Keystatic Admin UI. Start the Astro dev server: @@ -138,7 +183,7 @@ Keystatic provides its own [Reader API](/docs/reader-api) to pull data from the The following example displays a list of each post title, with a link to an individual post page: -```tsx +```astro // src/pages/posts/index.astro --- import { getCollection } from 'astro:content' @@ -148,23 +193,32 @@ const posts = await getCollection('posts') ``` +{% aside icon="💡" %} +With the content layer, an entry's identifier is `post.id` (generated from the +file name relative to the loader's `base`), not `post.slug`. `slug` is only +available as a reserved frontmatter field. +{% /aside %} + ### Displaying a single collection entry -To display content from an individual post, you can import and use Astro's `` component to [render your content to HTML](https://docs.astro.build/en/guides/content-collections/#rendering-content-to-html): +To display content from an individual post, fetch the entry and compile it with +`render()`: -```tsx +```astro // src/pages/posts/my-first-post.astro --- -import { getEntry } from 'astro:content' +import { getEntry, render } from 'astro:content' const post = await getEntry('posts', 'my-first-post') -const { Content } = await post.render() +if (!post) throw new Error('Post not found') + +const { Content } = await render(post) ---
@@ -173,7 +227,9 @@ const { Content } = await post.render()
``` -If you want to generate pages for all entries of a collection, [this Astro guide](https://docs.astro.build/en/tutorials/add-content-collections/#generate-pages-from-a-collection) will help. +To generate pages for all entries of a collection, use the `id` property in +`getStaticPaths()` and render each entry with `render()`. See [Generating routes +from content](https://docs.astro.build/en/guides/content-collections/#generating-routes-from-content). --- @@ -183,6 +239,32 @@ If you want to use The `` component from Astro to display images in yo ## Deploying Keystatic + Astro -Because Keystatic needs to run serverside code and use Node.js APIs, you will need to add an [Astro adapter](https://docs.astro.build/en/guides/server-side-rendering/#adding-an-adapter) to deploy your project. +Keystatic's admin UI is server-rendered, so `astro build` fails with +`[NoAdapterInstalled]` until you either add an Astro adapter or keep the +integration out of production builds. + +### Using local storage in production + +If content is only edited locally, don't mount Keystatic in production. [This +recipe](/docs/recipes/astro-disable-admin-ui-in-production) shows the full setup: + +```diff +// astro.config.mjs +- integrations: [react(), markdoc(), keystatic()], ++ integrations: [react(), markdoc(), ...(process.env.SKIP_KEYSTATIC ? [] : [keystatic()])], +``` + +Set `SKIP_KEYSTATIC=true` in your host's environment variables. The site then +builds as a fully static project, without an adapter. + +### Serving the admin UI in production + +If you want to edit content on the deployed site, add an [Astro +adapter](https://docs.astro.build/en/guides/on-demand-rendering/#adding-an-adapter), +for example: + +```bash +npx astro add node +``` You will also probably want to [connect Keystatic to GitHub](/docs/github-mode) so you can manage content on the deployed instance of the project.