← Back to Blog
development

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

ConventionExampleRules
Singleton fileprivacy-policy.tskebab-case, matches path
Collection fileblog.tskebab-case
Content pathcontent/privacy-policykebab-case, matches singleton path
Astro pageprivacy.astrokebab-case, file name = route
Folderssingletons/, 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:

  1. Page-levelentry.seoTitle, entry.seoDescription, entry.ogImage
  2. Site-wide — Site Settings homepageSeo defaults
  3. Config fallbacksiteConfig.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/.