npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

contentmap

v1.2.0

Published

The type-safe content layer for every framework.

Readme

contentmap

Your Markdown, as typed TypeScript modules.

npm CI install size license

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.

Quick look

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 body

That last line is the whole design. Every document is its own module, so listing posts never loads their contents.

Why people switch

Your bundle stays small

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.

Your build fails when it should

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.

That holds through every integration too: next build, vite build, nuxt build, astro build and webpack stop on the same report contentmap build prints.

Your install doesn't balloon

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 | ⚠️ quiet since Aug 2025 | | 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.

It keeps working when your bundler changes

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.

Install

npm i contentmap zod
npx contentmap init

init detects your framework, writes a config and a sample document, registers the tsconfig path, and updates .gitignore.

npx contentmap build

Features

| | | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | 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 | | After the build | A hook with every document in hand — search indexes, feeds, tag counts — run identically by the CLI and every integration | | Search | Pagefind, Orama or MiniSearch built from your collections — via @contentmap/search, which keeps the body searchable without shipping it | | Diagnostics | Grouped by kind, with code frames, did-you-mean hints, and --json for CI |

Transform context

Inside transform, ctx gives you: meta, sourcePath, body, markdown(), mdx(), plain(), excerpt(), toc(), readingTime(), image(), asset(), emitFile(), documents(), siblings(), reference(), addWatchFile(), cache() and skip().

meta.filePath is relative to the collection's directory; sourcePath is the absolute path of the file on disk, for a transform that wants to stat its own source, read a sibling, or ask git about it. It is undefined for documents a loader produced, which never had a file.

After the build

afterBuild runs once a build has succeeded, with every document in hand — the place for a search index, a feed, or tag counts.

import { defineCollection, defineConfig } from 'contentmap'
import { z } from 'zod'

const posts = defineCollection({
  directory: 'content/posts',
  include: '**/*.md',
  schema: z.object({ title: z.string(), tags: z.array(z.string()).default([]) })
})

export default defineConfig({
  collections: { posts },
  afterBuild: async ctx => {
    const search = ctx.documents(posts).map(({ title, _meta }) => ({ title, path: _meta.path }))
    await ctx.writeFile('public/search.json', JSON.stringify(search))
  }
})

It runs inside the build, so contentmap build, contentmap dev and every framework integration run it the same way, and CI diffs its output between them. ctx.writeFile skips unchanged bytes, so a dev server doesn't reload for nothing, and the watcher never mistakes it for an edit. A hook that throws fails the build; none runs after a build that failed, because its documents are incomplete.

CLI

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.

Frameworks

| 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.

Coming from Contentlayer, Velite or Content Collections?

npx @contentmap/migrate

Reads 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 _raw.flattenedPath becomes ctx.meta.path — exact equivalents, which is what makes rewriting them automatically safe. Your documents keep the shape your pages already read (body.raw, body.code, body.html), MDX compiles so useMDXComponent keeps rendering it, the helpers and imports your config uses come along, and onSuccess becomes afterBuild. Every pattern it handles is a fixture that CI migrates and builds for real. Details.

It has been done to whole applications. tailwind-nextjs-starter-blog — MDX, a search index, an RSS feed, tag counts — migrated off Contentlayer and built with next build: 20 files of application code changed, the generated data down from 1.66 MB to 0.91 MB, and feed.xml byte-identical. svgl — a SvelteKit site with a shiki-highlighted docs pipeline — migrated off Content Collections in 6 files, every field it derives from content identical. neobrutalism-components — 54 MDX documents — migrated off Velite in 3 files, all 53 prerendered pages identical. None of them was automatic, and the write-up says exactly which parts needed a human, and which bugs each migration found here — including the one number that came out worse, which is why @contentmap/mdx now minifies.

FAQ

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.

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.

Yes, via @contentmap/mdx. It compiles to the function body that Velite produces and run() evaluates. With compat: 'mdx-bundler', the same string also renders under mdx-bundler's getMDXComponent — what Contentlayer and Content Collections pages use — so rendering code ports across unchanged.

No. Any Standard Schema validator works — valibot, arktype and effect are all tested for parity.

Yes, and that's the point. contentmap build is the product; plugins are a convenience whose output CI diffs against the CLI's.

684 bytes, minified and gzipped. Zero dependencies, no eval, no Proxy.

Yes. CI runs Linux, macOS and Windows on Node 22 and 24.

Stability

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.

Roadmap

Shipped, and what's next — the detail lives in ROADMAP.md.

  • [x] Typed pipeline, per-document output, typed projections
  • [x] Markdown, MDX, YAML, JSON, JSONC, TOML, raw, and custom parsers
  • [x] Syntax highlighting, images, assets, cross-collection references
  • [x] Remote sources, watch mode, incremental cache
  • [x] Five framework adapters, each proven against its real toolchain, failing a build exactly where the CLI would
  • [x] A codemod for all three incumbents, built for real against every config pattern it handles
  • [x] afterBuild — search indexes, feeds and tag counts, run identically everywhere
  • [ ] Real-world validation — three applications migrated so far, one per incumbent, with their numbers published
  • [x] Search index helpers for Pagefind, Orama and MiniSearch — @contentmap/search
  • [ ] @contentmap/git — dates and authors from history
  • [ ] Documentation site

Open an issue to move something up the list.

License

MIT