Content
The blog, authors, and portfolio content collections — schemas, frontmatter, images, drafts, and a CMS pathway.
The blog, portfolio, and this documentation are all backed by Astro
content collections,
defined in src/content.config.ts. Four collections exist: docs, blog,
authors, and projects.
Blog
Each post lives in its own folder under src/content/blog/<slug>/, with an
index.md and (conventionally) a hero.svg or hero.jpg alongside it:
src/content/blog/my-post-slug/
├── index.md
└── hero.jpg
Frontmatter schema:
| Field | Type | Required | Notes |
|---|---|---|---|
title |
string | yes | |
description |
string | yes | Used in listings and page <meta description>. |
publishDate |
date | yes | Any format z.coerce.date() accepts, e.g. 2026-06-02. |
updatedDate |
date | no | |
author |
reference to authors |
yes | The author’s filename without extension, e.g. ava-stone. |
tags |
string array | no | Defaults to []. Powers /blog/tags/[tag]. |
heroImage |
image | yes | A path relative to the post’s own folder, e.g. ./hero.jpg. |
draft |
boolean | no | Defaults to false. See Drafts below. |
Body content is standard Markdown, rendered through the shared .prose
class (src/styles/global.css) — the same styling used for the docs pages
you’re reading now.
Authors
One JSON file per author under src/data/authors/:
{
"name": "Ava Stone",
"role": "Design Lead",
"bio": "One or two sentences."
}
The filename (minus .json) is the id a post’s author field references.
Each author automatically gets a profile page at /authors/<id> listing
their published posts.
Projects (portfolio)
One JSON file per project under src/data/projects/:
{
"title": "Ledger — expense tracking",
"eyebrow": "Web App",
"description": "A dashboard for freelancers to track invoices and expenses.",
"accent": "yellow",
"order": 1
}
accent must be one of the six theme accents (yellow, pink, blue,
green, orange, lavender). /portfolio reads this collection directly
and sorts by order.
Drafts
Set draft: true on a post to keep working on it without publishing it.
Drafts are visible in pnpm dev (so you can preview them) and excluded
everywhere in a production build — the listing, tag pages, author pages,
the RSS feed, and the sitemap. This is centralized in src/lib/blog.ts’s
getPublishedPosts(), which every blog route calls instead of
getCollection("blog") directly — if you add a new blog route, use that
helper too, or drafts will leak into it.
Empty collections
The site builds successfully with zero entries in any collection — the blog listing, portfolio, and RSS feed all render a plain “nothing here yet” message instead of an empty grid or crashing. Tag and author pages simply don’t generate any routes if there’s nothing to show.
Dates
Use src/lib/format-date.ts’s formatDate() for any date you display —
it forces UTC when formatting, which matters because a bare frontmatter
date like 2026-08-02 parses as UTC midnight, and formatting that in a
build machine’s local timezone can silently shift the displayed date
back a day. Every date shown on the site should go through this function.
Connecting a headless CMS
Nothing here is hard-wired to the filesystem. Astro’s content collections
support remote loaders
in addition to the local glob() loader used here — to move blog to a
CMS (Contentful, Sanity, a headless WordPress, a Git-based CMS like Keystatic
or Tina), replace that one collection’s loader in src/content.config.ts
with the CMS’s loader (or a small custom one that fetches and returns
matching shapes), while keeping the same schema. Every page in
src/pages/blog/ and src/pages/authors/ queries the collection through
getCollection()/getEntry() and would need no changes at all.