Next.js + Markdown-it でMarkdownなコンテンツを表示できるようにした

先日このブログをHugoからNext.js+microCMSな構成に移行しました。

それにあたり、既存の記事をMarkdownを維持したまま以降し、引き続き書きたいなと思いJavaScriptでMarkdownでパースする方法を探して実装したのでその際にやったことを書いておきます。

やりたいこと

  1. microCMS上ではMarkdownを書く
  2. Next.js で作ったアプリケーションで記事データを取得し、Markdownをパースする

リッチエディタのフィールドでMarkdownを書くとすぐさまHTMLに変換してくれる(microCMS側でHTMLとして保管してくれるみたい)事と、ずっと使い続けるかは分からないので、テキストエリアのフィールドにMarkdownのまま書く想定です。

環境

  • Node.js v14.17.6
  • Next.js v11.1.2

ライブラリの選定

Markdownのパースを行うライブラリを探していたのですが、今回は更新頻度も高くない事から事前にSSG(SG)する事と、投稿者が自分しかいないことから安全性よりかは実行速度を優先しました

markedjs/marked (+ cure53/DOMPurify)

よく出てくるのは markedjs/marked でした。
あくまでMarkdownのパーサーで、sanitizeはしないので合わせて DOMPurify みたいなライブラリを使ってねていう代物。

import marked from "marked";
import DOMPurify from "dompurify";

const html = DOMPurify.sanitize(marked(result.body));

こんな感じになるみたい。

ただ、調べているともう少しちゃんと使う必要があるらしくて、上手く解決してるらしいライブラリが出てきました

markedで安全にMarkdownからHTMLを生成するsafe-marked | Web Scratch

両ライブラリのオプションが指定できたりと良さそうでした。

markdown-it/markdown-it

markdown-it/markdown-it: Markdown parser, done right. 100% CommonMark support, extensions, syntax plugins & high speed

次に出てきたのがこちらのライブラリで、設定できるオプションも marked と殆ど差がない印象です。必要に応じてコミュニティで作っているプラグインを使う使って拡張できるようです。
比較のために書いておくと、こちらはHTMLのオプションを有効にしなければ sanitizer 無しでも安全にパースできるけど、必要に応じてsanitizer入れてもいいよな考えっぽかったです。

参考: markdown-it/security.md

実装

今回の構築には、速度感を優先したかったので markdown-it を採用しました

make benchmark-deps
benchmark/benchmark.js readme

Selected samples: (1 of 28)

README

Sample: README.md (7774 bytes)

commonmark-reference x 1,222 ops/sec ±0.96% (97 runs sampled)
current x 743 ops/sec ±0.84% (97 runs sampled)
current-commonmark x 1,568 ops/sec ±0.84% (98 runs sampled)
marked x 1,587 ops/sec ±4.31% (93 runs sampled)

markdown-it/markdown-it: Markdown parser, done right. 100% CommonMark support, extensions, syntax plugins & high speed

結構差が出てますが、実際手元で組んでビルドした際も記事によっては 150~200ms/記事 ほど差が出たのを覚えています。

Next.jsのアプリケーションに組み込む

$ npm i markdown-it
$ npm i -D @types/markdown-it

page/[slug].tsx

export const getStaticProps: GetStaticProps<PostProps, Params> = async (context) => {
    // 中略  microCMSから記事データ取得したり
    // post が記事のオブジェクト

    const md: MarkdownIt = new MarkdownIt({
        html: true,  // 過去記事でHTML使っている部分があるのでしばらくtrue
        breaks: true,
        typographer: true
    })
    post.body = md.render(post.body)

    return { props: { post } }
}

今回はSSGするので、getStaticProps でデータ取得後、本文部分をパースする形にしました。
あとは、JSXに渡してレンダリングさせるだけです

<div dangerouslySetInnerHTML={{ __html: post.body }} />

こっちのライブラリに変えたおかげでビルド時間も短縮出来ていい感じに組めたように思えます。

はやく html: false にしたい