Skip to content

Fix: Contentlayer Not Working — Content Not Generated, Types Missing, or Build Errors

FixDevs ·

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 document

Or 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/generated module is empty or missing.
  • Document type definitions must match content structure — each content file must have frontmatter that matches the fields defined in contentlayer.config.ts. Missing required fields cause build errors.
  • The generated directory must be in tsconfig.json — TypeScript needs to know about the .contentlayer/generated directory. If it’s not in paths or include, imports fail.
  • Original Contentlayer is unmaintained — the original contentlayer package has compatibility issues with newer Next.js versions. contentlayer2 is 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/core

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

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles