Content Page Procedure
Step-by-step procedure for creating content-driven pages using Astro Content Collections (Content Layer API) with dynamic routing.
Prerequisites
src/styles/global.cssexists with design tokens (created by css-procedure)docs/stitch/reference.mdexists (field information from Stitch screens)
Input / Output
[Input] [Output]
docs/stitch/reference.md ──┐
(Screen field info) │
progress.md notes ──────────┼──→ src/content/config.ts (schema)
(Section structure, │ src/content/<collection>/<slug>/index.mdx (data)
design decisions) │ src/pages/<collection>/[...slug].astro (routing)
│ src/components/*.astro (section components)
Steps
Step 1: Schema Definition (src/content/config.ts)
Astro 5 Content Layer API uses the glob loader.
1-1. Define Collection
import { defineCollection, z } from "astro:content";
import { glob } from "astro/loaders";
const myCollection = defineCollection({
loader: glob({ pattern: "**/index.mdx", base: "./src/content/my-collection" }),
schema: ({ image }) =>
z.object({
title: z.string(),
// ... define fields
heroImage: image(), // Use image() helper for images
}),
});
export const collections = { myCollection };
1-2. Schema Design Guidelines
- Start minimal, extend incrementally. Define only fields needed for current page rendering; add more when implementing components
- Use
image()helper for image fields to enable build-time path validation and optimization - Use
.optional()for fields not required in every entry - Use optional arrays for section-level data to control section visibility per page:
// Schema moments: z.array(z.object({ title: z.string(), ... })).optional()<!-- Page template: conditional rendering --> {data.moments && <MomentsSection moments={data.moments} />}
1-3. YAML Frontmatter Pitfalls
- Month names (March, May, etc.) and Yes/No are auto-converted by YAML. Quote them if they should be strings:
date: "March" - Image paths are relative to the MDX file:
heroImage: ./hero.jpg
Step 2: Create Content Data
2-1. Directory Structure
src/content/<collection>/<slug>/
├── index.mdx # frontmatter + MDX body
├── hero.jpg # Images co-located with content
└── ...
2-2. MDX File Structure
---
title: "Page Title"
heroImage: ./hero.jpg
description: "Description text"
---
## MDX Body
Body content is retrieved via `render()` and rendered as `<Content />`.
2-3. Image Management
- Store images inside
src/content/co-located with their content (not inpublic/) - Resize images larger than 2560px before committing
- When using
image()helper in schema, frontmatter image paths resolve toImageMetadatatype. UseImageMetadatain component Props as well
Step 3: Dynamic Routing Page
Create src/pages/<collection>/[...slug].astro:
---
import type { GetStaticPaths } from "astro";
import { getCollection, render } from "astro:content";
export const getStaticPaths = (async () => {
const items = await getCollection("myCollection");
return items.map((item) => ({
params: { slug: item.id },
props: { item },
}));
}) satisfies GetStaticPaths;
const { item } = Astro.props;
const { Content } = await render(item);
---
Key Points
getCollection()fetches all entries;getStaticPathsgenerates static pathsrender()returns MDX body as aContentcomponentitem.dataprovides type-safe access to frontmatter fields- Conditional rendering for optional fields:
{data.field && <Component ... />}
Step 4: Page Data Files (src/data/<page>.md)
User-editable content (text, labels, URLs) for each page should be defined in Markdown files with YAML frontmatter under src/data/. This keeps the format consistent with Content Collections and makes it easy for users to find and edit their content.
Note: The
src/data/directory is not created by the scaffold skill. Create it in this step.
4-1. Create a data file for each page
src/data/
├── index.md # Works (top) page content
├── about.md # About Me page content
└── skills.md # Skills page content
4-2. Define editable content as YAML frontmatter
---
# ==============================
# Works Page Content
# Edit this file to customize your page.
# ==============================
hero:
headingPrefix: "Hi, I'm a"
highlightedText: "Web Developer."
description: "I build scalable, user-centric applications..."
badgeText: "Available for new projects"
cta:
heading: "Ready to build something together?"
description: "I'm currently open to freelance opportunities..."
buttons:
- label: "Get in Touch"
href: "mailto:[email protected]"
variant: "primary"
- label: "Download CV"
href: "#"
variant: "secondary"
---
- Put only user-editable text in the YAML (strings, labels, URLs, arrays of text)
- Keep non-editable data (SVG icon paths, etc.) as TypeScript constants in the
.astropage file
4-3. Import data in the page
Use import.meta.glob() to load the frontmatter:
---
const content = Object.values(
import.meta.glob("../data/index.md", { eager: true })
)[0] as { frontmatter: Record<string, unknown> };
const { hero, cta } = content.frontmatter;
---
Step 5: Section Component Implementation
Create each section (hero, moments, etc.) as an independent component in src/components/.
Follow develop-guideline for component structure and CSS rules.
Image Display: <Picture> vs <img>
| Method | When to use | Example |
|---|---|---|
<Picture> | Static images known at build time | Hero backgrounds, entry images |
<img> | Images dynamically swapped by client JS | Modal gallery slideshows |
<Picture> renders at build time, so it cannot be used when src is swapped by client JS.
Client-Side JavaScript
<script> tags in Astro components run as client JS. Interactive features can be built without frameworks (React, etc.).
Data passing pattern: Use data-* attributes to pass frontmatter data to client JS. <script> cannot directly reference frontmatter variables.
Accessibility: Modal / Overlay
Checklist for modal dialog implementation:
| Requirement | Implementation |
|---|---|
| Semantics | role="dialog" + aria-modal="true" + aria-label |
| Open/close | Toggle hidden attribute + CSS opacity transition |
| Scroll lock | Open: document.body.style.overflow = "hidden" / Close: restore to "" |
| Keyboard | Escape to close, arrow keys for navigation |
| Focus trap | Tab/Shift+Tab cycles through buttons within modal |
| Focus restore | focus() trigger element on close |
| Backdrop close | Close when click target is not an interactive element |
| z-index | Higher than existing fixed elements (e.g., header 50 → modal 60) |
Step 6: Build Verification
npm run build
Content Collections schema validation errors surface at build time with clear messages.
Common Errors
| Error | Cause | Fix |
|---|---|---|
Expected type "string", received "object" | YAML auto-converted a value (month name → Date, etc.) | Quote the value |
Could not find image | Incorrect image path | Verify relative path from MDX file |
does not match collection schema | Missing field or type mismatch in frontmatter | Check against config.ts schema |
Step 7: Dev Server Verification
npm run dev
- Schema changes may require dev server restart (content store clearing)
- If errors persist after restart, delete
.astro/directory and restart
Checklist
-
src/content/config.tshas schema defined - MDX frontmatter conforms to schema
- Images are inside
src/content/and resized to ≤2560px -
[...slug].astroreturns all entries fromgetStaticPaths - Optional field sections use conditional rendering
-
npm run buildsucceeds - Visual confirmation in browser for all sections (including mobile width)