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:
| Route | Purpose |
|---|---|
/my-collection/ | Listing page — shows all entries |
/my-collection/my-slug | Detail 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:
npm run add-collectionThe script will:
-
Show the collections that already exist.
-
Ask for a collection name — lowercase letters, numbers, and hyphens only (e.g.
projects,press-releases). -
Ask for a collection type to pick a starting schema:
Type Frontmatter fields blogtitle,description,date,author,tags,image,draftdocstitle,description,order,category,draftgenerictitle,description,draft -
Create or update these files automatically:
File What changes usr/content/config.tsNew defineCollectionblock added; name registered in thecollectionsexportusr/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:
npm run add-contentThe script will:
- List all collections that already have a content directory.
- Ask which collection to add the file to (by name or number).
- Ask for the file format —
mdormdx(defaults to whichever format is already used in that collection). - Ask for a slug — this becomes the file name and the URL path. You can include subfolders (e.g.
2026/my-first-post→ saved asusr/content/blog/2026/my-first-post.mdand reachable at/blog/2026/my-first-post). - Prompt for each frontmatter field declared in the schema, with smart defaults:
datefields default to today’s dateauthordefaults to yourgit config user.namedraftdefaults totrue- 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-10author: "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
TableOfContentscore 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 propsconst 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:
- Update every existing content file to include (or remove) the changed field, or mark new fields as
.optional()so old files remain valid. - 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-10draft: 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.