Nuxt.jsでパンくずリストを自動生成する(階層構造型)

シェアする:

この記事をおすすめしたい人

  • Nuxt.jsで全自動パンくず機を作りたい人
  • つまりオレ

WordPress Nuxt化の一環です。

今回は、Nuxt.jsで動的にパンくずリストを生成するコンポーネント、題して「全自動パンくず機」を作っていきます。

WordPressの上でな!

※ このブログはまだWordPress製です

最も簡単なパンくずリストの書き方

最初に思いつくのは「パンくずの要素を直接指定してやる方法」です。

例えばこんな↓の。


export default {
  data() {
    return {
      items: [
        { to: '/category/', name: 'カテゴリページ' },
        { to: '/category/wp/', name: 'WordPressカテゴリ' },
        { to: '/category/wp/page/', name: 'WordPressに関する記事' },
      ],
    }
  },
}

<script>内でパンくずに表示する内容をページごとに指定します。

そして<template>内で上記itemsをv-forで表示します。


<ul>
<li v-for="item in items">
<nuxt-link :to="item.to">{{item.name}}</nuxt-link>
</li>
</ul>

チョー簡単です。

以降の差別化のために、この方法を「直接指定法」と呼ぶことにします。

もうちょっと実践的な直接指定法

上のコードだと次のような問題点があります。

  • トップページ(パンくずの要素がない)でも<ul>が出力される
  • 最上位にトップページがない
  • 記事ページはタイトルが長くなりがちなので「この記事」表記にしたい
  • 記事ページ(今いるページ)もリンクになっている

これらを改善したコードがこんな↓感じ。


<ul v-if="items.length > 0">
<li><nuxt-link to="/">トップページ</nuxt-link></li>
<li v-for="( item, i ) in items">
<nuxt-link v-if="i < items.length-1" :to="item.to">{{item.name}}</nuxt-link>
<span v-else>この記事</span>
</li>
</ul>

<ul>タグに「v-if="items.length > 0"」を付けることで、パンくずの指定があった場合にのみパンくずリストを出力するようにします。

最上位のトップページはv-forの外側に置き、必ず出力、かつ指定しなくてもいいようにします。

v-forは要素と配列番号でループさせ、最後の要素でなければリンクを、最後の記事なら<span>で「この記事」と出力します。

ページごとにこのコードを書くのはさすがに非効率なので、上の<ul>をまるままコンポーネントにし、「items」はpropsで受け取る形がよいでしょう。

直接指定法のメリット

ページごとに自分で自由に設定できるため、複雑なコードを考えなくて済みます。

要素についても、自分で指定するので、自分が間違わない限り間違いは起きません。

カテゴリなどAPIから情報を取得する場合にも、ページ内で既にAPIを取得しているので、パンくず側で再取得を考える必要がありません。

また、ルール変更も自由自在で、「WordPressカテゴリ」としていたけど特定のページでは「WP」と表記したい、みたいな場合にも柔軟に対応できます。

後から出てきますが、親ページの情報を取得…というか設定しやすいのも直接指定法のメリットです。

直接指定法のデメリット

めんどくさい!!

これに尽きるでしょう。

さらに!

ページごとに指定しないとパンくずを表示しないので、面倒な上に、書き忘れが生じる可能性があります。

手間的にもメンテナンス性の面でも、他の手段がどうしても思いつかなかった場合の最終手段とします。

できれば採用したくない。

というわけで、以下、自動でパンくずリストを生成する方法を考えていきます。

パンくずリストのタイプ

全自動パンくず機を考える前に、パンくずリストのタイプについてちょっと触れたいと思います。

個人的に、パンくずリストは2タイプに分類できます。

タイプによってパンくず生成の方法がかなり異なってきます。

階層構造型

URLの階層構造のままにパンくずリストを生成するタイプです。

例えば「/category/php/for-beginner/」というページだった場合、

  1. トップページ
  2. カテゴリ一覧
  3. カテゴリ【PHP】
  4. 初心者のためのPHP講座

の4階層になります。

おそらく最もスタンダードなタイプです。

ですが注意点として、「/category/php/」というページはあるけど、「/category/」は存在しない、みたいな場合。

この方法は「階層ごとにページが必ず存在している」という前提なので、この場合は「/category/」は除外などのひと手間追加が必要になります。 あるいは下の「カテゴリ型」を使用します。

カテゴリ型

先ほどの例の場合、「/category/php/for-beginner/」というページにもかかわらず、以下のようなパンくずになります。

  1. トップページ
  2. カテゴリ【PHP】
  3. 初心者のためのPHP講座

が、あくまでこれは一例で、要は「URLに依存しないパンくずリスト」です。

このサイトのパンくずリストがこのタイプで、記事ページは「/post/for-beginner/」のような構造になっていて、そもそもURLにカテゴリを含みませんが、パンくずリストは上記のカテゴリ型になっています。

いやいや、そんなんパンくずリストちゃうやろ!

と思われるかもしれません。

ですがGoogleの検索結果ではちゃんと上記パンくずリストの構造で認識されています。
(これはおそらく構造化データで明示しているせいですが)

何が言いたいかというと、パンくずリストは必ずしもURL通りの構造にする必要はなく、構造を無視して意味的に便利な内容になっていても問題ないということです。

このタイプのパンくずリストはどのように生成するかは次回に書きます。

とりあえずカテゴリ型はいったん置いておいて、階層構造型の全自動パンくず機を作っていきたいと思います。

方法① スラッシュで区切る

完全自動化を目的としているので、パンくず生成用のコンポーネントを用意し、その中で作業をします。

URLが先ほどの「/category/php/for-beginner/」だった場合、パンくずリストは次の4階層になります。

  • /
  • /category/
  • /category/php/
  • /category/php/for-beginner/

これを現在のルートから処理するのは簡単で、コードはこんな↓感じ。


export default {
  computed: {
    items() {
      let path = this.$route.path
      
      let items = []
      
      while ( path != '/' ) {
        items.push(path)
        
        path = path.replace(/[^\/]+\/$/, '')
      } 
      
      return items
    },
  },
}

僕は「trailingSlash: true」でやっているので、falseの人は正規表現やwhileの条件式を変えてください。

whileの回し方の都合、「/」を除く3つが「items」の中に入ります。

このままだと順序が逆順なのと、パンくずリストを作るために不足している情報があるので、ちょっと変えます。


export default {
  computed: {
    items() {
      let path = this.$route.path
      
      let items = []
      
      while ( path != '/' ) {
        items.push({ to: path, name: path })
        
        path = path.replace(/[^\/]+\/$/, '')
      } 
      
      return items.reverse()
    },
  },
}

<template>の中身がこんな↓感じだとすると、


<ul v-if="items.length > 0">
<li><nuxt-link to="/">トップページ</nuxt-link></li>
<li v-for="item in items">
<nuxt-link :to="item.to">{{item.name}}</nuxt-link>
</li>
</ul>

出力結果はこう↓なります。


<ul>
<li><a href="/">トップページ</a></li>
<li><a href="/category/">/category/</a></li>
<li><a href="/category/php/">/category/php/</a></li>
<li><a href="/category/php/for-beginner/">/category/php/for-beginner/</a></li>
</ul>

直接指定法の時と同様に、トップページでは出力されず、最上位のトップページは指定なしでも追加されます。

現在のページ(最下層)をリンクにしなかったり、「このページ」と表記を変える場合は、直接指定法の時と同様にv-ifで指定します。


<li><nuxt-link to="/">トップページ</nuxt-link></li>
<li v-for="( item, i ) in items">
<nuxt-link v-if="i < items.length-1" :to="item.to">{{item.name}}</nuxt-link>
<span v-else>このページ</span>
</li>

あとは、各階層のページ名を取得できればもう完成です。 それはもうちょっと後で書きます。

方法② VueRouterのmatchedを使う

あまり直接的な説明ではないんですが、$route.matchedの説明です。

「/category/php/for-beginner/」のページで、「this.$route.matched」の中身を見ると次のような配列が入っています。

  • 0: {path: “/category/”, …}
  • 1: {path: “/category/php/”, …}
  • 2: {path: “/category/php/for-beginner/”, …}

これはパンくずリストを作る上で欲しい情報そのものです。

先ほどのコードを流用して、こんな↓感じにしてみました。


<ul v-if="$route.path != '/'">
<li><nuxt-link to="/">トップページ</nuxt-link></li>
<li v-for="item in $route.matched">
<nuxt-link :to="item.path">{{item.path}}</nuxt-link>
</li>
</ul>

注意点と合わせて変更点を説明します。

vueファイルの配置の仕方に注意

Nuxt.jsで「/test/」というルートを作るには次の2つの方法があります。

  • /test/index.vue
  • /test.vue

僕はもっぱら前者の方法を使っていたんですが、こちらだと深い階層でも「$route.matched」にひとつの要素しか入りません。


0: {path: "/category/php/for-beginner/", …}

後者の方法だとうまくいきました。

最初、要素がひとつしか返らないもんだからイライラしながら調べていると、Nuxt.js公式の説明に書いてありました…。

ネストされたルートの親コンポーネントを定義するには、子ビューを含む ディレクトリと同じ名前 の Vue ファイルを作成する必要があります。

つまり、「/category/php/for-beginner/」というルートの場合は以下のような構造が必要ということになります。

  • /category.vue
  • /category/php.vue
  • /category/php/for-beginner.vue

ちゃんと説明は読まないとダメですね!

<ul>のv-ifについて

「$route.matched」は1階層目(/category/など)ではひとつの要素が入っていますが、トップページでも1つの要素が入っています。


0: {path: "/", …}

つまり、「matched.lengthが1以上かどうか」では「トップページかどうか」を判定できないので、pathで直接判定しています。

これまでの方法でも同様にpathによる判定ができるので、最初からこっちにしておけばよかったですね。

ネストしたルートの大問題点

実験も終盤で気付きました。

パンくず生成に関してはうまくいっていたのですが、「/category/php/for-beginner/」を開いてもページの中身が「/category/」のものになっています。

これは公式の説明にあった「<nuxt-child/>を親コンポーネントに入れてね」という部分だったようで、<nuxt-child/>を入れると解決…

と思いきや、今度はcategory.vueの中の<nuxt-child/>の部分にphp.vueの中身が展開されてしまっています。 for-beginner.vueまで開くと3つのページを合わせたような内容になりました。

ページに階層を持たせるというよりはレイアウトのように機能しているようです。

これはこれで知っていると使いどころがありそうですが、今回の目的には合わなさそうなので没…ですかね。

う~ん、なんか違うな。 たぶん僕のルーティングに関する知識が浅いせいで失敗している気がする…。

方法①を採用し、ページタイトルを取得する

期待していた方法②がどうにもうまくいかなかったので、方法①で続けます。

後は親ページの情報を取得して、リンクに名前をつけるだけです。

指定したルートのページ情報取得には、ルートの存在チェックの時に登場したVueRouterのresolve()を使います。

詳細は上のページで書いているのでそっちを見てもらうとして、今回は戻り値の中の「resolved.meta」に注目しました。

metaはVue Routerのルートメタフィールド

$route.matchedの説明でも出てきたルートメタフィールドです。 ルートを設定する際にメタ情報を追加することができます。

これを使えば簡単にできそうでしたが、Vue.jsは手動でルート設定をしないといけないそうなのでその際にメタ情報を追加できるのに対して、Nuxt.jsは「pages」以下のファイル構成から自動でルート設定をしてくれます。

このせいでメタ情報を設定する場所がないんです。

一応やり方を調べてみて、以下の2つを検討。

  1. nuxt.config.jsでrouter>extendRoutesを使う方法
  2. ミドルウェアを使ってmetaを設定できるようにする方法

「extendRoutes」は、可能だったとしてもページ個別の情報をnuxt.config.jsで管理するというのがどうもナンセンスな気がします。

ミドルウェアを使う方法は、成功するとページ単位で設定できるため魅力的です。 ですが、ミドルウェアやストアに対する理解が浅いというか無勉強なせいで、再現できず…。 インスタンスが作られた後じゃないと取得できない…?

力業で解決

というわけで今回は力業を使います。


export default {
  computed: {
    items() {
      let path = this.$route.path
      
      let items = []
      
      while ( path != '/' ) {
        items.push({ to: path, name: this.getRouteName(path) })
        
        path = path.replace(/[^\/]+\/$/, '')
      } 
      
      return items.reverse()
    },
  },
  methods: {
    getRouteName(path) {
      const list = [
        { path: '/category/', name: 'カテゴリページ' },
        { path: '/category/php/', name: 'カテゴリ【PHP】' },
        { path: '/category/wp/', name: 'カテゴリ【WordPress】' },
      ]
      
      const match = list.find(item => item.path == path)
      
      if ( match ) {
        return match.name
      }
      
      const r = this.$router.resolve(path)
      
      return r.resolved.name
    }
  },
}

getRouteName()という関数を用意し、そこにあらかじめパスに対する名前を一覧で定義しておきます。

該当するものがあればその名前を、なければresolve()の結果のname(category-phpなど)を返します。

だいぶ力業ですが、名前を管理する場所がパンくずコンポーネント1箇所になるので、これはこれで管理しやすいんじゃないかなと。

まとめ

Nuxt.jsで全自動パンくず機を作る方法を考えてみましたが…

WordPressほど簡単にはいかないですね…。

これはNuxt.jsの仕組み上、仕方がないのかなと感じています。

それでもできる限り自動でパンくずリストを作る方法をテストしてみたので、参考にしてもらえると幸いです。

以上、WordPressからお届けしました!

シェアする: