Skip to content

Content Collections & Data

FieldValue
TypeAgent Reference
Source~/.copilot/agents/_refs/astro/content-and-data.md
DescriptionNot specified

Source Content

Content Collections & Data

Content Collections

Content Collections are the Astro-native way to manage structured content (MDX, Markdown, JSON, YAML). They are Zod-validated at build time.

src/content/config.ts
import { defineCollection, z } from 'astro:content'
const legalTemplatesCollection = defineCollection({
type: 'content', // MDX or Markdown files
schema: z.object({
title: z.string(),
documentType: z.enum(['motion', 'declaration', 'affidavit', 'agreement', 'petition']),
jurisdiction: z.string().default('virginia'),
tags: z.array(z.string()).default([]),
status: z.enum(['draft', 'review', 'stable']).default('draft'),
lastReviewed: z.string().datetime().optional(),
requiredFields: z.array(z.string()).default([]),
}),
})
const guidesCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
publishedAt: z.string().datetime(),
updatedAt: z.string().datetime().optional(),
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false),
}),
})
export const collections = {
'legal-templates': legalTemplatesCollection,
guides: guidesCollection,
}

Querying Collections

src/pages/templates/index.astro
---
import { getCollection, getEntry } from 'astro:content'
// Get all non-draft templates
const templates = await getCollection('legal-templates', ({ data }) =>
data.status !== 'draft'
)
// Sort by document type
const sorted = templates.sort((a, b) =>
a.data.documentType.localeCompare(b.data.documentType)
)
// Get a single entry
const motionTemplate = await getEntry('legal-templates', 'motion-to-compel')
if (!motionTemplate) {
return Astro.redirect('/templates')
}
---

Rendering MDX Content

---
import { getEntry, render } from 'astro:content'
const entry = await getEntry('legal-templates', Astro.params.slug)
if (!entry) return Astro.redirect('/templates')
const { Content, headings } = await render(entry)
---
<article>
<h1>{entry.data.title}</h1>
<!-- Content renders MDX with any configured plugins -->
<Content />
</article>

For legal documents that use the dmwd-io design system, use legalRemarkPlugins and legalRehypePlugins:

astro.config.ts
import { defineConfig } from 'astro/config'
import mdx from '@astrojs/mdx'
import { legalRemarkPlugins, legalRehypePlugins } from '@dmwd-io/design-system/mdx'
export default defineConfig({
integrations: [
mdx({
remarkPlugins: legalRemarkPlugins,
rehypePlugins: legalRehypePlugins,
}),
],
})

Never configure legalRemarkPlugins / legalRehypePlugins inline in a consuming build file — always import from @dmwd-io/design-system/mdx. Legal documents are available in MDX without imports via MDXLegalProvider.

Data Fetching Patterns

In Astro pages (server)

---
// Fetch directly — runs on the server, can use DB, env secrets, etc.
import { db } from '../lib/db'
const cases = await db.cases.findMany({
where: { userId: Astro.locals.user.id },
orderBy: { createdAt: 'desc' },
take: 20,
})
---

In React islands (client)

Islands that need dynamic data use TanStack Query against the API endpoints:

Components/CaseList.tsx
import { useQuery } from '@tanstack/react-query'
import { z } from 'zod'
const casesResponseSchema = z.array(caseSchema)
async function fetchCases(filters: CaseFilters) {
const params = new URLSearchParams(caseFiltersSchema.parse(filters) as Record<string, string>)
const res = await fetch(`/api/cases?${params}`)
if (!res.ok) throw new Error('Failed to fetch cases')
return casesResponseSchema.parse(await res.json())
}
export function useCases(filters: CaseFilters) {
return useQuery({
queryKey: ['cases', filters],
queryFn: () => fetchCases(filters),
staleTime: 60_000,
})
}

Static Paths for Dynamic Routes

src/pages/cases/[id].astro
---
import type { GetStaticPaths } from 'astro'
import { db } from '../../lib/db'
// Only needed for SSG (output: 'static')
// For SSR (output: 'server'), remove this and use Astro.params directly
export const getStaticPaths: GetStaticPaths = async () => {
const cases = await db.cases.findMany({ select: { id: true } })
return cases.map((c) => ({ params: { id: c.id } }))
}
const { id } = Astro.params
const case_ = await db.cases.findUnique({ where: { id } })
---

For SSR (output: 'server'), remove getStaticPaths entirely — every route is server-rendered on demand.

Rendering Modes

ModeConfigWhen
Static (SSG)output: 'static'Content that doesn’t change per user: landing pages, docs, templates
Server (SSR)output: 'server'Authenticated, personalized, dynamic pages
Hybridoutput: 'hybrid'Mostly static with a few dynamic routes
astro.config.ts
import { defineConfig } from 'astro/config'
import node from '@astrojs/node'
export default defineConfig({
output: 'server', // or 'hybrid' or 'static'
adapter: node({ mode: 'standalone' }),
})

For Hybrid mode, opt individual pages out of SSR:

---
// This page is pre-rendered statically
export const prerender = true
---

Deployment (Docker + k8s)

For SSR mode with the Node adapter, the build output is a Node.js server:

FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json .
ENV HOST=0.0.0.0
ENV PORT=4321
EXPOSE 4321
CMD ["node", "./dist/server/entry.mjs"]

Hand off to Platform SRE for Kubernetes for the k8s deployment spec.