Create a Next.js 14 Blog Using Markdown with Contentlayer2

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.