TL;DR

Next.jsを使ってブログを書く際に、Next.jsはmdxフォーマットをサポートしている@next/mdxがあります。

mdxの特徴として、

  1. Markdown内でJSXがかける
  2. 外部のコンポーネントをimportできる
  3. 内部の変数などをexportできる

というものがあります。しかし、ブログ記事を書くだけならこれらの機能はオーバースペックに見えます。

記事書くだけならそんなに拡張性はいらない気がします。なので、今回は記事を書くときにmdxを採用しなかった理由について書きます。 簡単にまとめると、以下です。

  1. Next.jsdynamic importが動かなかったので、メタデータのexportができない。
  2. VSCodeの拡張の補完がMarkdownと比べてかなり弱い。

これらの理由は単純に発展途上であることに起因する問題なので、これ以後の流行りによっては改善されていくと期待されます。しかし、現時点では問題です。

なので、このブログではmd拡張子を使って記事を書き、レンダリングする際にmdxへと変換しています。また、exportの代わりにfrontmatterを使うことでメタデータを表現しています。

mdxのデメリット

個人的にブログ書くときにmdxを使う際に不安だったのが、

import snowfallData from './snowfall.json'
import BarChart from './charts/BarChart'
# Recent snowfall trends
2019 has been a particularly snowy year when compared to the last decade.
<BarChart data={snowfallData} />

上のようなコードです。サイトなどを作る際にはすごく便利に見えます。しかし、記事のテンプレートとしての統一がしにくくて、<BarChart>とかをもし変更してしまうと割と容易にエラーが出たりしそうでちょっといやだなーという感じでした。

また、もう1つの理由が、mdxは、

export meta = {
    title: "new Blog!"
}

みたいなことができます。しかし、dynamic importするとうまくexportされたメタデータを取得できませんでした。 いろんなブログとか見たんですけど、exampleが下の感じで、

const meta = dynamic(() => import(`../_posts/${filename}`)).then((m) => m.meta);

見た目いけそうなんですけど、実際にはエラーが起こってうまく動かない。なので、記事用のコンポーネントとして切り出すときに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.jsremark, rehypeプラグインを書くだけでMarkdownを容易に拡張でき、ページとしてレンダリングできます。そのためにやることはシンプルで、next.config.js内のpageExtensions.mdを加えるだけです。

例えば、以下のような感じで、コードシンタクスとかkatexとかに対応でき、Markdownをレンダリングできます(参考)

next.config.js
// remark plugins
const remarkMath = require('remark-math')
const remarkFrontmatter = require('remark-frontmatter')
const remarkSlug = require("remark-slug");
const remarkHeadings = require('remark-autolink-headings')
const remarkFootnotes = require('remark-footnotes')

// rehype plugins
const rehypeKatex = require('rehype-katex')
const rehypePrism = require('@mapbox/rehype-prism')

const withMDX = require('@next/mdx')({
  extension: /\.mdx?$/,
  options: {
    remarkPlugins: [
      remarkFrontmatter, remarkSlug, remarkHeadings, remarkFootnotes, remarkMath],
    rehypePlugins: [rehypeKatex, rehypePrism],
  }
})

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

もしMarkdownを自前でレンダリングするなら、react-markdownを使ったり、remarkとrehypeでパースしたHTMLをdangerousInnerHTMLで埋め込むことになります。それに比べると、@next/mdxを利用するのが非常に楽な方法だという印象を受けました。

レンダリングする方法

Markdownをレンダリングする方法を一番素直に考えると、以下のようなコードが想定されます。[postId].jsx的な感じです。contentLoaderは名前で察してくれるとありがたいです。

import next/dynamic;
import Layout from "../components/Layout";
import {
    getFilePath,
    getFileMeta,
    getMdNames,
} from "../lib/contentLoader"

const BlogPostPage = ({filename, meta}) => {
    const MDContent = dynamic(() => import(`../post/${filename}`))
    return (
        <Layout meta={meta}>
            <MDContent />
        </Layout>
    )
}

export async function getStaticProps({ params} ) {
    const filename = params.postId + ".md"
    const filepath = await getFilePath(filename);
    const meta = await getFileMeta(filepath);
    return {
        props: {
            filename: filename,
            meta: meta,
        }
    }
}

export async function getStaticPaths() {
    const mdNames = await getMdNames();
    const paths = mdNames.map((mdName) => ({
        params: {
            postId: mdName
        }
    }));

    return {
        paths,
        fallback: false,
    }
}

getStaticPathsfsモジュールを使ってmarkdownファイルの一覧を取得します。その後、getStaticPropsでファイルの場所に戻して、ついでにメタデータをとってきて、そのパスに対応するmarkdownファイルと、Layoutにメタデータを渡します。 このときにLayoutもメタデータで指定したいなら、

layout:
    path: /path/to/Layout.tsx
    component: Layout

みたいなメタデータを作ってdynamicを使えばそれも実現できます。 もはや大体これでいいじゃん、って思ったのですが、サイドバーが作れない。headerをうまくとってきてそれを元にサイドバーが作りたい。

そこで考えたのが、remarkのcustom loaderを作る方法です。 remark-mdxを使うと、だいたい下のmdxファイルは以下のようにパースされます。

ただのfrontmatter付きのマークダウンファイルは以下のような感じでMDASTに変換されます。

---
title: A
date: a/a/a/a
layout:
    path: ../../components/Layout
    component: Layout
---

# a

## aa

### aaa

remark-frontmatterを使うと、frontmatter部分はtype === yamlのchildrenとして取得できるようになります。また、header部分はtype === headingsを探せば取得できます。つまり、MDASTをparseしてfrontmatter部分とheaderをmetadataとして取得できます。 そして、layoutで指定されたコンポーネントを行頭でimportし、メタデータをexportし、importしたコンポーネントをexportします。

つまり、上のようなfrontmatter付きのMarkdownを

import Layout from "../../components/Layout"

export const meta = {
    title: "A",
    date: "a/a/a/a",
    headers: [
        {
            value: "a",
            depth: 1,
        },
        {
            value: "aa",
            depth: 2
        },
        {
            value: "aaa",
            depth: 3,
        }
    ]
}

export default ({meta, children}) => <Layout meta={meta} children={children} />

# a

## aa

### aaa

こうすれば、markdownファイルを置くだけでカスタムコンポーネント付きのmdxに解釈されてレンダリングされるようになります。

さらにheaderの情報を含んだmetadataをコンポーネントが受け取れるので、sidebarやtocをJSX側で作ることができます。リファクタリングがしたくなれば、ほとんどカスタムローダー側を触れば解決しそうなところもいけてる気がします。あとこの方法のメリットは突如として

<button>Push!!!!!!!</button>

とか入れたくなったときに入れられることです。パースはmdxに準拠してやってるので、突然mdxフォーマットで書いても自動で対応されます。

個人的にいい案だろって思ってるんですが、誰もこんなアプローチとってないので少し不安だったりします。なんか問題があるのだろうか(もっといい案が知りたい)。

記事に間違い等ありましたら、お気軽に以下までご連絡ください

E-mail: illumination.k.27|gmail.com ("|" replaced to "@")

Twitter: @illuminationK

当HPを応援してくれる方は下のリンクからお布施をいただけると非常に励みになります。

Ofuse

Other Articles

Site Map

Table of Contents

    TL;DR

      mdxのデメリット

      mdxのメリット

    レンダリングする方法


当HPを応援してくれる方は下のリンクからお布施をいただけると非常に励みになります。

Ofuse
Privacy Policy

Copyright © illumination-k 2020-2021.