Nuxt.jsでMarkdownを静的に埋め込む

初めまして、インターンシップの一環でとある Web サイト改修のお仕事をさせてもらっています。

その内容の 1 つに、現在 **marked** で動的に読み込んでいる記事を静的なページにあらかじめ埋め込んで置けるようにする、という要求があります。さらに、図表番号などを前処理でつける必要があります。現在は一枚の html から動的に読み込みつつ処理しているようなので、もしかすると結構な大工事になりそうです。
Vue を触るのは初めてなので雰囲気を掴みつつ良さげな方法を探していこうと思います。

## フレームワークは？
さて、Vue で静的というと **Vuepress** や **Nuxt.js** が上がると思いますが、実際のページは単なる記事サイトではないので一旦 Vuepress は置いておくことにします。Nuxt ならば静的サイトを生成できるはずなので、Markdown を html に変換したものを Vue コンポーネントの中に埋め込むことができれば、いい感じに展開してくれるはずです（理解が正しければ）。
というわけで Nuxt を使うとするとコンポーネントとして読み込むのが良さそうなので、ひとまず Vue コンポーネントの中に Markdown を埋め込む(コンポーネントとして読み込む)というのを考えてみることにしました。

ここで少し注意ですが、`nuxt generate`へのちょっとした勘違いから **@nuxtjs/markdownit** を候補から外していました。そのため試した順は一番最後となっていますが、**@nuxtjs/markdownit** でも静的に埋め込まれました。お急ぎの方はそちらからどうぞ。


## Webpackのローダー

標準的な Vue プロジェクトは Webpack を使っているようなので、手始めに Webpack のローダーで何かないかと調べました。そうすると結構出てくるんですが、どれもスターが微妙に少なかったり少し不安ですね。一応調べたものを列挙してみます。


### [markdown-to-vue-loader](https://github.com/fengyuanchen/markdown-to-vue-loader)
調べると真っ先に出てきたのがこちらです。現状で GitHub のスター数が 23 とかなり心もとないですが、markdown をコンポーネントとして読み込みできそうです。まずは試してみましょう。

```index.vue
<template>
    <Md />
</template>

<script lang="ts">
import Vue from 'vue'
import Logo from '~/components/Logo.vue'

import Md from '~/md/0.md'

export default Vue.extend({
  components: {
    Logo,
    Md,
  },
})
</script>
```

```nuxt.config.js
// 一部省略
{
    build: {
        extend(config, { isDev }) {
            config.module.rules.push({
                test: /\.md$/,
                exclude: /node_modules/,
                use: [
                'vue-loader',
                {
                    loader: 'markdown-to-vue-loader',
                },
                ],
            })
            if (isDev) config.mode = 'development'
        },
    },
}
```

```shims-md.d.ts
// .mdを読み込める、ということにしておく
declare module '*.md' {}
```

ちゃんと埋め込みができているようです。markdown-it にはしっかりと設定も渡せるみたいです。


### [ware-loader](https://github.com/liril-net/ware-loader) + markdown-it

[ware-loader](https://github.com/liril-net/ware-loader) というのは初めて聞きました。どうやら middleware として関数を渡しておくと、入力ソースに対して独自の処理ができるパッケージのようです。middleware の中で markdown-it に処理をさせてしまえば良いわけですね。

```index.vue
<template>
    <Md />
</template>

<script lang="ts">
import Vue from 'vue'
import Logo from '~/components/Logo.vue'

import Md from '~/md/0.md'

export default Vue.extend({
  components: {
    Logo,
    Md,
  },
})
</script>
```

```nuxt.config.js
import MarkdownIt from 'markdown-it'
const md = new MarkdownIt()

// 一部省略
{
    build: {
        extend(config, { isDev }) {
            config.module.rules.push({
                test: /\.md$/,
                exclude: /node_modules/,
                use: [
                'vue-loader',
                {
                    loader: 'ware-loader',
                    options: {
                    raw: true,
                    middleware: (source) =>
                        `<template><div>${md.render(source)}</div></template>`,
                    },
                },
                ],
            })
            if (isDev) config.mode = 'development'
        },
    }
}
```

```shims-md.d.ts
// .mdを読み込める、ということにしておく
declare module '*.md' {}
```

こちらもしっかりと埋め込みができました。

スターは 18 と非常に心もとないです。これは聞いたことがなくても仕方なさそう。ですがコードはかなりシンプルで、驚きの 62 行です。これなら最悪自分でメンテナンスできそうではあります。
この形式の良い点として、自前で処理用の関数を作れば Webpack の流れに乗せて前処理をしつつ、コンポーネント化までやってしまうことができそうです。
悪い点としては先ほどと同じく、メジャーではないパッケージを使うという不安さくらいでしょうか。開発者側としては利用者の多いパッケージを使いたくなりますね。


## Nuxtプラグイン

### [@nuxtjs/markdownit](https://github.com/nuxt-community/modules/tree/master/packages/markdownit)

最後の紹介になります。

Nuxt のコミュニティですでに Markdown を読み込めるプラグインが存在しました。スターも 1.1K となかなか信頼できそうなパッケージです。普通に読み込むだけならこのプラグインで良さそうですね。

```index.vue
<template>
    <div v-html="md"></div>
</template>

<script lang="ts">
import Vue from 'vue'
import md from '~/md/0.md'

export default Vue.extend({
  computed: {
    md() {
      return md
    },
  },
})
</script>
```

```nuxt.config.js
// 一部省略
{
    modules: [
        '@nuxtjs/markdownit',
    ],
    markdownit: {
    preset: 'default',
    linkify: true,
    breaks: true,
    use: ['markdown-it-div', 'markdown-it-attrs'],
  },
}
```

```shims-md.d.ts
// stringとして.mdを読み込める、ということにしておく
declare module '*.md' {
  const src: string
  export default src
}
```

こちらは Nuxt.js のコミュニティパッケージのようです。上の 2 つとは違って html 形式の文字列としてインポートできるようになっており、**computed** なメソッドを通して`v-html`で埋め込む流れのようです。(computed なのは再描画頻度を下げるためだと思います。)

ここで少し勘違いをしてしまって、computed とはいえメソッドを通せば動的に処理されるのだろうと思っていました。しかし念の為`nuxt generate`で試してみると、何と静的に埋め込まれていました！　どうやら`nuxt generate`では静的に処理できる **computed** なものは html に埋め込まれてしまうようです。

なるほどと思いながら、さらに **methods** ではどうなのか試して見ましたが、何とこちらも埋め込みされました...。これには流石に驚きですね。nuxt は魔術でも使っているんでしょうか...。

さらに気になるのは文字列として読み込んでいるところです。もし関数をインポートして適用したらどうなるのでしょうか。
結論から言うと、関数を適用した結果が html に埋め込まれました。もうびっくりです。前処理が必要だとしても、(Webpack などを使って)事前に行わなくて良いと言うことです。

静的に処理できないパターンの罠はありそうですが、少なくとも今回のユースケースではかなり効果が出そうです。


## どうするか

とにかく何らかの手段で埋め込みができることは確認できました。

色々調べましたが、`nuxt generate`を使うならば使い勝手は**@nuxtjs/markdownit**の圧勝ですね... 静的に処理できるものは勝手に埋め込んでくれるのが強すぎます。
正直まだよくわかっていませんが、`nuxt generate`で静的に処理されるとすれば **markdown-it** を直接使ってもいいのかもしれません。**raw-loader** で md を文字列として読み込んでしまえば、html 化前に前処理を挟んでしまうこともできそうです。

ところで、**vuepress** の良いところは md ファイルを特定のディレクトリに置いておけば、勝手にページを生成してくれる点です。リンクなどもできますし、かなりいい感じです。
もし前述の手段で Nuxt を使ったサイトを作るとすると、md ごとにページ用のコンポーネントが必要になりそうです。もしページが増えた時にコンポーネントも新しく作らなければならないとするとなかなか面倒です。この辺りを考えると、結局 Webpack の実行前に前処理が必要になってくるかもしれません。(ここまでの魔術を見せられると何らかの手段で出来そうな気もします。)


## まとめ

Markdown を静的に埋め込む手段について調査しました。

今回の結論は Nuxt.js を使うなら`@nuxtjs/markownit`を使っておけば面倒がなさそうと言うことです。`nuxt generate`ならばかなり賢く処理してくれることがわかりました。
また、html 文字列へ変換する前に前処理が必要であれば **raw-loader** を使って文字列として読み込んでおき、任意の処理をした後に **markdown-it** なり **marked** で変換したものを埋め込むのも良さそうです。

色々とあったのでかなり混乱しました。もし Nuxt.js ではなく Vue.js を使った開発なら他に挙げた手段が使えそうです。特に置換など md に少し手を加えたい場合には **ware-loader + markdown-it** も便利そうです。

今回はこのくらいでまとめておきたいと思います。何かの参考になれば幸いですが、間違いなどあれば教えていただけると嬉しいです。