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?
powershell
Nov 12, 2022

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: Check for an Existing Profile Run the following command to check if you have an existing profile set up in PowerShell: ```powershell test-path $profile ``` ### STEP 2: Create a New Profile (if needed) If the result is `false`, create a new profile: ```powershell new-item -path $profile -itemtype file -force ``` ### STEP 3
markdown
Apr 9, 2022

Markdown Syntax Guide

Markdown is a lightweight markup language that allows you to format text in a plain-text 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 human-readable and machine-readable
Code-Block-HTML
Dec 21, 201

Custom Content Code Block Using HTML CSS JS

Code blocks are an integral part of developer-facing content, enabling clear and organized presentation of code. In this guide, we will walk through creating custom, aesthetically pleasing code blocks with both single-tab and multi-tab functionality using modern web technologies. This guide is targeted at developers with a working knowledge of HTML, CSS, and JavaScript. ### Why Custom Code Blocks? While libraries like Prism.js and Highlight.js provide excellent syntax highlighting, creating a custom code block allows for: - **Complete Control**: Tailor the design and functionality to your needs. - **Seamless Integration**: Align the blocks wi