Create a Next.js 14 Blog Using Markdown with Contentlayer 2

Candra Kriswinarto
4 min readSep 20, 2024

--

Photo by Cathryn Lavery on Unsplash

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, and summary.
  • Sets up a computed url field for each post.
  • Configures Contentlayer to look for .mdx files in a posts 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

--

--

Candra Kriswinarto
Candra Kriswinarto

Written by Candra Kriswinarto

👋 Front-End Developer & YouTuber. I write about web dev, share coding tips, and create tutorials to help others learn. https://youtube.com/@CandDev

No responses yet