【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をインストール

Terminal window
npm i -D schema-dts

2. BreadcrumbListのオブジェクトを生成

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>

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