A Guide

My MDX blog setup for Next.js

I'll share how this blog was built. It relies on Next.js pages directory and Tailwind. Tailwind is not required, just omit the classes and the final optional section. This blog saves me from having a barrel file where I need to add all of my new posts. All is handled dynamically from the filesystem so that you don't need to add new posts in multiple places. Just create a new file, write MDX and forget about it. It suports meta tags for SEO, pagination and syntax higlighting for code snippets. Overall I'm very happy with this setup.

Setup

Let's start by adding two packages. The remark-gfm package is for parsing and compiling GitHub Flavored Markdown (GFM) in Remark, and the @mapbox/rehype-prism one is for adding syntax highlighting to code blocks in HTML using Prism(code highlither) in Rehype.

npm install remark-gfm @mapbox/rehype-prism

You'll need to update your next.config.js. Since we're using ESM imports we'll need to rename next.config.js to next.config.mjs if you haven't done it before. See here for more information on Next and ES modules on your config file. Here's how it should look.

// File: next.config.js
import remarkGfm from 'remark-gfm'
import rehypePrism from '@mapbox/rehype-prism'
import nextMDX from '@next/mdx'

const withMDX = nextMDX({
  extension: /\.mdx?$/,
  options: {
    remarkPlugins: [remarkGfm],
    rehypePlugins: [rehypePrism],
  },
})

/** @type {import('next').NextConfig} */
const nextConfig = {
  pageExtensions: ['js', 'jsx', 'mdx'], 
  ...your other properties
}

export default withMDX(nextConfig)

Index page

To have an index page that dynamically populates we'll rely on Next.js getStaticProps paired with our own getAllArticles function.

// File: src/pages/blog/index.jsx

//This is just a part of the src/pages/blog/index.jsx file. Will add the complete file at the bottom to tie everything up.
export async function getStaticProps() {
  const { articles, totalPages } = await getAllArticles(1, 10)
  return {
    props: {
      articles,
      totalPages,
      currentPage: 1,
    },
  }
}

Here's how we dynamically load the blog posts form the filesystem. This is a great way of avoiding the manual hassle of having to add your new blog posts to a central file where all posts are listed. Just create a new file with the desired content and you're good to go!

// File: src/lib/getAllArticles.js
import glob from 'fast-glob'
import * as path from 'path'

async function importArticle(articleFilename) {
  let { meta, default: component } = await import(
    `../pages/blog/${articleFilename}`
  )
  return {
    slug: '/blog/' + articleFilename.replace(/(\/index)?\.mdx$/, ''),
    ...meta,
    // component,
  }
}

export async function getAllArticles(page = 1, perPage = 10) {
  let articleFilenames = await glob(['*.mdx', '*/index.mdx'], {
    cwd: path.join(process.cwd(), 'src/pages/blog'),
  })

  let articles = await Promise.all(articleFilenames.map(importArticle))
  return {
    articles: articles.sort((a, z) => new Date(z.date) - new Date(a.date)),
    totalPages: Math.ceil(articles.length / perPage),
  }
}

Pagination

For pagination to work we'll need a new route. This route will also use the getStaticProps as the index page does so but it will also need a getStaticPaths method so that it knows how many pages of content you have available.

//File src/pages/blog/page/[page].jsx

import { getAllArticles } from '@/lib/getAllArticles.js'
import BlogIndex from '@/pages/blog/index' // Notice we just import the index, no need to redefine it. Will post the index below to tie up everything up

export default BlogIndex

export async function getStaticPaths() {
  const { totalPages } = await getAllArticles(1, 10)
  const paths = Array.from({ length: totalPages }, (_, i) => ({
    params: { page: (i + 1).toString() },
  }))

  return { paths, fallback: false }
}

export async function getStaticProps({ params }) {
  const { articles, totalPages } = await getAllArticles(
    parseInt(params.page, 10),
    10
  )
  return {
    props: {
      articles,
      currentPage: parseInt(params.page, 10),
      totalPages,
    },
  }
}

The final index page for the blogs with pagination, no need for barrel files and great MDX setup looks like this.

// File: src/pages/blog/index.jsx

import Head from 'next/head'

import { Footer } from '@/components/Footer'
import { Header } from '@/components/Header'
import { getAllArticles } from '@/lib/getAllArticles'
import Link from 'next/link'

export default function BlogIndex({ articles, totalPages, currentPage }) {
  return (
    <>
      <Head>
        <title>Blog - Codereader</title>
        <meta
          name="description"
          content="The Ultimate Mobile Code Reading and Note-Taking App. Reading, exploring and discovery of code on your phone with our mobile app."
        />
      </Head>
      <Header />
      <main>
        <div className=" pt-24 sm:pt-32">
          <div className="mx-auto max-w-7xl px-6 lg:px-8">
            <div className="mx-auto max-w-2xl">
              <h2 className="text-3xl font-bold tracking-tight text-gray-300 sm:text-4xl">
                Our posts
              </h2>
              <p className="mt-2 text-lg leading-8 text-gray-200">
                This is a collection of our blog posts. We hope you enjoy them!
              </p>
              <div className="mt-10 space-y-16 border-t border-gray-200 pt-10 sm:mt-16 sm:pt-16">
                {articles.map((post, i) => (
                  <Post key={i} post={post} />
                ))}
              </div>
              <div className="mx-auto mt-20 mb-6 flex flex-row px-6">
                {currentPage > 1 ? (
                  <Link href={`/blog/page/${currentPage - 1}`}>
                    <a className="rounded-lg border border-zinc-500 py-2 px-4 text-zinc-300">
                      Previous
                    </a>
                  </Link>
                ) : (
                  <></>
                )}
                {currentPage < totalPages ? (
                  <Link href={`/blog/page/${currentPage + 1}`}>
                    <a className="rounded-lg border border-zinc-500 py-2 px-4 text-zinc-300">
                      Next
                    </a>
                  </Link>
                ) : (
                  <></>
                )}
              </div>
            </div>
          </div>
        </div>
      </main>
      <Footer />
    </>
  )
}

function Post({ post }) {
  return (
    <article className="flex max-w-xl flex-col items-start justify-between rounded-md px-6 py-4 hover:bg-zinc-600">
      <div className="flex items-center gap-x-4 text-xs">
        <time dateTime={post.datetime} className="text-gray-300">
          {post.date}
        </time>
      </div>
      <div className="group relative">
        <h3 className="mt-3 text-lg font-semibold leading-6 text-gray-300 group-hover:text-gray-100">
          <Link href={post.slug}>
            {post.title}
            {post.subtitle && ` - ${post.subtitle}`}
          </Link>
        </h3>
        <p className="line-clamp-3 mt-5 text-sm leading-6 text-gray-200">
          {post.description}
        </p>
      </div>
    </article>
  )
}

export async function getStaticProps() {
  const { articles, totalPages } = await getAllArticles(1, 10)
  return {
    props: {
      articles,
      totalPages,
      currentPage: 1,
    },
  }
}

Creating your first MDX post

Now with all the setup done you just need to create a new file, say src/pages/blog/mdx-blog-setup.mdx and you're good to go! Here's the first few lines of this exact blog post.

// File:  src/pages/blog/mdx-blog-setup.mdx 
import  BlogLayout  from '@/components/Blogs/BlogLayout'
export const meta = {
  date: '2023-10-11',
  title: 'My MDX blog setup for Next.js',
  subtitle: "A Guide",
  description:
   'A step-by-step guide on setting up MDX with Next.js for a seamless blogging experience.',
}

export default (props) => <BlogLayout meta={meta} {...props} />

Ill share how this blog was built. It relies on Next.js Pages dir and tailwind. Tailwind is not required but I like it, just omit it if you use something else. This blog saves me from having a barrel file where i need to add all of my new posts. All is handled dynamically from the filesystem so that you dont need to add new posts in multiple places. Just create a new file, write MDX and forget about it. It suports meta tags for SEO, pagination and syntax higlighting for code snippets.

## Setup 

Lets start by adding two packages

// remove the backslashes for your real site
\`\`\`shell
npm install remark-gfm @mapbox/rehype-prism
\`\`\`

Optional: styling with tailwindcss

If you happen to use tailwind, you can use "tailwind/prose" to get a great readable font and defaults.

Start by adding the package if you don't have it

npm install -D @tailwindcss/typography

Then you'll need to add the tailwind plugin and define your variables for the package to pick it up.

// File: tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
  plugins: [
    require('@tailwindcss/typography'),
    // ...
  ],
  theme: {
    // ...
    typography: (theme) => ({
      invert: {
        css: {
          // deleted this since we only have dark mode for now. See tailwindui/spotlight for original config
        },
      },
      DEFAULT: {
        css: {
          // Set your color variables here.
          '--tw-prose-body': theme('colors.zinc.300'),
          '--tw-prose-headings': theme('colors.zinc.200'),
          '--tw-prose-links': theme('colors.violet.400'),
          '--tw-prose-links-hover': theme('colors.violet.400'),
          '--tw-prose-underline': theme('colors.violet.400 / 0.3'),
          '--tw-prose-underline-hover': theme('colors.violet.400'),
          '--tw-prose-bold': theme('colors.zinc.200'),
          '--tw-prose-counters': theme('colors.zinc.200'),
          '--tw-prose-bullets': theme('colors.zinc.200'),
          '--tw-prose-hr': theme('colors.zinc.700 / 0.4'),
          '--tw-prose-quote-borders': theme('colors.zinc.500'),
          '--tw-prose-captions': theme('colors.zinc.500'),
          '--tw-prose-code': theme('colors.zinc.300'),
          '--tw-prose-code-bg': theme('colors.zinc.200 / 0.05'),
          '--tw-prose-pre-code': theme('colors.zinc.100'),
          '--tw-prose-pre-bg': 'rgb(0 0 0 / 0.4)',
          '--tw-prose-pre-border': theme('colors.zinc.200 / 0.1'),
          '--tw-prose-th-borders': theme('colors.zinc.700'),
          '--tw-prose-td-borders': theme('colors.zinc.800'),
         
        },
      },
    }),
  },

}

Written by Matias Andrade

@mag_devo

Download

Now Free

It‘s time to slay those downtimes.

download on play store