MDX or Markdown ?
TL;DR
Next.jsを使ってブログを書く際に、Next.jsはmdxフォーマットをサポートしている@next/mdxがあります。
mdxの特徴として、
- Markdown内でJSXがかける
- 外部のコンポーネントをimportできる
- 内部の変数などをexportできる
というものがあります。しかし、ブログ記事を書くだけならこれらの機能はオーバースペックに見えます。
記事書くだけならそんなに拡張性はいらない気がします。なので、今回は記事を書くときにmdxを採用しなかった理由について書きます。
簡単にまとめると、以下です。
Next.jsのdynamic importが動かなかったので、メタデータのexportができない。- VSCodeの拡張の補完がMarkdownと比べてかなり弱い。
これらの理由は単純に発展途上であることに起因する問題なので、これ以後の流行りによっては改善されていくと期待されます。しかし、現時点では問題です。
なので、このブログではmd拡張子を使って記事を書き、レンダリングする際にmdxへと変換しています。また、exportの代わりにfrontmatterを使うことでメタデータを表現しています。
mdxのデメリット
個人的にブログ書くときにmdxを使う際に不安だったのが、
上のようなコードです。サイトなどを作る際にはすごく便利に見えます。しかし、記事のテンプレートとしての統一がしにくくて、<BarChart>とかをもし変更してしまうと割と容易にエラーが出たりしそうでちょっといやだなーという感じでした。
また、もう1つの理由が、mdxは、
みたいなことができます。しかし、dynamic importするとうまくexportされたメタデータを取得できませんでした。
いろんなブログとか見たんですけど、exampleが下の感じで、
見た目いけそうなんですけど、実際にはエラーが起こってうまく動かない。なので、記事用のコンポーネントとして切り出すときにexportなどが使えないので日付やタイトルなどのメタデータを扱いにくいという問題点があります。
また、記事を書くときに不満なのが、VSCodeのmdx向けの拡張です。markdownならVSCodeのExtensionとかが充実しているので、補完やlintなども効いて生産効率が非常に高いです。しかし、mdxは補完がまだまだかなーという印象を受けました。
なので、ブログ記事はできるだけ純粋なMarkdownで書けるようにしたいです。
mdxのメリット
Next.jsでmdxを使うメリットとして、Next.jsのmdxに関するレンダリングシステムが挙げられます。Next.jsでは、mdxをpages/posts/hoge.mdxにおくとlocalhost:3000/posts/hogeにmdxがページとしてレンダリングされます。また、remarkとかrehype系のプラグインをnext.config.jsに書くだけでMdxに対して適用できます。
とはいえ、これらのシステムはMarkdownにもNext.jsで適用できます。つまり、next.config.jsにremark, rehypeプラグインを書くだけでMarkdownを容易に拡張でき、ページとしてレンダリングできます。そのためにやることはシンプルで、next.config.js内のpageExtensionsに.mdを加えるだけです。
例えば、以下のような感じで、コードシンタクスとかkatexとかに対応でき、Markdownをレンダリングできます(参考)
next.config.js
もしMarkdownを自前でレンダリングするなら、react-markdownを使ったり、remarkとrehypeでパースしたHTMLをdangerousInnerHTMLで埋め込むことになります。それに比べると、@next/mdxを利用するのが非常に楽な方法だという印象を受けました。
レンダリングする方法
Markdownをレンダリングする方法を一番素直に考えると、以下のようなコードが想定されます。[postId].jsx的な感じです。contentLoaderは名前で察してくれるとありがたいです。
getStaticPathsでfsモジュールを使ってmarkdownファイルの一覧を取得します。その後、getStaticPropsでファイルの場所に戻して、ついでにメタデータをとってきて、そのパスに対応するmarkdownファイルと、Layoutにメタデータを渡します。
このときにLayoutもメタデータで指定したいなら、
みたいなメタデータを作ってdynamicを使えばそれも実現できます。
もはや大体これでいいじゃん、って思ったのですが、サイドバーが作れない。headerをうまくとってきてそれを元にサイドバーが作りたい。
そこで考えたのが、remarkのcustom loaderを作る方法です。
remark-mdxを使うと、だいたい下のmdxファイルは以下のようにパースされます。
ただのfrontmatter付きのマークダウンファイルは以下のような感じでMDASTに変換されます。
remark-frontmatterを使うと、frontmatter部分はtype === yamlのchildrenとして取得できるようになります。また、header部分はtype === headingsを探せば取得できます。つまり、MDASTをparseしてfrontmatter部分とheaderをmetadataとして取得できます。
そして、layoutで指定されたコンポーネントを行頭でimportし、メタデータをexportし、importしたコンポーネントをexportします。
つまり、上のようなfrontmatter付きのMarkdownを
こうすれば、markdownファイルを置くだけでカスタムコンポーネント付きのmdxに解釈されてレンダリングされるようになります。
さらにheaderの情報を含んだmetadataをコンポーネントが受け取れるので、sidebarやtocをJSX側で作ることができます。リファクタリングがしたくなれば、ほとんどカスタムローダー側を触れば解決しそうなところもいけてる気がします。あとこの方法のメリットは突如として
とか入れたくなったときに入れられることです。パースはmdxに準拠してやってるので、突然mdxフォーマットで書いても自動で対応されます。
個人的にいい案だろって思ってるんですが、誰もこんなアプローチとってないので少し不安だったりします。なんか問題があるのだろうか(もっといい案が知りたい)。