MDX or Markdown ?

published: 2020/9/7 update: 2020/9/9

Table of Contents

TL;DR

最近なんかmdx流行ってますよね。markdownでjsx使えるのなかなかすごいと思うんですが、Blog書くときにこの機能いる?って感じがします。 記事書くだけならそんなに拡張性はいらないと思う。なので、今回は記事を書くときにmdxを採用しなかった理由について書こうと思います。厳密にいうと、書くフォーマットはMarkdownを使って、レンダリングする際にmdxに変換してしまいます。mdxも使おうと思えば使えます。

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>とかをもし変更してしまうと割と容易にエラーが出たりしそうでちょっといやだなーという感じでした。

それともう一つの理由が、mdxは、

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

みたいなことができるんですけど、dynamic importするとうまく動かないんですよね。そもそもfontmatterでいいじゃんみたいな。

いろんなブログとか見たんですけど、exampleが下の感じで、

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

見た目いけそうなんですけど、実際にはエラーが起こってうまく動かない。という感じで、ちょっとまだ記事に使うには不安が残るなーという感じを受けました。

それと記事書くときに不満なのが、markdownならVSCodeのExtensionとかが充実しているので、 生産効率が非常に高いんですけど、mdxはなんかこう、そのへんはまだまだかなーという気がします。なので、ブログ記事はできるだけ純粋なMarkdownで書けるようにしたいです。

mdxのメリット

ただ、next.jsのmdxシステムに関するシステムは結構すごくて、mdxをpages/posts/hoge.mdxとかにおくととlocalhost:3000/posts/hogeにそのままページとしてレンダリングできるんですよね。さらに、ピュアなMarkdownに対してもそれが適応できる。 その上、remarkとかrehype系のプラグインをnext.config.jsに書くだけで全体に適用できるようになる、というの機能があり、すごく魅力的です。

以下のような感じです

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']
})

pageExtensionsにmdを追加することでmarkdownを直でレンダリングできるようになります。さらにコードシンタクスとかkatexとかに対応しています(参考)。AMPには対応してないようですが。 もし自前でやろうと思ったら多分getStaticPropsとかの中でファイル読み込んでremarkとrehypeでパースしてdangerousInnerHTMLとかで読み込むことになる気がしますし、それに比べると良い気がしました。 この便利さを考えると、mdx pluginを使ってmarkdown書くのが一番ラクだなあと思いました。

レンダリングする方法

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

のように変換してしまいます(実際はもう少し色々やりたいですね、headersを木にするくらいはしておきたい)。

こうすれば、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.