ymmooot

Nuxt.js における meta タグを TDD で一元管理【i18n 対応】

この記事は Vue #3 Advent Calendar 2019 14日目の記事です。

概要

  • Nuxt.js に限らず、meta タグの管理は意外と面倒
  • 漏れなく全てのページで設定されてることを確認するために meta データ生成関数を作る
  • それを page コンポーネントで呼び出して利用する

はじめに

meta の管理、面倒ですよね。
Nuxt.js の場合はコンポーネントにて head メソッドを利用するのですが、ここにロジックを書いてくとメンテが非常に大変なので、meta タグのロジックをコンポーネントから切り出し、コンポーネントではそれを呼び出すだけにします。

meta タグ生成

好きなところに適当にフォルダを切ります。

libs
├── head
│   ├── index.ts
│   ├── core
│      ├── article.ts
│      ├── common.ts
│      ├── index.spec.ts
│      ├── index.ts
│      └── utils.ts

こんな感じ

core

core は情報を受け取ってタグに変換する層です。common.ts は以下のように全ページに必要なタグを生成する関数を提供します。

core/common.tsimport { Route } from 'vue-router';
import { resolve, renderTitle, getCountryName } from './utils';

export default (
  baseTitle: string,
  description: string,
  route: Route,
  updatedAt: Date,
  imageUrl: string = 'https://example.com/noimage.png',
  ogType: 'article' | 'website' = 'website',
) => {
  const HOST_NAME = process.env.BASE_URL || 'https://example.com/';
  // not use vue-meta's titleTemplate to reuse title in other tags
  const title = renderTitle('{title} | Site Name {country}', baseTitle, getCountryName(route.name));

  return {
    title,
    link: [
      {
        hid: 'alternate-hreflang-x-default',
        rel: 'alternate',
        hreflang: 'x-default',
        href: resolve(HOST_NAME, 'sg'),
      },
      { hid: 'canonical', rel: 'canonical', href: resolve(HOST_NAME, route.fullPath) },
    ],
    meta: [
      { hid: 'og:site_name', property: 'og:site_name', content: 'Site Name' },
      { hid: 'og:url', property: 'og:url', content: resolve(HOST_NAME, route.fullPath) },
      { hid: 'og:type', property: 'og:type', content: ogType },
      { hid: 'og:title', property: 'og:title', content: title },
      { hid: 'og:description', property: 'og:description', content: description },
      { hid: 'og:image', property: 'og:image', content: imageUrl },
      { hid: 'og:updated_time', property: 'og:updated_time', content: updatedAt.toISOString() }
      { hid: 'twitter:card', property: 'twitter:card', content: 'summary_large_image' },
      { hid: 'fb:app_id', property: 'fb:app_id', content: '111111111111111' },
    ],
  };
};

i18n 系のタグは全て nuxt-i18n に任せていますが、hreflang: x-default だけはいれてくれないので自分でいれます。


article.tsog:typearticle であり、article:published_timearticle:modified_time などさらに詳細な情報を common.ts で生成したタグに追加する関数を提供します。

core/article.tsexport default (
  title: string, 
  description: string, 
  route: Route, 
  updatedAt: Date,
  publishedAt: Date, 
  imageUrl?: string,
) => {
  const head = commonHead(title, description, route, updatedAt, imageUrl, 'article');

  return {
    ...head,
    meta: [
      ...head.meta,
      { hid: 'article:section', property: 'article:section', content: 'Financial Contents' },
      {
        hid: 'article:publisher',
        property: 'article:publisher',
        content: 'https://www.facebook.com/ExmapleSite/',
      },
      {
        hid: 'article:published_time',
        property: 'article:published_time',
        content: publishedAt.toISOString(),
      },
      {
        hid: 'article:modified_time',
        property: 'article:modified_time',
        content: updatedAt.toISOString(),
      },
    ],
  };
};

上記のようにさらにタグを追加します。


あとは以下のように export してあげます。

core/index.tsimport { Route } from 'vue-router';
import commonHead from './common';
import article from './article';

export const websiteHead = (title: string, description: string, route: Route, updatedAt: Date) =>
  commonHead(title, description, route, updatedAt);
export const articleHead = article;

ここまでのテストは head/core/index.spec.ts などで済ませておきます。

各ページの meta タグのテストを書く

head のメソッドを呼び出して望んだ meta タグのオブジェクトが帰ってくるかテストを書きます。

index.spec.tsdescribe('productDetailHead', () => {
  it('returns meta tag objects', () => {
    const route = routeFactory('index', '/', 'sg');
    const product = productFactory(/*ここにスタブするデータ*/);
    const head = productDetailHead(route, product);
    expect(head).toEqual(/*望むべき meta タグ*/);
  });
  // 他にもサムネイルがセットされてない Product の場合など、テストを書いていく
});

具体的なロジックを head に書いていく

例えば Product の一覧ページ用の meta タグ関数は

index.tsimport { Route } from 'vue-router';
import { articleHead } from './core';

const makeTitleWithProduct = (product: Product): string => {
  // ここにタイトル生成処理
  return `Example product ${product.name} (${product.category.name})`;
};

export const productDetailHead = (route: Route, product: Product) => articleHead(
  makeTitleWithProduct(product),
  product.description,
  route,
  updatedAt: product.updatedAt,
  publishedAt: product.publishedAt,
  imageUrl: product.thumbnailUrl,
);

こんな感じですね。

makeTitleWithProduct は Product が関わる他のページでも使い回すことができ、core に渡す情報を生成するロジックを容易に共通化できます。
ここまでテストが通っていれば、page コンポーネント側ではモックされた productDetailHead を呼んでいるかどうかだけを担保すればよくなります。

終わりに

こういった描画に直接関係ないものはコンポーネントにロジックをモリモリ書かずにうまく切り出して、通常の関数としてテストしていくのがいいでしょう。同じく head メソッドを駆使することになるであろう、構造化データの JSON-LD 生成を楽に管理できるツール nuxt-jsonld も作ったので良かったらお使いください。(去年のアドベントカレンダーで紹介しました)
 👉 SEO に強くなる構造化データマークアップ ~ Nuxt.js で JSON-LD をコンポーネント指向で管理する ~


参照