This guide explains how content is organised in s:CMS, how to scaffold new collections with a single command, and how to customise the pages that render them.


What is a collection?

A content collection is a folder of Markdown (.md) or MDX (.mdx) files that share the same shape — the same set of frontmatter fields. For example, a blog collection might contain one file per article, each with a title, a date, and a draft flag.

Collections are registered in usr/content/config.ts, where a Zod schema declares exactly which frontmatter fields each collection expects and what type they must be. Astro validates every file against this schema at build time, so typos or missing required fields are caught before the site is published.

Each collection also gets two routes:

RoutePurpose
/my-collection/Listing page — shows all entries
/my-collection/my-slugDetail page — renders a single entry

These pages live in usr/pages/my-collection/ and are plain Astro files you can edit freely.


Adding a new collection

Run the interactive scaffolding script:

Terminal window
npm run add-collection

The script will:

  1. Show the collections that already exist.

  2. Ask for a collection name — lowercase letters, numbers, and hyphens only (e.g. projects, press-releases).

  3. Ask for a collection type to pick a starting schema:

    TypeFrontmatter fields
    blogtitle, description, date, author, tags, image, draft
    docstitle, description, order, category, draft
    generictitle, description, draft
  4. Create or update these files automatically:

    FileWhat changes
    usr/content/config.tsNew defineCollection block added; name registered in the collections export
    usr/content/<name>/sample-*.mdA ready-to-edit sample file with pre-filled frontmatter
    usr/pages/<name>/index.astroListing page for all entries in the collection
    usr/pages/<name>/[...slug].astroDetail page for individual entries

After scaffolding, open usr/content/config.ts and look for the TODO comment inside the new block — that is where you adjust the schema fields to match your actual data needs.


Adding a content file to an existing collection

Run:

Terminal window
npm run add-content

The script will:

  1. List all collections that already have a content directory.
  2. Ask which collection to add the file to (by name or number).
  3. Ask for the file formatmd or mdx (defaults to whichever format is already used in that collection).
  4. Ask for a slug — this becomes the file name and the URL path. You can include subfolders (e.g. 2026/my-first-post → saved as usr/content/blog/2026/my-first-post.md and reachable at /blog/2026/my-first-post).
  5. Prompt for each frontmatter field declared in the schema, with smart defaults:
    • date fields default to today’s date
    • author defaults to your git config user.name
    • draft defaults to true
    • Optional fields can be skipped by pressing Enter

The generated file looks like this:

---
title: "My First Post"
description: "A short description."
date: 2026-03-10
author: "Jane Smith"
tags: ["news"]
draft: true
---
# My First Post
<!-- Write your content here -->

Open the file, write your content, and set draft: false when the entry is ready to publish. The development server reloads automatically when you save.


Customising the listing and detail pages

The pages generated by npm run add-collection are intentionally minimal starting points. They live in usr/pages/<collection-name>/ and are regular Astro components — edit them freely.

Listing page — index.astro

This page fetches all entries and renders a list or grid. Common things to customise:

  • Filter out drafts: the default already does .filter(e => !e.data.draft), but you can add more conditions (e.g. filter by tag or category).
  • Sort order: change .sort(...) to order by any frontmatter field.
  • Layout: swap the Bootstrap card grid for a table, a timeline, or any HTML structure you like.
  • Pagination: add a simple slice or use a library if the collection is large.

Example — keeping only entries tagged "featured":

const entries = (await getCollection('projects'))
.filter(e => !e.data.draft && e.data.tags?.includes('featured'))
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());

Detail page — [...slug].astro

This page receives a single entry as a prop, renders its body with <Content />, and displays the frontmatter fields around it. Common things to customise:

  • Add a sidebar with a table of contents (use the TableOfContents core component).
  • Show related entries by filtering the collection on a shared tag.
  • Display an image from entry.data.image.
  • Add previous / next navigation by sorting the collection and finding the adjacent entries.

Example — adding a next/previous navigation bar:

---
// inside getStaticPaths, pass neighbours as props
const sorted = entries.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
return sorted.map((entry, i) => ({
params: { slug: entry.id.replace(/\.mdx?$/, '') },
props: { entry, prev: sorted[i + 1] ?? null, next: sorted[i - 1] ?? null },
}));
---
{prev && <a href={`/blog/${prev.id.replace(/\.mdx?$/, '')}`}>← {prev.data.title}</a>}
{next && <a href={`/blog/${next.id.replace(/\.mdx?$/, '')}`}>{next.data.title} →</a>}

Changing the layout

Both pages import BaseLayout by default. Replace it with any layout from usr/layouts/ or create a new one that extends BaseLayout:

---
import BlogLayout from '../../layouts/BlogLayout.astro';
---
<BlogLayout title={entry.data.title}>
<Content />
</BlogLayout>

Editing the schema after creation

You can add, rename, or remove fields in usr/content/config.ts at any time. Remember to:

  1. Update every existing content file to include (or remove) the changed field, or mark new fields as .optional() so old files remain valid.
  2. Update the listing and detail page templates to display the new field.

Using MDX for richer content

If you choose the .mdx format, content files can import and render any component:

---
title: "My Interactive Post"
date: 2026-03-10
draft: false
---
import { DataTb } from '@core';
# My Interactive Post
Here is a live data table:
<DataTb
source={{ type: 'csv', url: '/data/results.csv' }}
client:idle
/>

All core components (maps, galleries, data tables, etc.) are importable from @core. See the components section of the documentation for the full list.