Adding Pages to the Template
Decision Tree
Need a new page?
├── Unique page (home, about, contact, legal)?
│ └── Add a singleton in packages/keystatic-config/src/singletons/
├── Repeatable content type (blog posts, services)?
│ └── Add a collection in packages/keystatic-config/src/collections/
└── Mix (listing page as singleton, detail from collection)?
└── Combo: singleton for listing + collection for items
Naming Conventions
| Convention | Example | Rules |
|---|---|---|
| Singleton file | privacy-policy.ts | kebab-case, matches path |
| Collection file | blog.ts | kebab-case |
| Content path | content/privacy-policy | kebab-case, matches singleton path |
| Astro page | privacy.astro | kebab-case, file name = route |
| Folders | singletons/, collections/ | Plural directories |
Recipes
1. Add a Singleton Page
// packages/keystatic-config/src/singletons/my-page.ts
import { singleton, fields } from '@keystatic/core';
import { seoFields } from '../seo-fields';
export const myPage = singleton({
label: 'My Page',
path: 'content/my-page',
schema: {
title: fields.text({ label: 'Title' }),
body: fields.markdoc({ label: 'Content' }),
...seoFields,
},
});
Register in index.ts:
import { myPage } from './singletons/my-page';
// Add to singletons object
Create Astro page apps/www/src/pages/my-page.astro:
---
import BaseLayout from '../layouts/BaseLayout.astro';
import { reader } from '../lib/reader';
import Markdoc from '@markdoc/markdoc';
const entry = await reader.singletons.myPage.read();
const body = await entry?.body();
const content = body ? Markdoc.renderers.html(Markdoc.transform(body.node, { nodes: Markdoc.nodes })) : '';
const seo = { title: entry?.seoTitle, description: entry?.seoDescription };
---
<BaseLayout seo={seo}>
<article class="mx-auto max-w-3xl px-4 py-16 prose" set:html={content} />
</BaseLayout>
2. Add a Collection
// packages/keystatic-config/src/collections/faqs.ts
import { collection, fields } from '@keystatic/core';
export const faqs = collection({
label: 'FAQs',
slugField: 'title',
path: 'content/faqs/*',
schema: {
title: fields.slug({ name: { label: 'Question' } }),
answer: fields.markdoc({ label: 'Answer' }),
},
});
Register in index.ts and create listing/detail pages in apps/www/src/pages/faqs/.
3. Combo: Singleton + Collection for Listing
Singleton stores the listing page content (intro, blocks). Collection holds items. Pass both to the page:
--- const page = await reader.singletons.myListingPage.read(); const items = await reader.collections.myCollection.all(); ---
4. SEO Fields
Every singleton/collection that needs SEO should spread ...seoFields:
schema: {
// content fields...
...seoFields,
}
The 3-tier fallback in BaseLayout:
- Page-level —
entry.seoTitle,entry.seoDescription,entry.ogImage - Site-wide — Site Settings
homepageSeodefaults - Config fallback —
siteConfig.seo.defaultTitle
5. Customizing Block Fields
To split a block field into separate fields (e.g. heading parts):
import { fields } from '@keystatic/core';
export const myCustomBlock = {
label: 'My Block',
schema: fields.object({
headingLine1: fields.text({ label: 'Heading Line 1' }),
headingAccent: fields.text({ label: 'Heading Accent' }),
headingLine2: fields.text({ label: 'Heading Line 2' }),
content: fields.text({ label: 'Content', multiline: true }),
}),
};
Register in packages/keystatic-config/src/blocks/index.ts and add a renderer in apps/www/src/components/sections/.