Google Search Console分析から内部リンク実装へ — Nuxtブログに関連記事セクションとタグページ553枚を一日で載せた

開発mdx-playground

Google Search Console分析から内部リンク実装へ — Nuxtブログに関連記事セクションとタグページ553枚を一日で載せた

自サイトの検索パフォーマンスが、大して上がっていない。7月4日にインデックス登録まわりの技術修正をやり切って、あとは伸びるのを待つだけ——そう思っていた。ひと月経って、Google Search Console を Chrome DevTools 経由で見てもらい、直近28日間の数字を6月9日の定点観測と突き合わせた。返ってきた結論はこうだった。検索が伸びない原因は、もう技術ではない。コンテンツと内部リンク構造だ。

調査の詳細は非公開のブログ記事としてレポート化してもらった(devサーバーで描画確認済み。あとから読み返せる)。それで、結局どうすればいいのか。サマリーを一問で聞いたら、答えは一つに絞られて返ってきた。「検索意図に正対する単体ページを増やし、記事同士をつなぐ」。技術修正はもう終わっていて、これ以上いじっても伸びない、と。

タグは付いているのに、足りない?

引っかかった。公開記事の frontmatter にはタグもカテゴリも付けてある。それで足りないのか。

計画を作る前に、現状のタグ実装(表示・リンク先・リダイレクト設定)を確認させたら、引っかかりの正体が出た。タグはデータとして存在するだけで、記事画面にタグチップは無く、タグ一覧ページも無く、タグURLへのアクセスはトップへ301されていた。読者もクローラーも、タグを辿って隣の記事へ行く道が一本も無い。タグを「付けている」ことと「つながっている」ことは別物だった。

計画書は memo/2026-07-19/internal-linking-plan.md に書かせた。リダイレクト再生成とデプロイを含む計画なのでルール上は Codex レビュー必須だが、レビューがうまく通らない可能性があったので今回はパスすると先に決めて、GO を出した。

関連記事の選定は純粋関数 app/utils/related-articles.ts に切り出させた。タグの共有数で関連度を測り、表記ゆれは正規化キーで吸収する。並び順はこれだけ。

// スコア: 正規化タグの共有数 降順 → publishedAt 降順 → path 昇順(安定)
.sort((a, b) =>
  b.sharedTagCount - a.sharedTagCount
  || publishedAtTime(b.publishedAt) - publishedAtTime(a.publishedAt)
  || a.path.localeCompare(b.path),
)

呼び出し側の [...slug].vue では、useAsyncData の戻りを plain POJO に詰め替えてから返す。queryCollection の戻りをそのまま返すと、本番SSGで payload が null に化ける——このリポジトリで過去に一度踏んでルール化してある罠なので、対策をそのまま当てた。ユニットテスト12件を先に通してから、DocPage への統合と dev でのスモーク確認に進んだ。

タグ側は、タグインデックスの生成スクリプトを作らせたら555タグが出てきた。package.json の pre* フックに組み込んで、ビルドのたびに再生成される形にする。タグページ本体 tags/[tag].vue の実装、prerender への登録、トップへの301撤去、記事末尾のタグチップまで進めた。

一箇所つまずいた。タグチップが画面に出てこない。コンソールエラーとDOMを追わせて修正し、テストを回し直すと全18,577件が通った。カバレッジは utils 3ファイルが95%で止まっていたので、未カバー分岐(publishedAt 未定義・ルートパス)に2ケース足して100%で締めた。E2Eスモークも1本追加した。

デプロイと本番確認

/deploy を実行した。プリレンダー対象が3,129ルートに膨らんだ。前回比で約550増——タグ一覧とタグページがそのまま載った計算だ。ビルド込みで20分超を待つ。

デプロイ後の検証は次の順で通した。

  • payload の related スロットが配列で返ること(null 化していないこと)を fetch で確認
  • タグページを本番 curl で叩いて描画を確認
  • 本番に載ったもの: 関連記事セクション・タグページ・記事末尾のタグチップ・タグURLのトップ301撤去

この検証で一つ発覚した。スラッシュを含むタグ2個(UI/UX@nuxt/content)へのリンクが404になる。その場で修正して553タグ・27テストパス。本番反映は次回デプロイに回した。Search Console 側の後処理として、URL検査に新しいURLを入れるところまでやってもらった。

コミットを頼むついでに「本番環境も含めて表示してチェックしてくれてますか」と聞いたら、curl と payload の確認まででブラウザ目視はまだだった、という答えが返ってきた。数字の検証と、画面が実際に見えていることは別物だ。本番をブラウザで表示させて、関連記事セクションとタグチップが出ていることを目で確認してからコミットに進んだ。

コミット7件と学習ゲート

コミットは機能単位で7件に分割した。pre-commit の学習ゲートが発動したので /learn を起動し、クイズに答えて通した。7件のうち2件は機械的な変更として LEARN_SKIP で通過。コミット6・7は一度止まったが、修正して再実行し、全件積み終えた。

積み残し

  • スラッシュ入りタグ修正の本番反映 — 再デプロイをバックグラウンドで開始済み(前回実績で約23分)。完了後に /tags/ が553タグになり、404リンクが消えたことを確認する
  • 2週間後の効果測定 — 関連記事とタグページが Search Console の数字をどう動かしたかを見る。これはまた今度

学び

  • タグは frontmatter に書いた時点では検索に何も寄与しない。チップ・一覧ページ・prerender まで揃って、初めてクローラーが辿れる「道」になる
  • queryCollection の戻りを useAsyncData からそのまま返さない。plain POJO への詰め替えをルール化しておいたおかげで、今回は本番の payload null 化を踏まずに済んだ
  • 「curl で200が返る」と「ブラウザで表示されている」は別の検証。完了と言う前に画面を見る

内部リンクの道は敷き終わった。クローラーがそれを辿って評価を変えるかどうかは、まだ分からない。答え合わせは2週間後の Search Console にある。

#Google Search Console #SEO#内部リンク #Nuxt#関連記事