この記事をおすすめしたい人
- 各種CMSから取得した記事をNuxt.jsで表示しようとしている人
- 記事内のリンクがSPAにならなくて困っている人
- v-htmlで<nuxt-link>を有効にしたい人
- つまりオレ
WordPress Nuxt化の一環です。
Nuxt.jsで、各種CMSから取得した記事本文内の<a>を<nuxt-link>に置き換えます。
表記を<nuxt-link>に置き換えるわけではなく、<nuxt-link>としてしっかり機能させるところまでやります。
最近順調だったのに久々にドハマリしました。
長くなりそうなので先に答えからいきますね。
WordPressの上でな!
※ このブログはまだWordPress製です
このページの目次
結論:「nuxt-interpolation」モジュールを使うだけ
コードも手順も笑えるくらいに簡単でした。
「nuxt-interpolation」というモジュールを使います。
「nuxt-interpolation」のインストールとセットアップ
まずはターミナルからNuxt.jsのプロジェクトフォルダへ移動し、モジュールを組み込みます。
$ npm i nuxt-interpolation
その後、nuxt.config.jsにモジュールの設定を追加。
export default {
modules: [
'nuxt-interpolation'
],
}
「nuxt-interpolation」の使用方法
取得した記事本文が変数「content」に入っているものとして、こんな↓感じで出力していると思います。
<div v-html="content"></div>
ここにちょこっと書き加えるだけ↓です。
<div v-interpolation v-html="content"></div>
呆れるほどに簡単でしょ?
この問題にぶち当たった人は<a>タグを相対パスにした後だと想定していますが、まだの場合は正規表現などで相対パスに変換しておく必要があります。
僕は「cheerio」というモジュールで変換しました。
const cheerio = require('cheerio')
const $ = cheerio.load(content, { decodeEntities: true })
$('a[href^="https://dev.ore-shika.com/"]').attr('href', function () {
let href = $(this)[0].attribs.href
return href.replace('https://dev.ore-shika.com', '')
})
content = $.html()
「nuxt-interpolation」使用の注意点
記事の中の<a>を<nuxt-link>に書き換えておく必要はありません。 むしろ書き換えたら機能しませんでした。
<a>のままで大丈夫です。
あと、このモジュールはあくまで<a>タグに関するモジュールで、「v-htmlで出力する際に、中のコンポーネントを有効にする」というものではありません。 そういう目的の場合は今回僕が試した他の方法が必要なんではないかと思います。
以上です!
この先は原理とか、僕の試行錯誤の過程とか、そういうのなので、興味ない人はここまで! またどこかで会いましょう!
なぜ<a>を<nuxt-link>にする必要があるのか
実際にやってみると分かります。 <a>タグのままだとページ遷移がめちゃくちゃ遅いんです。
いや、遅くはないです。 普通です。
Nuxt.jsは静的HTMLで出力してもSPA(Single Page Application)になっていて、ページ移動の際にページの差分だけ?を読み込むことでページ移動が爆速になります。 Nuxt.jsで作ったテストサイトがあるので見てみてください。
これは<a>を<nuxt-link>にして初めて実現できることで、<a>のままでは普通に全リロードがかかります。 せっかくNuxt.jsで作るからにはSPAとして機能して欲しい。
これがスタート地点です。
ちなみに<nuxt-link>はHTMLタグではないのでそのままHTMLに出力しても機能しません。 コンパイル時には<a>に戻されます。 あくまでコンパイルの段階でNuxt.jsに通知するためのタグ(コンポーネント)です。
APIから取得した記事本文をNuxt.jsで出力する方法
それでは記事本文を出力する流れを段階的に説明していきます。
Nuxt.jsの<template>内で変数を出力するには、通常、この↓ような書き方をします。
<p>{{ title }}</p>
「title」が変数です。
ですが「title」にHTMLタグが含まれていた場合、例えばこんな↓の。
title = '今日は<span class="bold">雨</span>です'
CMSのAPIから取得した記事はほぼ間違いなくこういう形になっているでしょう。
これを先ほどの二重中括弧({{}})で出力するとこう↓なります。
今日は<span class="red">雨</span>です
HTMLタグがエスケープされてタグのまま出力されてしまいます。
HTMLタグとしてちゃんと機能させたまま出力するには「v-html」を使います。
<p v-html="title"></p>
「v-html」で指定した値はタグの内側に出力されるので、中には何も書かなくてよいです。 書いても無視されます。
v-htmlで出力するとこう↓なります。
今日は雨です
これで記事内のタグが有効のままHTMLに出力できるところまできました。 もちろん今回のテーマである<a>タグもリンクとして機能します。
<a>を<nuxt-link>に書き換える
SPAを有効にするために、REST APIから取得した記事本文の<a>を<nuxt-link>に書き換えます。
場所はasyncDataやfetchなど、APIを取得した場所でいいと思います。
正規表現で処理することもできますが、本プロジェクトには「data-n-head」などを削除するために「cheerio」というモジュールが入っているので、今回はこれを利用します。
変数「content」に記事本文が入っているものとして、こんな↓感じのコードにしました。
//モジュールの読み込み
const cheerio = require('cheerio')
//記事本文を読み込み
const $ = cheerio.load(content, { decodeEntities: true })
//サイト内リンクの<a>タグを置換
$('a[href^="https://dev.ore-shika.com/"]').replaceWith(function() {
//開始タグ
let tag = '<nuxt-link'
//<a>タグの属性を<nuxt-link>にコピー
Object.keys($(this)[0].attribs).forEach(key => {
//<nuxt-link>は「href」ではなく「to」なので変換
if ( key == 'href' ) {
//絶対パスの場合は相対パスに変換
tag += ' to="' + $(this)[0].attribs[key].replace('https://dev.ore-shika.com', '') + '"'
} else {
tag += ' ' + key + '="' + $(this)[0].attribs[key] + '"'
}
})
//終了タグ
tag += '>' + $(this)[0].firstChild.data + '</nuxt-link>'
return tag
})
今回使用しているCMSがWordPressなので、「https://dev.ore-shika.com/」から始まる絶対パスをサイト内リンクと判断しています。 URLは適宜読み替えてください。
最初から相対パスになっている場合は、セレクタは「a[href^="/"]」で大丈夫です。
相対パスと絶対パスが混在している可能性がある場合は、先に絶対パスを相対パスへ変換、その後に書き換え処理でいけます。
あと、cheerioはjQueryのような書き方ができるモジュールらしく、僕はjQueryは無勉強に近いので不細工なコードになっていると思います。 意図した動作にはなっているのでご容赦を。
これをv-htmlで出力した結果↓。
<nuxt-link to="/test/">テストページへ</nuxt-link>
なんでやねん!!
書き換え自体はうまくいきましたが、<nuxt-link>がそのまま出力されてしまっています。
HTMLには<nuxt-link>なんてものはないので、当然なんの機能も持ちません。
何故v-htmlはコンポーネントを展開しないのか
Nuxt.jsのベースになっているVue.jsの仕様のようです。
外部APIから取得したデータというのは、必ずしも管理の行き届いたものではなく、例えばあなたがこのサイトのAPIから記事を取得することもできます。 もしそこに何かしらの攻撃性のあるコードが仕込まれていたら…。
これを防ぐためにこんな仕様になっているようです。
v-htmlで<nuxt-link>を展開する方法を探す
変数に含まれるコンポーネントを展開できれば今回の目的が達成できることが分かったので、そこに的を絞って調べていきます。
日本語の記事がほとんど見つからず、主に海外のフォーラムで探しました。
試したのは以下のような手法。
- Vue.compile()
- v-runtime-template
- <component :is="">
- createElement
結果は、そもそも動かなかったり、中身が二重に出力されたり、<html><head>ごと出力されたりと、どれもうまくいきませんでした。
フォーラム見ているとちゃんと動いている人がいるっぽかったので、僕のやり方がまずかったのか、Vue.jsでは動くけどNuxt.jsでは動かないとか、そんな感じだったのかもしれません。
海外のフォーラムから正解を発見
結論を見つけたのはココでした。
メインの話題に埋もれて救世主がぼそっと↓発言。
I think you can use this module: https://github.com/ daliborgogic/ nuxt-interpolation
このモジュール使えばいけんじゃね?
イイネが1個付いているだけで誰もレスしていなかったので僕も最初は流していたんですが、これが正解でした。
「nuxt-interpolation」はどういうモジュールなのか
実はよく分かっていないので公式の説明を。
Nuxt.js module as directive for binding every link to catch the click event, and if it’s a relative link router will push.
For improved security rel=”noopener” will be added automatically if target is _blank
えー、英語も苦手なのでテキトーですが、「全てのリンクのクリックイベントを捕らえるためのディレクティブを追加するモジュールですよ」でしょうか。
これがこのモジュールの主作用のようですが、今回は関係のない部分です。 そのせいで「あ、これ関係ないやつや」と思い一度スルーしてしまっていました。
ディレクティブという言葉は、HTMLタグの属性の位置に書く、Vue.jsやNuxt.jsの独自機能のこと(v-bindとか)みたいです。
さらに「ついでに、それが相対パスのリンクならrouter.pushしますよ」と。
これが今回の目的を達成できた部分です。 Vue.jsのページですがrouter.pushの説明です。 要はプログラム的にパスを認識させてSPAを有効にするみたいです。
僕がやろうとしていたのはどうにかして<nuxt-link>を認識させる方法。 このモジュールがやっているのは<a>タグそのままに、<nuxt-link>と同じ機能をプログラム的に追加する方法。
自分の無知を思い知りました。
気になったのでソースを見てみた
nuxt-interpolationのソースの該当部分(と思しき部分)。
const href = event.target.getAttribute('href')
if (href && href[0] === '/') {
event.preventDefault()
router.push(href)
}
ディレクティブの作り方やプラグインの作り方を勉強していないので全体的な構造はいまいち理解できませんでしたが、要は「href」属性を抽出して1文字目が「/」(=相対パス)ならそのパスをrouterにpushしてるだけっぽいです。
これなら自分でも書けそう!
と思ってやってみましたが、router.push()は指定したURLへ移動させる関数のようで無限にページ移動を続けてしまいました…。
これを抑制するのがevent.preventDefault()の部分っぽいです。
この先はまた高い壁がありそうだし、モジュールで期待通りに動いているので、今回はここまでとします…。
最後にちょっと言わせて欲しい
APIから取得した記事内のリンクを<nuxt-link>にする問題。
これ、Nuxt.js(Vue.jsも?)でブログ書いている人なら100%当たる問題だと思うんですよね。
それがこれだけ探さないと見つからなかったということは、Nuxt.jsでブログ書いている人が実はほとんどいないのか、Nuxt.jsでブログ書くような人なら説明するまでもない簡単なことだったのか…。
WordPressからNuxt.jsへ移行しようとしている初心者の助けになると幸いです。
まとめ
Nuxt.jsでAPIから取得した記事をv-htmlで出力して<nuxt-link>を有効にするには、「nuxt-interpolation」モジュールをインストールして出力部分で「v-interpolation」ディレクティブを追加するだけです!
<div v-interpolation v-html="content"></div>
このモジュールは相対パス(/から始まるパス)でだけ動作するので、絶対パスの場合は先に相対パスへ変換しましょう。
以上、WordPressからお届けしました!