· Ben · engineering · 6 min

Contentlayer is dead. Here's the migration that doesn't touch your MDX.

The README says unmaintained, your Next 15 install throws a peer-dep error, and every replacement reads the exact same files you already have. A loader swap, with the lines that change.

The notice sits at the top of the Contentlayer README: the project is no longer maintained due to lack of funding, and here's a fork. That's the whole announcement. No blog post, no sunset timeline, no migration path. Just a line in a README on a repo with 3.5k stars that a lot of Next.js blogs still depend on.

Most people searching for a Contentlayer alternative got the news from an error message. Fresh npm install on Next 15, peer dependency conflict, build red. Or .contentlayer never generated and every import of contentlayer/generated resolves to nothing.

Here's the part the roundups bury. Your .mdx files are fine. Every replacement on that list reads the same files, out of the same directory, with the frontmatter you already wrote. The number of content files that change in this migration is zero.

The README is the whole announcement

The cause is public, and it isn't technical. Issue #429, "State of the project," filed April 2023, has the maintainer committing to monthly releases "for as long as there is sustainable financial backing." That conditional did the work. Dub's writeup of their own migration fills in the rest: sponsor Stackbit was acquired by Netlify, funding stopped, and the project went quiet while the App Router landed underneath it.

The project didn't rot. It ran out of money. Hold onto that, because it changes what there is to learn here.

What actually broke, with issue numbers

If you want to know how dead it is, the issue tracker is more honest than the tweets.

  • #415 — no .contentlayer folder generated on Next.js 13.3.
  • #506 — build error on 0.3.4 with the App Router, throwing from inside @opentelemetry/sdk-trace-base.
  • #674 — Next.js 15 peer-dependency conflict, because next-contentlayer@0.3.4 pins an older Next. Opened November 14, 2024. Closed as stale.
  • timlrx/tailwind-nextjs-starter-blog #844 — "loads of build errors on first try due to contentlayer," on a fresh clone of one of the most-cloned blog starters on GitHub.

The last official release line is 0.3.x. The stale-close on #674 is the signal. A funded project answers a peer-dep issue against the current major version of its only framework. This one let a bot close it.

Your content was never the problem

Two layers, and people keep collapsing them.

The bottom layer is a folder of .mdx files with YAML frontmatter. Title, date, tags, description, whatever your site reads. That's yours. It's plain text, it's in git, it has a commit history, and it has outlived at least one build tool now.

The top layer turns those files into typed objects your components can import. Parse the frontmatter, validate it against a schema, run remark and rehype, emit types, cache the output. That layer is rented. You rented it from Contentlayer and the landlord stopped paying the mortgage.

So this isn't a content migration. It's a loader swap. You're rewriting one config file, changing one import path, and fixing a handful of field accessors where the new library names things differently. Nothing under content/ moves. Nothing under content/ even gets touched.

The Contentlayer alternatives, and how to pick in about a minute

Five destinations. Pick by constraint, not by stars.

contentlayer2, if you want the build green tonight

A maintained fork by Tim Lin, the same author as the Tailwind Next.js starter blog. Currently on 0.5.8. Next 15 and React 19 work. Roughly 22k weekly downloads on next-contentlayer2, which is small but alive.

The migration is a rename:

"contentlayer": "^0.3.3"      → "contentlayer2": "^0.4.6"
"next-contentlayer": "^0.3.3" → "next-contentlayer2": "^0.4.6"

Then update the imports in contentlayer.config.ts from contentlayer/source-files to contentlayer2/source-files, and the generated import from contentlayer/generated to contentlayer2/generated. That's the diff. Your schema definitions, your computedFields, your MDX plugin chain all stay as written.

The honest read: the README states no major breaking changes are expected while discussions about the project's future continue. That is a fork keeping the lights on, not a roadmap. It buys you a green build and defers the decision, which is a legitimate thing to want on a Tuesday night.

Content Collections, if you want the one with an ecosystem

The closest thing to a successor, with real adoption behind it. Dub runs it in production, and there's an official migration guide from Contentlayer.

Collections are defined with a Zod schema instead of defineDocumentType, and computedFields becomes a transform function. The rest of the work is find-and-replace on field names, and the gotchas people hit are consistent enough to list:

  • doc.body.raw becomes doc.content
  • filtering on slug becomes filtering on doc._meta.path
  • the pick helper is gone from contentlayer/client, use lodash
  • contentDirPath splits into directory plus an include glob

Budget an hour if your site has a couple of collections and a handful of computed fields. Budget an afternoon if your computedFields are doing reading-time calculation, heading extraction and structured data generation, because all of that moves into transform.

Velite, if you're not only on Next

Framework-agnostic Zod-schema data layer for Markdown, MDX, YAML and JSON. The docs say plainly that it's inspired by Contentlayer and that Contentlayer is unmaintained, which I appreciate more than a comparison table.

One real difference in behavior: Velite copies static files referenced from your Markdown into public at build time, so an image can live in the same folder as the post that uses it. Contentlayer never processed static assets, so you hand-placed everything in public and hardcoded the path. If your posts are image-heavy, that alone is worth the switch. We wrote up the setup end to end in a separate post on getting started with Velite if you want the config.

Bus factor, said out loud: it's primarily one developer, which is the same shape of risk that just cost you a weekend. Worth knowing before you adopt it, not after.

Fumadocs MDX, if the site is really docs

A content processing layer that started life as a Contentlayer alternative and describes itself as similar to Content Collections, explicitly not a full CMS. If your content/ folder is a docs tree with a sidebar, versioning and search, what you're adopting is a docs framework's data layer, loader included. That's a fine trade when you want the framework. It's an odd one for a five-post blog.

Nothing, which is a real answer

fs.readdir, gray-matter, a remark pipeline, and about sixty lines you own forever. No config DSL, no generated .contentlayer folder, and nothing pinned to your framework's major version.

What you give up is generated TypeScript types for your frontmatter and incremental rebuilds in dev. If your blog is under a hundred posts and your frontmatter is six fields you already know by heart, that trade is correct and I'd take it. If you've got four content types with cross-references and 400 documents, don't. Types are the thing you'll miss on day two.

The top results answer this at the wrong layer

Search for this and half of what ranks tells you to move your content into a hosted CMS. A build tool lost its sponsor, so put 200 markdown files in a database. That's answering a build-tooling question with a hosting decision, and it's a category error.

Look at what you'd be trading. Git history on every post. Branches and PR review on a copy change. The ability to git diff a paragraph, revert a bad edit, or grep the entire archive. Preview deploys that include the content. You'd hand all of that over because your loader's funding dried up, and you'd hand it over to a vendor with the same funding risk and a much worse exit. That's the argument in why a headless CMS can't take a pull request, and the abandonment is the strongest evidence for it, not against it.

The other half of the SERP is roundups dated July and August 2024. They predate Next 15, React 19, and #674 getting stale-closed. Check the dates before you trust the recommendations.

The part nobody puts in the migration guide

One thing to notice while you're renaming imports. For a lot of people reading this, the build has been broken for months and nobody noticed. It broke silently because nothing new was being written. You found out when you finally went to add a post, or when Dependabot filed a PR against a repo whose last content commit was in the spring.

The tool churn is real and it cost you an evening. It is not what killed the blog.

The folder of markdown is the asset

Contentlayer died and every post survived it. That's the whole case for keeping content in the repo. The loader is a dependency with a funding model. The .mdx file is a text file with your name in the git blame, and it'll still parse in ten years with a thirty-line script.

Which is why Contentcron writes into that layer and nothing above it. Articles land as pull requests against your content directory, full MDX, with frontmatter inferred from the posts already in the repo, field names and date format and slug style included. It doesn't know or care whether contentlayer2, Content Collections or sixty lines of fs.readdir picks the file up on the other side.

An article page toggled between rendered preview and raw MDX source, showing frontmatter matched to the repo's existing posts

If your blog is a folder of markdown and the most recent commit to it is a loader upgrade, that's the problem worth fixing next. First article is free, no card.