40
32

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-09-11

ほそぼそと作っている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を使うとエラーになるため、ご注意ください...

追記: 2019/09/13

Vue Routerの公式ドキュメントを見てたら、ちゃんと書いてあった。。

Nuxt.jsのContextにあるrouterをわたして、InjectしてあげればOK!
なので、カスタムディレクティブでrouterを渡すように変更していく

それぞれこんな感じに。

.vueファイル
<template>
  <!-- **v-md**でmarked()とrouterの結果を渡す -->
  <div class="marked" v-md="{ text: text, router: $router }"></div>
</template>
カスタムディレクティブ
import Vue from "vue";
import { Tweet } from "vue-tweet-embed";

Vue.directive("md", {
  inserted: function(el, binding) {
    // v-md="value"のvalue部分を取得
    const { text, router } = binding.value;
    
    // コンポーネントを作成。ルートが1つになるように<div>で囲む
    // Tweetをコンポーネントで使うので、componentsも設定
    const cmp = Vue.extend({
      components: { Tweet },
      template: `<div>${val}</div>`
    });
    
    // Vueコンポーネントをコンパイルして、インスタンスを取得
    // <nuxt-link>を使えるようインスタンス生成時にrouterを渡す
    const instance = new cmp({ router }).$mount();
    
    // インスタンス化からHTML要素を取得して、
    const element = instance.$el
    // カスタムディレクティブを設定したHTML要素の子に挿入
    el.appendChild(element);
  }
});

おわりに

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

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

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

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

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

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

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

40
32
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
40
32

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?