かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

Next.js 構造化されたパンくずリストを作りたい

昨年メディアのような SEO が大切になる Next.js の案件があり、ページの構造を表すパンくずリスト (BreadcrumbList) も重要なのでちゃんと構造化されたものを作ってみたのでそのメモ。

構成

  • Next.js v13 (Pages Router)
  • React v18
  • TypeScript v5.2

パンくずリスト( BreadcrumbList) の構造化データ

Google が公開しているページを参考にした 古の昔はパンくずリストの HTML にプロパティを付ける RDFa や microdata のような形式が主流だった記憶だが、案件を実装当時 (2023年末) では構造を <script type="application/ld+json"> として別途マークアップする JSON-LD が推奨されているらしい

構造化データに関する一般的なガイドライン
リッチリザルトを表示できるようにするには、サポートされている 3 つの形式のいずれかを使用して、サイトのページをマークアップする必要があります。
JSON-LD(推奨)
cf. Google 検索上の構造化データガイドライン | Google 検索セントラル  |  ドキュメント  |  Google for Developers

パンくずリストJSON-LD

{
  "@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"
  }]
}

cf. パンくずリスト(BreadcrumbList)のマークアップを追加する方法 | Google 検索セントラル  |  ドキュメント  |  Google for Developers

"@context": "https://schema.org", "@type": "BreadcrumbList" の部分は固定なので itemListElement 内のリストを動的に作成すればOK、更に現在のページの URL (item) は不要なのでサイトトップと現在のページの親のデータを持たせておけば良さそうです

JSON-LD なパンくずリストの実装

パンくずリストJSON-LD の型を作成

// types/breadcrumb.d.ts

export type BreadcrumbItem = {
  '@type': 'ListItem';
  position: number;
  name: string; // title
  item?: string; // URL
};

export interface BreadcrumbJsonLd {
  '@context': 'https://schema.org',
  '@type': 'BreadcrumbList',
  itemListElement: BreadcrumbItem[],
}

パンくずリストの HTML と JSON-LD を出力するコンポーネント

// /components/BreadcrumbList.tsx
import { FC } from 'react';
import Link from 'next/link';
import Script from 'next/script';
import { BreadcrumbJsonLd, BreadcrumbItem } from '@/types/breadcrumb';

type BreadcrumbListProps = {
  breadcrumbData: BreadcrumbItem[];
};

export const BreadcrumbList: FC<BreadcrumbListProps> = ({ breadcrumbData }) => {
  return (
    <>
      <JsonLd breadcrumbData={breadcrumbData} />
      <ol className='breadcrumb'>
        {breadcrumbData.map((item) => (
          <li key={item.position} className="breadcrumbItem">
            <BreadcrumbItem {…item} />
          </li>
        )}
      </ol>
    </>
  );
};

const BreadcrumbItem: FC<BreadcrumbItem> = ({ name, item }) => {
  const label = name === SITE_NAME ? 'Home' : name;
  if (item) {
    return <Link href={item} className='breadcrumbLink'>{title}</Link>;
  }
  // current
  return <span className="breadcrumbLink current">{label}<span>;
}

// JSON-LD を出力するコンポーネント
type JsonLdProps = {
  breadcrumbData: BreadcrumbItem[];
};

const JsonLd: FC<JsonLdProps> = ({ breadcrumbData }) => {
  const jsonLdSchema: BreadcrumbJsonLd = {
    '@context': 'https://schema.org',
    '@type': 'BreadcrumbList',
    itemListElement: breadcrumbData,
  };

  return (
    <Script
      id='BreadcrumbList-JSON-LD'
      type='application/ld+json'
      strategy='beforeInteractive'
    >
      {JSON.stringify(jsonLDSchema)}
    </Script>
  );
};

パンくずリスのデータを各ページから渡して出力すればOK

import { BreadcrumbItem } from '@/types/breadcrumb';
import { BreadcrumbList } from '@/components/BreadcrumbList';

const breadcrumbData: BreadcrumbItem[] = [
  {
    '@type': 'ListItem',
    position: 1,
    name: SITE_NAME,
    item: SITE_HOME_URL,
  },
  {
    '@type': 'ListItem',
    position: 2,
    name: PARENT_PAGE_NAME,
    item: PARENT_PAGE_URL,
  },
  {
    '@type': 'ListItem',
    position: 3,
    name: THIS_PAGE_TITLE
  }
] ;

export default function Page() {
  return (
    <PageLayout>
      <BreadcrumbList breadcrumbData={breadcrumbData} />
      <PageContent>
    </PageLayout>
  );
}

動的にページが決まる場合は breadcrumbData を動的に作成してしまえば OK。
こんな感じで構造化された JSON-DL のパンくずリストを作成することができました!

今回は基本的に SSG をする仕様だったので JSON-DL を出力する Scriptstrategy='beforeInteractive' を指定してページが表示される前に JSON-LD のスクリプトがロードされるようにしたが、JSON-LD と next/script の strategy の仕様に明るくないので strategy の最適な指定方法が理解できているわけではないです。

おわり


[参考]

app router も Remix もまだ全然さわれてないのでそろそろ危機感覚えてきてる…