How to build a blog with Next.js and MDX
typescriptnextjsmdx

How to build a blog with Next.js and MDX

Using Next.js to build a blog is very easy!

Author logo

Aayush Bharti

Preface

The packages to be used:

How to create a blog

First, we create the Next.js project with the following command:

yarn create next-app nextjs-mdx-blog

Next, create the following file structure:

  • components/layout.jsx - Wrap all components in a container (optional, just the style)
  • data/blog/*.mdx - Blog Articles
  • lib/format-date.js - Format the date as MM DD, YYYY
  • pages/blog/[slug].jsx - Article page, using dynamic routes

How to handle Markdown files

First, const root - the root directory, and the process.cwd() method returns the current working directory of the Node.js process.

const root = process.cwd()

Next, create the POSTS_PATH constant for the path where the article files are stored:

import path from "path"
 
const POSTS_PATH = path.join(root, "data", "blog")
// Output: A:/nextjs-mdx-blog/data/blog

Then use fs to read the contents of that directory (all the file names under data/blog):

import fs from "fs"
 
export const allSlugs = fs.readdirSync(POSTS_PATH)
// Output: ['markdown.mdx', 'nextjs.mdx', 'react.mdx']

Now, write a function to remove the file extension (useful later):

export const formatSlug = slug => slug.replace(/\.mdx$/, "")
/**
 * Example: formatSlug('markdown.mdx')
 * Output: 'markdown'
 */

The next step is to get the article content by slug:

export const getPostBySlug = async slug => {
  const postFilePath = path.join(POSTS_PATH, `${slug}.mdx`)
  // Output: A:/nextjs-mdx-blog/data/blog/slug.mdx
  const source = fs.readFileSync(postFilePath)
 
  const { content, data } = matter(source)
  /*
   * Example:
   *  ---
   *  title: Hello
   *  slug: home
   *  ---
   *  <h1>Hello world!</h1>
   *
   * Return:
   *  {
   *    content: '<h1>Hello world!</h1>',
   *    data: {
   *      title: 'Hello',
   *      slug: 'home'
   *    }
   *  }
   */
 
  const mdxSource = await serialize(content)
 
  const frontMatter = {
    ...data,
    slug,
  }
 
  return {
    source: mdxSource,
    frontMatter,
  }
}

Get all blog posts

You can get all the articles to be displayed on the homepage:

export const getAllPosts = () => {
  const frontMatter = []
 
  allSlugs.forEach(slug => {
    const source = fs.readFileSync(path.join(POSTS_PATH, slug), "utf-8")
    const { data } = matter(source)
 
    frontMatter.push({
      ...data,
      slug: formatSlug(slug),
      date: new Date(data.date).toISOString(),
    })
  })
 
  return frontMatter.sort((a, b) => dateSortDesc(a.date, b.date))
}
 
const dateSortDesc = (a, b) => {
  if (a > b) return -1
  if (a < b) return 1
  return 0
}

Formatting Date

export const formatDate = date =>
  new Date(date).toLocaleDateString("en", {
    year: "numeric",
    month: "long",
    day: "numeric",
  })
/*
 * formatDate('2022-08-21T00:00:00Z')
 * Output: 'August 21, 2022'
 */

Home Page

import { formatDate } from "../lib/format-date"
import { getAllPosts } from "../lib/mdx"
import Link from "next/link"
 
export default function Home({ posts }) {
  return (
    <>
      <h1 className="mb-8 text-6xl font-bold">Blog</h1>
      <hr className="my-8" />
      <ul className="flex flex-col gap-3">
        {posts.map(({ slug, title, summary, date }) => (
          <li key={slug}>
            <Link href={`/blog/${slug}`}>
              <a className="block rounded-lg border border-solid border-gray-300 p-6 shadow-md">
                <div className="flex justify-between">
                  <h2>{title}</h2>
                  <time dateTime={date}>{formatDate(date)}</time>
                </div>
                <p className="mt-4">{summary}</p>
              </a>
            </Link>
          </li>
        ))}
      </ul>
    </>
  )
}
 
// Use getStaticProps to get all articles
export const getStaticProps = async () => {
  const posts = getAllPosts()
 
  return {
    props: {
      posts,
    },
  }
}

Article Page

import { formatDate } from "../../lib/format-date"
import { allSlugs, formatSlug, getPostBySlug } from "../../lib/mdx"
import { MDXRemote } from "next-mdx-remote"
 
export default function Blog({ post }) {
  const { title, date } = post.frontMatter
 
  return (
    <div>
      <h1 className="mb-2 text-6xl font-bold">{title}</h1>
      <time dateTime={date} className="text-lg font-medium">
        {formatDate(date)}
      </time>
      <hr className="my-8" />
      <article className="prose max-w-none">
        <MDXRemote {...post.source} />
      </article>
    </div>
  )
}
 
export const getStaticProps = async ({ params }) => {
  const post = await getPostBySlug(params.slug)
 
  return {
    props: {
      post,
    },
  }
}
 
export const getStaticPaths = async () => {
  const paths = allSlugs.map(slug => ({
    params: {
      slug: formatSlug(slug),
    },
  }))
 
  return {
    paths,
    fallback: false,
  }
}

On this page

Contact Drawer

OPEN TO WORK · OPEN TO WORK ·
OPEN TO WORK · OPEN TO WORK ·
Aayush Bharti Logo

FROM CONCEPT TO CREATION

LET's MAKE IT HAPPEN!

Get In Touch

I'm available for full-time roles & freelance projects.

I thrive on crafting dynamic web applications, and
delivering seamless user experiences.