今日、4つ同時に見つかった
ルーチンの点検で、当サイトの構造化データと sitemap を測り直した。4つ出てきた。全部、別々の場所の、別々のバグだった。だが形が同じだった。
- 全記事の
datePublishedが「1970-01-01」になっていた - og:type が記事ページでも "website" のままだった
- 用語ハイライトの
spanタグが、127本中76本(262件)で入れ子になっていた - sitemap の lastmod が、直した日から一度も動いていなかった
4つとも、エラーは出ていない。JSON-LD は出力されている。og:type タグも出ている。ハイライトも表示されている。sitemap の URL も載っている。出力は在った。だから誰も測らなかった。1つずつ、何が起きていたかを書く。
datePublished が「1970-01-01」だった
SEO記事(/seo_article/ 配下、当サイトの実務記事群)の JSON-LD(Article)を開くと、datePublished が(DB 総行数1,295件のうち、公開中の1,288件で)"1970-01-01" になっていた。原因は単純だった。DB の公開日カラム(valdate)は「2025年06月30日」のような和文形式の文字列で入っており、これを PHP の strtotime() に直接渡すと解釈に失敗して false が返る。date('Y-m-d', false) は false を数値 0 として扱うため、結果は 1970-01-01(Unix エポック)になる。
気づきにくかった理由は、JSON-LD 自体は正しく出力されていたことだ。プロパティは存在する。値も ISO 8601 形式として構文的には正しい。だからリッチリザルトテストは「有効」と判定する。値が実際の公開日と違うことまでは見ない。
"The structured data is incorrect in a way that the Rich Results Test was not able to catch."
(日本語訳: 構造化データが、リッチリザルトテストでは捕まえられない形で間違っていることがある)
"A valid item is an item that doesn't have any critical issues and can appear on Google as a rich result." "An invalid item has at least one critical issue preventing it from appearing as a rich result."
(日本語訳: 有効なアイテムとは、重大な問題を持たずリッチリザルトとして表示されうるアイテムのこと。無効なアイテムとは、リッチリザルト表示を妨げる重大な問題を少なくとも1つ持つアイテムのこと)
Google のガイドラインは datePublished を必須プロパティにしていない。「該当するプロパティを追加する」という書き方で、値の妥当性検査までは書かれていない。構文的に正しければ valid。それが「実際に正しい日付か」は別の話。
✅ 当サイト実施済 DB の1,295件を直すのではなく、読む側の日付パース処理に「和文形式(YYYY年MM月DD日)を正規表現で先に ISO 形式へ正規化してから strtotime() に渡す。解釈できなければ modified(更新日時)に倒す」処理を足した。DB は触っていない。test・本番の両方で反映済み(本記事の JSON-LD 自体、実際の公開日時が正しく出ている)。
何を測ったら出たか ── リッチリザルトテストの「valid」を疑った理由
リッチリザルトテストで見えるのは「有効」か「無効」かの二値だ。datePublished のように必須プロパティでない項目は、値が入っていれば構文的に妥当な限りチェックを通る。気づいたきっかけは、テストのボタンを押すことではなく、JSON-LD の生データを取り出して、値を日付として読み直したことだった。1970-01-01 という値は、日付としては特徴的すぎる。Unix エポック(タイムスタンプ 0)に対応する日付で、正常に生成された日付がここに揃うことは通常ない。だから機械的に検索をかければ、この値を持つ記事だけが浮かび上がる。
なぜ自動チェックはすり抜けたか ── 「型」は正しく、「値」だけが違う
Google のリッチリザルトテストや構造化データテストは、プロパティが存在するか・値の形式が仕様(ISO 8601)に沿っているかを見る。1970-01-01 という文字列は、日付の形式としては何の問題もない。構文としては満点で、意味としては全部が同じ誤った日付になっているという状態を、テストは区別しない。「バリデータは構文チェッカーであって、意味チェッカーではない」という一点を踏まえておくと、他の構造化データの不具合にも当てはめて考えられる。
あなたのサイトで確かめる手順
専用ツールは要らない。記事ページを開き、ブラウザの view-source(Ctrl+U など)で application/ld+json のブロックを探し、"datePublished" の値をそのまま目で読む。1970年になっていないか、逆に未来の日付になっていないか(こちらは予約投稿の初期化ミスで起きやすい)を確認する。複数記事をまとめて見たい場合は、記事一覧の URL を順に取得し、それぞれの JSON-LD から datePublished の値だけを抜き出して並べれば、異常値は一目で分かる。多くの記事が同じ値に揃っていたら、それは実際の公開日ではなく、パース失敗時の既定値である疑いが強い。
直したあと、何を見て「直った」と判断したか
まず確認したのは、この記事自身の JSON-LD だ。修正を当てた後、同じ view-source の手順で datePublished の値を読み直し、実際の公開日時と一致していることを確認した。ただし、これは「1件が直った」ことの確認であって、「全1,295件が直った」ことの確認ではない。DB 側の値そのものは触っていないので、修正前に生成されて配信済みのキャッシュや、まだこの読み替え処理を通っていない別の出力経路が残っていないかは、全数で洗い切れていない。ここは正直に書いておく。また、確認したのは datePublished だけで、同じ和文形式の日付を読む別のプロパティ(更新日時を示す dateModified など)が同じパース処理を通っているかどうかも、まだ個別には確認していない。同じ原因(和文形式の文字列を strtotime() にそのまま渡す)が別のプロパティにも潜んでいる可能性は、今回の修正だけでは否定できない。
og:type が記事ページでも "website" のままだった
同じ SEO記事の詳細ページで、<meta property="og:type"> を見ると、"article" であるべきところが "website" のままになっていた。原因を追うと、判定ロジックが URL 文字列の中に /seo_article/detail/ という古いルーティングの名残を探していた。だが実際の URL 構造はすでに /seo_article/462 のような「番号だけ」の形に変わっていた。URL 構造を変えたとき、文字列で判定している場所だけが追従していなかった。
og:type がずれていても、ページは正常にレンダリングされる。SNS カードも一応表示される(type によって扱いが変わる項目は多くないため、崩れ方が目立たない)。これも「出力が在るのでエラーが出ない」形だった。
"The type of your object, e.g., "video.movie". Depending on the type you specify, other properties may also be required."
(日本語訳: オブジェクトの type。例えば "video.movie"。指定する type によって、他に必須になるプロパティが変わる)
✅ 当サイト実施済 URL 文字列でのパターン一致という判定方法そのものが、URL 構造の変化に弱かった。「一覧ページ(/seo_article/ のみ)は "website" のまま維持しつつ、記事番号が続く場合だけ "article" にする」正規表現を1行追加した。test・本番の両方で確認済み。
何を測ったら出たか ── SNS カードのプレビューでは気づけなかった理由
og:type がずれていても、SNS カードのプレビューは大きく崩れない。type によって扱いが変わるプロパティはそう多くないため、見た目のインパクトが小さく、プレビューを見ただけでは異常だと分からない。気づいたのは、プレビュー画面ではなく、ページのソースを開いて <meta property="og:type"> の値そのものを読んだときだった。SNS カードの見た目は「間違っていても崩れない」ことがある。崩れないものは、見た目のチェックでは検出できない。
なぜ気づけなかったか ── 判定ロジックが「型」ではなく「文字列」を見ていた
判定ロジックが URL の中にある特定の文字列パターンを探して「記事詳細ページかどうか」を決めていた。ところがそのパターンは、以前使っていた古いルーティングの名残だった。URL 構造を新しい形式に変えたとき、ページの表示自体は新しいルーティングに追従していたが、og:type の判定ロジックだけが古い文字列を探し続けていた。URL 構造の変更は「表示が動くか」で確認しがちだが、文字列パターンで判定している箇所は、表示とは別に個別に確認しないと追従漏れが起きる。
あなたのサイトで確かめる手順
記事の詳細ページと、記事の一覧ページの両方を開き、それぞれの view-source で og:type の値を比較する。詳細ページは "article"、一覧ページは "website" になっているのが正しい状態だ。特に、過去に URL 構造(パーマリンクの形式)を変更した記憶があるサイトは確認する価値が高い。URL を変えた作業そのものは正しく終わっていても、文字列一致で判定している別の箇所が古いパターンのまま取り残されていることがある。
検証の仕方 ── 一覧と詳細、両方を見て初めて確認になる
正規表現を直した後、詳細ページだけを見て「article になった」と確認するのでは足りない。一覧ページ側が意図せず "article" に変わっていないか(記事番号を含まない URL まで拾ってしまっていないか)も併せて見る必要がある。修正は「詳細ページで article を出す」ことと「一覧ページで website を維持する」ことを同時に満たして初めて正しい。片方だけを見て安心すると、直したつもりの修正が別の場所を壊していないかを見落とす。もう一つ、正直に書いておくことがある。og:type と同じように種別によって扱いが変わるメタタグが他にもあるなら、それらが同じ古い文字列パターンを参照していないかは、まだ横断的には確認していない。今回直したのは og:type の判定ロジック1箇所であって、「型に依存する判定ロジック全部」を洗い出したわけではない。
用語ハイライトの span が入れ子になっていた ── そして俺は3本だけ見て「他では起きていない」と決めていた
用語ハイライト機能は、本文中の専門語(「Google Search Console」など)に <span> を付けて強調表示する。ここに入れ子が起きていた。「Google Search Console」に span を付けたあと、その span の中に含まれる「Search Console」という短い語が、別のハイライト対象として再度マッチしていた。すでに付けた範囲をスキップする処理が無かったため、span の中に span が入る構造になっていた。
ブラウザは崩れずに表示する。ハイライトの色も付く。だから誰も気づかなかった。気づいたきっかけは、別の機能に同じ仕組みを流用しようとしたときだった。そこで正直に書く。俺は最初、サンプルとして3本の記事だけを見て「この新機能固有の問題だ」と誤診した。3本だけで「他の記事では起きていない」と一般化していた。実際に127本すべてを測ったら、76本(60%)・262件で同じ入れ子が起きていた。
もう一つ、正確に書いておく必要がある。HTML の仕様上、span の入れ子は許される。「仕様違反」ではない。
"The span element doesn't mean anything on its own, but can be useful when used together with the global attributes." (コンテンツモデル: Phrasing content)
(日本語訳: span 要素はそれ自体では意味を持たないが、グローバル属性と組み合わせて使うと有用になりうる。コンテンツモデルはフレージングコンテンツ)
実務上の問題は仕様違反ではなく、意図しない二重マークアップによって、同じ文字列に複数のスタイルが重ねて適用され、装飾が想定より濃くなったり、将来の機能(クリックイベントなど)を span 単位で足すときに範囲が予測できなくなったりすることにある。
✅ 当サイト実施済 全数の実測は完了した(127本中76本・262件)。対処(既にマークした範囲をスキップする1行)を当てた。test・本番の両方で確認済み(127本を再実測し、入れ子は0件になったことを確認した)。
どうやって気づいたか ── 3本だけ見て「他では起きていない」と決めていた経緯
最初にこの入れ子に気づいたのは、別の機能に同じハイライトの仕組みを流用しようとしたときだった。手元にあった数本の記事を開いて動作を確認し、そのうち3本だけを見て「表示は崩れていない。この新機能特有の問題だ」と判断した。だが実際には、127本全部に対して同じ検出ロジックを機械的に走らせるまで、影響範囲は分からなかった。3本のサンプルで見えていたのは「たまたま入れ子が起きていた3本」だったのか「入れ子が起きていない3本」だったのかを、当時の俺は区別できていなかった。
なぜ気づけなかったか ── ブラウザが崩れずに表示するから
span の入れ子は HTML の仕様として許されている。ブラウザは崩れずにレンダリングし、ハイライトの色も普通に付く。見た目が壊れないバグは、見た目だけをチェックしている限り検出できない。今回は装飾が二重に重なる程度の影響で済んだが、同じ span 単位で将来クリックイベントなどの機能を足そうとした場合、範囲が二重になっていることで意図しない挙動を招く可能性があった。見た目に出ないバグほど、後から効いてくる。
あなたのサイトで確かめる手順
同様の自動マークアップ機能(本文中の特定の語に自動でタグを付ける仕組み)を持っているなら、生成された HTML のソースを開き、同じ種類のタグが自分自身の内側に入っていないかを確認する。目視では気づきにくいので、対象のタグを機械的に数え、開始タグと終了タグの対応がネストしていないかをチェックするのが確実だ。件数が少ないサンプルだけで「問題ない」と判断せず、対象記事すべてに同じチェックをかけることが、今回の教訓そのものになる。
検証の仕方 ── 「0件になった」をどう確かめたか
修正を当てた後の確認は、3本のサンプルには戻らなかった。同じ検出ロジックを127本全部に対してもう一度走らせ、入れ子が0件になったことを確認した。ここで一つ気をつけたのは、検出ロジック自体が正しく動いているかを先に確かめることだ。意図的に入れ子を含むテキストを通して、検出ロジックがそれを正しく拾うことを確認してから、本番の127本に対する「0件」を主張した。検出できない検出ロジックで「0件でした」と言っても、それは「見つからなかった」のか「探していなかった」のか区別がつかない。
sitemap の lastmod が、直した日から動いていなかった
直近の点検で sitemap の状態を確認したとき、点検スクリプトは「Nothing to fix」を返していた。これをそのまま受け取っていた。だが sitemap の中身を実際に開くと、記事を修正・公開した日が過ぎているのに <lastmod> の値だけが古いままの URL があった。
原因は点検スクリプトの設計にあった。スクリプトは「その URL が sitemap に載っているか」しか見ていなかった。載っていれば「Nothing to fix」。lastmod の値が実際の更新日と一致しているかは、そもそも見る対象に入っていなかった。追加専用のチェックであって、更新専用のチェックではなかった。
"The date of last modification of the page. ... Note that the date must be set to the date the linked page was last modified, not when the sitemap is generated."
(日本語訳: ページが最後に更新された日付。……この日付は、sitemap が生成された日ではなく、リンク先のページが実際に最後に更新された日にすること)
"Google uses the<lastmod>value if it's consistently and verifiably (for example by comparing to the last modification of the page) accurate."
(日本語訳: Google は、lastmod の値が一貫して検証可能な形(例えばページの実際の更新日時との比較)で正確である場合に、その値を使う)
言い換えると、lastmod がでたらめだと Google はその値自体を信用しなくなりうる。「URL が載っている」ことと「載っている情報が正しい」ことは、sitemap でも同じように分かれていた。
🔧 当サイト着手中 点検スクリプトが「載っているか」だけでなく「載っている日付が実際の更新日と一致しているか」も見るように直す。まだこれから。
どうやって気づいたか ── 点検スクリプトの「Nothing to fix」を疑った理由
点検スクリプトが「Nothing to fix」と返したとき、そのまま受け取っていれば今回の穴には気づかなかった。気づいたのは、スクリプトの結果を鵜呑みにせず、sitemap ファイルの中身を実際に開いて lastmod の値を目で読んだときだった。ある記事は数週間前に修正・公開し直しているのに、sitemap 上の lastmod は修正前の日付のままだった。スクリプトが正常終了を返していても、それは「チェックした範囲の中では問題がなかった」という意味でしかない。
なぜ気づけなかったか ── 「追加専用」のチェックは「更新専用」ではなかった
点検スクリプトの設計は、「その URL が sitemap に載っているか」だけを見ていた。載っていれば OK、載っていなければ追加する。これは新しい記事を sitemap に反映し忘れる事故は防げるが、既に載っている URL の lastmod が古いままになっている事故は防げない。「追加」と「更新」は別のチェック項目であり、片方のロジックだけを実装すると、もう片方は無条件で見逃される。チェックの対象範囲を決めるとき、「何を確認しているか」だけでなく「何を確認していないか」を意識しておく必要があった。
あなたのサイトで確かめる手順
sitemap.xml を開き、数記事分の URL を選んで <lastmod> の値を書き出す。次に、その記事の実際の更新日時(管理画面の更新日や、記事ページに表示されている更新日)と突き合わせる。「sitemap に URL が載っている」ことと「載っている日付が正しい」ことは別の確認だ。載っているかどうかだけを見て安心していると、日付側のズレには一生気づかない。
まだこれから、の中身 ── 何を直せば「完了」と言えるか
この項目だけは、まだ 🔧 のまま据え置いている。「完了」と呼べる状態は、点検スクリプトの比較対象に「lastmod の値」と「実際の更新日時」の差分チェックが加わり、両者がズレている URL を検出して知らせてくる状態だ。現状のスクリプトは「載っているか」の判定しか持っていないので、ここに「載っている日付が正しいか」の判定を追加する必要がある。まだ手を付けていない理由を正直に書けば、これまでは「載っているか」だけを見れば足りると思い込んでいたからで、その思い込み自体が今回の発見だった。
4つに共通していたもの
4つとも原因は別々だった。日付パースの失敗、URL パターンの追従漏れ、マークアップの重複、点検範囲の狭さ。だが形は同じだった。
- 出力は在った(JSON-LD・og:type・span・sitemap URL、すべて「存在する」状態だった)
- 自動チェックは通った(リッチリザルトテストは valid、ページは正常表示、sitemap は「Nothing to fix」)
- 値だけが違った(日付が1970年、type が違う種別、装飾が重複、日付が古い)
4つを並べて比べると、共通の骨格がもう少しはっきりする。
| バグ | 出力の有無 | 自動チェックの結果 | 実際にずれていた値 | 気づいた方法 |
|---|---|---|---|---|
| datePublished | JSON-LD は出力あり | リッチリザルトテスト valid | 1970-01-01(Unix エポック) | JSON-LD の値を機械的に読み直す |
| og:type | meta タグは出力あり | ページは正常表示・SNS カードも表示 | 詳細ページでも "website" のまま | view-source で meta タグを直接確認 |
| 用語ハイライト | span は出力あり・装飾も表示 | ブラウザは崩れず表示 | span が自分自身の中に入れ子(仕様違反ではない) | 127本全数を機械的に再検出 |
| sitemap lastmod | URL は sitemap に掲載あり | 点検スクリプトは「Nothing to fix」 | lastmod の日付が更新前のまま | sitemap を直接開いて目視 |
「気づいた方法」の列を見ると、4つとも自動チェックの外側で、人間が生の値を読み直したときに見つかっている。自動チェックが優れていなかったわけではない。自動チェックが見る範囲の外側に、今回の4つが揃って落ちていた。
なぜ4つ同時に見つかったか ── 別々のバグを、同じ日の点検でまとめて見つけた理由
4つは互いに無関係なバグだ。直した箇所も別々、原因の種類も別々(日付パース・文字列判定・マークアップ処理・チェック範囲)。それでも同じ日に4つとも見つかったのは、偶然というより定期的なルーチン点検の対象を、機能ごとに分けて持っていたからだ。構造化データを見る手順、OGP タグを見る手順、自動マークアップの出力を見る手順、sitemap を見る手順を、それぞれ別の確認項目として回している。1つの巨大なチェックリストに全部を混ぜていたら、どれか1つを見ている間に他の3つを見落とした可能性がある。点検の項目を分けておくと、1つずつは地味でも、まとめて回したときに複数のズレが同時に見える。
自動チェックの多くは「プロパティが存在するか」「構文が正しいか」を見る。「値が実態と一致しているか」までは見ない。これは怠慢ではなく、そもそも自動チェックの守備範囲が違う。archives/126「llms.txt が v2 になった日 ── 200 が返ることと、対応していることは別だった」で書いた「200 が返ることと、対応していることは別」と、根が同じだ。「エラーが出ない」は「正しい」の証明にならない。
あなたのサイトでも、今日できること
4つとも、専用ツールを使わなくても目視で確認できる。
- 記事ページの view-source で
datePublishedの値を実際に読む。リッチリザルトテストが「有効」と出ていても、値が1970年や未来の日付になっていないか、自分の目で確かめる。 - 記事ページで
<meta property="og:type">を見る。"article" になっているか。URL 構造を変えた記憶があるサイトほど確認する価値がある。 - 自動でマークアップを付ける機能があるなら、3本のサンプルで安心しない。全数を測る。件数が少ないほど「他では起きていない」という結論に飛びつきやすい。
- sitemap の lastmod を数記事サンプルして、実際の更新日と突き合わせる。「URL が載っている」ことと「日付が正しい」ことは別のチェック項目だと分けて考える。
- 「テストが通った」を「値が正しい」の代わりに使わない。この一行を疑う癖だけで、4つのうち3つは見つけられた。
ここまでの手順を、確認場所と正常な状態だけの形にまとめておく。
| 確認すること | どこで見るか | 正常な状態 |
|---|---|---|
| datePublished の値 | 記事ページの view-source、application/ld+json のブロック | 実際の公開日と一致した日付(1970年や未来日付になっていない) |
| og:type の値 | 記事ページの view-source、<meta property="og:type"> | 記事詳細ページなら "article"、一覧ページなら "website" |
| 自動マークアップの入れ子 | 同機能を使った本文の view-source | 同じ種類のタグが自分自身の内側に入っていない |
| sitemap の lastmod | sitemap.xml の対象記事の URL 行 | その記事の実際の更新日時と一致している |
今日から始めるなら、どの順番でやるか
4つとも今日中に確認できるが、優先順位を付けるなら「読者や検索エンジンへの影響が大きい順」で見るのが早い。datePublished と og:type は、記事の鮮度や共有時の見え方に直接効くので先に見る。用語ハイライトの入れ子は表示崩れが起きていないなら緊急度は低いが、同じ仕組みを他の機能に流用する予定があるなら先に潰しておく価値がある。sitemap の lastmod は、検索エンジンがクロールの優先度を決める材料の1つなので、更新頻度が高いサイトほど早めに直す効果が出やすい。4つとも「エラーが出ない」バグなので、優先順位を決めるのを後回しにしても誰にも急かされない。だからこそ、自分で順番を決めて着手日を決める。
関連 archives(連載軸として読む)
- archives/126「llms.txt が v2 になった日 ── 200 が返ることと、対応していることは別だった」 ── 「出力は在るが値が違う」という今日の記事の直接の前段(2026-08-16)
- archives/127「llms.txt に 59 本 積んでいた ── ただし『読まなくていい』と書いてある棚に」 ── 同じ点検の翌日に、別の場所で同じ形の穴が出た記録(2026-08-16)
- archives/91「Verified AI Agent 37体を個別allowした実装ログ ── 警告した側の次の責任を、当サイトで完結させた日」 ── 「見つけたら、自分のサイトで実施まで終える」という自己実証の起点
- archives/98「1,238記事の8割が『土俵に上がっていない』ことに気づいた日 ── addview.html未生成という静かなnoindex地獄、一夜での脱出」 ── 「出力の有無」だけを見て「値の正しさ」を見ていなかった、初期の同型の事例
- archives/64「順位1位でも、AIには引用されない ── エンティティSEOという『次の入口』」 ── 著者スキーマを実在人物に据え直した回。当サイトの自己実証バッジの起点
一次情報出典
- Google Search Central: Structured data general guidelines
- Google Search Console ヘルプ: 構造化データの有効性ステータス
- Google Search Central: Article(NewsArticle, BlogPosting)structured data
- The Open Graph protocol
- WHATWG HTML Living Standard: The span element
- sitemaps.org: Sitemap XML format
- Google Search Central: Build and submit a sitemap
WEBサイト