この記事は 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.ts
は og:type
が article
であり、article:published_time
や article: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 をコンポーネント指向で管理する ~