Fix: Contentlayer Not Working — Content Not Generated, Types Missing, or Build Errors
Part of: React & Frontend Errors
Quick Answer
How to fix Contentlayer and Contentlayer2 issues — content source configuration, document type definitions, MDX processing, computed fields, Next.js integration, and migration to alternatives.
The Problem
allPosts is undefined or empty:
import { allPosts } from 'contentlayer/generated';
console.log(allPosts); // undefined or []Or the build fails with a content error:
Error: Contentlayer: Content directory not found
// Or: Error: Missing required field "title" in documentOr TypeScript can’t find the generated types:
Module '"contentlayer/generated"' has no exported member 'Post'Why This Happens
Contentlayer transforms content files (Markdown/MDX) into type-safe JSON data during the build process. It has been in a maintenance state, and Contentlayer2 is the community fork:
- Content must be generated before use — Contentlayer generates TypeScript types and JSON data from your content files during the build. If the generation step hasn’t run, the
contentlayer/generatedmodule is empty or missing. - Document type definitions must match content structure — each content file must have frontmatter that matches the
fieldsdefined incontentlayer.config.ts. Missing required fields cause build errors. - The generated directory must be in
tsconfig.json— TypeScript needs to know about the.contentlayer/generateddirectory. If it’s not inpathsorinclude, imports fail. - Original Contentlayer is unmaintained — the original
contentlayerpackage has compatibility issues with newer Next.js versions.contentlayer2is the maintained fork.
The deeper failure mode is timing. Contentlayer is a build-time tool: it runs before Next.js’s webpack/Turbopack pipeline, writes JSON and .d.ts files into .contentlayer/generated, and exits. Anything downstream that imports contentlayer/generated assumes those artifacts already exist. If the generation step crashes silently — usually because a single MDX file has invalid frontmatter — Next.js continues with stale or empty output. The symptom is a runtime “undefined” on allPosts even though tsc passes. The cause is two builds out of sync.
The second source of pain is the upstream maintenance gap. The original contentlayer package has not received releases that track Next.js 14/15 changes (App Router internals, ESM-first module resolution, React 19 transitions). When you upgrade Next.js, the original package often throws on plugin hooks that no longer exist, or fails to write the generated directory at all. The community fork contentlayer2 keeps pace, but only if you migrate the imports cleanly. Mixed imports — contentlayer in one file and contentlayer2 in another — leave two separate generated trees and break type inference.
The third trap is editor tooling. TypeScript Language Server caches the .contentlayer/generated directory aggressively. If you add a new document type or change a field, the file system updates but VS Code still serves stale types. The build succeeds, the IDE reports errors, and you waste an afternoon debugging code that already works. Restarting the TS server (TypeScript: Restart TS Server in the command palette) is part of the workflow.
Production Incident Lens: When the Docs Site Goes Dark
The blast radius for a Contentlayer failure is the entire documentation or blog surface. When the Vercel build pipeline reports Error: Contentlayer: Content directory not found, your /docs and /blog routes return 404 or render empty arrays. For a product whose docs are the primary support channel, this is a customer-facing outage even though no server crashed.
The on-call playbook should distinguish three failure modes:
- Hard build failure —
npm run buildexits non-zero. Vercel keeps the previous deployment live; no user impact, but the next merge is blocked until the broken MDX file is fixed. - Silent generation failure — generation logs a warning but writes an empty
allPostsarray. The build succeeds, the new deployment ships, and the docs page renders “No posts found.” This is the dangerous mode because monitoring sees a 200 response. - Type-only failure — code generation succeeds at runtime but
.d.tsfiles are stale. CI passes the build, but PR reviewers see TypeScript errors in their editors and waste cycles debugging.
Treat Contentlayer like any other code-generation pipeline: pin the version, run generation in CI with --strict mode, and add a smoke test that asserts allPosts.length > 0 before the deploy promotes. A trivial integration test that imports allPosts and exits 1 on empty arrays catches the silent failure before it ships. Track the Contentlayer maintenance signal too — if the upstream repo has not seen a commit in 90 days and you depend on a feature for a public-facing surface, plan a migration sprint before Next.js’s next major release forces it.
Fix 1: Setup with Contentlayer2
# Use the maintained fork
npm install contentlayer2 next-contentlayer2// contentlayer.config.ts
import { defineDocumentType, makeSource } from 'contentlayer2/source-files';
export const Post = defineDocumentType(() => ({
name: 'Post',
filePathPattern: 'posts/**/*.mdx',
contentType: 'mdx',
fields: {
title: { type: 'string', required: true },
description: { type: 'string', required: true },
date: { type: 'date', required: true },
published: { type: 'boolean', default: true },
tags: { type: 'list', of: { type: 'string' }, default: [] },
image: { type: 'string' },
author: { type: 'string', default: 'Unknown' },
},
computedFields: {
slug: {
type: 'string',
resolve: (doc) => doc._raw.flattenedPath.replace('posts/', ''),
},
url: {
type: 'string',
resolve: (doc) => `/blog/${doc._raw.flattenedPath.replace('posts/', '')}`,
},
readingTime: {
type: 'string',
resolve: (doc) => {
const words = doc.body.raw.split(/\s+/).length;
const minutes = Math.ceil(words / 200);
return `${minutes} min read`;
},
},
},
}));
export const Page = defineDocumentType(() => ({
name: 'Page',
filePathPattern: 'pages/**/*.mdx',
contentType: 'mdx',
fields: {
title: { type: 'string', required: true },
description: { type: 'string' },
},
computedFields: {
slug: {
type: 'string',
resolve: (doc) => doc._raw.flattenedPath.replace('pages/', ''),
},
},
}));
export default makeSource({
contentDirPath: 'content', // Content files directory
documentTypes: [Post, Page],
mdx: {
remarkPlugins: [],
rehypePlugins: [],
},
});# Content directory structure
content/
├── posts/
│ ├── getting-started.mdx
│ ├── advanced-typescript.mdx
│ └── react-patterns.mdx
└── pages/
├── about.mdx
└── contact.mdx---
title: Getting Started with TypeScript
description: A comprehensive guide to TypeScript basics
date: 2026-03-29
tags: [typescript, tutorial]
published: true
---
# Getting Started with TypeScript
This is the content of your MDX file.
You can use **React components** here too.Fix 2: Next.js Configuration
// next.config.mjs
import { withContentlayer } from 'next-contentlayer2';
const nextConfig = {
// Your Next.js config
};
export default withContentlayer(nextConfig);// tsconfig.json — add Contentlayer paths
{
"compilerOptions": {
"paths": {
"contentlayer/generated": ["./.contentlayer/generated"]
}
},
"include": [
".contentlayer/generated"
]
}// app/blog/page.tsx — list all posts
import { allPosts } from 'contentlayer/generated';
import { compareDesc } from 'date-fns';
import Link from 'next/link';
export default function BlogPage() {
const posts = allPosts
.filter(post => post.published)
.sort((a, b) => compareDesc(new Date(a.date), new Date(b.date)));
return (
<div>
<h1>Blog</h1>
{posts.map(post => (
<article key={post.slug}>
<Link href={post.url}>
<h2>{post.title}</h2>
</Link>
<p>{post.description}</p>
<span>{post.readingTime}</span>
<time>{new Date(post.date).toLocaleDateString()}</time>
<div className="flex gap-2">
{post.tags.map(tag => (
<span key={tag} className="text-sm bg-gray-100 px-2 py-1 rounded">{tag}</span>
))}
</div>
</article>
))}
</div>
);
}
// app/blog/[slug]/page.tsx — single post
import { allPosts } from 'contentlayer/generated';
import { notFound } from 'next/navigation';
import { useMDXComponent } from 'next-contentlayer2/hooks';
export async function generateStaticParams() {
return allPosts.map(post => ({ slug: post.slug }));
}
export async function generateMetadata({ params }: { params: { slug: string } }) {
const post = allPosts.find(p => p.slug === params.slug);
if (!post) return {};
return { title: post.title, description: post.description };
}
export default function PostPage({ params }: { params: { slug: string } }) {
const post = allPosts.find(p => p.slug === params.slug);
if (!post) notFound();
const MDXContent = useMDXComponent(post.body.code);
return (
<article className="prose dark:prose-invert max-w-none">
<h1>{post.title}</h1>
<time>{new Date(post.date).toLocaleDateString()}</time>
<MDXContent />
</article>
);
}Fix 3: Custom MDX Components
// components/mdx-components.tsx
import Image from 'next/image';
import Link from 'next/link';
const mdxComponents = {
h2: ({ children, ...props }: React.HTMLProps<HTMLHeadingElement>) => {
const id = children?.toString().toLowerCase().replace(/\s+/g, '-');
return <h2 id={id} {...props}>{children}</h2>;
},
a: ({ href, children }: { href?: string; children: React.ReactNode }) => {
if (href?.startsWith('/')) return <Link href={href}>{children}</Link>;
return <a href={href} target="_blank" rel="noopener noreferrer">{children}</a>;
},
img: ({ src, alt }: { src?: string; alt?: string }) => (
<Image src={src || ''} alt={alt || ''} width={800} height={400} className="rounded-lg" />
),
Callout: ({ type = 'info', children }: { type?: string; children: React.ReactNode }) => (
<div className={`border-l-4 p-4 my-4 ${
type === 'warning' ? 'border-yellow-400 bg-yellow-50' :
type === 'error' ? 'border-red-400 bg-red-50' :
'border-blue-400 bg-blue-50'
}`}>
{children}
</div>
),
};
// Use in post page
<MDXContent components={mdxComponents} />Fix 4: Remark and Rehype Plugins
// contentlayer.config.ts
import { makeSource } from 'contentlayer2/source-files';
import remarkGfm from 'remark-gfm';
import rehypeSlug from 'rehype-slug';
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
import rehypePrettyCode from 'rehype-pretty-code';
export default makeSource({
contentDirPath: 'content',
documentTypes: [Post, Page],
mdx: {
remarkPlugins: [remarkGfm],
rehypePlugins: [
rehypeSlug,
[rehypeAutolinkHeadings, { behavior: 'wrap' }],
[rehypePrettyCode, {
theme: 'github-dark',
keepBackground: false,
}],
],
},
});Fix 5: Table of Contents Generation
// Computed field for TOC
const Post = defineDocumentType(() => ({
name: 'Post',
// ...
computedFields: {
toc: {
type: 'json',
resolve: (doc) => {
const headingRegex = /^(#{2,3})\s+(.+)$/gm;
const headings: { level: number; text: string; id: string }[] = [];
let match;
while ((match = headingRegex.exec(doc.body.raw)) !== null) {
const text = match[2].trim();
headings.push({
level: match[1].length,
text,
id: text.toLowerCase().replace(/[^a-z0-9]+/g, '-'),
});
}
return headings;
},
},
},
}));
// Display TOC in post layout
function TableOfContents({ headings }: { headings: { level: number; text: string; id: string }[] }) {
return (
<nav>
<h3>Table of Contents</h3>
<ul>
{headings.map(h => (
<li key={h.id} style={{ paddingLeft: `${(h.level - 2) * 16}px` }}>
<a href={`#${h.id}`}>{h.text}</a>
</li>
))}
</ul>
</nav>
);
}Fix 6: Alternatives (If Contentlayer Doesn’t Work)
If Contentlayer compatibility issues persist, consider these alternatives:
// Option 1: Velite — modern Contentlayer alternative
// npm install velite
// velite.config.ts
import { defineConfig, s } from 'velite';
export default defineConfig({
collections: {
posts: {
name: 'Post',
pattern: 'posts/**/*.mdx',
schema: s.object({
title: s.string(),
date: s.isodate(),
description: s.string(),
body: s.mdx(),
}),
},
},
});
// Option 2: Astro Content Collections (if using Astro)
// Option 3: next-mdx-remote with manual file reading
// Option 4: @content-collections/coreStill Not Working?
allPosts is undefined — the generated directory doesn’t exist. Run npm run dev (Contentlayer generates during dev/build). Check that .contentlayer/generated exists. If using Contentlayer2, ensure you import from contentlayer/generated (the alias set in tsconfig.json).
“Content directory not found” — contentDirPath in contentlayer.config.ts must point to an existing directory relative to the project root. Default is 'content'. Create the directory and add at least one content file.
TypeScript errors on generated types — add .contentlayer/generated to tsconfig.json’s include array and paths. Restart the TypeScript server in your IDE after the first generation.
MDX components don’t render — pass components to useMDXComponent: <MDXContent components={mdxComponents} />. Without the components prop, custom components render as undefined HTML elements.
Build succeeds but deployed site shows empty post list — the .contentlayer/generated directory is gitignored (correctly) but the deploy environment skipped generation. Confirm that next build runs withContentlayer and that the build command on Vercel/Netlify is next build, not a custom script that bypasses the wrapper. Add a smoke test that fails the build if allPosts.length === 0.
Generation hangs in monorepos — Contentlayer reads the entire content tree on each run. In a Turborepo or pnpm workspace, the file watcher may pick up sibling packages and OOM. Set contentDirExclude in makeSource to exclude node_modules, .next, and other workspace directories.
Stale types after pulling new content from git — Contentlayer caches generation results in .contentlayer/.cache. After a git pull that adds or modifies MDX files, delete the cache and regenerate: rm -rf .contentlayer && npm run dev. CI builds should always start from a clean cache.
Generation succeeds but newly added documents are missing from allPosts — filePathPattern in defineDocumentType uses glob syntax relative to contentDirPath. A pattern like 'posts/**/*.mdx' matches MDX files but skips .md files. If you renamed .mdx to .md or added a subdirectory the pattern doesn’t cover, those documents are silently excluded. Print allDocuments.length versus the on-disk file count before each deploy as a smoke check.
For related content issues, see Fix: MDX Not Working, Fix: Nextra Not Working, Fix: Next.js Build Failed, and Fix: Astro Content Collections Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: UploadThing Not Working — File Upload Failing, CORS Errors, or Route Not Found
How to fix UploadThing issues — file router configuration, Next.js App Router and Pages Router setup, CORS, file type restrictions, progress callbacks, and deployment.
Fix: Clerk Not Working — Auth Not Loading, Middleware Blocking, or User Data Missing
How to fix Clerk authentication issues — ClerkProvider setup, middleware configuration, useUser and useAuth hooks, server-side auth, webhook handling, and organization features.
Fix: Fumadocs Not Working — Pages Not Found, Search Not Indexing, or MDX Components Missing
How to fix Fumadocs documentation framework issues — Next.js App Router setup, content source configuration, sidebar generation, MDX components, search, OpenAPI integration, and custom themes.
Fix: next-safe-action Not Working — Action Not Executing, Validation Errors Missing, or Type Errors
How to fix next-safe-action issues — action client setup, Zod schema validation, useAction and useOptimisticAction hooks, middleware, error handling, and authorization patterns.