Adding a Collection with Detail Pages
Collections store repeatable content. Each entry gets its own URL. This guide covers creating a collection and wiring listing + detail pages.
When to Use a Collection
Use a collection when:
- You need multiple entries of the same type (blog posts, team members, FAQs)
- Each entry needs its own URL
- Editors need to add/remove entries freely
Step 1: Create the Collection Schema
In packages/keystatic-config/src/collections/, create a file like 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' }),
category: fields.text({ label: 'Category' }),
},
});
Step 2: Register in index.ts
import { faqs } from './collections/faqs';
export const collections = {
// ...existing collections
faqs,
};
Step 3: Create Content
Each FAQ entry lives in content/faqs/{slug}/answer.mdoc with a content/faqs/{slug}.yaml frontmatter.
Step 4: Build the Listing Page
Create apps/www/src/pages/faqs/index.astro:
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import { reader } from '../../lib/reader';
const faqs = await reader.collections.faqs.all();
---
<BaseLayout seo={{ title: 'FAQs' }}>
<section class="mx-auto max-w-3xl px-4 py-16">
<h1 class="text-4xl font-bold mb-8">Frequently Asked Questions</h1>
<ul>
{faqs.map(faq => (
<li><a href={`/faqs/${faq.slug}`}>{faq.entry.title}</a></li>
))}
</ul>
</section>
</BaseLayout>
Step 5: Build the Detail Page
Create apps/www/src/pages/faqs/[slug].astro:
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import { reader } from '../../lib/reader';
import Markdoc from '@markdoc/markdoc';
export async function getStaticPaths() {
const faqs = await reader.collections.faqs.all();
return faqs.map(faq => ({ params: { slug: faq.slug } }));
}
const { slug } = Astro.params;
const entry = await reader.collections.faqs.read(slug);
const body = await entry?.answer();
const content = body ? Markdoc.renderers.html(Markdoc.transform(body.node, { nodes: Markdoc.nodes })) : '';
---
<BaseLayout>
<article class="mx-auto max-w-3xl px-4 py-16 prose" set:html={content} />
</BaseLayout>
For SSG builds, getStaticPaths ensures all entries are pre-rendered.