SEO に強くなる構造化データマークアップ  ~ Nuxt.js で JSON-LD をコンポーネント指向で管理する ~

この記事は Retty Advent Calendar 2018 13日目の記事です。
昨日は @RyoNkmr さんの、 「[ESLintの力で秩序あるAtomic Designを後世に残す ](https://ryonkmr.com/eslint-keeps-an-atomic-designed-project-from-the-chaos)」 でした。
Atomic Design はコンポーネント粒度を決定する銀の弾丸ではなく、チームに合わせたルールを導入するのが大切なので、そのルールに違反してないかを ESLint で検知できるのは嬉しいですね。

# はじめに

WEB サービスを開発・運営してる皆さんこんにちは。構造化データ、書いてますか。
SEO をやる上でコンテンツを充実させることと、その内容を正しくクローラーに伝えることはとても大切です。
コンテンツの内容をしっかり理解してもらうことは、SEO だけでなく検索エンジンからサービスに訪れるユーザーの UX 向上にも寄与し、それによりクリック数が増えればそれもまた SEO に繋がります。

ただ、構造化データのマークアップはエンジニア目線ではやや退屈です。
Microdata や RDFa は HTML のマークアップに直接入り込むため、マークアップコストや変更コストが引き上がり、JSON-LD は HTML マークアップとは別に script タグを設置しないといけません。
構造化データそのものはユーザーの目には直接触れない上、SEO として効果が出るのには時間がかかったりもします。
構造化データにエラーがあると、表示順位下落の容疑者にあげられることもあるでしょう。

なんとか楽にしたいですね :innocent: 

## 構造化データとは

> 構造化データとは、ページに関する情報を提供し、ページ コンテンツ（たとえばレシピのページでは、原材料、加熱時間と加熱温度、カロリーなど）を分類するための標準化されたデータ形式です。
> [構造化データについて | 検索 |  Google Developers](https://developers.google.com/search/docs/guides/intro-structured-data?hl=ja)

引用元にもあるとおり、コンテンツの内容を構造化データとして出力することで、HTML では伝えきれない属性などの情報を補完し、パンくずリストや会社情報など様々なデータを検索エンジンに理解・表示してもらうことができます。

# コンポーネント指向開発と JSON-LD

昨今のフロントエンドでは要素をコンポーネントに分割し、責務を適切にコンポーネントに割り振ることで、規模の拡張や変更に耐えうるアプリケーションを作るのが主流ですね。

JSON-LD とは構造化データの記法の一つで、以下のように JSON を含む script タグを設置します。

```html
<script type="application/ld+json">
{
  "@context": "http://schema.org",
  "@type": "Organization",
  "url": "http://www.example.com",
  "name": "Unlimited Ball Bearings Corp.",
  "contactPoint": {
    "@type": "ContactPoint",
    "telephone": "+1-401-555-1212",
    "contactType": "Customer service"
  }
}
</script>
```

[Nuxt.js](https://github.com/nuxt/nuxt.js/) で開発中にコンポーネントを作ってて思いました。
あるコンポーネントが表示の責務を負ってるコンテンツは、そのコンポーネントが  JSON-LD を吐き出せばいいのでは？

## それ、vue-meta でできるよ

Nuxt.js は [vue-meta](https://github.com/declandewet/vue-meta) というメタ情報を管理するための Vue.js のプラグインを内包しており、以下のように `head` メソッドをコンポーネントに追加することで、メタ情報を head タグに出力することができます。

```vue:SampleTitle.vue
<template>
  <h1>{{ title }}</h1>
</template>

<script>
export default {
  data () {
    return {
      title: 'Hello World!',
    };
  },
  head () {
    return {
      title: this.title,
      meta: [
        { hid: 'description', name: 'description', content: 'My custom description' },
      ],
    };
  },
};
</script>
```

これを利用すると、パンくずリストを表示するコンポーネントは以下のようにすることができます。

```vue:BreadcrumbList.vue
<template>
  <ol>
    <li
      v-for="(breadcrumb, index) in breadcrumbs"
      :key="index"
    >
      <breadcrumb-list-item v-bind="breadcrumb"/>
    </li>
  </ol>
</template>

<script>
import BreadcrumbListItem from '@/components/pc/atoms/BreadcrumbListItem.vue';

export default {
  components: {
    BreadcrumbListItem,
  },
  props: {
    breadcrumbs: {
      type: Array,
      required: true,
      default() {
        return [];
      },
    },
  },
  computed: {
    jsonld() {
      const items = this.breadcrumbs.map((item, index) => ({
        '@type': 'ListItem',
        position: index + 1,
        item: {
          '@id': item.url,
          name: item.text,
        },
      }));

      return  {
        '@context': 'http://schema.org',
        '@type': 'BreadcrumbList',
        itemListElement: items,
      };
    },
  },
  head() {
    const hid = `jsonld-${this._uid}`;

    return {
      script: [
        {
          hid,
          type: 'application/ld+json',
          innerHTML: JSON.stringify(this.jsonld, null, 2),
        },
      ],
      __dangerouslyDisableSanitizersByTagID: {
        [hid]: 'innerHTML',
      },
    };
  },
};
```

computed プロパティ `jsonld` でオブジェクトを作ります。
JSON は JavaScript のサブセットですし、お手のものですね。
もちろん ESLint や Prettier の恩恵を得ることもできます。

`head` メソッドでそれを呼び出し、 `__dangerouslyDisableSanitizersByTagID` で `innerHTML` をサニタイズしない設定と共にリターンします。

JSON-LD を組み立てるのはコンポーネントインスタンスのメソッドなので、data・props・computed・filter なども利用でき、表示のためにデータから文字列を組み立てて filter を通してから表示しているコンテンツなどもそのまま JSON-LD に使えます。


これを表示の責務を負うコンポーネントそれぞれでやっていけば…

面倒くさいですね！ :innocent: :innocent: :innocent: 

## nuxt-jsonld を作った

コンポーネントが担うのは JSON-LD の管理に集中させて、vue-meta を利用して出力するのは別の部分に任せたい。
そこで上記の computed プロパティ `jsonld` 以外の処理に当たる部分を自動でやってくれるプラグイン [nuxt-jsonld](https://www.npmjs.com/package/nuxt-jsonld) を作成しました。

コンポーネントのトップレベルに `jsonld` メソッドを生やすだけで出力してくれるようにしたため、コンポーネントの script 部分は以下のようになります。

```js
export default {
  components: {
    BreadcrumbListItem,
  },
  props: {
    breadcrumbs: {
      type: Array,
      required: true,
      default() {
        return [];
      },
    },
  },
  jsonld() {
    const items = this.breadcrumbs.map((item, index) => ({
      '@type': 'ListItem',
      position: index + 1,
      item: {
        '@id': item.url,
        name: item.text,
      },
    }));
    return {
      '@context': 'http://schema.org',
      '@type': 'BreadcrumbList',
      itemListElement: items,
    };
  },
};
```

オブジェクトをリターンするだけです :tada: 
これで、構造化データを書きたくなったときにメソッドを生やして内容をオブジェクトのままリターンすればいいので楽になりました。
ユニットテストも簡単です。

`nuxt-jsonld` がやってるのは `head` メソッドのミックスインを用意して、コンポーネント側の `head` とのマージストラテジーを定義してるだけです。 (カスタムオプションのミックスインはデフォルトだと[上書きになってしまう](https://jp.vuejs.org/v2/guide/mixins.html#カスタムオプションのマージストラテジ)ので、どのようにマージするかを書く必要があります。)

以下のように script タグが追加されます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/181570/dd31b035-919a-5a75-e5e7-030c7ad86877.png)


# まとめ

JSON-LD をコンポーネントで管理することで、構造化データの内容に集中し容易に変更できる環境を整え、開発速度を維持しつつ、価格・口コミ・パンくずリストなどのリッチスニペットやリッチカードの表示などに役立てていきましょう。

<img width="400" src="https://qiita-image-store.s3.amazonaws.com/0/181570/78674366-e07f-c050-fd83-39c49208a7bd.png">

↑Retty のリッチカードはこんな感じです。
ちなみに最近美味しかった十番ランチは 「[とんかつ 都](https://retty.me/area/PRE13/ARE14/SUB1405/100001359302/)」 の限定5食のカツ丼です :rice:

以上です :muscle: