SuperAPI AI Directory

keystatic-astro

General & other

View repository

Updated 10-15 21:42

About this skill

Keystatic + Astro

Keystatic writes Git-tracked files; Astro reads them via the Content Layer (getCollection, getEntry, render). Do not use createReader.

Match @astrojs/* and @keystatic/* APIs to the versions in the project's package.json / lockfile.

Official docs (check these before guessing APIs):

Docs LLM / agent access
Keystatic keystatic.com/docs · Astro integration No official llms.txt — fetch doc pages or search the docs site
Astro docs.astro.build Astro Docs MCP server — use search_astro_docs when the MCP is enabled (preferred over static exports). Setup: Building with AI

Do not duplicate upstream Keystatic or Astro API reference in this skill.

When to apply

  • Adding or changing a Keystatic collection or singleton
  • Wiring keystatic.config.* or src/content.config.ts
  • Choosing content vs data vs JSON format, folder layout under src/content/
  • Adding MDX custom blocks or image handling
  • Fetching external data into Astro collections at build time
  • Splitting local CMS content from API-backed content

Config locations

Purpose Typical path Notes
Keystatic root keystatic.config.ts or .tsx Storage, UI nav, GitHub repo, brand — site values inline here
Astro content registry src/content.config.ts Exports collections = { ... } from all *Astro* definitions
Per-collection Keystatic schema cms/{name}.keystatic.ts(x) collection() / singleton()
Per-collection Astro schema cms/{name}.astro.ts(x) defineCollection() + loader + Zod schema
Astro integration astro.config.mjs Start from astro.config.example.mjs
Glob loader helper cms/utils/loader.ts Wraps glob() from astro/loaders

Astro: Content collections · Keystatic: Configuration

Astro config & environment

Copy examples/astro.config.example.mjs into the project root as astro.config.mjs and extend with site-specific integrations. It covers:

  • loadEnv from Vite (.env is not auto-loaded into astro.config.mjs)
  • output: 'static' — Astro v5+ removed hybrid; static sites can still use on-demand routes with an adapter (on-demand rendering)
  • Conditional keystatic() via ASTRO_ENABLE_KEYSTATIC — off for static production CI builds
  • Netlify adapter via ASTRO_USE_NETLIFY_ADAPTER — SSR functions need their own Bun install config (tech-stack → bun-install.md)
  • env.schema for typed env vars used in config and client code

Pair with the matching .env example for each deploy target:

File Use
.env.example.local Local dev — Keystatic local, ASTRO_ENABLE_KEYSTATIC=true
.env.example.netlify CMS host — Keystatic github, adapter enabled
.env.example.ionos Static production build — Keystatic off, values for CI only

KEYSTATIC_STORAGE_KIND must be public + client context in env.schema so keystatic.config.* can import it from astro:env/client.

Astro: Environment variables · On-demand rendering · Keystatic & Astro · Configuring Astro

Dual-file pattern (required)

Every content type gets two paired files:

File Exports Role
{name}.keystatic.ts(x) keystatic{Name}Config, basePath / contentBase, shared enums Keystatic schema, paths, CMS labels
{name}.astro.ts(x) astro{Name}Definition Astro defineCollection, Zod schema, required loader

Register both in root configs:

  • keystatic.config.* → imports all keystatic*Config
  • src/content.config.ts → imports all astro*Definition into export const collections

Every Astro collection needs a loader (typically glob() or file() from astro/loaders, or a custom async loader). Import z from astro/zod.

Export naming:

  • Keystatic: keystaticFaqsConfig, keystaticItemsConfig
  • Astro: astroFaqsDefinition, astroItemsDefinition

Export shared enums and paths from the Keystatic file; import them in Astro:

// faqs.keystatic.ts
export const categoryEnumAndOrder = ['Allgemein', 'Planung & Bau', ...] as const
export const basePath = 'src/content/faqs'

// faqs.astro.ts
import { defineCollection } from 'astro:content'
import { z } from 'astro/zod'
import { basePath, categoryEnumAndOrder } from './faqs.keystatic'
import { loader } from './utils/loader'

export const astroFaqsDefinition = defineCollection({
  loader: loader(basePath, 'mdx'),
  schema: z.object({ category: z.enum(categoryEnumAndOrder), ... }),
})

Astro: Defining build-time collections · Keystatic: Collections · Astro integration

File splitting

All collection and singleton definitions live in cms/ at the project root.

Supporting files per concern

Concern Location Pattern
Shared Zod (API + Astro) {name}.schema.ts API response + collection validation
Local variant of remote schema {name}Schema.ts extends/omits/transforms remote schema for CMS
Glob loader helper cms/utils/loader.ts reused by all local file collections
MDX block registry (CMS) cms/components/mdxComponentsKeystatic.ts mdxComponentsKeystatic('faqs') per context
MDX block registry (site) cms/components/mdxComponentsAstro.astro passed to <Content components={...} />
Per-block files cms/components/{Block}/ keystatic.{block}.config.ts, {Block}.astro, optional KeystaticPreview.tsx
Custom Keystatic fields cms/components/keystaticComponents/ e.g. map picker, color picker

MDX blocks (three layers)

components/{Block}/
├── keystatic.{Block}.config.ts   # block() schema + ContentView preview
├── {Block}.astro                 # frontend render
└── KeystaticPreview.tsx          # optional CMS image preview

Disable built-in MDX images everywhere: fields.mdx({ options: { image: false } }). Use custom block() components for images instead.

Keystatic: MDX field · Content components · Astro: MDX integration

Content structure

Folder layout under src/content/

Paths in Keystatic path must match on-disk layout. Collection key in content.config.ts is camelCase (e.g. homepageFacts → src/content/homepageFacts/).

Type Keystatic Astro Files
MDX collection collection, format: { contentField: 'body' } loader(basePath, 'mdx') *.mdx
YAML data collection collection, no contentField loader(..., 'yaml') *.yaml
JSON data collection collection, format: { data: 'json' } loader(..., 'json') *.json
Singleton singleton, one folder registered in collections; access via getEntry('name', 'index') index.mdx, index.yaml, or multi-file

Singleton multi-file pattern

One logical singleton can produce multiple files in one folder:

src/content/homepage/
├── index.mdx              # frontmatter + bodyBeforeQuotes
├── bodyAfterQuotes.mdx
└── bodyAfterMilestones.mdx

Load each part separately: getEntry('homepage', 'index'), getEntry('homepage', 'bodyaftermilestones').

Relationships

Use fields.relationship({ collection: 'otherCollection' }) in Keystatic. In Astro, relationship values resolve to entry slugs:

apiSyncedItems (JSON, fetch-to-disk)
       ↑ relationship (externalId)
items (MDX)
       ↑ relationship (parent)
itemDetails (MDX)

CMS list ergonomics

  • slugField for slug-driven collections (title, name, imageAlt)
  • columns: ['title', 'position', 'externalId'] for list view
  • entryLayout: 'form' on content-heavy singletons
  • [READONLY] labels for API-synced collections editable only via fetch

Keystatic: Collections · Singletons · Relationship field · Astro: Collection references

Astro integration

Loading content

API Use for
getEntry(collection, id) Singletons ('index'), multi-file singleton parts
getCollection(name) All entries; sort/filter in page or component
getCollection(name, filter) Filtered load
render(entry) MDX: const { Content } = await render(entry) from astro:content
getStaticPaths + getCollection Dynamic routes ([slug].astro)

Pass shared MDX components on every render:

import { render } from 'astro:content'

const { Content } = await render(entry)
<Content components={mdxComponentsAstro} />

Loaders

Every collection declares loader in defineCollection. For on-disk Keystatic content, use glob() via the shared helper:

// cms/utils/loader.ts
import { glob } from 'astro/loaders'

export const loader = (contentBase: string, format: 'mdx' | 'json' | 'yaml') => {
  const base = contentBase.startsWith('/') ? `.${contentBase}` : contentBase
  return glob({ base, pattern: `**\/[^_]*.${format}` })
}

Pattern excludes _-prefixed files (partials/drafts). For a single JSON/YAML file with many entries, use file() instead.

Type generation

Run astro sync after schema changes → .astro/content.d.ts.

Astro: Querying collections · Rendering body content · Generating routes · glob() loader · MDX components in content

Remote / fetched collections

Pick a pattern based on CMS visibility, API reliability at build time, and whether editors need a local override.

Pattern Keystatic? Disk cache? When
A. In-memory fetch No No Pure API data, no editor workflow
B. Fetch + JSON fallback No Committed fallback file API unreliable; offline builds OK
C. Fetch-to-disk + glob Yes (readonly) Written each build Editors reference slugs; data visible in CMS
D. Local mirror + remote merge Local only Local JSON in src/content/ Same page template for CMS and API sources

Remote collections register only in src/content.config.ts, not in keystatic.config.

For implementation details, decision flow, and merge patterns, see remote-collections.md.

Astro: Custom build-time loaders · Content Loader API

Images

Image type Keystatic directory Astro
Content-colocated (hero, logos) src/content/{collection} image() in Zod → Picture from astro:assets
MDX block images src/assets/{context}/ getImage() + import.meta.glob (Keystatic Astro images recipe)
Public downloads public/downloads/{path}/ Direct URL, not fingerprinted

Astro: Images in content collections · Keystatic: Astro images recipe

Checklist: add a new local collection

  1. Create {name}.keystatic.ts — export basePath, enums, keystatic{Name}Config
  2. Create {name}.astro.ts — import shared exports, defineCollection with loader(basePath, format) and Zod schema
  3. Register in keystatic.config.* (collections or singletons) and src/content.config.ts
  4. Add UI navigation entry in keystatic.config.* ui.navigation
  5. Create src/content/{name}/ folder (or let Keystatic create on first save)
  6. Run astro sync and verify types
  7. Consume via getCollection / getEntry / render in pages

Checklist: add a remote collection

  1. Create Zod schema ({name}Schema.ts) — validate API response
  2. Create {name}Astro.ts with async loader: async () => { fetch → parse → return entries }
  3. Register in src/content.config.ts only
  4. Decide fail-fast vs fallback (see remote-collections.md)
  5. If merging with local CMS data: share schema, merge in page via [...local, ...remote]

Anti-patterns

  • Using createReader — use Astro Content Layer instead
  • Duplicating enums/paths between Keystatic and Astro files — export from .keystatic.ts
  • Enabling @keystatic/astro in static production builds
  • Native MDX images when using custom blocks — always options: { image: false }
  • Adding remote-only collections to Keystatic unless editors need readonly visibility (pattern C)