Next.jsで構造化データを指定する

## 構造化データとは
構造化データ(structured data)とは、簡単に説明すると検索エンジンなどのクローラーに対して、ページがどのようなコンテンツなのかを意味付けするためのデータのことです。

例えばhtml5にも似たようにarticleなどのセマンティックなタグがありますが、より詳細に定義することができます。検索エンジンのクローラーが単なる文字列としてではなく、構造化されたデータとして認識してくれて、検索結果ページの表示が変わったりすることもあるのでSEO対策として使用されます。

構造化データにもいくつか形式があるのですが、ここではGoogleが推奨している**JSON-LD**を使用します。
https://developers.google.com/search/docs/guides/intro-structured-data?hl=ja

## Next.js + 構造化データ
簡素な記事ページを作成して、構造化データを指定してみます。

### 準備
まずはNext.js本家のセットアップ通りにhello worldします。

```sh:shell
mkdir hello-structured-data
cd hello-structured-data
npm init -y
npm install --save next react react-dom
mkdir pages
```

package.jsonのscripts部分を修正。

```json:package.json
"scripts": {
  "dev": "next",
  "build": "next build",
  "start": "next start"
}
```

### サンプルページを追加
よく見る2カラムの記事ページを作成します。
pages/index.jsを作成して下記の内容を入力してください。data変数はAPIからのレスポンスをサンプルとして定義しています。

```jsx:pages/index.js
import Link from 'next/link'

const data = {
  "id": 1,
  "title": "ダミー記事",
  "body": 
    `それは時間けっしてその説明人というのの時より打ち明けたあり。どうも場合をお話目もほとんど同じ見当ですあっなりを言い直すと切らででは徹底かい摘んなけれませじゃ、とてもにはありたたたた。文学に聞いなけれ事はほとんど場合にまあたたまし。
    必ずしも張さんで話事どう啓発を聞きな懐手そのご存じそれか演説よりというご融和ないですなけれたから、その場合は私か人言葉からするし、大森君の訳に自分の私がもしお病気と云っが何常が同手続きにしようにことにお記憶からいうたたば、とにかくけっして挨拶をできるですて得るなけれ事がいうなけれだろ。それでだからお機械に行っのはどう同等と合うたて、その権力をもあったがという海鼠の思って来ありない。その日心の時この弟はあなた上が起りたかと岡田さんに見えますござい、料の十月ならというご相談うですですて、大学の所が権力に事実までの外国が事実みからしまって、あいにくの事実から飲んてそのためを同時に窮めなくっですと換えるますはずならて、ないですたてあまりご主義いううものでますで。また人か嫌いかお話しを延ばすたて、前上見識をできるけれどもいるだ所にお活動の今日にやむをえましょます。
    場合でも初めて思いてあるんたらませでて、ついにもっともするば教育もぴたりやむをえなかった方た。ただ皆満足が決するてはみなけれのならて、頭をは、ついあなたかもつれが片づけれますな仕上るられうだと込んから、権力はもってくるでしょう。すこぶるまるではできるだけ本位においてくるたて、あなたをは十一月ごろまで私のご学習は広い取り消せ下さいだです。
    そこもしかるに講演の事がごぼんやりは起るていないずたんから、四二の傾向でそう叱らたという活動でしと、つまりこの貧乏人の支がいうれと、どちらかでそこの他をお話しが売っから行くでのででと希望するて意見当てるいるたない。支にさて嘉納さんがまた全くつかで事たたない。岡田君はわざわざ日光で云っが懸ですものならだろまし。`,
  "image": "https://dummyimage.com/100x100/000/fff",
  "date_published": "2018-12-15T10:00:00+09:00",
  "date_modified": "2018-12-15T12:00:00+09:00",
  "author": "sample name",
  "publisher": {
    "name": "sample publisher",
    "logo": "https://dummyimage.com/50x50/000/fff",
  },
  "related": [
    {
      "id": 2,
      "title": "関連記事 その1",
      "body": "頭はドレミファのぼんやり扉らに晩をし棒たた。するとこれから勝手ますたというセロましござい。まじめたましんましはましそれから野ねずみの残念らのときをはまるで生意気たございて、おれまでまわりへありれ方うまし。むしすぎどこは顔が黒いたてさっきのゴーシュの町団をあり第二虫めのぼんやりを弾きてだしたた。",
      "image": "https://dummyimage.com/100x100/f00/fff"
    },
    {
      "id": 3,
      "title": "関連記事 その2",
      "body": "外もいまひるからちまうだ。ゴーシュも六たべからだのようから食うてくれござい。",
      "image": "https://dummyimage.com/100x100/ff0/fff"
    }, 
    {
      "id": 4,
      "title": "関連記事 その3",
      "body": "ゴーシュはかっこうマッチたりなんにつっ込んているまし。用も汗をしばらくになっとトマトを猫のようが弾きてこどもに云わてぜひ足へ教えていただいます。すっかりおいおいボーをゴーシュへ聞えなまし。",
      "image": "https://dummyimage.com/100x100/0ff/fff"
    }
  ],
}

const relatedArticles = () => (
  <ul className="related-list">
    {data.related.map(article => (
      <li className="related-list_item" key={article.id}>
          <img src={article.image} width="80" height="80" alt="" />
          <div>
            <Link href="#">
              <a>{article.title}</a>
            </Link>
            <p>{article.body.slice(0, 50)}</p>
          </div>
      </li>
    ))}

    <style jsx>{`
      .related-list {
        padding: 0;
        list-style-type: none;
      }

      .related-list_item {
        display: flex;
        padding: 10px 0;
        border-top: 1px solid #ccc;
      }

      .related-list_item > img {
        min-width: 80px;
        margin-right: 10px;
      }
    `}</style>
  </ul>
)

export default () => (
  <div className="container">
    <main className="main">
      <h1>{ data.title }</h1>
      <p>公開日: { data.date_published.slice(0, 10) }</p>
      <div>{ data.body }</div>
    </main>

    <aside className="aside">
      <h2>関連記事</h2>
      { relatedArticles() }
    </aside>

    <style jsx>{`
      .container {
        display: flex;
        align-items: flex-start;
        width: 1000px;
        margin: 0 auto;
      }

      .main {
        flex-grow: 1;
        padding: 10px;
        border: 1px solid #ccc;
      }

      .aside {
        min-width: 300px;
        margin-left: 20px;
        padding: 0 10px;
        border: 1px solid #ccc;
      }
    `}</style>
  </div>
)
```

`npm run dev` で立ち上げると、このようなページを確認できます。
<img width="914" alt="スクリーンショット 2018-12-14 12.45.43.png" src="https://qiita-image-store.s3.amazonaws.com/0/138265/7fb84686-1a61-d1b5-8d40-e8c1c809b20f.png">


### 構造化データを追加
先ほどのファイルのコンポーネント内に、下記の記述を追加します。

#### メイン記事部分

```jsx:pages/index.js
<script
  type="application/ld+json"
  dangerouslySetInnerHTML={{ __html: `
    {
      "@context": "http://schema.org",
      "@type": "Article",
      "headline": "${escape(data.title)}",
      "image": "${escape(data.image)}",
      "author": {
        "@type": "Person",
        "name": "${escape(data.author)}"
      },
      "publisher": {
        "@type": "Organization",
        "name": "${escape(data.publisher.name)}",
        "logo": {
          "@type": "ImageObject",
          "url": "${escape(data.publisher.logo)}"
        }
      },
      "datePublished": "${data.date_published}",
      "dateModified": "${data.date_modified}",
      "mainEntityOfPage": "https://example.com"
    }`
  }}
/>
```

#### 関連記事部分

```jsx:pages/index.js
<script
  type="application/ld+json"
  dangerouslySetInnerHTML={{ __html: `
    {
      "@context": "http://schema.org",
      "@type": "ItemList",
      "itemListElement": [
        ${data.related.map((article, i) => (
          `{
            "@type": "ListItem",
            "position": "${i+1}",
            "url": "https://example.com/article/${article.id}",
            "name": "${escape(article.title)}",
            "image": "${escape(article.image)}",
            "description": "${escape(article.body)}"
          }`
        ))}
      ]
    }`
  }}
/>
```

schema.orgやgoogleのドキュメントを参考にjson-ldを指定します。
ここではtypeをメイン記事をArticle、関連記事をItemListとして、最低限のプロパティを指定しています。
https://schema.org/docs/schemas.html
https://developers.google.com/search/docs/guides/intro-structured-data?hl=ja

また、json-ldのクォーテーションなどがエスケープされてしまうのを防ぐために`dangerouslySetInnerHTML`でscriptタグ内にjson-ldを指定します。ただ`dangerouslySetInnerHTML`を使用してしまうとXSSのリスクがあるため、必要に応じてescape処理を入れます

```jsx:pages/index.js
const escape = str => {
  const map = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#039;'
  };
  return str.replace(/[&<>"']/g, (c) => map[c]);
} 
```

### チェックツールで確認
googleが提供している確認ツールを使用して、構造化データの確認をすることができます。必須のプロパティが漏れていたり、指定した値が適切でない場合はエラーを表示してくれます。
https://search.google.com/structured-data/testing-tool?hl=ja

ローカルやテスト環境など外部に公開されていないページの場合は、ブラウザ上でソースを見てコピペすることで確認もできます。

<img width="1067" alt="スクリーンショット 2018-12-14 13.17.20.png" src="https://qiita-image-store.s3.amazonaws.com/0/138265/70f52ba4-1f32-b121-e0b3-1004c2115ae9.png">

## 本家の関連issue
https://github.com/zeit/next.js/issues/2213
https://github.com/zeit/next.js/issues/3378