Nuxt.js で v-html の内部リンクを処理する

Nuxt.js
公開日:
更新日:

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 に指定しておくと、その子となるページすべてでトップにスクロールされるようになります。