← Back to Blog
development

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.