home

Next.jsで作成したブログをhtmlからマークダウンに切り替えた

独自で作成したブログの記事をNotionから出力したhtmlを表示させていたが <code> タグが扱いにくかったのでので悩んでいた…そこでmarkdown形式のexportにも対応していたのを知り試してみることにした。

Next.jsのAppRouterでもマークダウンファイル( .md or .mdx )をHTMLに変換にしてくれるみたいなのでドキュメントを見ながら実装してみた。

※サーバーコンポーネントでも使用可能

QuickStart

  1. まずは以下をインストール
npm install @next/mdx @mdx-js/loader @mdx-js/react @types/mdx
  1. next.config.mjs に以下を記載
import createMDX from '@next/mdx'
 
/** @type {import('next').NextConfig} */
const nextConfig = {
  // Configure `pageExtensions` to include markdown and MDX files
  pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'],
  // Optionally, add any other Next.js config below
}
 
const withMDX = createMDX({
  // Add markdown plugins here, as desired
})
 
// Merge MDX config with Next.js config
export default withMDX(nextConfig)
  1. mdx-components.tsx を作成し以下を記載。
import type { MDXComponents } from 'mdx/types'
 
export function useMDXComponents(components: MDXComponents): MDXComponents {
  return {
    ...components,
  }
}
  1. /markdown にマークダウンファイルを格納し app/mdx-page/page.tsx を以下のように記載すればマークダウンがhtmlとして表示される
// ディレクトリ構造
  my-project
  ├── app
  │   └── mdx-page
  │       └── page.(tsx/js)
  ├── markdown
  │   └── welcome.(mdx/md)
  |── mdx-components.(tsx/js)
  └── package.json
  

// app/mdx-page/page.tsx
import Welcome from '@/markdown/welcome.mdx'
 
export default function Page() {
  return <Welcome />
}

※補足

そのままだとスタイルがあたってなくて質素なのでプラグインの @tailwindcss/typography を使用するとマシになるのでおすすめ。( tailwind.config.ts に以下を書くだけなので楽)

// tailwind.config.ts
mport type { Config } from "tailwindcss";

const config: Config = {
	// ...
  plugins: [require("@tailwindcss/typography")],
};
export default config;

htmlに変換する前にタグをカスタムしたい場合

残念ながらNotionから出力したマークダウンファイルにはタグの細かい属性が設定されていない。

なので以下のようにhtmlに出力する前に属性を設定する。

// mdx-components.tsx
import type { MDXComponents } from 'mdx/types'
import Image, { ImageProps } from 'next/image'
 
export function useMDXComponents(components: MDXComponents): MDXComponents {
  return {
    a: (props) => (
	    <a 
	      {...props}
	      target='_blank' rel='noopener noreferrer'
	      aria-label={`${props.href} へのリンク`}
	    >
	      {props.children}
	    </a>
	  ),
    ...components,
  }
}

あと属性だけでなくスタイルも追加できる。

import type { MDXComponents } from 'mdx/types'
import Image, { ImageProps } from 'next/image'
 
export function useMDXComponents(components: MDXComponents): MDXComponents {
  return {
    h1: ({ children }) => (
      <h1 style={{ color: 'red', fontSize: '48px' }}>{children}</h1>
    ),
    img: (props) => (
      <Image
        sizes="100vw"
        style={{ width: '100%', height: 'auto' }}
        {...(props as ImageProps)}
      />
    ),
    ...components,
  }
}

マークダウンからメタデータを出力したい場合

ブログ記事へ遷移する為のリンクリストを表示する際に、タイトルと日付を表示したかったのでメタデータから抽出する方法を調べて見た。

ドキュメントによるとFrontmatterの gray-matter を使用すればメタデータを抽出できるっぽい。

  1. gray-matter をインストール
npm install --save gray-matter
  1. マークダウンファイル( .md or .mdx )の最上部に以下の記述を追加する
---
title: "GWにしたこと"
date: "2024.05.07"
---

// ここから下はマークダウン
  1. 取得したいコンポーネント or ページで以下のように記述
const filePath = // ここに取得する.mdx or .md のディレクトリパス
const fileContents = fs.readFileSync(filePath, 'utf8');
const { data: metadata } = matter(fileContents);

console.log(metadata) // { title: 'GWにしたこと', date: '2024.05.07' }

マークダウンに記述したメタデータがhtmlにも表示されてしまう

メタデータ を QuickStart の方法で実装すると、メタデータがhtmlにも表示されてしまう問題が発生した。これはうざいので他の方法を考えてみる。

調べてみるとNext.jsのドキュメント<MDXRemote/> コンポーネントを使用する方法があったのでやってみた。

import { MDXRemote } from 'next-mdx-remote/rsc'
import matter from 'gray-matter';
 
export default function RemoteMdxPage() {
	const filePath = // ここに取得する.mdx or .md のディレクトリパス
	const fileContents = fs.readFileSync(fullPath, 'utf8');
	const { data: metadata, content } = matter(fileContents);
  return <MDXRemote source={content} />
}

このように gray-mattercontent のみ取得できたのでそれを <MDXRemote source={content}/> にpropsで渡すだけでいい。めちゃ楽。

MDXRemoteでタグの属性やスタイルを追加する

<MDXRemote/> を使用すると先ほど記述した useMDXComponents が使用できない&タグをカスタムできなくなる。

なので独自で <CustomMDX> コンポーネントを作成してタグに属性とスタイルを追加する。

// CustomMDX.tsx
import { MDXRemote, MDXRemoteProps } from 'next-mdx-remote/rsc'
import type { MDXComponents } from 'mdx/types'
import Image, { ImageProps } from 'next/image'

const components: MDXComponents = {
  h1: ({ children }) => (
    <h1 style={{ color: 'red', fontSize: '48px' }}>{children}</h1>
  ),
  img: (props) => (
    <Image
      sizes="100vw"
      style={{ width: '100%', height: 'auto' }}
      {...(props as ImageProps)}
    />
  ),
  a: (props) => (
    <a 
      {...props}
      target='_blank' rel='noopener noreferrer'
      aria-label={`${props.href} へのリンク`}
    >
      {props.children}
    </a>
  )
}

export function CustomMDX(props: MDXRemoteProps) {
  return (
    <MDXRemote
      {...props}
      components={{ ...components, ...(props.components || {}) }}
    />
  )
}

// page.tsx
export default function RemoteMdxPage() {
	const filePath = // ここに取得する.mdx or .md のディレクトリパス
	const fileContents = fs.readFileSync(fullPath, 'utf8');
	const { data: metadata, content } = matter(fileContents);
  return <CustomMDX source={content} />
}

これでやっとうまくいった。

さらにDynamicRoutingで全てサーバーコンポーネントで実装できたのでありがたい…

ただ、気のせいかもしれないがLighthouseのパフォーマンスは低下した気がする…(気のせいであって欲しい。)

Monkey Logo

konpay.eth

福井県出身のエンジニアです。31歳です。田舎からフロントエンドを開発しています。TpeScript, React をよく書きます。バックエンドは Node.js をお遊び程度で書いています。将来的には TypeScript と Node.js でフルスタック開発をしたいです。仮想通貨や NFT など Web3.0 領域が好きです。