Fix: Contentlayer Not Working — Content Not Generated, Types Missing, or Build 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.
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.
For related content issues, see Fix: MDX Not Working and Fix: Nextra 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: 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.
Fix: Nextra Not Working — Pages Not Rendering, Sidebar Missing, or MDX Components Broken
How to fix Nextra documentation site issues — Next.js integration, _meta.json sidebar configuration, custom MDX components, search setup, theme customization, and static export.