Next.jsで作成したブログをhtmlからマークダウンに切り替えた
独自で作成したブログの記事をNotionから出力したhtmlを表示させていたが <code>
タグが扱いにくかったのでので悩んでいた…そこでmarkdown形式のexportにも対応していたのを知り試してみることにした。
Next.jsのAppRouterでもマークダウンファイル( .md
or .mdx
)をHTMLに変換にしてくれるみたいなのでドキュメントを見ながら実装してみた。
※サーバーコンポーネントでも使用可能
QuickStart
- まずは以下をインストール
npm install @next/mdx @mdx-js/loader @mdx-js/react @types/mdx
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)
mdx-components.tsx
を作成し以下を記載。
import type { MDXComponents } from 'mdx/types'
export function useMDXComponents(components: MDXComponents): MDXComponents {
return {
...components,
}
}
/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
を使用すればメタデータを抽出できるっぽい。
gray-matter
をインストール
npm install --save gray-matter
- マークダウンファイル(
.md
or.mdx
)の最上部に以下の記述を追加する
---
title: "GWにしたこと"
date: "2024.05.07"
---
// ここから下はマークダウン
- 取得したいコンポーネント 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-matter
で content
のみ取得できたのでそれを <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のパフォーマンスは低下した気がする…(気のせいであって欲しい。)