fixmyberlin/fixmyskills

keystatic-astro

>- Keystatic + Astro Content Layer: config locations, dual-file CMS schemas, content collection structure, glob loaders, MDX blocks, and remote/fetched collections. Use when scaffolding or changing Keystatic collections, Astro content.config.ts, keystatic.config, MDX blocks, or build-time API-backed content.

First seen Jul 2, 2026

Installation

$ npx skills add fixmyberlin/fixmyskills --skill keystatic-astro

Also in this package

Other skills from fixmyberlin/fixmyskills · top by installs.

npx skills add fixmyberlin/fixmyskills

Browse all from fixmyberlin/fixmyskills

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

License LICENSE
Default branch main
Open issues 0
Status Active

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 19,170 B
  • docs SUMMARY.md 329 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 6 installs

SKILL.md

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 searchastrodocs 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](examples/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](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 ASTROENABLEKEYSTATIC — off for static production CI builds
  • Netlify adapter via ASTROUSENETLIFY_ADAPTER
  • 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](examples/.env.example.local) Local dev — Keystatic local, ASTROENABLEKEYSTATIC=true
[.env.example.netlify](examples/.env.example.netlify) CMS host — Keystatic github, adapter enabled
[.env.example.ionos](examples/.env.example.ionos) Static production build — Keystatic off, values for CI only

KEYSTATICSTORAGEKIND 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 keystaticConfig
  • 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. homepageFactssrc/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](references/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](references/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)