はじめに
静的サイト(ブログ、ドキュメント、ポートフォリオ)は、ページ数が増えるほど「小さな壊れ」が確実に増えます。しかも厄介なのは、壊れていてもビルド自体は通ることが多い点です。
- 内部リンクのURLを変えたのに、古いリンクが残っている
#sectionのアンカーが、見出し編集で消えたのに参照が残っている- 画像ファイルを移動したのに、参照パスだけ古い
- 表示はされるけど、相対パスが環境によって壊れる(ローカルはOK、公開でNG)
こういう事故は、公開後に気づくと地味に信用を削ります。さらに、リンク切れは検索エンジンにもユーザーにも痛い。
だからこそ、ビルド後に生成物(dist/ など)を対象に自動検査し、壊れたらCIで落とすのが強いです。
この記事では、静的サイトの「参照整合」を守るために、次の3点を軸にしたパイプラインを作ります。
- 内部リンク(ページ間リンク)が存在するか
- 画像(
img srcやsource srcset)の参照先が存在するか - アンカー(
#id)が参照先ページ内に存在するか
最後に、ローカル(pre-commit / npm script)→ CI(GitHub Actions)まで繋いで、「壊れたら公開できない」状態にします。
座学
1) なぜ“ビルド後の生成物”を検査するのか
テンプレートやMarkdownの段階で検査したくなるのですが、実務では生成後HTMLを検査する方が事故が減ります。
- Markdown → HTML 変換で見出しIDが自動生成され、結果的に
#idが変わることがある - ベースパス(
/か/blog/か)で相対パスが変わり、公開環境だけ壊れることがある - 画像の最適化(コピー/圧縮/変換)でファイル名が変わることがある
つまり「ソースは正しそう」でも「成果物が壊れている」ことが普通に起きます。
だから、dist/ を“配るものそのもの”として検査するのが一番堅いです。
2) 検査対象を分解する(落とし穴の種類)
参照整合の検査は、大きく4カテゴリに分けると設計しやすいです。
- 内部リンク:
<a href="/about/">など- 生成されたファイルがあるか
- 末尾スラッシュ運用(
/about/vs/about.html)に統一されているか
- アンカー:
<a href="/docs/#install">/<a href="#install">- 対象ページ内に該当IDが存在するか
- 見出しの自動ID生成ルール変更で壊れやすい
- 画像:
<img src="/assets/x.png">/srcset- ファイルのコピー漏れ、ディレクトリ移動で壊れやすい
- 大文字小文字差(macは気づかず、Linuxで壊れる)が地雷
- 外部リンク:
https://...- “相手都合”で落ちる。CIで毎回厳格にやるとノイズになりやすい
- 重要リンクだけホワイトリストで検査、など運用設計が必要
この記事の主役は 内部リンク・アンカー・画像です(外部リンクは次のステップで触れます)。
3) “落とし方”を設計する(CIがうるさすぎる問題)
自動検査の失敗条件(fail criteria)を設計しないと、運用が破綻します。目安はこうです。
- 内部リンク切れ:即Fail(これは確実に直すべき)
- 画像参照切れ:即Fail(表示崩れが直撃する)
- アンカー不整合:即Fail(記事内導線が死ぬ)
- 外部リンクタイムアウト:最初はWarn扱いか、週次ジョブで通知に回す
大事なのは「直すべき事故だけを、確実に落とす」ことです。
4) ツール選定の考え方(特定ツールに依存しない)
リンク検査ツールは山ほどありますが、選び方はシンプルです。
- dist/ をクロールできる(ローカルサーバでもファイルでもOK)
- 内部リンク + アンカーをチェックできる
- 画像参照も拾える(少なくとも存在チェック)
- CIで動く(Node/Go/単体バイナリなど)
ハンズオンでは、Nodeで扱いやすい htmltest(静的サイト向けのリンク検査)を軸にします。
加えて、画像や外部リンクも含めて柔軟に見たい場合の代替として lychee も紹介します(同時導入しなくてもOKです)。
ハンズオン
前提:静的サイトのビルド成果物が dist/ に出る想定(Eleventy/SSG/自作ビルドでもOK)。
ここでは以下を作ります。
npm run build→ dist生成npm run check:links→ distを検査npm run ci→ build→検査をまとめて実行- GitHub ActionsでPR時に自動実行(壊れていたら落とす)
1) htmltest を導入する
npm i -D htmltest
設定ファイル .htmltest.yml をプロジェクト直下に作成します。
# dist/ を検査対象にする
DirectoryPath: "dist"# これがないと内部リンクの扱いが期待とズレる場合があるので明示
CheckExternal: false# アンカー(#id)検査
CheckAnchors: true# 画像参照(img/src や srcset)の存在検査
CheckImages: true# よくある除外(必要に応じて調整)
IgnoreURLs:
- "^mailto:"
- "^tel:"# 末尾スラッシュ運用や index.html の扱いでコケる場合は、サイトの実態に合わせて調整
補足:SSGの出力形式(
/about/index.htmlなのか/about.htmlなのか)でリンクの正解が変わります。
まずは “現在のdist” に合う設定で動くことを優先し、運用しながら整えていくのが現実的です。
2) npm scripts を整備する
package.json に検査スクリプトを追加します。
{
"scripts": {
"build": "npx @11ty/eleventy",
"check:links": "npx htmltest",
"ci": "npm run build && npm run check:links"
}
}
手元で実行してみます。
npm run ci
壊れているリンクやアンカー、画像があればエラーになります。
ここで初めて「これ、人間の目視だと絶対見落としてたな…」という種類の壊れが出てくるはずです。
3) ありがちな失敗例を“わざと”作って確認する(理解が早い)
以下を一度やってみると、検査の価値が腹落ちします。
- 存在しないページにリンクする
- 例:
<a href="/no-page/">
- 例:
- 存在しないアンカーにリンクする
- 例:
<a href="#no-anchor">(対象のidが無い)
- 例:
- 存在しない画像にする
- 例:
<img src="/assets/no.png">
- 例:
その後 npm run check:links を叩くと、どれがどう落ちるか確認できます。
落ち方が分かると「どの修正をどこでやるべきか」の判断も早くなります。
4) CI(GitHub Actions)に載せる
.github/workflows/verify.yml を作成します。
name: verifyon:
pull_request:
push:
branches: [ main ]jobs:
build-and-verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4 - uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm" - run: npm ci
- run: npm run ci
これで、PRを出した時点で リンク切れ/アンカー不整合/画像欠落があれば落ちるようになります。
運用上のポイントは、「落ちたら直す」が徹底されること。
“検査があるのに無視してマージ”が常態化すると、パイプラインは形骸化します。
5) もう一歩:外部リンクも見たい場合(ノイズ対策つき)
外部リンクは毎回CIで厳格にやると、相手サイトの一時障害で落ちて面倒になりがちです。
そこでおすすめは次のどちらかです。
- PRでは外部リンクは見ない(内部だけを堅く守る)
- 外部リンク検査は夜間の定期ジョブで回して、失敗は通知(マージは止めない)
参考として、lychee(リンクチェッカー)を併用する例を置いておきます(任意)。
npm i -D lychee
{
"scripts": {
"check:external": "npx lychee --no-progress --max-concurrency 8 \"dist/**/*.html\""
}
}
外部リンクは運用設計がすべてです。最初から完璧を狙うより、「重要なリンクだけ検査」「週次で検査」など、現場に合う落とし方にするのが成功しやすいです。
まとめ
リンク切れ・画像欠落・アンカー不整合は、静的サイト運用で最も起きやすいのに、最も見落とされやすい事故です。
この種の事故は「人間の注意力」ではなく、仕組み(ビルド後検査+CI)で潰すのが正攻法です。
- dist/(生成物)を検査対象にする
- 内部リンク/アンカー/画像は即Failで守る
- CIに載せて“壊れたら公開できない”状態を作る
- 外部リンクはノイズを見ながら、定期ジョブや重要リンク限定で運用する
このパイプラインがあるだけで、「更新が怖い」状態がかなり減り、安心して記事やページを増やせるようになります。
コメントを残す