Nuxt.js で v-html の内部リンクを処理する
- 公開日:
- 更新日:
Headless CMS で書いた記事の中で、別の記事へのリンクを張りたい場合があると思います。
しかし、v-html に渡すコンテンツはプレーンな HTML として解釈されるため、その中で <nuxt-link>
を使うことはできません。
代わりに、従来の JS のようにイベントで処理します。
nuxt-interpolation というモジュールもあるのですが、要素にメソッド生やしてるのと、rel="noreferrer noopener"
を問答無用で rel="noopener"
に書き換えてしまうのが個人的にアレだったので、そちらを参考に自分で書いてみました。ディレクティブのフック関数については、Vue.js のドキュメントで説明されています。
イベントリスナの中でごちゃごちゃやってますが、これは拡張子付きのリンク(/sitemap.xml
など)を除いた上で、パラメータやハッシュ付きのリンク(/disclaimer/?search=params#hash
など)は許可したいからです。
// plugins/v-content-links.js
import Vue from 'vue'
export default ({ app }) => {
const pushRoute = ev => {
const href = ev.currentTarget.getAttribute('href')
if (!href || href[0] !== '/') return
// router.resolve でリンク先が登録されているルートにマッチするか調べる
const { resolved } = app.router.resolve(href)
// マッチするルートがあり、パス部分にトレイリングスラッシュがあるなら router.push で遷移する
if (resolved.matched.length && resolved.path.slice(-1) === '/') {
ev.preventDefault()
app.router.push(resolved.fullPath) // resolved 自体を渡したいがトレイリングスラッシュが消えてしまう
}
}
const bind = function(el) {
const internalLinks = el.querySelectorAll('a[href^="/"]')
for (let i = internalLinks.length; i--;) internalLinks[i].addEventListener('click', pushRoute, false)
}
const unbind = function(el) {
const links = el.getElementsByTagName('a')
for (let i = links.length; i--;) links[i].removeEventListener('click', pushRoute, false)
}
Vue.directive('content-links', {
bind,
unbind,
componentUpdated(el) {
unbind(el)
bind(el)
},
})
}
これを nuxt.config.js の plugins に追加します。
// nuxt.config.js
export default {
plugins: [
'~/plugins/v-content-links',
},
}
これで v-content-links ディレクティブが使えるようになったので、v-html を使っている要素に追加します。
<div v-html="post.content" v-content-links />
この要素と初めて紐付けられる(bind)と、その要素内の href
が /
で始まる a 要素にイベントリスナを設定します。
ページ遷移などにより要素との紐付けがなくなる(unbind)と、全ての a 要素からイベントリスナを取り除きます。
componentUpdated(el)
は、ここでは v-html に指定しているデータに変化があったときに呼び出され、unbind 時の処理を行なってから bind 時の処理を行ないます。
スクロール位置が保存されてしまう
記事から他の記事へのリンクを張れるようにはなりましたが、記事ページを <nuxt-child>
で表示している場合は、リンク先の記事は途中から表示されてしまいます。
これを防ぐには、記事を表示するテンプレートか、その親となるテンプレートに scrollToTop: true
を設定します。
<template>
<article>
<div v-html="post.content" v-content-links />
</article>
</template>
<script>
export default {
scrollToTop: true,
async asyncData({ params }) {
// 記事を取得する処理
},
}
</script>
これで、ブラウザの「戻る・進む」以外で記事ページに来たら、ページの一番上までスクロールされます。
親コンポーネント(<nuxt-child>
コンポーネントがあるテンプレート)で true
に指定しておくと、その子となるページすべてでトップにスクロールされるようになります。