Skip to content

ADR-027: Content Collections Schema Design

Accepted (amended 2026-08-02: collection count updated — ADR-062 added a sixth collection, adr; the draft-support driver is scoped to blog and projects)

Astro’s Content Collections API provides type-safe, schema-validated content management. The starter ships with six collections pre-configured in src/content.config.ts (amended 2026-08-02: originally five — ADR-062 later added the adr collection, which publishes docs/adr/ as web routes and is documented there). These represent the most common content types for portfolio and small production sites — the primary audience for this template.

The schema design decisions are non-obvious and affect how users extend the template. Without documentation, users frequently ask: why these collections, why these fields, why JSON for some and MDX for others.

  • Cover the common case: A portfolio/small-site user should be able to ship without defining their own schemas
  • Type safety: All fields must be validated with Zod at build time
  • Extensibility: Schemas should be easy to extend without breaking existing content
  • Format appropriateness: MDX for rich content, JSON for structured data
  • Draft support: All content collections support a draft field to hide unpublished content (amended 2026-08-02: in practice only blog and projects carry draftbio, experience, navigation, and adr have no draft field)

Purpose: Blog posts, articles, tutorials

Format: MDX (.mdx) — rich content with embedded components

Key schema decisions:

FieldTypeRationale
titlestringRequired — used in <title>, OG tags, listing pages
descriptionstringRequired — used in meta description and post cards
datedateRequired — enables chronological sorting
updateddate (optional)Shows “last updated” for evergreen content
authorstring (default: “Your Name”)Defaults to placeholder — replace with your name
tagsstring[] (default: [])Enables tag-based filtering
technologiesstring[] (default: [])Tech stack used in post
coverimage (optional)Astro image type — enables build-time optimisation
coverAltstringRequired for accessibility when cover image is used
cardImageimage (optional)Separate thumbnail for listing cards
featuredboolean (default: false)Pins post to featured placement
draftboolean (default: false)Excludes from production builds when true
readingTimenumber (optional)Can be calculated automatically
canonicalUrlurl (optional)For cross-posted content
relatedPostsstring[] (optional)Slugs of related posts

Purpose: Portfolio case studies, project showcases

Format: MDX — projects benefit from rich narrative content with images

Key schema decisions:

FieldTypeRationale
titlestringRequired
descriptionstring (max 160)Required — used in project cards and meta description
datedateRequired — enables chronological ordering
coverimageRequired — project hero image
coverAltstringRequired for accessibility
tagsstring[]Required — enables tag-based filtering
technologiesstring[]Required — tech stack used
cardImageimage (optional)Separate thumbnail for listing cards
featuredboolean (default: false)Pins project to featured placement
draftboolean (default: false)Excludes from production builds when true
clientstring (optional)Client name for case studies
durationstring (optional)Project duration (e.g. “3 months”)
rolestring (optional)Your role on the project
outcomes{metric, value, description?}[] (optional)Measurable results
externalUrlurl (optional)Live project or case study link
sortOrdernumber (default: 0)Manual sort override

Purpose: Author/about information — name, contact, social links, and biography prose

Format: MDX (.mdx) — supports narrative bio content alongside structured data fields

Key schema decisions:

FieldTypeRationale
namestringRequired
titlestringJob title / professional descriptor
locationstring (optional)City/region — no more specific for privacy
avatarimageAstro image type for optimisation
socialobject (optional)Typed social link map (github, linkedin, twitter, email)
skills{category, items[]}[] (optional)Skill groups for about page display

Why a collection not a config file? Collections are type-safe and queryable via getEntry(). A plain config file would require a separate import pattern and has no Zod validation.

Purpose: Work history / CV entries

Format: MDX (.mdx) — supports rich descriptions alongside structured fields

Key schema decisions:

FieldTypeRationale
titlestringRequired — job title
companystringRequired
locationstring (optional)Office location
startDatedateRequired — enables chronological sort
endDatedate (optional)Null = current role
currentboolean (default: false)Flag for current position
descriptionstringBrief summary of responsibilities
highlightsstring[] (optional)Bullet-point achievements
technologiesstring[] (optional)Tech stack used
ordernumber (default: 0)Manual sort override

Purpose: Header navigation link list (amended 2026-08-13: the footer’s links are hardcoded in Footer.astro, not sourced from this collection)

Format: JSON — pure structured data; no content, no MDX needed

Why a collection not hardcoded in components? Separating navigation from component code means non-developers can update nav links without touching .astro files. It also enables type-safe validation of href values.

Key schema decisions:

FieldTypeRationale
labelstringLink label
hrefstringURL path or external URL
isExternalboolean (default: false)Adds target="_blank" and rel attributes
iconstring (optional)Icon component name for navigation items
ordernumber (default: 0)Controls display order
Use MDX when…Use JSON when…
Content has narrative proseContent is purely structured/tabular
Authors need to embed componentsData is queried programmatically
Content varies significantly between entriesAll entries share identical shape
Rich text formatting is neededSimple string/number/boolean fields only

To add a field to an existing collection:

  1. Add the Zod field to src/content.config.ts
  2. Make it optional with .optional() or provide a .default() to avoid breaking existing content files
  3. Run pnpm run check to validate
  4. Update existing content files if the field is required

To add a new collection:

  1. Define the schema in src/content.config.ts
  2. Create the directory src/content/<name>/
  3. Add at least one content file
  4. Run pnpm run check to confirm type generation
  • Users can ship a portfolio site without writing any schema code
  • All content is type-safe — typos in frontmatter are caught at build time
  • JSON collections are easily edited by non-developers
  • Consistent draft pattern across all collections (amended 2026-08-02: blog and projects only)
  • Five pre-configured collections may feel opinionated for users with different content models
  • JSON collections cannot contain rich text — users needing narrative bio content must switch to MDX
  • Schemas should be treated as the source of truth for content shape; component props should derive from collection types, not duplicate them

This ADR supersedes ADR-017 for the experience collection schema. ADR-017 provided the initial rationale for creating the collection; the schema details here are authoritative.

  • Testable consequences:
    • TC-1: every content entry validates against its Zod schema — the build fails otherwise.
  • Checks:
    • TC-1 → astro check + build in quality:ci (status: block, pre-existing gate)
  • Not machine-checkable: whether the schemas still cover the common case for new adopters.
  • Graduation log: (empty at creation; entries added when a check changes status)

Date: 2026-02-18
Participants: Template maintainers
Outcome: Accepted