Contentlayer3 brings your content (be it local files or remote content via APIs) into Next.js as fully typed, Zod-validated data, fetched at request time instead of baked in at build.
Note
The npm library is coming soon. This repository is a work in progress.
The original Contentlayer is unmaintained. Velite and content-collections process content only at build time, requiring a full rebuild to pick up content changes.
Contentlayer3 runs at request time with Next.js ISR and revalidateTag, giving you:
- Runtime-first: Fetch and validate content on every request (with intelligent caching)
- Edge-safe remote sources: The
@contentlayer3/source-remotepackage is compatible with Cloudflare Workers and Vercel Edge Functions; the local filesystem source requires Node.js - Zod-only: Single schema system, no competing frameworks
- Computed fields: Derive slugs, URLs, reading time, and more at definition time
- Collection references: Link collections together with type-safe
reference()fields - Remote sources: Pull content from any HTTP API
- Postman governance: Keep remote source schemas in sync with Postman collections
- Search plugins: Orama and Pagefind integration out-of-the-box
- Actively maintained: New phases ship regularly
npm add contentlayer3 zodimport { defineCollection } from "contentlayer3";
import { filesystem } from "contentlayer3/source-files";
import { z } from "zod";
export const posts = defineCollection({
name: "posts",
source: filesystem({
contentDir: "content/posts",
pattern: "**/*.mdx",
}),
schema: z.object({
title: z.string(),
date: z.string(),
excerpt: z.string(),
_filePath: z.string().optional(),
}),
computedFields: {
slug: (post) =>
post._filePath
?.replace(/\.mdx?$/, "")
.split("/")
.pop() ?? "",
url: (post) =>
`/posts/${post._filePath
?.replace(/\.mdx?$/, "")
.split("/")
.pop()}`,
},
});Create content/posts/hello.mdx:
---
title: Hello World
date: 2025-01-01
excerpt: My first post
---
This is my first post!import { getCollection } from 'contentlayer3'
import { posts } from '../contentlayer3.config'
export default async function Blog() {
const allPosts = await getCollection(posts)
return (
<main>
<h1>Blog</h1>
<ul>
{allPosts.map(post => (
<li key={post._filePath}>{post.title}</li>
))}
</ul>
</main>
)
}Create app/api/revalidate/route.ts:
import { revalidateCollection } from "contentlayer3";
import { posts } from "../../../contentlayer3.config";
export async function POST(request: Request) {
const token = request.headers.get("x-revalidate-token");
if (token !== process.env.REVALIDATE_TOKEN) {
return new Response("Unauthorized", { status: 401 });
}
revalidateCollection(posts.name);
return new Response("Revalidated", { status: 200 });
}| Package | Purpose | Edge-Safe† |
|---|---|---|
contentlayer3 |
Collection definition, validation, in-memory cache, Next.js integration, MDX, and filesystem source | 🟢 (core + remote) |
@contentlayer3/source-remote |
HTTP remote content source with offset/cursor pagination | 🟢 |
@contentlayer3/search-orama |
Full-text search with Orama v3 | 🟢 |
@contentlayer3/search-pagefind |
Pagefind manifest generation for static search | 🔴 |
@contentlayer3/devtools |
CLI tools: validate, inspect, watch |
— |
@contentlayer3/postman |
Postman governance CLI: sync remote source schemas with Postman collections | — |
@contentlayer3/graphql |
GraphQL API plugin: expose collections via a type-safe GraphQL endpoint | 🟢 |
@contentlayer3/mcp |
MCP server for AI-assisted governance: query collections, diff Postman specs, validate schemas | — |
† Contentlayer3 can run in edge runtimes such as Cloudflare Workers, Vercel Edge Functions, and similar environments that are not full Node.js.
| Import | Contents |
|---|---|
contentlayer3 |
Core engine, Next.js adapter, revalidation |
contentlayer3/source-files |
Filesystem source (md, mdx, json, yaml) |
contentlayer3/mdx |
MDX compilation to function-body JSX |
| Feature | contentlayer3 | Velite | content-collections | contentlayer2 |
|---|---|---|---|---|
| Runtime-first | 🟢 | 🔴 | 🔴 | 🔴 |
| Zod schemas | 🟢 | 🟢 | 🟢 | 🔴 |
| revalidateTag integration | 🟢 | 🔴 | 🔴 | 🔴 |
| Turbopack compatible | 🟢 | 🟡1 | 🟢 | 🟡1 |
| Computed fields | 🟢 | 🟢 | 🟢 | 🟢 |
| Collection references | 🟢 | 🔴 | 🔴 | 🟢 |
| Remote sources | 🟢 | 🔴 | 🔴 | 🔴 |
| Edge-safe (remote source) | 🟢 | 🔴 | 🔴 | 🔴 |
| Search hooks | 🟢 | 🔴 | 🔴 | 🔴 |
| Postman governance | 🟢 | 🔴 | 🔴 | 🔴 |
| GraphQL API | 🟢 | 🔴 | 🔴 | 🟢 |
| Build-time/static output | 🔴2 | 🟢 | 🟢 | 🟢 |
| Actively maintained | 🟢 | 🟢 | 🟢 | 🟢 |
1 Partial support. Build-step and webpack plugin dependencies cause known issues with Turbopack. Contentlayer3 has no build-time dependency, making Turbopack compatibility a non-issue.
2 By design. Contentlayer3 fetches content at request time, sidestepping bundler dependency.
| Rendering mode | Supported | Notes |
|---|---|---|
| App Router SSR (dynamic) | 🟢 | getCollection() uses unstable_cache with a configurable TTL |
| App Router ISR | 🟢 | revalidateCollection() triggers tag-based cache invalidation |
App Router SSG (generateStaticParams) |
🔴 | No build-time static generation helper; content loads at runtime |
Pages Router SSR (getServerSideProps) |
🟢 | Use getCollectionPages() in getServerSideProps |
Pages Router SSG (getStaticProps, no revalidate) |
🟢 | Use getCollectionPages() in getStaticProps |
Pages Router ISR (getStaticProps + revalidate) |
🟢 | Uses in-memory cache; revalidation handled by Next.js ISR interval |
Static export (output: 'export') |
🔴 | Requires a server runtime; static export has no runtime |
| Edge runtime (filesystem source) | 🔴 | node:fs is unavailable in edge runtimes |
Edge runtime (@contentlayer3/source-remote) |
🟢 | Remote source has no Node.js dependencies |
| Partial Prerendering (PPR) | 🔴 | Not yet implemented |
Derive values from validated items at definition time. Computed fields are applied after Zod validation, before caching, so they're always present when you call getCollection.
import { defineCollection } from "contentlayer3";
import { filesystem } from "contentlayer3/source-files";
import { z } from "zod";
export const posts = defineCollection({
name: "posts",
source: filesystem({ contentDir: "content/posts", pattern: "**/*.mdx" }),
schema: z.object({
title: z.string(),
date: z.string(),
_filePath: z.string(),
}),
computedFields: {
slug: (post) =>
post._filePath
.replace(/\.mdx?$/, "")
.split("/")
.pop(),
url: (post) =>
`/posts/${post._filePath
.replace(/\.mdx?$/, "")
.split("/")
.pop()}`,
// async fields are awaited automatically
readingTime: async (post) => estimateReadingTime(post._filePath),
},
});Link collections together with reference(). The field stores the ID at rest; use resolveReference or resolveReferences to hydrate when needed.
import { defineCollection, reference, resolveReference } from "contentlayer3";
import { z } from "zod";
export const authors = defineCollection({
name: "authors",
source: filesystem({ contentDir: "content/authors", pattern: "**/*.md" }),
schema: z.object({ name: z.string(), slug: z.string() }),
});
export const posts = defineCollection({
name: "posts",
source: filesystem({ contentDir: "content/posts", pattern: "**/*.mdx" }),
schema: z.object({
title: z.string(),
author: reference(authors), // stored as slug string
}),
});
// In your page:
const post = await getCollectionItem(posts, (p) => p.slug === params.slug);
const author = await resolveReference(authors, post.author);resolveReference matches on slug, id, or _filePath. Use resolveReferences(collection, ids[]) for array fields like tags or coAuthors.
@contentlayer3/postman keeps your remote source schemas in sync with Postman collections via a lock-file governance workflow.
npm add -D @contentlayer3/postman
export POSTMAN_API_KEY=your-key
contentlayer3-postman init # first-time setup
contentlayer3-postman pull <name> # fetch latest spec from Postman
contentlayer3-postman apply <name> # promote pulled spec, regenerate schema
contentlayer3-postman sync # CI drift check (exits non-zero on drift)See Postman Governance for the full command reference.
@contentlayer3/graphql exposes your collections as a type-safe GraphQL endpoint. Zod schemas are automatically converted to GraphQL types.
npm add @contentlayer3/graphql// app/api/graphql/route.ts
import { withCollections } from "@contentlayer3/graphql";
import { getCollection } from "contentlayer3";
import { posts } from "../../../contentlayer3.config";
export const { GET, POST } = withCollections([
{ name: "posts", schema: posts.schema, getItems: () => getCollection(posts) },
]);Generate a schema.graphql SDL file:
contentlayer3-graphql generateSee GraphQL Plugin for full documentation.
All governance CLIs support a --json flag for scripting and CI integration:
contentlayer3-postman status --json # workspace + per-source sync state
contentlayer3-postman discover --json # all sources with governance status
contentlayer3-postman pull <name> --json # diff vs Postman spec
contentlayer3-postman sync --json # drift check with structured output
contentlayer3 validate --json # validation results per collection
contentlayer3 inspect --json # schema fields per collection
contentlayer3-graphql validate --json # GraphQL schema validation errorsExit codes are preserved in --json mode so CI pipelines can use both structured output and error detection.
@contentlayer3/mcp exposes contentlayer3 governance as an MCP server, enabling AI assistants (Claude, Cursor, etc.) to query and triage your content layer using natural language.
npm add -D @contentlayer3/mcpConfigure in your MCP client (e.g. claude_desktop_config.json):
{
"mcpServers": {
"contentlayer3": {
"command": "contentlayer3-mcp"
}
}
}| Tool | What it does |
|---|---|
get-collection |
Load a collection by name from contentlayer3.mcp.json |
validate-collection |
Validate all items against the collection schema |
get-schema |
Return field names and types for a collection |
postman-status |
Read sync state from contentlayer3.lock |
postman-diff |
Fetch the latest diff for a governed collection |
graphql-validate |
Validate the GraphQL schema from contentlayer3.graphql.json |
Create a contentlayer3.mcp.json sidecar in your project root to configure collections:
{
"collections": [
{
"name": "posts",
"fields": {
"title": "string",
"date": "string",
"excerpt": { "type": "string", "optional": true }
}
}
]
}- Core / Next.js: collection definition, validation, in-memory cache, Next.js integration, MDX, and filesystem source
Docs for optional add-ons:
- Remote Source: HTTP remote content source with offset/cursor pagination
- Orama Search: full-text search with Orama v3
- Pagefind Search: Pagefind manifest generation for static search
- Developer Tools: CLI tools for
validate,inspect, andwatch - Postman Governance: sync remote source schemas with Postman collections
- GraphQL Plugin: expose collections via a type-safe GraphQL endpoint
- MCP Server: AI-assisted governance, query collections, diff Postman specs, validate schemas
- Migration Codemod: CLI to migrate existing Contentlayer v1/v2 projects to Contentlayer3
Every document loaded from the filesystem source receives these fields automatically, no schema declaration needed:
| Field | Type | Description |
|---|---|---|
_filePath |
string |
Relative path from project root e.g. content/posts/hello.mdx |
_content |
string |
Raw body text with frontmatter stripped |
body.raw |
string |
Same as _content, provided for v1/v2 compatibility |
_raw.sourceFilePath |
string |
Same as _filePath |
_raw.sourceFileName |
string |
Basename e.g. hello.mdx |
_raw.sourceFileDir |
string |
Directory portion e.g. content/posts |
_raw.flattenedPath |
string |
Path without extension e.g. content/posts/hello |
_raw.contentType |
string |
"md", "mdx", or "data" |
The _raw and body.raw shapes are intentionally compatible with Contentlayer v1/v2 so existing code continues to work without changes.
| v1/v2 | Contentlayer3 |
|---|---|
doc._id |
doc._filePath |
doc._raw.flattenedPath |
doc._raw.flattenedPath (same) |
doc._raw.sourceFilePath |
doc._raw.sourceFilePath (same) |
doc.body.raw |
doc.body.raw (same), also doc._content |
doc.body.code |
(await compileMDX(doc._content)).code |
defineDocumentType(() => ({ ... })) |
defineCollection({ ... }) |
computedFields: { slug: { type: 'string', resolve: (doc) => ... } } |
computedFields: { slug: (doc) => ... } |
Use the contentlayer3-migrate codemod to automate the majority of the migration:
npx contentlayer3-migrate check # preview changes
npx contentlayer3-migrate run # apply transforms and generate migration-report.mdSee the Migration Codemod docs for the full list of transforms, field type mappings, and what requires manual review.
MIT