From harness-claude
Provides type-safe, schema-validated content management in Astro for Markdown, MDX, YAML, and JSON files—no CMS needed. Validates frontmatter with Zod schemas, generates TS types, enables querying/filtering/sorting, and renders with <Content />.
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeThis skill uses the workspace's default tool permissions.
> Type-safe, schema-validated content management built into Astro — no CMS required for structured Markdown, MDX, and data files.
Provides setup and troubleshooting for Content Collections, transforming Markdown/MDX into type-safe TypeScript data with Zod schemas in Vite+React apps for blogs and docs sites.
Builds content-focused websites with Astro's zero-JS islands architecture, multi-framework components (React/Vue/Svelte), and Markdown/MDX support. Triggers on .astro files, Astro.props, content collections.
Guides Nuxt Content v3 for markdown/CMS features: collections (local/remote/API), queryCollection API, MDC rendering, database config (SQLite/PostgreSQL/D1/LibSQL), hooks, i18n, NuxtStudio, LLMs.
Share bugs, ideas, or general feedback.
Type-safe, schema-validated content management built into Astro — no CMS required for structured Markdown, MDX, and data files.
src/content/<Content /> and get full type safety on the rendered entryCreate a collection config at src/content/config.ts. This is the single source of truth for all collections.
Define each collection with defineCollection() and a Zod schema. Every frontmatter field should be declared:
// src/content/config.ts
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({
type: 'content', // 'content' for .md/.mdx, 'data' for .json/.yaml
schema: z.object({
title: z.string(),
description: z.string().max(160),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
heroImage: z.string().optional(),
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false),
}),
});
const authors = defineCollection({
type: 'data',
schema: z.object({
name: z.string(),
email: z.string().email(),
avatar: z.string().url(),
}),
});
export const collections = { blog, authors };
Place content files under src/content/<collection-name>/. The directory name must match the collection key exported from config.ts.
Use getCollection() to fetch all entries. Apply filters inline — do not load all entries and filter in the template:
// src/pages/blog/index.astro
---
import { getCollection } from 'astro:content';
// Exclude drafts in production
const posts = await getCollection('blog', ({ data }) => {
return import.meta.env.PROD ? !data.draft : true;
});
// Sort by date descending
const sorted = posts.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
---
getEntry() to fetch a single entry by collection name and slug:const post = await getEntry('blog', 'my-first-post');
if (!post) return Astro.redirect('/404');
const { Content } = await post.render();
<Content /> component returned from entry.render():---
const { Content, headings, remarkPluginFrontmatter } = await post.render();
---
<article>
<Content />
</article>
Use z.coerce.date() for date fields in frontmatter — raw frontmatter dates are strings, and coercion converts them to Date objects automatically.
Reference images in frontmatter using the image() helper for build-time optimization integration:
import { defineCollection, z, reference } from 'astro:content';
schema: ({ image }) =>
z.object({
cover: image(), // local image, processed by astro:assets
author: reference('authors'), // type-safe cross-collection reference
});
Content Collections were introduced in Astro 2.0 to solve the problem of unstructured, unvalidated frontmatter. Without collections, a typo in a date field or a missing required property would either silently pass or cause a runtime error deep in your template. With collections, Astro validates every entry at build time using Zod and surfaces clear errors.
The type distinction:
type: 'content' — for .md and .mdx files. Entries have body (raw Markdown string), render() method, and slug (auto-derived from filename).type: 'data' — for .json and .yaml files. Entries have only data (the parsed object). No render() or slug.Generated types:
Astro generates .astro/types.d.ts from your collection config. This gives you full IntelliSense on entry.data.title, entry.data.pubDate, etc., without manual type declarations.
Slugs and IDs:
entry.slug — the path relative to the collection directory, without the extension. For src/content/blog/2024/my-post.md, the slug is 2024/my-post.entry.id — same as slug but with the extension included. Primarily used for data collections.Cross-collection references:
Use reference('collectionName') in a Zod schema to create typed cross-collection references. Astro validates that the referenced entry exists at build time. Resolve references with getEntry(entry.data.author).
Content Layer API (Astro 5+):
Astro 5 introduced the Content Layer API, which extends collections to support external data sources (remote APIs, databases, CMSes). Use loader in defineCollection():
const products = defineCollection({
loader: async () => {
const res = await fetch('https://api.example.com/products');
return res.json(); // array of objects with an `id` field
},
schema: z.object({ id: z.string(), name: z.string(), price: z.number() }),
});
Performance: getCollection() reads from the file system at build time (SSG) or at request time (SSR). In SSG, results are used to generate static pages via getStaticPaths(). In SSR, consider caching results if collections are large.
https://docs.astro.build/en/guides/content-collections