Create a Next.js 14 Blog Using Markdown with Contentlayer 2
In this tutorial, we’ll walk through the process of building a blog website using Next.js 14 and Contentlayer 2. You’ll learn how to seamlessly integrate Markdown files to create dynamic content, resulting in a fast, SEO-friendly, and easily maintainable blog.
Prerequisites
- Basic knowledge of React and Next.js
- Node.js installed on your machine
Setting Up the Project
1. Create a New Next.js Project
First, let’s create a new Next.js project:
npx create-next-app@latest my-markdown-blog
cd my-markdown-blog
This command creates a new Next.js project with the latest version (14 at the time of writing).
2. Install Required Packages
Now, let’s install the necessary packages:
npm install contentlayer2 next-contentlayer2 date-fns
Here’s a brief explanation of each package:
- contentlayer2: A content SDK that validates and transforms your content into type-safe JSON data you can easily import into your application.
- next-contentlayer2: The Next.js plugin for Contentlayer.
- date-fns: A modern JavaScript date utility library.
Configuring the Project
1. Update Next.js Configuration
Create or modify next.config.mjs
in your project root:
import { withContentlayer } from 'next-contentlayer2';
/** @type {import('next').NextConfig} */
const nextConfig = {};
export default withContentlayer(nextConfig);ty
This configuration wraps your Next.js config with Contentlayer, allowing it to process your content during builds.
2. Update TypeScript Configuration
Modify your tsconfig.json
or jsconfig.json
file:
{
"compilerOptions": {
// ... other options ...
"paths": {
"@/*": ["./src/*"],
"contentlayer/generated": ["./.contentlayer/generated"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".contentlayer/generated"],
"exclude": ["node_modules"]
}
These changes ensure that TypeScript recognizes the Contentlayer-generated files and provides proper type checking.
3. Update .gitignore
Add the following line to your .gitignore
file:
# contentlayer
.contentlayer
This prevents the generated Contentlayer files from being committed to your repository.
Setting Up Contentlayer
Create a new file called contentlayer.config.ts
in your project root:
import { defineDocumentType, makeSource } from 'contentlayer2/source-files';
export const Post = defineDocumentType(() => ({
name: 'Post',
filePathPattern: `**/*.mdx`,
contentType: 'mdx',
fields: {
title: { type: 'string', required: true },
date: { type: 'date', required: true },
summary: { type: 'string', required: true },
},
computedFields: {
url: {
type: 'string',
resolve: (post) => `/posts/${post._raw.flattenedPath}`,
},
},
}));
export default makeSource({ contentDirPath: 'posts', documentTypes: [Post] });
This configuration does the following:
- Defines a
Post
document type with required fields:title
,date
, andsummary
. - Sets up a computed
url
field for each post. - Configures Contentlayer to look for
.mdx
files in aposts
directory.
Creating Content
Create a new directory called posts
in your project root, and add a sample post:
---
title: "My First Blog Post"
date: '2024-03-08'
summary: "This is a summary of my first blog post using Next.js and Contentlayer."
---
# Welcome to My Blog
This is the content of my first blog post. You can use all the power of Markdown here!
## Subheading
- List item 1
- List item 2
- List item 3
[Link to Next.js](https://nextjs.org)
Displaying Posts on the Homepage
Update your src/app/page.tsx
file:
import { allPosts, Post } from 'contentlayer/generated';
import { compareDesc, format, parseISO } from 'date-fns';
import Link from 'next/link';
function PostCard(post: Post) {
return (
<div className='mb-8'>
<h2 className='mb-1 text-xl'>
<Link href={post.url} className='text-blue-700 hover:text-blue-900'>
{post.title}
</Link>
</h2>
<time dateTime={post.date}>
{format(parseISO(post.date), 'LLLL d, yyyy')}
</time>
<p>{post.summary}</p>
</div>
);
}
export default function Home() {
const posts = allPosts.sort((a, b) =>
compareDesc(new Date(a.date), new Date(b.date))
);
return (
<div className='max-w-xl mx-auto my-8'>
<h1 className='text-center'>My Markdown Blog</h1>
{posts.map((post) => (
<PostCard {...post} key={post._id} />
))}
</div>
);
}
This code:
- Imports all posts from Contentlayer’s generated files.
- Sorts posts by date in descending order.
- Renders a list of post cards, each linking to the full post.
Creating Individual Post Pages
Create a new file at src/app/posts/[slug]/page.tsx
:
import Mdx from '@/components/mdx-components';
import { allPosts } from 'contentlayer/generated';
import { format, parseISO } from 'date-fns';
import { getMDXComponent } from 'next-contentlayer2/hooks';
interface PostPageProps {
params: {
slug: string;
};
}
export default function PostPage({ params }: PostPageProps) {
const post = allPosts.find((post) => post._raw.flattenedPath === params.slug);
if (!post?.body.code) {
return <div>Post not found</div>;
}
return (
<article className='py-8 mx-auto max-w-xl'>
<div className='mb-8 text-center'>
<time dateTime={post.date}>
{format(parseISO(post.date), 'LLLL d, yyyy')}
</time>
<h1>{post.title}</h1>
</div>
<Mdx code={post.body.code} />
</article>
);
}
This creates dynamic routes for each post and renders the post content.
Creating an MDX Component
Create a new file at src/components/mdx-components.tsx
:
import { useMDXComponent } from 'next-contentlayer2/hooks';
const components = {
h1: ({ ...props }) => (
<h1
className={'mt-2 text-4xl font-bold tracking-tight text-red-300'}
{...props}
/>
),
h2: ({ ...props }) => (
<h2
className={'mt-10 pb-1 text-3xl font-semibold tracking-tight'}
{...props}
/>
),
p: ({ ...props }) => <p className='mt-8 text-base leading-7' {...props} />,
};
interface MdxProps {
code: string;
}
export default function Mdx({ code }: MdxProps) {
const Component = useMDXComponent(code);
return (
<div>
<Component components={components} />
</div>
);
}
This component allows you to customize how different Markdown elements are rendered.
If you prefer watching a video: https://youtu.be/ICOBQCvbtNc?si=Z9a-EMFQuM29oGkx
Conclusion
You now have a fully functional Markdown blog using Next.js 14 and Contentlayer 2! This setup provides a great foundation for a performant, easy-to-maintain blog. You can easily add more posts by creating new Markdown files in the posts
directory.
Some next steps you might consider:
- Add pagination for the blog list
- Implement categories or tags for posts
- Add a search functionality
- Customize the styling to match your brand