reading and coding ...

Next.js + MDX でブログを書いています

12/8/2019, 3:02:14 PM - 223 days ago

Next.js Advent Calendar 2019 9日目の記事です。

今年の2月に Next.js + MDX でブログシステムを構築しました。
あなたが今見ているこのブログは Next.js 製です。
リポジトリは以下の通りです👇

7ma7X/hellorusk.net: Blog (Next.js + MDX)

日本においては Next.js でブログやポートフォリオを作成している人は GatsbyJS などと比較するとあまり居らず、ましてや Next.js + MDX でブログを書いている人は珍しいという状況かと思います。
私もこのブログを作り始めた頃は、日本語の記事がほぼ皆無という中で、海外の流行の最先端を追うような気持ち(?)で英語のドキュメントやリポジトリを読み漁ったものでした。

ややマイナーな Next.js + MDX ですが、私はとても気に入っています。
Next.js は(他の Now などの ZEIT のプロダクトと同様)シンプルさを売りにしているため、その分フレキシブルに楽しくブログをカスタマイズしていくことができます。
今回 Next.js + MDX でのブログの書き心地を紹介していくことで、日本でも Next.js や MDX を使う人が少しでも増えればなあと(勝手に)願っています。


What is MDX ?

MDX は Markdown に直接 JSX を挿入できるフォーマットです。拡張子は .mdx

mdx-js/mdx: JSX in Markdown for ambitious projects

例えば、こんな見た目をしています。

import Button from '../components/button.js'

# MDX + Next.js

Look, a button! 👇

<Button>👋 Hello</Button>

Markdown の中で、普通に React の import 文が書けたり、Component が使えたりするということですね。


私のこのブログにおいては、以下のような MDX ファイルをテンプレートとして用意しています。

import BlogLayout from "../../components/blog-layout";
import BlogMeta from "../../components/blog-meta";

export const meta = {
  date: '2019-12-09',
  title: 'Next.js + MDX でブログを書いています',
  url: '/blog/2019/12/09',
  description: '2019-12-09 の日記'
}

# Next.js + MDX でブログを書いています
#### 2019-12-09

ここにブログの本文があります

<BlogLayout meta={meta} />
<BlogMeta meta={meta} />

まず、先ほどの例と同様に import 文を使っています。
BlogLayout はブログのレイアウトの CSS を styled-jsx で記述したファイルです。
BlogMeta は OGP の部分ですね。他にも、各記事の末尾にあるツイートボタンを実現しているのもこの Component です。

次に、meta というオブジェクトを export している点に注目です。MDX はこのように export 文も使えるので、他の js ファイルからMDXを import できるのです。
この仕組みは、ブログ記事一覧ページを作るのに大いに役立っています。


さて、このような MDX ですが、Next.js と合わせて使うのはとっても簡単です。Next.js では公式プラグインとして @next/mdx があり、使い方としては

npm install @zeit/next-mdx

してから next.config.js

const withMDX = require('@next/mdx')();

module.exports = withMDX({
  pageExtensions: ['tsx', 'mdx'],
})

このように編集するだけです。

Next.js + MDX + Syntax Highlight

既にお気づきになっているように、このブログにもシンタックスハイライトが効いているのが分かると思います。
これは

npm install @mdx-js/loader
npm install @mapbox/rehype-prism

してから withMDX の部分を

const rehypePrism = require("@mapbox/rehype-prism");
const withMDX = require("@zeit/next-mdx")({
  options: {
    rehypePlugins: [rehypePrism]
  }
});

このように編集しています。

具体的な色付けは Prism.js の CDN を入れることで行います。
直接 MDX に書いてもいいですし、_document.js(私の場合は TypeScript 化しているので _document.tsx)に

<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.9.0/themes/prism-tomorrow.min.css" rel="stylesheet"/>

を追加するのでも良いかと思います(私は当初は後者を採用していましたが、パフォーマンスの観点から前者に切り替えました)。

Next.js + MDX + LaTeX

TeX\TeX が使えるとテンションが上がりますね。

(k=1nakbk)2(k=1nak2)(k=1nbk2)\displaystyle \left( \sum_{k=1}^n a_k b_k \right)^2 \leq \left( \sum_{k=1}^n a_k^2 \right) \left( \sum_{k=1}^n b_k^2 \right)

MDX ではこのように数式も美しく書くことができます。
これは

npm install @mdx-js/loader
npm install remark-math
npm install rehype-katex

してから withMDX の部分を

const remarkMath = require("remark-math");
const rehypeKatex = require("rehype-katex");
const withMDX = require("@zeit/next-mdx")({
  options: {
    remarkPlugins: [remarkMath],
    rehypePlugins: [rehypeKatex]
  }
});

このように編集し、さらに KaTeX CSS

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.11.0/dist/katex.min.css"/>

_document.js に挿入しています。
こうすれば MDX に

$\TeX$ が使えるとテンションが上がりますね。
$$
\displaystyle \left( \sum_{k=1}^n a_k b_k \right)^2 \leq \left( \sum_{k=1}^n a_k^2 \right) \left( \sum_{k=1}^n b_k^2 \right)
$$

と書くだけで上の数式が再現できます。


ここまで見てきたように、MDX はオプションを指定することで remarkrehype のプラグインのエコシステムの恩恵を得られるという特長があります。
私もまだこの部分は深掘りしていないので、さらに Next.js + MDX を拡張していける可能性がありそうです。


Next.js + MDX + Embedded Tweet

ツイートの埋め込みはやりたくなると思います。
が、そもそも論として、クライアントサイドで色々やる <iframe> とサーバーサイドの Node.js/React の相性は悪い...
特に、Twitter は widget.js という js を読み込んで <iframe> を展開しますが、そのまま貼り付けようとすると中々上手くいきません。

例えば、

https://twitter.com/hashimoto_lo/status/340640143058825216

このツイートの埋め込み html は

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">スマイルプリキュア</p>&mdash; 橋下徹 (@hashimoto_lo) <a href="https://twitter.com/hashimoto_lo/status/340640143058825216?ref_src=twsrc%5Etfw">2013年6月1日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

となっていますが、これを MDX にそのまま貼り付けても上手くいきません。


ではどうしているかというと、私は react-twitter-embed を使っています。
このモジュールを使えば

import { TwitterTweetEmbed } from "react-twitter-embed";

<TwitterTweetEmbed
  tweetId={'340640143058825216'}
/>

と書くと以下のように

表示されます。MDX は JSX をそのまま使えるというメリットを活かした形です。


まとめ

Next.js では

  • ゼロコストで MDX を導入できる
  • MDX を使えばシンタックスハイライトもできる
  • MDX を使えば TeX も打てる
  • MDX を使えばツイートも埋め込める

ことを紹介しました。
この記事が Next.js + MDX を検討している方の参考に少しでもなれば幸いです。

2020.04.05 追記

ブログシステムを自作のものから unix/unix.bio に移管しました。
こちらも Next.js + MDX で構成されているブログシステムですが、作者の witt 氏が ZEIT 社風の非公式の UI コンポーネント zeit-ui の設計を手掛けていることもあって、とても洗練されておりオススメです。