WordPressで求人ページを作ると、「募集中」の表示だけを消して安心しがちです。しかし実際には、応募ボタン、求人一覧、JobPosting構造化データも同じ募集状態に連動させる必要があります。
とくにJobPostingは、画面上の表示とは別のテンプレートやフックから出力されることがあります。募集終了後もJSON-LDだけが残る、というズレを防ぐには、各所で個別に判定するのではなく、1つの状態判定を共有するのが安全です。
この記事では、製品固有のコードに依存しない最小例で、次の4つを同じ状態から制御します。
- 求人詳細を表示するか
- 求人一覧へ載せるか
- 応募導線を出すか
- 求人詳細へ
JobPostingを出すか
なぜ表示と構造化データを一緒に止めるのか
Googleの求人情報ガイドでは、JobPostingは1件の求人を詳しく掲載するページへ実装し、ページ上で求職者が読める内容と一致させる必要があります。求人一覧や検索結果ページへ追加するものではありません。
また、応募を受け付けていない求人は、期限切れを示す、ページを削除する、またはJobPostingを削除するなどの対応が必要です。期限切れの求人に応募フローが残ることや、終了後もJobPostingが残ることは避けなければなりません。
ここで重要なのは、Google向けだけ別条件にしないことです。
募集状態
├─ 詳細ページ
├─ 求人一覧
├─ 応募導線
└─ JobPosting
4つの出力が同じ判定結果を見るようにします。
最初に状態表を決める
この例では、編集者が入力する状態をopen、paused、closedの3つに絞り、expiredは期限から自動判定します。
| 状態 | 詳細 | 一覧 | 応募導線 | JobPosting |
|---|---|---|---|---|
open |
表示 | 表示 | 表示 | 出力 |
paused |
表示 | 表示 | 非表示 | 非出力 |
closed |
表示 | 非表示 | 非表示 | 非出力 |
expired |
表示 | 非表示 | 非表示 | 非出力 |
| 非公開投稿 | 非表示 | 非表示 | 非表示 | 非出力 |
closedやexpiredでも詳細を残すのは、「この募集は終了しました」と案内できる設計例です。pausedを一覧へ残す場合も、一時停止中で応募できないことを画面上で明示します。終了時に404または410へする運用を選ぶ場合でも、応募導線とJobPostingを止めるゲートは共通化できます。
状態resolverをWordPressから分離する
まず、WordPress関数を呼ばない純粋なresolverを作ります。入力が同じなら出力も同じなので、境界条件を小さくテストできます。
<?php
function example_resolve_job_state(array $job, DateTimeImmutable $now): array
{
if (!$job['published']) {
return [
'state' => 'not-public',
'detail' => false,
'list' => false,
'application' => false,
'jobposting' => false,
];
}
$state = in_array($job['state'], ['open', 'paused', 'closed'], true)
? $job['state']
: 'closed';
if ($state === 'open' && $job['valid_through'] !== '') {
try {
$valid_through = new DateTimeImmutable($job['valid_through']);
// 期限と現在時刻が同じ瞬間も終了として扱う。
if ($valid_through <= $now) {
$state = 'expired';
}
} catch (Throwable $error) {
// 壊れた期限で募集を継続しない。安全側へ閉じる。
$state = 'closed';
}
}
return [
'state' => $state,
'detail' => true,
'list' => in_array($state, ['open', 'paused'], true),
'application' => $state === 'open' && $job['application_ready'],
'jobposting' => $state === 'open' && $job['schema_ready'],
];
}
不明な状態や壊れた期限をopenへ寄せないのがポイントです。求人情報が不完全なときは、出し続けるより止めて確認できる方が安全です。
WordPressの値をresolverへ渡す
次に、投稿とカスタムフィールドから入力を組み立てます。キー名や投稿タイプ名はサイトに合わせて変更してください。
<?php
function example_job_context(int $post_id): array
{
$application_url = trim((string) get_post_meta(
$post_id,
'job_application_url',
true
));
// 必須情報を検査し、完全なときだけ配列を返すサイト固有関数。
$payload = example_build_validated_jobposting_payload($post_id);
$gate = example_resolve_job_state(
[
'published' => get_post_status($post_id) === 'publish',
'state' => (string) get_post_meta($post_id, 'job_state', true),
'valid_through' => (string) get_post_meta($post_id, 'job_valid_through', true),
'application_ready' => $application_url !== '',
'schema_ready' => is_array($payload),
],
current_datetime()
);
return [
'gate' => $gate,
'application_url' => $application_url,
'payload' => $payload,
];
}
current_datetime()を使うと、WordPressに設定されたタイムゾーンの現在時刻をDateTimeImmutableとして扱えます。保存する期限もISO 8601形式へ統一し、日付だけでなく「その時刻を過ぎたらどうなるか」まで決めておきます。
example_build_validated_jobposting_payload()は、少なくともGoogleが必須としているdatePosted、description、hiringOrganization、jobLocationまたは適切なリモート勤務情報、titleを確認し、有効期限がある場合はvalidThroughも含める関数です。画面で見える求人内容とJSON-LDの内容が一致しない場合は、配列を返さずnullで止めます。
応募導線も同じgateを見る
応募URLがあるだけでは表示しません。募集状態とURLの両方が有効なときだけ出力します。
<?php
function example_application_markup(array $context): string
{
if (!$context['gate']['application']) {
return '';
}
return sprintf(
'<a class="job-apply" href="%s">応募する</a>',
esc_url($context['application_url'])
);
}
実運用ではURLの形式だけでなく、許可するドメイン、応募フォームの公開状態、送信先の管理責任も別途確認します。
JobPostingは求人詳細だけに出す
JSON-LDは、求人詳細であり、募集状態と必須情報が有効なときだけ出力します。
<?php
function example_jobposting_markup(
array $context,
bool $is_singular_job
): string {
if (
!$is_singular_job
|| !$context['gate']['jobposting']
|| !is_array($context['payload'])
) {
return '';
}
return '<script type="application/ld+json">'
. wp_json_encode(
$context['payload'],
JSON_UNESCAPED_UNICODE
| JSON_UNESCAPED_SLASHES
| JSON_HEX_TAG
| JSON_HEX_AMP
| JSON_HEX_APOS
| JSON_HEX_QUOT
)
. '</script>';
}
function example_print_jobposting(): void
{
if (!is_singular('sample_job')) {
return;
}
$post_id = get_queried_object_id();
$context = example_job_context($post_id);
echo example_jobposting_markup($context, true);
}
add_action('wp_head', 'example_print_jobposting', 20);
一覧テンプレートではexample_jobposting_markup()を呼ばず、フック側でもis_singular('sample_job')を確認します。二重に境界を置くことで、求人一覧へ複数のJobPostingが混ざる事故を避けます。
JSON-LDは文字列連結で手書きせず、wp_json_encode()でエンコードします。構造化データの完全性は出力直前の真偽値ではなく、payloadを作る段階で検査します。
最低限の状態テスト
resolverはWordPressから分離しているため、少なくとも次を自動テストできます。
<?php
$now = new DateTimeImmutable('2026-08-30T12:00:00+09:00');
$cases = [
'open' => [
['published' => true, 'state' => 'open', 'valid_through' => '2026-08-31T12:00:00+09:00', 'application_ready' => true, 'schema_ready' => true],
['detail' => true, 'list' => true, 'application' => true, 'jobposting' => true],
],
'paused' => [
['published' => true, 'state' => 'paused', 'valid_through' => '', 'application_ready' => true, 'schema_ready' => true],
['detail' => true, 'list' => true, 'application' => false, 'jobposting' => false],
],
'closed' => [
['published' => true, 'state' => 'closed', 'valid_through' => '', 'application_ready' => true, 'schema_ready' => true],
['detail' => true, 'list' => false, 'application' => false, 'jobposting' => false],
],
'expired-boundary' => [
['published' => true, 'state' => 'open', 'valid_through' => '2026-08-30T12:00:00+09:00', 'application_ready' => true, 'schema_ready' => true],
['detail' => true, 'list' => false, 'application' => false, 'jobposting' => false],
],
];
foreach ($cases as $name => [$input, $expected]) {
$actual = example_resolve_job_state($input, $now);
foreach ($expected as $key => $value) {
if ($actual[$key] !== $value) {
throw new RuntimeException("{$name}: {$key} failed");
}
}
}
これに加えて、WordPressへ接続するテストでは次を確認します。
- 下書き投稿はすべての出力が止まる
- 必須schema情報が欠けた募集中投稿は、詳細・一覧・応募導線だけを出し、
JobPostingは出さない - 期限と現在時刻が等しい境界で、一覧・応募導線・
JobPostingが止まる - 一覧queryへ
closedとexpiredを混ぜても、表示対象へ入らない - 一覧ページのHTMLに
application/ld+jsonが出ない
公開前に画面と機械の両方で確認する
コードが正しくても、キャッシュや別プラグインが古いJSON-LDを出す可能性があります。公開前後には、少なくとも次を実URLで確認します。
- 募集中の詳細で応募導線が使える
- 一時停止・終了・期限切れで応募導線が消える
- 終了・期限切れが求人一覧から消える
- 求人一覧のソースに
JobPostingがない - 募集中の詳細に
JobPostingが1件だけある - 終了・期限切れの詳細に
JobPostingがない - JSON-LDの内容と画面上の職種・説明・雇用主・勤務地・期限が一致する
- リッチリザルト テストでエラーがない
Googleは、終了した求人についてvalidThroughを過去にする、404または410にする、JobPostingを削除する、という選択肢を示しています。どれを採用するかはURL維持方針と運用体制で決めます。ただし、応募終了後も応募導線と有効なJobPostingが残る状態は避けます。
まとめ
求人ページの終了処理は、ボタンを1つ消す作業ではありません。
- 編集者が入力する募集状態を絞る
- 期限切れをresolverで自動判定する
- 詳細、一覧、応募導線、
JobPostingを同じ判定結果へ接続する - 不明値や不完全なschemaは安全側へ閉じる
- 一覧では
JobPostingを出さない - 公開後の実HTMLでも残存を確認する
この構造にしておくと、「画面では終了しているのに応募できる」「一覧から消したのにJSON-LDだけ残る」といったズレを、状態テストで早めに検出できます。
参考資料
- Google検索セントラル: 求人情報(JobPosting)の構造化データ
- Schema.org: JobPosting
- WordPress Developer Resources: current_datetime()
- WordPress Developer Resources: get_post_meta()
- WordPress Developer Resources: is_singular()
- WordPress Developer Resources: wp_json_encode()
採用サイト案件全体の確認項目は、WordPress採用サイトの制作・引き渡しチェックリストにまとめています。