Oteto Blogのロゴ

【Next.js】構造化データ (JSON-LD) を型安全に実装する (パンくずリストを添えて)

サイト内で記事(Article、NewsArticle、BlogPosting) やパンくずリスト(BreadcrumbList)などの構造化データ (JSON-LD) を実装したい場面が多々ある。

<script type="application/ld+json">
  {
    "@context": "https://schema.org",
    "@type": "BreadcrumbList",
    "itemListElement": [{
      "@type": "ListItem",
      "position": 1,
      "name": "Books",
      "item": "https://example.com/books"
    },{
      "@type": "ListItem",
      "position": 2,
      "name": "Science Fiction",
      "item": "https://example.com/books/sciencefiction"
    },{
      "@type": "ListItem",
      "position": 3,
      "name": "Award Winners"
    }]
  }
</script>

しかしNext.jsで提供されているMetadata APIに該当の機能は含まれておらず、自前で上記のようなオブジェクトを定義して<script>内に埋め込む必要がある。

そこで調べたところGoogle製のschema-dtsを使うことでTypeScriptの型安全性を享受できそうだったので、今回はパンくずリストを例に構造化データを実装してみる。

1. schema-dtsをインストール

npm i -D schema-dts
import type { BreadcrumbList, WithContext } from 'schema-dts';

export type BreadcrumbItem = {
  pathname: string;
  title: string;
};

export function createBreadcrumbJsonLd(
  items: BreadcrumbItem[],
): WithContext<BreadcrumbList> {
  return {
    '@context': 'https://schema.org',
    '@type': 'BreadcrumbList',
    itemListElement: items.map(({ pathname, title }, i) => ({
      '@type': 'ListItem',
      position: i + 1,
      name: title,
      item: pathname,
    })),
  };
}

schema-dtsから提供される型 (今回はBreadcrumbList) を利用することで、上記のように型安全に対象スキーマのオブジェクトを生成できる。もちろんitemListElementの各要素にも型補完が効くのでとても便利。

もし@contextを含めたい場合はWithContextでラップする。

3. <script>内に埋め込む

import type { ReactElement } from 'react';
import type { Thing, WithContext } from 'schema-dts';

type Props = {
  schema: WithContext<Thing>;
};

export default function JsonLd({ schema }: Props): ReactElement {
  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
    />
  );
}

JSON-LD出力用のコンポーネントを実装する。Propsの型をThingにすることでschema-dtsのスキーマの型を幅広く受け入れるようにしている。

4. パンくずリストのコンポーネントを実装

import type { ReactElement } from 'react';
import Link from 'next/link';
import JsonLd from '@/presentation/components/common/jsonLd';
import {
  type BreadcrumbItem,
  createBreadcrumbJsonLd,
} from '@/domain/utils/schema/breadcrumb';

type Props = {
  items: BreadcrumbItem[];
};

export default function Breadcrumb({ items }: Props): ReactElement {
  return (
    <>
      <ol>
        {items.map(({ pathname, title }, i) => (
          <li key={pathname}>
            {items.length === i + 1 ? (
              title
            ) : (
              <Link href={pathname}>{title}</Link>
            )}
          </li>
        ))}
      </ol>
      <JsonLd schema={createBreadcrumbJsonLd(items)} />
    </>
  );
}

先ほど実装した汎用関数でJSON-LDを生成し、パンくずリスト本体と共に出力するコンポーネントを実装する (JSON-LDは<body>内に含めても無問題) 。

export default async function Page(): Promise<ReactElement> {
  return (
    <Breadcrumb
      items={[
        {
          pathname: 'https://example.com',
          title: 'トップ',
        },
        {
          pathname: 'https://example.com/categories',
          title: 'カテゴリ一覧',
        },
        {
          pathname: 'https://example.com/posts/xxx',
          title: 'xxx',
        },
      ]}
    />
  );
}

あとは上記のようにサイト構造やページ階層に応じて適切なitemsを渡してあげる。

<script type="application/ld+json">
  {
    "@context": "https://schema.org",
    "@type": "BreadcrumbList",
    "itemListElement": [
      {
        "@type": "ListItem",
        "position": 1,
        "name": "トップ",
        "item": "https://example.com"
      },
      {
        "@type": "ListItem",
        "position": 2,
        "name": "カテゴリ一覧",
        "item": "https://example.com/categories"
      },
      {
        "@type": "ListItem",
        "position": 3,
        "name": "xxx",
        "item": "https://example.com/posts/xxx"
      }
    ]
  }
</script>

これで目当ての構造化データが出力された。