ほそぼそと作っているWebサービスの積読ハウマッチ、
Gigazineさんに紹介されました🎉🎉🎉
おかげさまでユーザやアクセスが増えたのですが、
問い合わせやコメントも急増したので、FAQを用意したほうがいいなと...
Nuxt.jsのページのうち、FAQのような静的なページだけ、
Markdownで作成できないかを調査したときの備忘録。
環境はnuxt(2.8.1) / typescript(3.6.2)
ちょっと長めですが...
- Markdownだけのシンプルな方法と
- 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だけのシンプルな方法
シンプルな方法はこんな感じ
- Markdownファイルを読み込んで、
- markedでHTML化して、
- 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>
こんな感じに...
ツイッターで、
Vueのv-htmlのテキストにコンポーネントをいれることってできないかなぁ?
— 積読ハウマッチ📚きらぷか (@kira_puka) September 9, 2019
markdownのなかにコンポーネント書けると色々便利そう。。
と悩んでたところ、@mizuki_rさんから、こんなアドバイスが(´ω`)
v-htmlだとvueの解釈は出来ないので、独自のdirective定義してvueをコンパイルできるようにしたことはあります
vueのtemplateオプションはHTML文字列を渡してvueのcomponentとしてcompileします。つまり、directiveで受け取ったHTML文字列をvueのtemplateオプションに渡してvueのコンポーネントを作ると…?
なるほど...( ゚д゚)!
大まかな流れ
- カスタムディレクティブを用意
- カスタムディレクティブ内でコンポーネントを作成し、
- コンポーネントのtemplateに
marked()
のHTMLを渡して、 - Vueのコンパイル(=
$mounte()
の呼び出し) - Vueのコンパイルした結果からHTML要素を取得(
$el
)して、 - カスタムディレクティブを設定した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 },
],
}
これでいけるかと思いきや...
まっしろ(´ω`)
タイトルすら出ない...
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);
}
});
なんかいじってたら、前できなかった.mdファイルにnuxt-link入れるのできた(*´ω`*)
— 積読ハウマッチ📚きらぷか (@kira_puka) September 12, 2019
なるほど、routerを渡してなかったからダメなのか...
気合い入れるとダメだけど、横道にそれるとうまくいくことが最近多い(*´ω`*) pic.twitter.com/3Hg8qw3nGp
おわりに
marked+highlightjs+raw-loaderを使って、
Markdownファイルでページコンテンツを作れるように!
カスタムディレクティブを使うと、
Markdownファイル内でVueコンポーネントを使えるように!
今回のアップデートでFAQや更新情報のページに利用してますが、
サイトの説明や使い方ページなどにも使えたり、
Nuxt.jsアプリにmarkdownで書いた記事なども簡単にできそう(´ω`)
こんなのつくってます!!
上記を使ってFQAや更新情報ページを追加した、積読用の読書管理アプリ「積読ハウマッチ」!
積読ハウマッチはNuxt.js(SPA)+Firebaseで開発してます!
もしよかったら、遊んでみてくださいヽ(=´▽`=)ノ
要望・感想・アドバイスなどあれば、
公式アカウント(@MemoryLoverz)や開発者(@kira_puka)まで♪