Next.js (Pages Router) and Contentlayer Setup

4 min read
Dec 21, 2024

Building a Scalable MDX Blog with Next.js and Contentlayer

As engineering leads, one of our recurring challenges is balancing scalability, readability, and extensibility when building content-heavy frontend applications. A lightweight, composable blog system using Next.js (Pages Router) and Contentlayer checks all the right boxes—type safety, statically generated performance, and full MDX control.

In this post, you'll set up a production-grade foundation for a statically rendered blog using Contentlayer and Next.js, with an emphasis on correctness, long-term maintainability, and modern DX.


Project Structure

Here’s the minimal directory layout you’ll be working with:

.
├── public/
│   └── content/
│       └── blog/
│           └── markdown-syntax-guide/
│               └── index.mdx
├── contentlayer.config.ts
├── tsconfig.json
└── next.config.ts

We place content under public/content/ for long-term portability (e.g., CDN assets), though contentlayer supports custom locations.


Step 1: Install Dependencies

Install the following packages to enable MDX parsing, syntax highlighting, and advanced remark/rehype plugins:

npm install \
  contentlayer next-contentlayer \
  shiki rehype-shiki rehype-pretty-code \
  rehype-autolink-headings rehype-slug rehype-rewrite rehype-stringify \
  remark-gfm unified \
  date-fns reading-time

Step 2: Configure Contentlayer

In your root, create a contentlayer.config.ts file:

import { defineDocumentType, makeSource } from '@contentlayer/source-files'
import rehypeSlug from 'rehype-slug'
import rehypeAutolinkHeadings from 'rehype-autolink-headings'

const Blog = defineDocumentType(() => ({
  name: 'Blog',
  filePathPattern: 'blog/**/*.mdx',
  contentType: 'mdx',
  fields: {
    title: { type: 'string', required: false },
    publishedAt: { type: 'date', required: false },
    description: { type: 'string', required: false },
    isPublished: { type: 'boolean', default: false },
  },
  computedFields: {
    url: {
      type: 'string',
      resolve: (doc) => `/${doc._raw.flattenedPath}`,
    },
  },
}))

export default makeSource({
  contentDirPath: './public/content',
  documentTypes: [Blog],
  mdx: {
    rehypePlugins: [
      rehypeSlug,
      [rehypeAutolinkHeadings, { behavior: 'append' }],
    ],
  },
})

This schema gives you full control over per-document metadata while ensuring all types remain statically verifiable.


Step 3: Update tsconfig.json

Add the generated types from Contentlayer:

{
  "compilerOptions": {
    "target": "ES2018",
    "paths": {
      "@/*": ["./src/*"],
      "contentlayer/generated": ["./.contentlayer/generated"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules"]
}

This ensures IDE autocompletion and compile-time checks for your MDX content.


Step 4: Extend next.config.ts

Wrap the config with withContentlayer for seamless integration:

import type { NextConfig } from 'next'
import { withContentlayer } from 'next-contentlayer'

const nextConfig: NextConfig = {
  reactStrictMode: true,
}

export default withContentlayer(nextConfig)

Step 5: Write Your First Blog Post

Place your first MDX file at: /public/content/blog/markdown-syntax-guide/index.mdx

---
title: "Markdown Syntax Guide"
publishedAt: "2022-04-09"
updatedAt: "2022-04-09"
description: List of markdown patterns and examples for structured writing in MDX.
author: "Gautam Ankoji"
username: "gautamankoji"
isPublished: true
tags:
  - markdown
  - syntax
---

Markdown is a lightweight markup language that allows you to format text in a plain-text editor while still producing structured and readable output. It’s widely used for documentation, blogging, and developer-focused content.

Step 6: Render Blog Index

Create a document listing view:

// pages/docs/index.tsx

import Link from 'next/link'
import { allDocuments } from 'contentlayer/generated'

const Docs = () => (
  <div>
    <h1>Docs</h1>
    <ul>
      {allDocuments.map((doc) => (
        <li key={doc.url}>
          <Link href={doc.url}>{doc.title}</Link>
        </li>
      ))}
    </ul>
  </div>
)

export default Docs

Step 7: Render Individual Blog Pages

// pages/docs/[slug].tsx

import { allDocs, Doc } from 'contentlayer/generated'
import { useMDXComponent } from 'next-contentlayer/hooks'

const DocsPage = ({ doc }: { doc: Doc }) => {
  const MDXContent = useMDXComponent(doc.body.code)

  return (
    <main>
      <h1>{doc.title}</h1>
      <MDXContent />
    </main>
  )
}

export async function getStaticPaths() {
  return {
    paths: allDocs.map((doc) => ({
      params: { slug: doc._raw.flattenedPath.replace('docs/', '') },
    })),
    fallback: false,
  }
}

export async function getStaticProps({ params }: { params: { slug: string } }) {
  const doc = allDocs.find(
    (doc) => doc._raw.flattenedPath === `docs/${params.slug}`
  )
  if (!doc) return { notFound: true }

  return { props: { doc } }
}

export default DocsPage

Final Result

Start your dev server:

npm run dev

Visit:

http://localhost:3000/docs

You’ll see your content list. Click a link to view the full blog page.


Closing Thoughts

This setup provides a robust foundation for building content-focused apps. By using Contentlayer with Next.js, you retain:

  • Static performance (SSG)
  • Typed Markdown documents
  • Easily composable content system

You can layer on features like full-text search, RSS feeds, dynamic theming, or analytics without disrupting the core.

In future iterations, consider extending this with a CMS like TinaCMS or pushing content management into a headless repo-backed system like [GitHub Issues-as-CMS].

Let your blog scale the way your codebase does—cleanly, predictably, and with type safety in every step.

Wanna Discover more Interesting Topics?
markdown
Apr 9, 2022

Markdown Syntax Guide

Markdown is a lightweight markup language that allows you to format text in a plaintext editor while still having a structured and readable output. It is often used for writing documentation, readme files, blog posts, and other content where readability and simplicity are important. Markdown syntax is simple and intuitive, making it an ideal choice for writing and formatting text without the need for complex HTML tags. Its flexibility and ease of use make it a popular tool among developers, writers, and content creators alike. By using basic symbols and characters, you can structure documents that are both humanreadable and machinereadable, w
nextjs
Dec 21, 2024

Next.js (Pages Router) and Contentlayer Setup

In this blog we will look how to setup Next.js and Contentlayer for an Blog page. Step one Install necessary deps bash npm install \ contentlayer nextcontentlayer \ shiki rehypeshiki rehypeprettycode \ rehypeautolinkheadings rehypeslug rehyperewrite rehypestringify \ remarkgfm unified \ datefns readingtime create contentlayer.config.ts file add contentlayer.config.ts in your root of the project and you have to create your blog files in this folder folder strcture of the necessary files and folders bash /public /content /blog /markdownsyntaxguide index.mdx contentlayer.config.ts tsconfig.json nextjs.config.ts ts import { defineDocumentType, ma
powershell
May 5, 2023

Configuring PowerShell (shell prompt)

In Windows PowerShell, customizing your shell prompt can enhance your development environment by providing valuable information at a glance. This guide walks you through configuring Windows PowerShell to display a customized prompt with the username, date, and current folder name. Checking for Existing Profile STEP 1: Before proceeding, check if you have an existing profile set up in PowerShell: powershell testpath $profile STEP 2: If the result is false, create a new profile: powershell newitem path $profile itemtype file force STEP 3: Now, open the profile in Notepad: powershell notepad $profile STEP 4: Paste the following function to displ