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-271件の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-05NG 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 の警告が出ていないかは、目で確認します。
掲載終了日の扱いを決める
募集を終えた求人は、ガイドが次のいずれかで処理するよう求めています。どれを選ぶかは、サイトの運用で決まります。
募集を終えた求人の扱いを決める
- Q募集終了後も、求人ページを残す必要があるか?答えが残さない場合答えがはい
ページを完全に削除し、リクエストに404または410を返します。
- 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件になったらリッチリザルトテストで確認し、定期実行へ広げてください。期限切れの検査を最初から定期実行に入れておけば、構造化データが静かに違反へ変わる事態を避けられます。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。