What is Contentlayer?
Contentlayer transforms your content (Markdown, MDX, JSON) into type-safe, validated data that integrates seamlessly with Next.js. Think of it as a content SDK that turns your files into a queryable, type-checked database.
No CMS needed. No API calls. Just files → types → components.
Quick Setup
npm install contentlayer2 next-contentlayer2
Note: The community fork
contentlayer2is the actively maintained version.
Define Your Content Schema
// 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 },
date: { type: "date", required: true },
description: { type: "string", required: true },
published: { type: "boolean", default: true },
tags: { type: "list", of: { type: "string" } },
},
computedFields: {
slug: {
type: "string",
resolve: (post) => post._raw.flattenedPath.replace("posts/", ""),
},
readingTime: {
type: "number",
resolve: (post) => Math.ceil(post.body.raw.split(/\s+/).length / 200),
},
},
}));
export default makeSource({
contentDirPath: "content",
documentTypes: [Post],
});
Use Content in Next.js
// app/blog/page.tsx
import { allPosts } from "contentlayer/generated";
import { compareDesc } from "date-fns";
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}>
<h2>{post.title}</h2>
<p>{post.description}</p>
<span>{post.readingTime} min read</span>
<time>{post.date}</time>
</article>
))}
</div>
);
}
Everything is fully typed. post.title is string, post.date is string, post.tags is string[]. TypeScript catches errors at build time.
Dynamic Page Routes
// app/blog/[slug]/page.tsx
import { allPosts } from "contentlayer/generated";
import { useMDXComponent } from "next-contentlayer2/hooks";
import { notFound } from "next/navigation";
export function generateStaticParams() {
return allPosts.map((post) => ({ slug: post.slug }));
}
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>
<h1>{post.title}</h1>
<MDXContent />
</article>
);
}
Multiple Content Types
export const Project = defineDocumentType(() => ({
name: "Project",
filePathPattern: "projects/**/*.mdx",
fields: {
title: { type: "string", required: true },
github: { type: "string" },
demo: { type: "string" },
stack: { type: "list", of: { type: "string" } },
},
}));
export const Author = defineDocumentType(() => ({
name: "Author",
filePathPattern: "authors/**/*.json",
contentType: "data",
fields: {
name: { type: "string", required: true },
avatar: { type: "string" },
twitter: { type: "string" },
},
}));
Each content type gets its own generated module: allProjects, allAuthors.
Build-Time Validation
If a post is missing a required field:
Error: Missing required field "title" in content/posts/my-post.mdx
Content errors are caught at build time, not runtime. No more broken pages from missing frontmatter.
Why Contentlayer?
| Feature | Contentlayer | Manual MDX | CMS API |
|---|---|---|---|
| Type Safety | ✅ Full | ❌ None | ⚠️ Manual |
| Build Validation | ✅ | ❌ | ❌ |
| Computed Fields | ✅ | Manual | Manual |
| Hot Reload | ✅ | ⚠️ | ❌ |
| No External API | ✅ | ✅ | ❌ |
Building a content-heavy site? I help developers create tools and automation that save hours.
📧 spinov001@gmail.com
🔧 My tools on Apify Store
How do you manage content in your Next.js apps? Let me know below!
Top comments (0)