How to Easily Set Up Contentlayer with Next.js 13

My portfolio website has undergone a massive overhaul using Next.js 13 and utilizing the App Router. I also used Contentlayer for MDX blog posts to ensure that my content is optimised for both search engines and readers. I wanted to create a step-by-step guide on how to integrate Contentlayer into your Next.js application.

What is Contentlayer?

Before we dive in, let's briefly discuss what Contentlayer is. Contentlayer is a powerful tool that allows you to manage your content in a structured and efficient manner. It's a static content generator for React that works seamlessly with Next.js, making it a fantastic choice for developers looking to streamline their content management process.

Let's see how we can set up Contentlayer with Next.js 13.

Initiating Your Next.js Project

We start by initialising our Next.js 13 project. Create a new project using this command:

npx create-next-app@latest
# or
yarn create next-app
# or
pnpm create next-app

You will see some questions that allow you to modify the configuration settings of your app. These questions relate to adding TypeScript, ESLint, Tailwind, App Router, and so on. These options don't change much about the way we implement Contentlayer but here are the options I used:

  • Would you like to add TypeScript with this project? Y
  • Would you like to use ESLint with this project? Y
  • Would you like to use Tailwind CSS with this project? Y
  • Would you like to use the src/ directory with this project? N
  • Use App Router (recommended)? Yes
  • Would you like to customize the default import alias? N

From here onwards, we will use npm but the commands will be very similar across pnpm and yarn.

This command will generate a new Next.js application for you. To ensure everything is functioning as intended, navigate to your project's directory with cd project-name and launch your application by running npm run dev from the terminal. You should see your application up and running at http://localhost:3000.

Hot Tip: With pnpm and yarn, you can omit run and just type pnpm/yarn dev and it will do the same thing as pnpm/yarn run dev

Adding Contentlayer

Next, we need to add Contentlayer and the Next.js plugin to our project. Install it with the following command:

npm install contentlayer next-contentlayer

We need to install the Next.js plugin because Contentlayer works for many different frameworks, we need to install the additonal packge so Contentlayer can hook into the Next.js processes.

Next.js Configuration

Inside of the next.config.js file, add the following code:

next.config.js
const { withContentlayer } = require('next-contentlayer')
 
/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  swcMinify: true,
}
 
module.exports = withContentlayer(nextConfig)

The withContentlayer function is used to integrate Contentlayer with Next.js's live-reload and build process. It is necessary to wrap the nextConfig in the withContentlayer function because Contentlayer generates data in the .contentlayer directory at the project root by looking for files that match the specified pattern. The generated data is then used by the Next.js application to render pages. This improves the overall Developer Experience, as edited Markdown / MDX files can utilise hot reloading.

Ignore Build Output

Add the .contentlayer directory into your .gitignore file to ensure each build of your app will have the latest generated data and you do not run into issues with Git.

TSConfig Options

If you setup your Next.js app with TypeScript, add the following options to your tsconfig.json.

tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "contentlayer/generated": ["./.contentlayer/generated"]
    }
  },
  "include": [
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx",
    ".next/types/**/*.ts",
    ".contentlayer/generated"
  ]
}

Create Post Document Type

Create a contentlayer.config.js file in the project's root. This defines the structure of your content. Here is the configuration I use:

contentlayer.config.js
import { defineDocumentType, makeSource } from 'contentlayer/source-files'
 
export const Post = defineDocumentType(() => ({
  name: 'Post',
  filePathPattern: '**/*.mdx',
  contentType: 'mdx',
  fields: {
    title: { type: 'string', required: true },
    date: { type: 'date', required: true },
    description: { type: 'string', required: true },
  },
  computedFields: {
    url: {
      type: 'string',
      resolve: (post) => `/blog/${post._raw.flattenedPath}`,
    },
  },
}))
 
export default makeSource({
  contentDirPath: 'posts',
  documentTypes: [Post],
})

This configuration tells Contentlayer that you have a 'Post' document type, located in the 'posts' directory, and each post includes 'title', 'date', and 'description' fields. The computed url field, as it sounds, generates a field based on the posts' filename (without the file extension)

Creating Posts

Now the fun part, create some posts! Make sure they are located under the posts directory, as specified in contentlayer.config.js. A cool feature of Contentlayer is it will tell you when you are missing required fields.

Contentlayer showing missing fields errors in terminal

Adding Posts to Next.js Pages

Now that we have Contentlayer configured and some MDX posts created, we can focus on pulling the posts into our Next.js pages. The below code pulls the MDX posts onto the home page.

app/page.tsx
import Link from 'next/link'
import { allPosts } from 'contentlayer/generated'
 
export default function Home() {
  return (
    <div className="mx-auto max-w-xl py-8">
      <h1 className="mb-8 text-center text-2xl font-black">Posts</h1>
      {allPosts.map((post) => (
        <article key={post._id} className="mb-8">
          <h2 className="text-xl">
            <Link href={post.url} className="text-blue-700 hover:text-blue-900">
              {post.title}
            </Link>
          </h2>
          <time
            dateTime={post.date}
            className="mb-2 block text-xs text-gray-600"
          >
            {new Date(post.date).toLocaleDateString('en-US', {
              day: 'numeric',
              month: 'long',
              year: 'numeric',
            })}
          </time>
          <p className="relative z-10 mt-2 text-sm text-zinc-600 dark:text-zinc-400">
            {post.description}
          </p>
        </article>
      ))}
    </div>
  )
}

allPosts is an array of posts made available by Contentlayer

When looping over allPosts, the type of each post is inferred as Post and you get nice autocomplete suggestions based on the fields specified in the contentlayer.config.js file! If you prefer, you can import the Post type from contentlayer/generated to declare the type explicitly. (For TypeScript projects)

import { allPosts, type Post } from 'contentlayer/generated'
 
allPosts.map((post: Post) => ...)
TypeScript autocomplete suggestions in posts loop

Now, when you visit http://localhost:3000, you should see a post feed on the home page.

Add Dynamic Post Page

Currently, when clicking on a post, we get a 404. Let's make a dynamic page in Next.js at app/blog/[slug]/page.tsx and use the code below.

app/posts/[slug]/page.tsx
import type { Metadata } from 'next'
import Image from 'next/image'
import { notFound } from 'next/navigation'
import { allPosts } from 'contentlayer/generated'
import { useMDXComponent } from 'next-contentlayer/hooks'
 
export const generateStaticParams = async () =>
  allPosts.map((post) => ({ slug: post._raw.flattenedPath }))
 
export const generateMetadata = ({
  params,
}: {
  params: { slug: string }
}): Promise<Metadata | undefined> => {
  const post = allPosts.find((post) => post._raw.flattenedPath === params.slug)
 
  if (!post) return
 
  return {
    title: post.title,
    description: post.description,
  }
}
 
export default function Post({ params }: { params: { slug: string } }) {
  const post = allPosts.find((post) => post._raw.flattenedPath === params.slug)
 
  if (!post) notFound()
 
  const MDXContent = useMDXComponent(post.body.code)
  return (
    <article className="mx-auto max-w-xl py-8">
      <div className="mb-8 text-center">
        <time dateTime={post.date} className="mb-1 text-xs text-gray-600">
          {new Date(post.date).toLocaleDateString('en-US', {
            day: 'numeric',
            month: 'long',
            year: 'numeric',
          })}
        </time>
        <h1 className="text-3xl font-bold">{post.title}</h1>
      </div>
      <div className="prose mt-8 dark:prose-invert prose-headings:font-display prose-a:text-cyan-500 prose-img:rounded-2xl">
        <MDXContent components={{ Image }} />
      </div>
    </article>
  )
}

Hot Tip: If an if statement only has one line, you can omit the curly brackets.

Congratulations! You now have a basic blog starter with Next.js and Contentlayer. This is only the starting point. You can explore and incorporate all the necessary features of Contentlayer. Their documentation is a great place to learn.