Your Markdown, as typed TypeScript modules.
Quick look · Why switch · Install · Frameworks · Migrating · FAQ
contentmap is a type-safe content layer for JavaScript and TypeScript. It reads Markdown, MDX, YAML, JSON, TOML and remote APIs, validates every document against a schema you write, and emits typed modules your app imports directly.
It replaces Contentlayer (unmaintained), Velite and Content Collections — and npx @contentmap/migrate converts your config from any of them.
You write a schema. Anything implementing Standard Schema works — zod, valibot, arktype, effect.
// contentmap.config.ts
import { defineCollection, defineConfig } from 'contentmap'
import { z } from 'zod'
const posts = defineCollection({
directory: 'content/posts',
include: '**/*.md',
schema: z.object({
title: z.string(),
date: z.coerce.date(),
draft: z.boolean().default(false),
content: z.string()
}),
transform: async (doc, ctx) => ({
...doc,
slug: ctx.meta.slug,
html: await ctx.markdown(),
readingTime: await ctx.readingTime()
})
})
export default defineConfig({ collections: { posts } })You get a typed query. No codegen step to remember, no any anywhere.
import { posts } from 'contentmap/generated'
const recent = posts
.select('title', 'slug') // narrows the type to exactly these fields
.where(p => !p.draft)
.sortBy('date', 'desc') // sort by a field you didn't select
.limit(5)
.all()
const full = await posts.load('hello-world') // loads ONE document's bodyThat last line is the whole design. Every document is its own module, so listing posts never loads their contents.
Rendering 10 cards from a 5,000-document collection — a 7.5 MB corpus:
| JavaScript shipped | |
|---|---|
| contentmap | 1.3 MB · 146 KB gzip — 17.9% of the corpus |
| Content Collections | 16.7 MB — the entire corpus |
Tools that emit one big array can't avoid this: importing a single field imports everything. contentmap emits a module per document plus a lazy index, so reading titles never touches bodies.
The client runtime that makes it work is 684 bytes minified and gzipped — no dependencies, no eval, no Proxy. It runs under a strict CSP and on React Native.
Under a low file-descriptor limit, Content Collections silently lost 2,758 of 3,000 documents and exited 0. contentmap reads the full corpus, and a truncated build is structurally impossible — it's gated in CI on every commit.
A document that violates its schema fails the build by default. Velite emits schema-violating data and exits 0.
1,000 Markdown documents, every tool configured for frontmatter plus schema validation. Reproduce with pnpm bench:compare.
| Install | Packages | Maintained | |
|---|---|---|---|
| contentmap | 7.0 MB | 10 | ✅ |
| Content Collections | 62.7 MB | 41 | ✅ |
| Velite | 54.6 MB | 131 |
|
| Contentlayer2 | 134.0 MB | 287 | ❌ unmaintained |
19× smaller than Contentlayer, 9× smaller than Content Collections. contentmap and its eight dependencies come to 2.7 MB; the tenth package is your validator.
contentmap build is the product. Plugins are convenience, and CI diffs their output against the CLI's to keep that true — which is what kept Contentlayer alive for exactly as long as webpack was the only option.
npm i contentmap zod
npx contentmap initinit detects your framework, writes a config and a sample document, registers the tsconfig path, and updates .gitignore.
npx contentmap build| Any validator | zod, valibot, arktype, effect — all four tested for parity |
| Any source | Markdown, MDX, YAML, JSON, JSONC, TOML, raw text, HTTP APIs, or your own via defineLoader / defineParser
|
| Typed projections |
select() narrows the row type; where, sortBy and groupBy still reach the whole index |
| MDX | JSX in content, components imported in, values exported out — via @contentmap/mdx
|
| Syntax highlighting | VS Code grammars and themes at build time, light/dark aware — via @contentmap/shiki
|
| Images | Dimensions read at build time so pages stop jumping, plus thumbhash placeholders — 21 bytes, zero client JS |
| Assets | Content-hashed copying, URL rewriting in rendered HTML, orphan cleanup |
| References | Cross-collection lookups with cycle detection, resolved on demand |
| Remote content | Digest-keyed revalidation, --frozen for offline CI, credentials screened out of the cache |
| Incremental | Transform cache keyed by content digest — never by mtime alone |
| Watch mode | Debounced, coalesced, one build at a time; a broken config keeps the last good output |
| Diagnostics | Grouped by kind, with code frames, did-you-mean hints, and --json for CI |
Inside transform, ctx gives you: meta, body, markdown(), mdx(), plain(), excerpt(), toc(), readingTime(), image(), asset(), emitFile(), documents(), siblings(), reference(), addWatchFile(), cache() and skip().
contentmap build Build once. Non-zero exit on error.
contentmap dev Build and watch.
contentmap check Validate only; emit nothing. For CI.
contentmap clean Remove generated output, keeping the cache.
contentmap init Scaffold config, sample content and tsconfig path.
Useful flags: --frozen (refuse the network), --json (machine-readable), --verbose (per-phase timings), --cache-dir, --concurrency, --on-validation-error.
| Framework | Package | Proven by |
|---|---|---|
| Vite · SvelteKit · SolidStart · Qwik · React Router · TanStack Start · Analog | @contentmap/vite |
a real vite build
|
| Next.js — Turbopack and webpack | @contentmap/next |
examples/next |
| Nuxt | @contentmap/nuxt |
examples/nuxt |
| Astro | @contentmap/astro |
examples/astro |
| webpack · Rspack | @contentmap/webpack |
examples/webpack |
| Anything else | — | run contentmap build in your build script |
pnpm verify:examples builds all four applications with their real toolchains on every commit. That gate isn't decoration — writing those examples turned up four bugs every hook-level test had passed straight over, including a Nuxt module that never ran.
npx @contentmap/migrateReads your existing config, writes a contentmap one beside it, and writes a report of anything needing a human. Your original config is never modified.
Contentlayer's field DSL becomes a Zod schema, computedFields become a transform, and the document shape is rewritten onto contentmap's context — _raw.flattenedPath → ctx.meta.path, body.raw → ctx.body. Those are exact equivalents, which is what makes rewriting them automatically safe. Details.
What is contentmap?
A build-time content layer: it reads your content files, validates them against a schema you define, and emits typed TypeScript modules your app imports.
Is it a replacement for Contentlayer?
Yes. Contentlayer is unmaintained — it died when its sponsor withdrew, and a volunteer maintainer's offer was closed by a stale bot. contentmap installs 19× smaller, works on Turbopack, and npx @contentmap/migrate converts your config.
Does it support MDX?
Yes, via @contentmap/mdx. It compiles to the same function-body string Contentlayer and Velite produce, so your rendering code ports across unchanged.
Do I have to use Zod?
No. Any Standard Schema validator works — valibot, arktype and effect are all tested for parity.
Does it work without a bundler plugin?
Yes, and that's the point. contentmap build is the product; plugins are a convenience whose output CI diffs against the CLI's.
How much JavaScript reaches my users?
684 bytes, minified and gzipped. Zero dependencies, no eval, no Proxy.
Does it work on Windows?
Yes. CI runs Linux, macOS and Windows on Node 22 and 24.
1.0 — semver from here. Every exported symbol of every package is recorded under api/, and CI fails on a signature change that wasn't deliberate. Patches fix bugs; minors add options and packages without breaking existing code; anything being removed is deprecated first with its replacement named. See CONTRIBUTING.md.
Shipped, and what's next — the detail lives in ROADMAP.md.
- Typed pipeline, per-document output, typed projections
- Markdown, MDX, YAML, JSON, JSONC, TOML, raw, and custom parsers
- Syntax highlighting, images, assets, cross-collection references
- Remote sources, watch mode, incremental cache
- Five framework adapters, each proven against its real toolchain
- A codemod for all three incumbents
- Search index generation for Pagefind, Orama and MiniSearch
-
@contentmap/git— dates and authors from history - Documentation site
Open an issue to move something up the list.
MIT