IT技術ブログ

構造化データの実装例:Organization・LocalBusiness・BreadcrumbList・ArticleをJSON-LDで書き、公開前後に検証する手順。1か所の設定から生成してページとの食い違いを防ぐ

Organization・LocalBusiness・BreadcrumbList・ArticleのJSON-LDを1つの設定から関数で生成する実装例です。@idのつなぎ方と、書き出し後の機械チェック、Googleのツールでの検証手順まで。

この記事の結論:構造化データは、手書きのJSON-LDをページごとに貼るより、社名・住所・著者などの値を1つの設定ファイルに集め、種類ごとの関数でJSON-LDを生成する形にすると、ページとの食い違いを構造的に防げます。Organization には @id を決めて、記事の著者や拠点の LocalBusiness からはその @id を参照します。BreadcrumbList は画面に出すパンくずと同じデータから作り、Article は記事の型(ブログなら BlogPosting)を選んで、著者を人として書きます。検証は、①書き出したHTMLの全JSON-LDを機械で読んで確かめる、②Googleの機能の対象になる種類はリッチリザルト テストで見る、③それ以外は Schema Markup Validator で見る、④公開後に URL 検査と種類ごとのレポートで見る、の4段で行います。

この記事は、自社サイトに構造化データを組み込む開発者と、制作会社に頼むときに中身を確かめたい担当者に向けた実装の記録です。どの種類を入れるかの入門ではなく、コードの組み方と確かめ方を扱います。例のうち Organization・BreadcrumbList・BlogPosting は、このブログ(Astro で静的に書き出すサイト)で実際に動いているコードの考え方です。LocalBusiness は、このブログには拠点が無いので入れておらず、一般的な書き方の例として示します。Googleの仕様は、末尾に並べた公式の説明で、2026年10月3日に一つずつ確認しています。

値は1つの設定ファイルに集め、種類ごとの関数でJSON-LDを作る

構造化データの不具合で多いのは、文法の誤りより「ページに書いてある値と違う」ことです。住所や電話番号を変えたのに、JSON-LD だけ古いまま残る、という形です。これを防ぐには、画面に出す値と JSON-LD に入れる値を、同じ変数から取るようにします。

このブログでは、サイト名・著者・会社の値を src/config/site.ts に集め、構造化データは src/lib/schema.ts の関数で作っています。

src/config/site.ts(抜粋)ts
export const SITE = {
  url: 'https://www.yamayamabloglink.com', // canonical・サイトマップ・JSON-LD はすべてこれを使う
  name: 'やまやまブログ',
};
export const AUTHOR = { name: '津嘉山 洸', jobTitle: 'CTO' /* ほかにプロフィールURL・SNS */ };
export const COMPANY = { name: '株式会社bundlyze', url: 'https://www.bundlyze.co.jp/' };
src/lib/schema.ts(抜粋)ts
const abs = (p: string) => new URL(p, SITE.url).href;
export const ORG_ID = `${COMPANY.url}#organization`;
export const PERSON_ID = abs('/about/#person');

export function organization() {
  return { '@context': 'https://schema.org', '@type': 'Organization', '@id': ORG_ID, name: COMPANY.name, url: COMPANY.url };
}

ページの側は、必要な関数の結果を配列で共通のレイアウトに渡すだけです。レイアウトは配列の中身を、JSON-LD 用の script 要素に1件ずつ入れて出力します。トップページは WebSite・Person・Organization、記事は BlogPosting・BreadcrumbList・Person(よくある質問があれば FAQPage も)、運営者のページは ProfilePage・Person・Organization に BreadcrumbList を加えた4つです。

URL を abs() で必ず絶対URLにしているのは、正式なホスト(www 付き)以外のURLが JSON-LD に混じらないようにするためです。canonical やサイトマップと同じ SITE.url から作るので、ホストの書き方がずれることがありません。

Organizationは@idを決めて、ほかの構造化データから参照する

Organization は、会社を表す1つのノードとして @id を決め、ほかの構造化データからはその @id で指します。同じ会社をページごとに別々に書くと、値がずれたときにどれが正しいのか分からなくなるためです。

このブログでは、会社の @id を会社サイトのURLに #organization を付けた値にし、著者(Person)の worksFor から参照しています。ブログと会社サイトという別のサイトでも、同じ @id の文字列で同じ組織を指せるようにする狙いです。

会社サイトのトップか会社概要に置く Organization は、Googleの説明に沿うと次のような形になります。Googleは Organization に必須の項目は無いとしたうえで、名前・URL・ロゴ・住所・連絡先・sameAs などを推奨しています。

会社サイトの Organization の例(値は架空)json
{
  "@context": "https://schema.org",
  "@type": "Organization",
  "@id": "https://www.example.co.jp/#organization",
  "name": "株式会社サンプル",
  "url": "https://www.example.co.jp/",
  "logo": "https://www.example.co.jp/images/logo.png",
  "address": {
    "@type": "PostalAddress",
    "postalCode": "530-0000",
    "addressRegion": "大阪府",
    "addressLocality": "大阪市北区",
    "streetAddress": "サンプル1-2-3",
    "addressCountry": "JP"
  },
  "contactPoint": { "@type": "ContactPoint", "telephone": "+81-6-0000-0000", "email": "[email protected]" },
  "sameAs": ["https://x.com/example", "https://www.instagram.com/example/"]
}

ロゴは、Googleが112×112ピクセル以上で、クロールできる画像を求めています。sameAs には、その会社が自分で管理している公式のアカウントだけを並べます。

LocalBusinessは、人を迎える拠点のページに最も具体的な型で書く

LocalBusiness は、店舗・教室・診療所のように、お客様が訪れる拠点があるときに使います。Googleは、Restaurant・HealthClub・DaySpa のように、できるだけ具体的な下位の型を選ぶよう求めています。必須は name と address で、営業時間・電話番号・座標・URL などが推奨です。

拠点ページの LocalBusiness の例(値は架空)json
{
  "@context": "https://schema.org",
  "@type": "HealthClub",
  "@id": "https://www.example.co.jp/studio/umeda/#place",
  "name": "サンプルスタジオ 梅田店",
  "url": "https://www.example.co.jp/studio/umeda/",
  "telephone": "+81-6-0000-0001",
  "address": {
    "@type": "PostalAddress",
    "postalCode": "530-0000",
    "addressRegion": "大阪府",
    "addressLocality": "大阪市北区",
    "streetAddress": "サンプル4-5-6 2階",
    "addressCountry": "JP"
  },
  "geo": { "@type": "GeoCoordinates", "latitude": 34.70245, "longitude": 135.49812 },
  "openingHoursSpecification": [
    { "@type": "OpeningHoursSpecification", "dayOfWeek": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"], "opens": "10:00", "closes": "21:00" },
    { "@type": "OpeningHoursSpecification", "dayOfWeek": ["Saturday", "Sunday"], "opens": "09:00", "closes": "18:00" }
  ],
  "parentOrganization": { "@id": "https://www.example.co.jp/#organization" }
}

書くときの決まりは、Googleの説明にそろえます。

項目 書き方
型 LocalBusiness のままにせず、業種に合う下位の型を選ぶ
電話番号 国番号と市外局番を含める(+81-6-…)
座標 小数点以下5桁以上
営業時間 24時間表記。日をまたぐ営業は1つの指定で開始と終了を書く。終日休みは開始・終了とも 00:00
会社との関係 parentOrganization で会社の @id を指す

拠点が複数ある場合は、拠点ごとのページにそれぞれ置きます。トップページに全拠点を並べるより、住所・営業時間が書かれているページと1対1にした方が、ページとの食い違いを確かめやすくなります。

Articleは記事の型を選び、著者を人として書く

記事の構造化データは、Googleの説明では必須の項目が無く、headline・image・datePublished・dateModified・author などが推奨です。型は Article のほか、ブログなら BlogPosting、報道なら NewsArticle を選べます。このブログは BlogPosting にしています。

src/lib/schema.ts(記事の部分を抜粋)ts
export function article(p) {
  return {
    '@context': 'https://schema.org',
    '@type': 'BlogPosting',
    headline: p.title,
    url: abs(p.url),
    mainEntityOfPage: { '@type': 'WebPage', '@id': abs(p.url) },
    datePublished: p.datePublished, // 例 2026-10-03T10:00:00+09:00
    dateModified: p.dateModified || p.datePublished,
    author: {
      '@type': 'Person',
      '@id': PERSON_ID,
      name: AUTHOR.name,             // 名前だけ。肩書きは jobTitle に分ける
      url: abs('/about/'),
      jobTitle: AUTHOR.jobTitle,
      worksFor: { '@type': 'Organization', '@id': ORG_ID, name: COMPANY.name },
      sameAs: SAME_AS,               // X・Instagram・会社サイトの執筆者ページ
    },
    publisher: { '@type': 'Person', '@id': PERSON_ID, name: AUTHOR.name },
  };
}

押さえている点は3つです。

  • 著者名に肩書きや会社名を入れない。 Googleは author.name に名前だけを入れ、肩書きは jobTitle などの項目に分けるよう説明しています
  • 著者を特定できるURLを付ける。 url に著者のプロフィールのページ、sameAs に本人のSNSを入れ、同姓同名と区別できるようにします
  • 日時はタイムゾーン付きで書く。 タイムゾーンが無いと Googlebot 側のタイムゾーンで解釈されます。front matter の日時を +09:00 付きで書き、そのまま ISO 8601 の文字列にしています

publisher を Person にしているのは、このブログが個人の発信だからです。会社のコラムなら、publisher は会社の Organization の @id を指すのが自然です。

パンくずの構造化データは、画面に表示するパンくずと必ず同じ並びにします。このブログでは、記事のカテゴリーから「ホーム > 親カテゴリー > カテゴリー > 記事」の配列を一度だけ作り、その配列を画面の部品と JSON-LD の関数の両方に渡しています。

src/lib/schema.ts(パンくず)ts
export function breadcrumbs(items: { name: string; url: string }[]) {
  return {
    '@context': 'https://schema.org',
    '@type': 'BreadcrumbList',
    itemListElement: [{ name: 'ホーム', url: '/' }, ...items].map((it, i) => ({
      '@type': 'ListItem', position: i + 1, name: it.name, item: abs(it.url),
    })),
  };
}

Googleの説明では、ListItem には position・name・item が要り、最後の項目だけは item を省くとそのページのURLが使われます。並びはURLの階層をなぞるのではなく、読む人がたどる典型的な道筋を表すものとされています。なお、検索結果でのパンくずの表示は、現在はパソコンでの表示が対象と書かれています。

検証は、書き出したHTMLの機械チェックとGoogleのツールの2段で行う

構造化データは、公開前にまとめて機械で確かめ、公開の前後に Google のツールで1ページずつ確かめます。

1. 書き出したHTMLの全JSON-LDを、スクリプトで読んで確かめる

静的に書き出すサイトなら、ビルド結果のHTMLをすべて開き、JSON-LD が JSON として読めるか、決めた項目が入っているかを確かめられます。次は、その考え方の最小の例です。

check-jsonld.mjs(例)js
for (const file of htmlFiles) {
  const html = fs.readFileSync(file, 'utf8');
  for (const m of html.matchAll(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/g)) {
    const d = JSON.parse(m[1]); // 読めなければここで止まる
    if (d['@type'] === 'BlogPosting' && !/[+-]\d\d:\d\d$/.test(d.datePublished)) errors.push(`${file}: 日時にタイムゾーンがない`);
    if (d['@type'] === 'BreadcrumbList') d.itemListElement.forEach((it, i) => { if (it.position !== i + 1) errors.push(`${file}: パンくずの順番`); });
  }
}

このブログの2026年10月3日の書き出しでこの確かめ方を流したところ、読めない JSON-LD・タイムゾーンの無い日時・番号のずれたパンくずは0件でした。記事を足すたびに数秒で流せるので、手で1ページずつ見るより取りこぼしが減ります。

2. Googleの機能の対象になる種類は、リッチリザルト テストで見る

Article・パンくず・LocalBusiness など、Google検索の機能の対象になる種類は、リッチリザルト テストにURLかコードを入れて確かめます。公開前はコードを貼り付けて、公開後はURLで試します。エラーは直し、警告はページに実際にある情報なら足します。

3. それ以外の種類は、Schema Markup Validator で見る

Person・WebSite・ProfilePage のように、Googleの機能の対象一覧に無い種類は、リッチリザルト テストでは中身を詳しく見られません。schema.org の語彙として正しいかは、Schema Markup Validator(validator.schema.org)で確かめます。@id での参照が意図どおりにつながっているかも、ここで見ると分かりやすくなります。

4. 公開後は、URL 検査と種類ごとのレポートで見る

URL 検査の「公開URLをテスト」では、Googlebot が実際に読んだページで構造化データが検出されるかを確かめられます。インデックスに登録されたあとは、パンくずなどの種類ごとのレポートに、有効な項目と問題のある項目の数が出ます。

食い違いが起きやすい所

実装を終えたあとも、運用の中でずれが生まれます。よく起きる場面と、防ぎ方を並べます。

場面 起きること 防ぎ方
住所・電話・営業時間を変えた ページだけ直り、JSON-LD が古いまま残る 画面と JSON-LD で同じ変数を使う
CMSのプラグインとテーマが両方出す 同じ種類が2つ出て、値が違う 書き出したHTMLで種類ごとの件数を数える
著者名に肩書きを付けた 著者として正しく扱われない 名前と jobTitle を分ける
ページに無い情報を足した ガイドライン違反になり、手動による対策の対象になりうる ページに見えている内容だけを書く
ホストを変えた 古いホストのURLが url や @id に残る URLを1つの定数から絶対URLにして作る

構造化データの組み込みは、制作の最後に足すより、サイトの設定の持ち方を決める段階で一緒に設計した方が、あとの手直しが少なく済みます。自社の会社サイトを直す際、株式会社bundlyzeでは JSON-LD とページの記載をそろえることを心がけています。サイト全体の検索とAIでの見え方を、設定の設計から一緒に整える相談先として、SEO・LLMO対策の支援の案内に進め方を載せています。

照合に使った資料(Google と schema.org、確認日 2026年10月3日)

よくある質問

JSON-LDはheadとbodyのどちらに置けばよいですか?

Googleは、JSON-LD の script 要素をページの head と body のどちらに置いても読み取れるとしています。このブログでは、共通のレイアウトの head の中で、ページごとに渡された配列を1件ずつ script 要素として出しています。置き場所よりも、ページに見えている内容と食い違わないことの方が大切です。

OrganizationとLocalBusinessは両方入れるべきですか?

来店や訪問を受け付ける拠点がある会社なら、会社全体を表す Organization と、拠点ごとの LocalBusiness を分けて置くと整理しやすくなります。拠点の側から parentOrganization で会社の @id を指せば、同じ会社の拠点だと伝えられます。拠点で人を迎えない事業なら、Organization だけで足ります。

リッチリザルト テストで警告が出たら、直さないといけませんか?

エラーはその機能の対象から外れる原因になるので直します。警告は推奨の項目が足りないという知らせで、対象から外れるわけではありません。ページに実際にある情報なら足し、ページに無い情報は足しません。構造化データだけに、ページに書いていない内容を書くのは、Googleのガイドラインに反します。