Nuxt.jsで効率的にheadを管理する方法 (ページごとにごちゃごちゃ設定したくないのよ)

シェアする:

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

  • Nuxt.jsを始めたけどheadがなんかごちゃごちゃする人
  • Nuxt.jsのheadを楽して効率的に管理したい人
  • つまりオレ

何も知らない超初心者が脱WordPressしたくてNuxt.jsでサイト構築していくシリーズです。

今回はNuxt.jsでウェブサイトを作る際にheadタグ内を効率的に管理する方法を考えます。

プロジェクトを分けてなんだかんだで10プロジェクト以上はNuxt.jsでウェブサイトを作っていて、初期バージョンに比べて何度か管理方法を変えてきた上での経験を踏まえてお話しますお。

なお、筆者はJavaScriptやNode.jsなどを体系的に学ぶことなくいきなりNuxt.jsに突っ込んだアホ無謀者なので、意味不明なことをしてる場合がままあると思います。 Nuxt.js上での動作は確認しているのでご容赦を。

本記事の内容はNuxt.jsのv2での話です。 もしかするとv3は違う可能性があるのでテキトーに読み替えてください。

Nuxt.jsでのhead管理

デフォルトだとまずnuxt.config.jsのコレ↓。


head: {
  title: 'XXXXXXXXX',
  htmlAttrs: {
    lang: 'en'
  },
  meta: [
    { charset: 'utf-8' },
    { name: 'viewport', content: 'width=device-width, initial-scale=1' },
    { hid: 'description', name: 'description', content: '' },
    { name: 'format-detection', content: 'telephone=no' }
  ],
  link: [
    { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
  ]
},

HTMLのheadタグ内をJavaScriptのオブジェクトに変換したような書き方です。 複数の要素を設定できるmetaとlinkに関しては配列で指定するとその全てがそれぞれのタグになります。

上の例で書き方はだいたい分かると思いますが詳しくは公式を見てください。

僕がNuxt.jsを触り始めたころはlayoutsのdefault.vueや、pagesのindex.vueにもhead定義があった気がするんですけど、今のバージョンにはないっぽいですね。

Nuxt.jsでヘッダ定義のできる場所は僕の知る範囲ではこんな↓感じ。

  • nuxt.config.js
  • layoutsのdefault.vue
  • pagesの個別のvueファイル
  • nuxt-childを使う場合は子ページ
  • ページ内で使用しているコンポーネント

下へ行くほど優先度が高く、同じ値を上書きできます。

そのまま追記していくとmetaやlinkは同じ要素が重複してしまいますが、この↓ようにhidで重複防止用のキーを設定しておくと


{ hid: 'description', name: 'description', content: '' },

同じキーのプロパティは後から上書きされるようになります。 キーにはハイフンやコロンなどの記号も使えるので、脳死でnameやrelの値を突っ込んでおけばいいと思います。

指定しない場合は複数が出力されます。

効率的に管理するにはどうすればいいか

headに設定する要素はcharsetやviewport、title、descriptionなどのほぼ必須の要素から、その他のmeta、link要素、OGPなど様々な要素があり、人によって設定する要素もかなり違ってくると思いますが、運用的には次の3つに大別できると思います。

  1. 全ページで共通の項目
  2. ページごとに異なる項目
  3. ページごとに異なるけど共通で処理できる項目

③はcanonicalとかですね。 ページごとに違うけど同じ処理で取得できる系のやつ。

と、なるとですね。

nuxt.config.jsにhead設定するのってかなり微妙なんですよね。

というのも上のリストの①と③は共通部分で設定できるんですが、nuxt.config.jsでは③ができないんですよ。 そうなるとnuxt.config.jsと共通処理とページ別の3箇所で分けて設定しなくてはいけなくなります。

というわけで、

nuxt.config.jsのheadは削除してしまへ!

ほんとに削除しても動作に問題ないことを確認しています。

ここではdefalt.vueとページ単位の2箇所で設定することにします。

事前準備

グローバル変数の設定

headを効率的に管理するためにそこで扱う定数やルーチンをグローバルで触れるようにしています。

そのためにinjectやVue.mixinという機能(あんまり理解してない)を使っているので、まずはグローバル変数の扱いを覚えてください。

レイアウト機能

Nuxt.jsにはレイアウトという機能があって、WordPressで言うところのsingle.phpとかpage.phpでやってるテンプレート機能と言えばいいでしょうか。 ヘッダやフッタ、サイドバーなんかをここに詰め込んでおくことで、ページごとの実装を簡略化することができます。

どこかのバージョンからデフォルトで作られなくなったみたいなので、ない場合はプロジェクト内にlayoutsフォルダを作ってその中に置いてください。

最小構成のコードは現物が残ってないのでアレですが、たぶんこんな↓感じだと思います。


<template>
<Nuxt />
</template>

Nuxtタグの部分で個別ページのvueファイルが読み込まれます。

機能的に使う場合はヘッダとかをコンポーネント化して、こんな↓感じ。


<template>
<div>

<Header />

<Nuxt />

<Sidebar />

<Footer />

</div>
</template>

template内はひとつの要素で囲う必要があるので外側にdivなり何なりが必要。

defalt.vueからheadの設定

defalt.vueではトップページを想定してheadを設定しておきます。

こうすることでトップページではheadの設定が必要なくなり、個別ページで上書き設定をし忘れた場合にも一応なんとなくの格好にできます。

大雑把な格好はこんな↓感じになります。


export default {
  head() {
    const lang = 'ja'
    const title = 'トップページのタイトル'
    
    let metas = []
    let links = []
    
    return {
      htmlAttrs: {
        lang: lang,
      },
      title: title,
      meta: metas,
      link: links,
    }
  },
}

lang、title、meta、linkの設定を個別に見て行きます。

langの設定

htmlタグ↓に付ける言語コードです。


<html lang="ja">

このページを見ている人はおそらく日本語の人だと思うので基本は「ja」固定でいいと思います。

i18nで多言語設定にしている場合は、こう↓すれば現在の言語コードが入ります。


const lang = this.$i18n.locale

titleの設定

トップページのタイトルってサイト名かそれ含むことが多いと思います。 サイト名はヘッダ以外でも使うことがあるので、インジェクトを使ってグローバルに設定しています。

$libでインジェクトしているものとして、こんな↓感じ。


const title = this.$lib.siteName

タイトルはこういう↓感じの2パターンだと思うので適宜調整してください。

  • オレDEV.com
  • プログラミング解説サイト オレDEV.com

titleはmetaとも関係があるので、metaの項目を見た上で変数名などを決めてもらったほうがいいと思います。

metaの設定

人によって設定項目がけっこう違うと思うので、これを参考に足したり消したりしてください。

まずは設定から読み込む項目↓はこんな感じ。


const siteName = this.$lib.siteName
const pageTitle = 'プログラミング解説サイト ' + siteName
const desc = this.$lib.siteDesc

const rootUrl = this.$lib.rootUrl
const currentUrl = rootUrl + this.$route.path
const imageUrl = root + '/img/banner.jpg'

上のtitleと違い、siteNameとpageTitleを分けています。 siteNameは完全にサイト名として使います。 pageTitleは「プログラミング解説サイト オレDEV.com」みたいな感じで使う場合に使います。 pageTitleが=サイト名で良い場合はpageTitleは不要です。

UTLは順にトップページ、現在のページ、SNSに貼った時のサムネイル画像のものです。

あとはhidを設定して配列↓にします。


let metas = [
  { charset: 'utf-8' },
  { hid: 'viewport', name: 'viewport', content: 'width=device-width,initial-scale=1' },
  { hid: 'robots', name: 'robots', content: 'index,archive,follow' },
  { hid: 'description', name: 'description', content: desc },
  
  // ogp関連
  { hid: 'og:title', property: 'og:title', content: pageTitle },
  { hid: 'og:type', property: 'og:type', content: 'website' },
  { hid: 'og:image', property: 'og:image', content: imageUrl },
  { hid: 'og:description', property: 'og:description', content: desc },
  { hid: 'og:url', property: 'og:url', content: currentUrl },
  { hid: 'og:locale', property: 'og:locale', content: 'ja_JP' },
  { hid: 'og:site_name', property: 'og:site_name', content: siteName },
  { hid: 'twitter:card', name: 'twitter:card', content: 'summary_large_image' },
]

ここからもうひと手間いれます。

og:typeはトップページではwebsite、それ以外ではarticleになると思います。 そこでこんな↓コードを追加しておきます。


if ( this.$route.path != '/' ) {
  meta.push({ hid: 'og:type', property: 'og:type', content: 'article' })
}

og:typeを2回定義することになりますがhidを設定しているので後の方が優先されます。

もひとつ、i18nで多言語化している場合はog:localeを現在のページの言語コードに合わせる必要があります。 i18nの使い方は解説を見てください。

nuxt.config.jsで言語コードをこの↓ように設定しているものとして、


{ code: 'ja', iso: 'ja_JP', name: '日本語' },
{ code: 'en', iso: 'en_US', name: 'English' },

まず現在のlocaleを取得↓して、


const locale = this.$i18n.locales.find(row => row.code == app.i18n.locale)

その値からog:localeを動的に設定↓します。


{ hid: 'og:locale', property: 'og:locale', content: locale.iso },

linkの設定

meta同様、過不足は調整してください。

設定から読み込む項目↓はこんな感じ。


const rootUrl = this.$lib.rootUrl
const currentUrl = rootUrl + this.$route.path

rootUrlもcurrentUrlもmetaと共通の変数なので再定義は必要ありません。

配列↓にします。


let links = [
  { hid: 'canonical', rel: 'canonical', href: currentUrl },
  
  //Favicon
  { rel: 'icon', type: 'image/x-icon', href: rootUrl + '/favicon.ico' },
]

あとは、i18nをalternateを設定しておいた方がいいらしいのでこんな↓感じで。


this.$i18n.locales.forEach(row => {
  links.push( { rel: 'alternate', hreflang: row.code, href: rootUrl + this.switchLocalePath(row.code) } )
})

switchLocalePathはi18nの関数です。

alternateは現在のページの言語も追加するそうなので、現在の言語をはじく必要はないです。

ページ単位でのheadの設定

default.vueで大半の設定が終わっているので、ページごとに変わる要素は次の4つです。

  • title
  • description
  • og:title
  • og:image

次の5つも変わりますが、default.vue側の処理で既に対応しています。

  • lang
  • og:type
  • og:url
  • og:locale
  • canonical
  • alternate

上記4項目をページごとに切り替える方法はいくつかありますが、今回はVue.mixinでやろうと思います。 実際に動かしてるのは上の$libの方に関数つくってるんですが、Vue.mixinだとthisをページ内と同様に扱えるので直感的に分かりやすそう。

Vue.mixin側の実装

大雑把な格好はこの↓ようになります。


Vue.mixin({
  methods: {
    getHead(title, desc, thumbnail) {
      let metas = []
      
      return {
        title: title,
        meta: metas,
      }
    },
  },
})

まずタイトルですが、記事の名前が「Nuxt.jsの初心者講座」だとしたらhead内のタイトルって「Nuxt.jsの初心者講座|オレDEV.com」みたいにサイト名を追加したりするじゃないですか。 その場合はこの↓ようにここで足しておきます。


title += ' | ' + this.$lib.siteName

meta要素はこんな↓感じ。


let metas = [
  { hid: 'description', name: 'description', content: desc },
  { hid: 'og:title', property: 'og:title', content: title },
  { hid: 'og:description', property: 'og:description', content: desc },
]

あとはサムネイル画像が指定されていた場合にはその画像で上書き↓します。


if ( thumbnail ) {
  metas.push({ hid: 'og:image', property: 'og:image', content: this.$lib.rootUrl + thumbnail })
}

まとめるとこんな↓感じ。


Vue.mixin({
  methods: {
    getHead(title, desc, thumbnail) {
      title += ' | ' + this.$lib.siteName
      
      let metas = [
        { hid: 'description', name: 'description', content: desc },
        { hid: 'og:title', property: 'og:title', content: title },
        { hid: 'og:description', property: 'og:description', content: desc },
      ]
      
      if ( thumbnail ) {
        metas.push({ hid: 'og:image', property: 'og:image', content: this.$lib.rootUrl + thumbnail })
      }
      
      return {
        title: title,
        meta: metas,
      }
    },
  },
})

ページ側の実装

head()をこの↓ように全てのページで実装するだけです。


export default {
  head() {
    return this.getHead(
      'このページのタイトル',
      'このページの説明',
      'このページのサムネイル画像の相対URL',
    )
  },
}

ここを簡略化するためにここまで頑張ってきたので成果が出てますね。

サムネイルが不要の場合はこれ↓でいけます。


export default {
  head() {
    return this.getHead(
      'このページのタイトル',
      'このページの説明',
      //'このページのサムネイル画像の相対URL',
    )
  },
}

コメントアウトするだけでサムネイルなしページにも対応できるので、後からサムネイルを追加する場合のも管理しやすいです。

もうちょっと発展させられそうな可能性

ここまでで個別ページ側の実装を最小限に効率良くhead管理ができるようになったと(個人的には)思います。

ですが一応もう1段階上があってですね、Vue.jsの$emitを使うと子(defalt.vueに対して個別ページのvue)の変数の変化でイベントを発生させられるそうなので、これを利用するとそもそも個別ページにhead()すら置かなくて同じことができると思います。

ただ、この仕様でプロジェクトを作ると子側に親の仕様を強制する形になるので今回は諦めました。

いや、実際は今の形でも同程度の強制があるのでメンテナンス性はあんまり変わんないと思うんですが、今回はね、

そう!疲れたんです!

また気が向いたら試してみたいと思います。

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

シェアする: