Content Collections & Data
| Field | Value |
|---|---|
| Type | Agent Reference |
| Source | ~/.copilot/agents/_refs/astro/content-and-data.md |
| Description | Not 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.
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
---import { getCollection, getEntry } from 'astro:content'
// Get all non-draft templatesconst templates = await getCollection('legal-templates', ({ data }) => data.status !== 'draft')
// Sort by document typeconst sorted = templates.sort((a, b) => a.data.documentType.localeCompare(b.data.documentType))
// Get a single entryconst 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>Legal MDX Layer
For legal documents that use the dmwd-io design system, use legalRemarkPlugins and legalRehypePlugins:
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:
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
---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 directlyexport const getStaticPaths: GetStaticPaths = async () => { const cases = await db.cases.findMany({ select: { id: true } }) return cases.map((c) => ({ params: { id: c.id } }))}
const { id } = Astro.paramsconst case_ = await db.cases.findUnique({ where: { id } })---For SSR (output: 'server'), remove getStaticPaths entirely — every route is server-rendered on demand.
Rendering Modes
| Mode | Config | When |
|---|---|---|
| Static (SSG) | output: 'static' | Content that doesn’t change per user: landing pages, docs, templates |
| Server (SSR) | output: 'server' | Authenticated, personalized, dynamic pages |
| Hybrid | output: 'hybrid' | Mostly static with a few dynamic routes |
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 staticallyexport 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 builderWORKDIR /appCOPY package.json pnpm-lock.yaml ./RUN corepack enable && pnpm install --frozen-lockfileCOPY . .RUN pnpm build
FROM node:20-alpine AS runnerWORKDIR /appCOPY --from=builder /app/dist ./distCOPY --from=builder /app/package.json .ENV HOST=0.0.0.0ENV PORT=4321EXPOSE 4321CMD ["node", "./dist/server/entry.mjs"]Hand off to Platform SRE for Kubernetes for the k8s deployment spec.