Nuxt.jsでMarkdownファイルからFAQページなどを作れるようしてみた(.mdファイル内でコンポーネントも使える)

読了目安:10分

ほそぼそと作っているWebサービスの積読ハウマッチ
Gigazineさんに紹介されました🎉🎉🎉

おかげさまでユーザやアクセスが増えたのですが、
問い合わせやコメントも急増したので、FAQを用意したほうがいいなと...

Nuxt.jsのページのうち、FAQのような静的なページだけ、
Markdownで作成できないかを調査したときの備忘録。

環境はnuxt(2.8.1) / typescript(3.6.2)

ちょっと長めですが...

  1. Markdownだけのシンプルな方法と
  2. Markdown内でVueコンポーネントを使う凝った方法

の2パターンをまとめています。

これを使うと一部のページをマークダウンで書けるので、
FAQとか以外にブログとか記事とかも簡単にmarkdownで書けるようになるかもと

長めですが、見出しとソースコードを見てくとだいたい分かるかも


できあがったもの: 1.シンプルな方法

こんな感じのMarkdownファイルを読み込んで

# よくある質問 / FAQ

## 積読本しか登録してはいけないのですか?

そんなことないです!**読み終わった本だけでも OK です ♪**
まだ積読ハウマッチで**誰も登録していない本**もあるので、
**おすすめの本**などあればどんどん登録してみてください!

また、同じ本を読んでいる人や積んでる人もわかるので、
**本の好みが近い人が見つかるかも知れません** 😊

こんな感じの画面を作れちゃいます!

できあがったもの: 2.凝った方法

ツイートを埋め込めるよう、Markdownファイルの中で
vue-tweet-embedを利用しています。

# 更新情報

## 2019/09/09  洋書検索に対応 ✨

<Tweet id="1170893520787271680" :options="{ conversation: 'none' }"></Tweet>

1.の方法だと<Tweet>タグがそのままのため、なにも表示されませんが、
2.の方法だと、こんな感じの画面を作れちゃいます!


ここから、それぞれの方法の説明

1. Markdownだけのシンプルな方法

シンプルな方法はこんな感じ

  1. Markdownファイルを読み込んで、
  2. markedでHTML化して、
  3. v-htmlでHTMLを挿入

Nuxt.jsでは、Markdownファイル(.md)のようなファイルを
そのまま取り込む方法が無いので、
Webpackプラグインのraw-loaderを使います。

raw-loaderなどのインストール

必要なパッケージをインストール

$ npm install --save raw-loader marked highlightjs

nuxt.config.jsの設定

.mdを読み込む際に、raw-loaderを使うように設定。

import NuxtConfiguration from "@nuxt/config";

const config: NuxtConfiguration = {
  build: {
    extend(config, ctx) {
      if (!!config.module) {
        // .mdファイルだったら、raw-loaderを使うように設定
        config.module.rules.push({ test: /\.md$/, use: ["raw-loader"] });
      }
    }
  },
}

.vueはこんな感じ

これだけで、.mdファイルを読み込んでページが作れちゃいます!
あとは読み込むファイルを変えるだけで、簡単に静的なページを量産(´ω`)

<template>
  <!-- v-htmlでmarked()の結果を渡す -->
  <div class="marked" v-html="text"></div>
</template>

<script lang="ts">
import { Component, Vue } from "nuxt-property-decorator";
import marked from "marked";
import hljs from "highlightjs";

// requireで.mdファイルを読み込む。
const mdText = require("~/assets/faq.md");

@Component
export default class FaqPage extends Vue {
  created() {
    // 作成時にmarkedの初期設定
    // ハイライトにhighlightjsを使うようにする
    marked.setOptions({
      langPrefix: "",
      breaks: true,
      highlight: function(code, lang) {
        return hljs.highlightAuto(code, [lang]).value;
      }
    });
  }
  // ****************************************************
  // * computed
  // ****************************************************
  private get text() {
    // 読み込んだ.mdファイルをmarkedでHTML化する
    return marked(mdText.default);
  }
}
</script>

そのままだといい感じの見た目にならないので、
class="marked"のようにしておいて、
markdown用のCSSを設定していけばOK。

どんなCSSがいいかは、前の記事を参照ください。

注意: ハマった点...

2つ注意というか、ハマった点...

A) .mdファイルを読み込むときはrequireを使う

ぼくの環境だけかも知れませんが、
import mdText from "~/assets/faq.md"とすると
VSCode上でエラーがきませんでした。。
TypeScriptの設定やVeturの問題?

そのため、requireを使い、
const mdText = require("~/assets/faq.md");
としてます。

それによりモジュールとして読み込まれてしまうため、
mdText.defaultでテキストを取得しています。

(本当はimport文がいいんですが、あきらめてrequireに...)

B) v-htmlで挿入したHTMLにScoped CSSが効かない

VueでScoped CSSを使う場合、data-v-*属性が付与されますが、
v-htmlでHTMLを挿入しているため、その属性がついていません。。

これにより、Scoped CSSが効かないため、
markdown用のCSSを利用する際はグローバルで設定する必要があります。

(これでだいぶハマりました...)


2. コンポーネントを使う凝った方法

1.の方法で、ツイートを埋め込んでみようと思ったらうまく行かず...
vueでツイートを埋め込むには、vue-tweet-embedが必要なようで、
以下のようにしてみたら、

# 更新情報

## 2019/09/09  洋書検索に対応 ✨

<Tweet id="1170893520787271680" :options="{ conversation: 'none' }"></Tweet>

こんな感じに...

ツイッターで、

と悩んでたところ、@mizuki_rさんから、こんなアドバイスが(´ω`)

v-htmlだとvueの解釈は出来ないので、独自のdirective定義してvueをコンパイルできるようにしたことはあります

vueのtemplateオプションはHTML文字列を渡してvueのcomponentとしてcompileします。つまり、directiveで受け取ったHTML文字列をvueのtemplateオプションに渡してvueのコンポーネントを作ると…?

なるほど...( ゚д゚)!

大まかな流れ

  1. カスタムディレクティブを用意
  2. カスタムディレクティブ内でコンポーネントを作成し、
  3. コンポーネントのtemplateにmarked()のHTMLを渡して、
  4. Vueのコンパイル(=$mounte()の呼び出し)
  5. Vueのコンパイルした結果からHTML要素を取得($el)して、
  6. カスタムディレクティブを設定したHTML要素の子に挿入(el.appendChild(instance.$el))

長いし、複雑...

.vueはこんな感じ

.vue自体はほぼ同じ。
v-htmlがカスタムディレクティブのv-mdに変わってるだけ。

<template>
  <!-- **v-md**でmarked()の結果を渡す -->
  <div class="marked" v-md="text"></div>
</template>

<script lang="ts">
// ... 略
</script>

とりあえず、vue-tweet-embedのインストール

とりあえず、vue-tweet-embedを使うのでインストール

$ npm install --save vue-tweet-embed

カスタムディレクティブ内で動的コンパイル

カスタムディレクティブの用意していく。~/plugins/v-md.tsに作成

import Vue from "vue";
import { Tweet } from "vue-tweet-embed";

Vue.directive("md", {
  inserted: function(el, binding) {
    // v-md="value"のvalue部分を取得
    const val = binding.value;

    // コンポーネントを作成。ルートが1つになるように<div>で囲む
    // Tweetをコンポーネントで使うので、componentsも設定
    const cmp = Vue.extend({
      components: { Tweet },
      template: `<div>${val}</div>`
    });

    // Vueコンポーネントをコンパイルして、インスタンスを取得
    const instance = new cmp().$mount();

    // インスタンス化からHTML要素を取得して、
    const element = instance.$el
    // カスタムディレクティブを設定したHTML要素の子に挿入
    el.appendChild(element);
  }
});

作ったプラグインをnuxt.config.tsに追加

作ったプラグインを有効にするため、nuxt.config.tsに設定を追加

import NuxtConfiguration from "@nuxt/config";

const config: NuxtConfiguration = {
  plugins: [
    { src: "~/plugins/v-md", ssr: false },
  ],
}

これでいけるかと思いきや...

まっしろ(´ω`)
タイトルすら出ない...

Consoleを見るとエラーが...

error [Vue warn]: You are using the runtime-only build of Vue where the template compiler is not available. Either pre-compile the templates into render functions, or use the compiler-included build.

メッセージを見てみると、
「ランタイム限定なのでコンパイルできないよ!」
とのこと...なるほど...

このあたりを見てみると、「ランタイム + コンパイラ」を選択できるので、
nuxt.config.tsの設定を変更していく。

実行時にコンパイルできるように設定を変更

公式ドキュメントの「さまざまなビルドについて」を見ると、
vue/dist/vue.commonを使えば良さそう。

なので、buildに以下の3行を追加。

import NuxtConfiguration from "@nuxt/config";

const config: NuxtConfiguration = {
  plugins: [
    { src: "~/plugins/v-md", ssr: false },
  ],

  build: {
    extend(config, ctx) {
      if (!!config.module) {
        config.module.rules.push({ test: /\.md$/, use: ["raw-loader"] });
      }

      // 「ランタイム限定」から「ランタイム + コンパイラ」に変更
      if (!!config.resolve && !!config.resolve.alias) {
        config.resolve.alias["vue$"] = "vue/dist/vue.common";
      }
    }
  },
}

すると...

出た(´ω`)!!

注意: 未解決...

1点対応していない部分が...
Markdownファイル内で<nuxt-link>を使うと、エラーが...

app.js:1971 Uncaught TypeError: Cannot read property 'resolve' of undefined

Nuxt側からcontextやvue-routerの情報を渡していないためだと思いますが、
詳しく見れていませんが、nuxt-linkを使うとエラーになるため、ご注意ください...

おわりに

marked+highlightjs+raw-loaderを使って、
Markdownファイルでページコンテンツを作れるように!

カスタムディレクティブを使うと、
Markdownファイル内でVueコンポーネントを使えるように!

今回のアップデートでFAQや更新情報のページに利用してますが、
サイトの説明や使い方ページなどにも使えたり、
Nuxt.jsアプリにmarkdownで書いた記事なども簡単にできそう(´ω`)

こんなのつくってます!!

上記を使ってFQAや更新情報ページを追加した、積読用の読書管理アプリ「積読ハウマッチ」!
積読ハウマッチはNuxt.js(SPA)+Firebaseで開発してます!

もしよかったら、遊んでみてくださいヽ(=´▽`=)ノ

要望・感想・アドバイスなどあれば、
公式アカウント(@MemoryLoverz)や開発者(@kira_puka)まで♪

Originally published at qiita.com

きらぷか@i18n補助ツール『トランスノート』開発者

フリーエンジニア/今はNuxt.js/いつかFlutter 受託&アプリ/Webサービス/ゲームを #個人開発 CS修士→SIer/R&D→フリー #paiza はAランクで満足/AtCoderしたい 仕事依頼やご相談はDMまで Kotlin/Python/Swift/Unity/Java/Haskell/DDD

Crieitは個人で開発中です。 興味がある方は是非記事の投稿をお願いします! どんな軽い内容でも嬉しいです。
なぜCrieitを作ろうと思ったか

また、「こんな記事が読みたいけど見つからない!」という方は是非記事投稿リクエストボードへ!

こじんまりと作業ログやメモ、進捗を書き残しておきたい方はボード機能をご利用ください!

ボードとは?

関連記事

コメント