非公開記事を検索結果から消すために noindex の meta タグを書こうとして、手を止めた。調べたら、本番ビルドが非公開記事を最初から焼かない仕組みが既にあって、meta robots より強い「そもそも HTML が存在しない」状態が出来上がっていた。今日は「noindex を入れる作業」だと思って着手したのに、終わってみたら「既存実装が要件を満たすことの確認作業」だった。
きっかけ:unpublished 一覧、検索に出てない?
ローカルの localhost:3000/blog/unpublished に億り人まとめ記事を出していた。これは unpublished: true を立てた記事だけを集める非公開の一覧ページだ。
ここで気になった。このページに並んでいる記事、本番に出たら Google にインデックスされてしまうのか。検索結果に出したくない。URL を直打ちされるアクセスは最悪しょうがないが、できればそれも防ぎたい。
最初に頭に浮かんだのは <meta name="robots" content="noindex"> だった。実際、app/composables/useNoindex.ts にパスパターンで noindex を当てる仕組みが既にある。フォームページや薄いチャートページをここで弾いている。
const NOINDEX_PATTERNS: RegExp[] = [
/^\/blog\/tax-consultation-form\/?$/,
/^\/blog\/company_chart_absolute\/?$/,
// ...
]
/blog/unpublished をこの配列に足せば終わり——そう思ってルーティング構造と既存の漏洩防止ロジックを読み始めた。
調べて手が止まった:noindex より強い壁が既にあった
読み進めて、考えが変わった。mark-unpublished.mjs、seo-noindex.ts、verify-unpublished-excluded.mjs という一式が既に存在していた。過去の自分が、同じ要望をもっと徹底した形で実装済みだった。
仕組みはこうだ。unpublished: true を立てた記事は、本番ビルドから6経路すべてで除外される。
- HTML 本体
_rawの md ファイル- payload(Nuxt のデータ)
- sitemap
- redirects
_nuxtバンドル
そして /blog/unpublished ページ自体が本番では 404 になる。
noindex の meta タグは「クロールはされるが検索結果には出すな」というお願いでしかない。今回の実装はそれより一段強くて、本番にそもそも HTML が生成されない。検索エンジンが取りに来ても 404 が返る。
ここで「URL 直打ちは最悪しょうがない」と諦めていた点も解決していることに気づいた。HTML が焼かれていないのだから、本番で URL を直打ちされても 404 になる。閲覧できるのは dev(localhost)だけだ。meta robots を当てる出番がなかった。
検証していたら別のビルド事故を踏んだ
「本当に除外されているか」を確かめるため、漏洩検出テストとビルド検証を回した。ここで2つ、別の問題が表に出てきた。
ひとつは、公開記事から非公開記事へのリンクが2本残っていたこと(5/22 作業の積み残し)。漏洩を解説する記事自身が、例示として Markdown のリンク記法を本文に書いてしまい、それ自体が漏洩源になっていた。自己参照漏洩だ。リンク記法をプレーンテキストに直して、検出テスト9件を緑にした。
もうひとつが厄介だった。pnpm generate がプリレンダー完了後(3541ルート生成済み)に exit 1 で落ちた。ビルド本体は成功していて、その後の検証スクリプト verify-blog-payload.mjs が落としていた。
[verify-blog-payload] dist/blog/index.html: 1 article hrefs (1 unique)
- Only 1 unique article links in /blog page HTML (expected at least 20).
「記事リンクが1本しかない、20本未満だ」という判定だった。だがフィルタは無傷で(article-date.ts のテスト24件は全部通る)、これは検証スクリプトの誤検知だった。
原因は日付依存だった。/blog のデフォルト表示はカレンダーで、SSG のプリレンダー HTML にはカレンダーだけが焼き込まれる。BlogCalendar.vue は当月を初期表示する。今日は6月1日。6月にはまだ記事が2本しかない。なのにスクリプトは「日付プレフィックス付きリンクが20本以上」を厳密カウントで要求していた。月をまたいだ瞬間に当月記事数が20を割って引っかかる。5/31までは5月カレンダーが満載だったから通っていただけだった。
verify-blog-payload.mjs を「件数」から「カバレッジ」に書き換えた
絶対件数の閾値という発想が脆かった。スクリプトを「カレンダー表示月のカバレッジ検証」に直した。
BLOG_MIN_ARTICLESという脆いゲートを撤廃- アプリと同じ規則で「プリレンダーされるカレンダーの表示月」を算出
- frontmatter から
updatedAtも読み、カレンダーのグルーピング日付に一致させる - 検証は3点:(1) content リンクが0本なら全面リグレッション、(2) 当月の最新記事が HTML に無ければ月末比較バグ、(3) 当月公開の記事が全部リンクされているか
カレンダーは1日の記事を v-for で省略せず全部 NuxtLink で描く(+N件 の切り詰めがない)。だから「当月記事は全部 HTML に出る」が day-of-month に依存しない不変条件になる。修正後にスクリプトを単体で回したら EXIT=0 で通った。記録は .claude/issues/2026-06-01-verify-blog-payload-month-start-false-positive.md に残した。
なお _redirects も再生成されていた(4804本)。SSG ビルドの副産物で、今回の noindex 論点とは別系統だ。
学び
noindex の meta タグは「載せないでください」というお願いに過ぎない。本当に出したくないものは、検索エンジンに渡す前に、ビルドの段階で成果物から消すのが確実だった。今回は新しく noindex を書く必要がなく、過去の自分が用意した「6経路除外+本番404」が要件を丸ごと満たしていた。
本番にはまだデプロイしていない。次にデプロイするときは、verify-unpublished-excluded.mjs が dist に対して緑であることと、unpublished 記事が本番で 404 になることを目で確認してから出す。