Create Blog with Next.js + MDX

Created at

2025.2.17

I reconstruct my blog with Next.js. The previous site is composed with Nuxt3 but some problems happened.

Environment

  • Node.js: v22.13.1
  • Next.js: 15.1.6 (App Router)

Use @next/mdx

If you don't use frontmatter (sentences that explain the content on the top of a MD/MDX file such as YAML), @next/mdx is useful.

Installation

terminal
$ npm install @next/mdx @mdx-js/loader @mdx-js/react @types/mdx

ref. Configuring: MDX | Next.js

Remark and Rehype

Some elements, such as table, are not supoorted by @next/mdx so you should use remark-gfm.

If you want to highlight the code, it is also neccessary to configure rehype-highlight.

terminal
$ npm i remark-gfm rehype-highlight
next.config.js
const withMDX = createMDX({
  options: {
    remarkPlugins: [['remark-gfm']],
    rehypePlugins: [['rehype-highlight']],
  },
})

* According to the documentation, remark and rehype plugins are not supported if you use Turbopack. The below code is not working.

next.config.js
import remarkGfm from 'remark-gfm'
import rehypeHighlight from 'rehype-highlight'

const withMDX = createMDX({
  options: {
    remarkPlugins: [remarkGfm],
    rehypePlugins: [rehypeHighlight],
  },
})

Use next-mdx-remote

Installation

@next/mdx package is provided to convert MDX but the frontmatter section is also displayed.

If you want to show the content that frontmatter exclude, you should use next-mdx-remote package.

terminal
$ npm i next-mdx-remote
next.config.js
const nextConfig: NextConfig = {
  transpilePackages: ['next-mdx-remote'],
}
page.tsx
import { MDXRemote } from 'next-mdx-remote/rsc'

const content = 'This is **MDX** style.'

export default async function Page() {
  return <MDXRemote source={content} />
}

Probably, 'rsc' means rendering server component. If you directly call MDXRemote from next-mdx-remote, it is used as the client component.

ref. GitHub - hashicorp/next-mdx-remote: Load MDX content from anywhere

Call the contents from MDX files

In the documentation, you must use fs or globby package to process the MDX files.

page.tsx
import fs from 'fs'
import path from 'path'

...

export function generateStaticParams() {
  const dirPath = path.join(process.cwd(), 'src/app/content/')
  const posts = fs.readdirSync(dirPath)

  return posts.map((n) => {
    return { slug: n.replace('.mdx', '') }
  })
}

Customize Each HTML Elements

mdx-remote.tsx
import { MDXRemote } from 'next-mdx-remote/rsc'

const components = {
  h2: (props: { children: string }) => (
    <h2 className="font-medium text-2xl">{props.children}</h2>
  ),
}

export function CustomMDX(props: { source: string; [key: string]: any }) {
  return (
    <MDXRemote
      {...props}
      components={{ ...components, ...(props.components || {}) }}
    />
  )
}

Frontmatter

terminal
$ npm i gray-matter
page.tsx
import matter from 'gray-matter'
import { CustomMDX } from '../mdx-remote'

export default async function Page({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const slug = (await params).slug
  const { data, content } = matter.read(
    path.join(process.cwd(), `src/app/content/${slug}.mdx`)
  )

  return <CustomMDX source={content} />
}

Remark and Rehype

Some elements, such as table, are not converted with next-mdx-remote package so you should use remark-gfm.

If you want to highlight the code, it is also neccessary to configure rehype-highlight.

terminal
$ npm i remark-gfm rehype-highlight
mdx-remote.tsx
import { MDXRemote } from 'next-mdx-remote/rsc'
import remarkGfm from 'remark-gfm'
import rehypeHighlight from 'rehype-highlight'

export function CustomMDX(props: { source: string; [key: string]: any }) {
  return (
    <MDXRemote
      {...props}
      components={{ ...components, ...(props.components || {}) }}
      options={{
        mdxOptions: {
          remarkPlugins: [remarkGfm],
          rehypePlugins: [rehypeHighlight],
        },
      }}
    />
  )
}

Some articles include LaTeX to write the mathematical formura so I also use these packages that it shows LaTeX as HTML.

Auto-Generate Images for OGP

When I build the app, the below error happened.

Error: export const dynamic = "force-static"/export const revalidate not configured on route "/opengraph-image" with "output: export".

Added the following code in opengraph-image.tsx.

opengraph-image.tsx
export const dynamic = 'force-static'

ref. Metadata Files: opengraph-image and twitter-image | Next.js

Configuration to Export Static Site

If you use the static rendering mode, the following option is required.

next.config.ts
const nextConfig: NextConfig = {
  output: 'export',
}
terminal
$ npm run build

Start rendering using exported files with the following command.

terminal
{
  "scripts": {
    "start": "npx serve@latest out"
  }
}
terminal
$ npm start

When you run this command for the first time, serve package is installed.

Import Google Services

Google Analytics

ref. Using Google Analytics with Next.js (through next/script) | Next.js

Google AdSense

ref. How to implement Google Adsense on App Router (Next.js) | by Kamo Tomoki | Medium

Convert the extension of files from MD to MDX at one time

I used MD files in my blog using Nuxt3, so I converted the files from MD to MDX with the following command.

terminal
$ for filename in *.md; do mv "$filename" "${filename%.md}.mdx"; done

Generate Sitemap

ref. Metadata Files: sitemap.xml | Next.js

mktia's note

Research & Engineering / Blockchain / Web Dev

© 2017-2025 mktia. All rights reserved.