Claude Media
Claude CodeでJobPosting構造化データを実装し、掲載期限まで検証する手順

Claude CodeでJobPosting構造化データを実装し、掲載期限まで検証する手順

求人票からJobPostingのJSON-LDを生成し、必須項目の抜けと掲載期限切れをスクリプトで検証する手順です。Claude Codeに回させる完了条件の渡し方も扱います。

JobPostingの構造化データは、実装した日には通っても、あとから違反に変わります。求人には掲載期限があり、コードを1行も変えなくても、期限が過ぎた日から違反になりうるからです。Claude Codeに任せるなら、JSON-LDを書かせる前に、期限切れまで拾える検証スクリプトを先に用意するのが近道です。

この記事では、求人票のデータからJobPostingのJSON-LDを生成し、必須項目の抜け、掲載終了日(validThrough)、ページ本文との不一致を検査する流れを組みます。記事ページ向けのArticleやBreadcrumbListはClaude CodeでJSON-LD構造化データを実装する手順にあります。ここでは求人に固有の部分だけを扱います。

JobPostingは求人詳細ページ1件につき1つだけ置く

JobPostingは、Googleの求人検索に求人を載せるための構造化データです。ガイドには、利用できる地域として日本が挙がっています。

置く場所の決まりは厳格です。求人を1件だけ載せた詳細ページにだけ置き、検索結果ページや求人一覧ページには置きません。一覧に置くと、Search Consoleに「リスティング ページに個々の求人の構造化データを含めることはできません」という趣旨のポリシー違反が出ます。サイトマップにも、一覧や検索結果の動的ページを含めないよう書かれています。

落とし穴

JobPostingで違反になりやすい3つの形

  • 置き場所の違反

    一覧ページに複数のJobPostingを並べる。詳細ページ以外には置けません。

  • 内容の不一致

    マークアップの職務名や給与が、ページに表示されていない。

  • 期限切れの放置

    募集終了後も、ページとJobPostingが公開されたまま残る。

この3つは、どれも構文チェックでは見つかりません。JSONとして正しくても、置き場所や中身、日付の関係が間違っていれば違反になります。以降の検証スクリプトは、この3つを拾うために作ります。

必須プロパティと、期限だけ条件付きの項目

ガイドの必須プロパティは、datePosted、description、hiringOrganization、jobLocation、title の5つです。validThrough は少し性格が違います。求人に有効期限があるなら必須で、期限が分からないなら指定しない項目です。

プロパティ区分実装で効く決まり
title区分必須実装で効く決まり職務名のみ。求人コード・住所・日付・給与・会社名を入れない
description区分必須実装で効く決まりHTML形式。title と同じ文字列にしない
hiringOrganization区分必須実装で効く決まり会社名。匿名なら name に confidential
jobLocation区分必須実装で効く決まり勤務地。addressCountry を含める
datePosted区分必須実装で効く決まりISO 8601。最初に投稿した日付
validThrough区分期限がある場合は必須実装で効く決まり期限が不明なら指定しない

推奨プロパティには employmentType、baseSalary、identifier、directApply、jobLocationType、applicantLocationRequirements があります。educationRequirements と experienceRequirements はベータ版の扱いです。

title は、求人票の見出しをそのまま入れる場所ではありません。「【急募】未経験歓迎!渋谷で月給30万円」のような見出しは、職務名として不適切です。記号の多用はスパムと見なされるおそれがある、とガイドにも書かれています。

求人票の項目をプロパティに対応づける

求人票は、日本の求人サイトでは「雇用形態」「給与」「勤務地」といった日本語の項目で管理されています。JSON-LDに変換するときは、項目ごとに対応先を決めておくと、Claude Codeの出力がぶれません。

求人票の項目プロパティ変換で決めること
職種名プロパティtitle変換で決めること装飾語・給与・勤務地を落とす
仕事内容・応募資格プロパティdescription変換で決めること<p>・<ul>・<li> で組む
雇用形態プロパティemploymentType変換で決めること定義値(大文字)に割り当てる
給与プロパティbaseSalary変換で決めること単位は HOUR / DAY / WEEK / MONTH / YEAR
勤務地プロパティjobLocation変換で決めること都道府県・市区町村・番地・郵便番号・国
掲載終了日プロパティvalidThrough変換で決めること時刻とオフセットまで付ける

雇用形態は、ガイドが FULL_TIME、PART_TIME、CONTRACTOR(契約社員)、TEMPORARY(派遣社員)、INTERN、VOLUNTEER、PER_DIEM(日雇い)、OTHER の定義を示しています。大文字と小文字を区別するので、Fulltime や full_time は定義外です。業務委託のように割り当てが迷う形態は、定義の説明と照らして一度決め、CLAUDE.mdに固定してください。

給与は、概算額ではなく雇用主が提示する実際の基本給を書く決まりです。幅がある場合は value ではなく minValue と maxValue を使います。マークアップに給与を書いたなら、ページ本文にも同じ給与を表示する必要があります。

description では、HTMLのうち <p>、<ul>、<li> が認識されます。<h1> や <strong> などは書式に影響しません。段落の区切りには <br>、<p>、\n のいずれかを使えます。

生成するJSON-LDの形

求人ごとにLLMへJSON-LDを書かせると、ページごとに出力がずれます。実装は、求人データを受け取ってオブジェクトを返す関数をClaude Codeに書かせ、JSON.stringify で出力する形が安定します。生成の対象は、次のような形になります。ガイドの標準例に沿い、日本の求人に置き換えたものです。

{
  "@context": "https://schema.org/",
  "@type": "JobPosting",
  "title": "バックエンドエンジニア",
  "description": "<p>決済APIの開発を担当します。</p>",
  "datePosted": "2026-10-01",
  "validThrough": "2026-12-31T23:59:00+09:00",
  "employmentType": "FULL_TIME",
  "hiringOrganization": {
    "@type": "Organization",
    "name": "株式会社サンプル",
    "sameAs": "https://example.com"
  },
  "jobLocation": {
    "@type": "Place",
    "address": {
      "@type": "PostalAddress",
      "addressRegion": "東京都",
      "addressLocality": "渋谷区",
      "addressCountry": "JP"
    }
  },
  "baseSalary": {
    "@type": "MonetaryAmount",
    "currency": "JPY",
    "value": {
      "@type": "QuantitativeValue",
      "minValue": 5000000,
      "maxValue": 8000000,
      "unitText": "YEAR"
    }
  },
  "directApply": true
}

validThrough には、日付だけでなく時刻とオフセットまで付けています。ガイドの例には 2017-02-24T19:33:17+00:00 の形があり、日本の求人なら +09:00 を付けると、終了の瞬間が一意に決まります。

CLAUDE.mdに求人の規約を固定する

CLAUDE.mdは、毎回の指示に書かなくても読ませたい規約を置く場所です。JobPostingで固定するのは、変換の規約と完了条件です。

## 求人ページのJobPosting
- JSON-LDは求人データからオブジェクトを組み、JSON.stringifyで出力する
- 置くのは求人詳細ページだけ。一覧・検索結果ページには出さない
- employmentType は FULL_TIME / PART_TIME / CONTRACTOR / TEMPORARY /
  INTERN / VOLUNTEER / PER_DIEM / OTHER のみ。業務委託は CONTRACTOR とする
- validThrough は終了日の23:59:00+09:00。期限が不明な求人には出さない
- マークアップの職務名・給与は、ページ本文にも同じ表記で出す
- 完了条件: node scripts/check-jobposting.mjs <URL> がNG 0件で終わること

業務委託をCONTRACTORとする行は、この記事の例としての決めです。ガイドが指定する割り当てではありません。自社の求人票の呼び方に合わせて決めてください。

CI上で claude --bare -p を使うときは注意が必要です。--bare はCLAUDE.mdの自動読み込みを省きます。規約はファイルにして --append-system-prompt-file で渡します。また、bareモードはサブスクリプションのログインを使わないので、ANTHROPIC_API_KEY が要ります。

検証スクリプトで抜け・期限・不一致を拾う

検証スクリプトは、HTMLから ld+json を取り出し、ガイドの決まりを1つずつ照らします。リッチリザルトテストが見る範囲を、期限と本文との一致で補う位置づけです。

// scripts/check-jobposting.mjs
// node scripts/check-jobposting.mjs <URL|ファイル> [--today=YYYY-MM-DD]
import { readFileSync } from "node:fs";
 
const src = process.argv[2];
const todayArg = process.argv.find((a) => a.startsWith("--today="));
const today = todayArg ? new Date(todayArg.slice(8)) : new Date();
const html = /^https?:/.test(src)
  ? await (await fetch(src)).text()
  : readFileSync(src, "utf8");
 
const re = /<script[^>]*type="application\/ld\+json"[^>]*>([\s\S]*?)<\/script>/g;
const visible = html.replace(/<script[\s\S]*?<\/script>/g, "")
  .replace(/<[^>]+>/g, " ").replace(/\s+/g, " ");
const jobs = [];
let ng = 0, warn = 0;
const NG = (m) => { console.log("NG   " + m); ng++; };
const WARN = (m) => { console.log("WARN " + m); warn++; };
 
for (const m of html.matchAll(re)) {
  try {
    const j = JSON.parse(m[1]);
    for (const x of [j].flat()) if (x["@type"] === "JobPosting") jobs.push(x);
  } catch { NG("JSONとして読めないld+jsonがある"); }
}
if (jobs.length === 0) NG("JobPostingが見つからない");
if (jobs.length > 1) NG(`JobPostingが${jobs.length}件ある(1ページ1求人)`);
 
const ISO = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2})?(Z|[+-]\d{2}:\d{2})?)?$/;
const TYPES = ["FULL_TIME","PART_TIME","CONTRACTOR","TEMPORARY","INTERN",
               "VOLUNTEER","PER_DIEM","OTHER"];
const UNITS = ["HOUR","DAY","WEEK","MONTH","YEAR"];
 
for (const j of jobs) {
  for (const k of ["title","description","datePosted"]) if (!j[k]) NG(`${k} が無い`);
  if (!j.hiringOrganization?.name) NG("hiringOrganization.name が無い");
  const remote = j.jobLocationType === "TELECOMMUTE";
  if (!remote && !j.jobLocation) NG("jobLocation が無い");
  for (const loc of [j.jobLocation].flat().filter(Boolean))
    if (!loc.address?.addressCountry) NG("jobLocation.address.addressCountry が無い");
  if (remote && !j.applicantLocationRequirements && !j.jobLocation)
    NG("TELECOMMUTE なのに応募者の所在地条件も職場も無い");
  if (j.title && j.title === j.description) NG("title と description が同一");
  if (j.title && /[!*!*]{2,}|\*\*\*/.test(j.title)) NG("title に記号の多用");
  if (j.title && !visible.includes(j.title)) NG("title がページ本文に見えない");
  if (j.datePosted && !ISO.test(j.datePosted)) NG("datePosted が ISO 8601 でない");
 
  if (!j.validThrough) {
    WARN("validThrough が無い(期限なしの求人でなければ設定する)");
  } else if (!ISO.test(j.validThrough)) {
    NG("validThrough が ISO 8601 でない");
  } else if (new Date(j.validThrough) < today) {
    NG(`validThrough ${j.validThrough} が過去(期限切れ: ページ削除かマークアップ削除)`);
  }
  if (j.datePosted && j.validThrough &&
      new Date(j.validThrough) < new Date(j.datePosted))
    NG("validThrough が datePosted より前");
 
  for (const t of [j.employmentType].flat().filter(Boolean))
    if (!TYPES.includes(t)) NG(`employmentType "${t}" は定義外(大文字小文字も区別)`);
 
  const s = j.baseSalary;
  if (s) {
    const v = s.value ?? {};
    if (!UNITS.includes(v.unitText)) NG(`unitText "${v.unitText}" は定義外`);
    if (v.value == null && (v.minValue == null || v.maxValue == null))
      NG("baseSalary に value も minValue/maxValue も無い");
    const n = v.value ?? v.minValue;
    const plain = visible.replace(/,/g, "");
    if (n != null && !plain.includes(String(n)))
      NG(`給与 ${n} がページ本文に見えない`);
  }
  if (j.directApply === undefined) WARN("directApply が無い(任意)");
}
console.log(`${jobs.length}件のJobPosting / NG ${ng} / WARN ${warn}`);
process.exit(ng ? 1 : 0);

NGにするのは、ガイドが「必須」「できない」と書く項目です。directApply のように任意の項目は、WARNに留めています。

正しいページと壊れたページの出力

前節の例を載せた正常なページと、わざと崩したページに、Node.js 20.18.0で実行しました。--today で日付を固定しています。

node scripts/check-jobposting.mjs good.html --today=2026-10-27
node scripts/check-jobposting.mjs bad.html  --today=2026-10-27
1件のJobPosting / NG 0 / WARN 0
 
NG   jobLocation.address.addressCountry が無い
NG   title と description が同一
NG   title に記号の多用
NG   title がページ本文に見えない
NG   datePosted が ISO 8601 でない
NG   validThrough 2026-09-30 が過去(期限切れ: ページ削除かマークアップ削除)
NG   validThrough が datePosted より前
NG   employmentType "Fulltime" は定義外(大文字小文字も区別)
NG   unitText "year" は定義外
NG   給与 6000000 がページ本文に見えない
WARN directApply が無い(任意)
1件のJobPosting / NG 10 / WARN 1

壊れたページ側は、datePosted を 2026/10/01 と書き、title に「【急募!!】」を入れ、給与をページ本文に出さないように作りました。1つの崩れが複数の指摘を呼ぶ例もあります。

正常なページを、実行日だけ変えてもう一度検査します。

node scripts/check-jobposting.mjs good.html --today=2027-01-05
NG   validThrough 2026-12-31T23:59:00+09:00 が過去(期限切れ: ページ削除かマークアップ削除)
1件のJobPosting / NG 1 / WARN 0

ファイルを1文字も変えていないのに、結果がNGに変わりました。JobPostingの検査が、実装時の1回では済まない理由がここにあります。

日付だけの validThrough(たとえば 2026-10-31)は、JavaScriptがUTCの0時として解釈します。日本時間では9時に当たり、この日の朝から期限切れ扱いになりえます。スクリプトの判定を日本時間の終日に合わせたいときは、+09:00 付きの時刻で書いてください。

Claude Codeに「NG 0件」まで回させる

スクリプトができたら、完了条件を渡して実装を任せます。

npm run dev &
claude -p "求人詳細ページにJobPostingの構造化データを実装して。\
完了条件は node scripts/check-jobposting.mjs \
http://localhost:3000/jobs/backend-engineer がNG 0件で終わること。\
NGが出たら原因を直して再実行する" \
  --allowedTools "Read,Edit,Bash"

claude -p は非対話モードで、--allowedTools は確認なしに使うツールの指定です。承認なしでファイル編集とシェル実行を許すので、巻き戻せるブランチで回してください。

終わったら、出力の末尾も読ませます。JobPostingが1件も無い場合は 見つからない がNGになるので、「NG 0件」だけで実装漏れは通りません。一方、WARN は通過扱いです。validThrough の警告が出ていないかは、目で確認します。

掲載終了日の扱いを決める

募集を終えた求人は、ガイドが次のいずれかで処理するよう求めています。どれを選ぶかは、サイトの運用で決まります。

切り分け

募集を終えた求人の扱いを決める

  1. Q募集終了後も、求人ページを残す必要があるか?
    答えが残さない場合

    ページを完全に削除し、リクエストに404または410を返します。

    答えがはい
  2. Q掲載の終了日は、事前に決まっているか?
    答えが決まっていない場合

    validThrough を指定しません。採用が決まったら、JobPosting構造化データをページから削除します。

    答えがはい

終了日が決まっている場合

validThrough を終了日の時刻で設定し、期限が過ぎたことを検査で確かめます。

ページを残したままJobPostingだけを外す方法もあります。期限切れの求人にJobPostingが残っていると、Search Consoleに「有効期限が切れた求人のJobPosting構造化データ」のポリシー違反が出ます。

変更をGoogleに伝える手段には、Indexing APIが勧められています。求人のURLでは、サイトマップから削除するより、Googlebotにクロールを早く促せるからです。サイトマップの送信は、サイト全体のカバーのために続けます。

落とし穴

期限を先延ばしにするだけの運用

終了日を決めないまま、validThrough を未来の日付で固定して放置するのは、期限切れの検査を形だけ通すことになります。ガイドは、期限が不明なら指定しない、採用が決まったら求人を削除する、と書いています。日付を入れる理由は、実際の終了日を伝えるためです。

期限は日付の経過で崩れるので、検査は定期実行にします。公開中の求人URLの一覧をスクリプトに通し、NGが出たらそのURLを通知する形が、最小の構成です。node だけで動くので、この定期実行にはClaude Codeを呼ぶ必要がありません。Claude Codeの出番は、NGが出た求人の原因調査と修正に絞れます。

リモートワークと勤務地の書き方

完全リモートの求人は、jobLocationType に TELECOMMUTE を指定します。ただし、使える条件は狭く、一時的な在宅勤務や、交渉次第でリモート可になる求人には設定できません。

応募者の所在地の条件は applicantLocationRequirements で書きます。省略すると、jobLocation に書いた国の求職者全員に求人が表示されます。日本国内の在住者に限るなら、国を明示してください。完全リモートの求人で、応募者が1つ以上の地域にいてよい場合は、このプロパティが必須です。

職場が複数あるときは、jobLocation を配列にします。表示する場所は、求職者の検索条件に応じてGoogleが選びます。

公開後はSearch Consoleのメッセージで切り分ける

公開前の検査で拾えるのは、ページ単体で判定できるものまでです。Search Consoleには、スクリプトが拾わない違反も表示されます。

Search Consoleのポリシー違反スクリプトで拾えるか見る場所
リスティング ページに個々の求人の構造化データを含めることはできませんスクリプトで拾えるか件数のみ見る場所一覧ページのテンプレート
ページ上のコンテンツがページの構造化データと異なっていますスクリプトで拾えるか職務名と給与のみ見る場所表示とマークアップの差
有効期限が切れた求人のJobPosting構造化データスクリプトで拾えるか拾える見る場所validThrough とページの状態
求人ページで申し込みを送信できませんスクリプトで拾えるか拾えない見る場所応募の導線

応募手段の有無は、HTMLからは判定しづらい項目です。応募方法が示されていない求人や、求人の説明を見るためにログインを求める求人は、ポリシーで掲載できないとされています。詳細ページに応募の導線があること、ログインなしで内容が読めることは、人の目で確かめます。

ロゴが違う場合は、hiringOrganization.logo で正しい画像を指定できます。画像の幅と高さの比は0.75〜2.5の範囲です。

公開直後に確認するのは、リッチリザルトのステータスレポートです。有効な項目が増え、無効な項目が増えていない状態が目安になります。新しいテンプレートをリリースしたあとは、無効な項目の増加を見ます。

なお、求人票の記載内容そのものの確認は、構造化データとは別の話です。明示事項や年齢制限の表現は、求人票の明示事項と年齢制限表現をClaudeで点検する手順で扱っています。

まとめ

JobPostingの実装で人が持つのは、求人票の項目とプロパティの対応づけ、期限の運用、応募導線の確認です。生成関数と検証スクリプトの作成、NGの修正の繰り返しは、完了条件を渡したClaude Codeが回せます。

最初の一歩は、求人を1件だけ選び、生成関数と check-jobposting.mjs を通すことです。NGが0件になったらリッチリザルトテストで確認し、定期実行へ広げてください。期限切れの検査を最初から定期実行に入れておけば、構造化データが静かに違反へ変わる事態を避けられます。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. developers.google.com/search/docs/appearance/structured-data/job-posting?hl=ja
  2. code.claude.com/docs/en/headless
  3. code.claude.com/docs/en/memory
この記事を共有:XはてブLinkedIn