September 3, 2024 (2mo ago)
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, 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);
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.
Access the Complete Source Code github
Watch the Full Video Tutorial Youtube
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.