Next.jsでMDX形式のブログを作る

作成日

2025年2月17日

Nuxt2 のサポート終了に伴って Nuxt3 で構築し直していましたが,いろいろと問題が発生してしまったので,Next.js で作り直しました.

基本的にドキュメント通りに進め,要点のみ記載しています.

環境

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

@next/mdx を使う場合

frontmatter(MD / MDX ファイル上部の YAML 形式などの説明文)を使わない場合は,@next/mdx を利用すると楽に導入できます.

導入

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

参考:Configuring: MDX | Next.js

Remark and Rehype

テーブルなど,いくつかの HTML 要素に対応していないため,remark-gfm を利用する必要があります.

ソースコードをハイライトする場合は,rehype-highlight も必要です.

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

※公式ドキュメントによると,Turbopack を使っている場合はこれらのプラグインが利用できないようです.

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

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

next-mdx-remote を使う場合

導入

@next/mdx を使うと簡単に表示できますが,Frontmatter も表示されてしまいます.

YAML での Frontmatter を利用したい場合は,next-mdx-remote を使うと良いです.

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} />
}

サーバコンポーネントとしてレンダリングしたいため,rsc 版を読み込みます.

参考:GitHub - hashicorp/next-mdx-remote: Load MDX content from anywhere

MDX ファイルを読み込む

複数の MDX ファイルを処理したいときには,fsglobby などのパッケージを利用するように,との記載があります.

ターミナル上のカレントディレクトリを基準に,MDX ファイルを格納しているディレクトリのパスを指定します.

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', '') }
  })
}

HTML 要素のカスタマイズ

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

@next/mdx を使う場合と同様です.

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],
        },
      }}
    />
  )
}

このブログでは LaTeX も使った記事もあるため,以下のパッケージも利用しています.

OGP 画像の自動生成

画像の自動生成も簡単にできますが,ビルドしようとしたところ,以下のエラーが発生しました.

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

下記の記述を追加しました.

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

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

静的サイトとして出力

いわゆる SSG として出力します.

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

静的コンテンツを手元のサーバで動かすには,以下のように設定を変更する必要があります.

package.json
{
  "scripts": {
    "start": "npx serve@latest <directory name>"
  }
}
terminal
$ npm start

初回のコマンド実行時は,パッケージがインストールされます.

Google サービスの導入

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

MDX ファイルへの変換

従来は MD ファイルでしたが,今回は MDX ファイルを使用するため,記事の移行にあたって全てのファイルの拡張子を変更しました.

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

Sitemap 生成

参考:Metadata Files: sitemap.xml | Next.js

Lighthouse による採点

従来の Nuxt3 の採点は以下の通り.

Mobile

85Performance
81Accessibility
100Best Practices
87SEO
0-49
50-89
90-100

Measured by Lighthouse

PC

90Performance
81Accessibility
100Best Practices
90SEO
0-49
50-89
90-100

Measured by Lighthouse

それに対し,Next.jsの採点は以下の通り.

Mobile

92Performance
90Accessibility
82Best Practices
100SEO
0-49
50-89
90-100

Measured by Lighthouse

PC

99Performance
91Accessibility
100Best Practices
100SEO
0-49
50-89
90-100

Measured by Lighthouse

パフォーマンスはかなり向上しました.

Best Practicesの点数が著しく低下していますが,原因はGoogle系サービスのAPIがDeplicated APIとなっているためのようです.

Nuxt3 運用時からの変更・改善点

Nuxt を利用していたときは,ビルド時間が非常に長いという問題が生じていました.

Nuxt2 から Nuxt3 への移行が影響したわけではなく,日本語と英語に対応させていた結果,ビルド対象のファイルが非常に多くなってしまったことが原因です.

また,Nuxt3 への移行後,MD ファイルを追加するとビルドが終わらず,新しい記事を掲載できなくなっていました.

Next.js(App Router)への移行によりビルド時間が短縮されたほか,当然ながらビルドが終わらない問題も解決しました.

mktia's note

Research & Engineering / Blockchain / Web Dev

© 2017-2025 mktia. All rights reserved.