Claude Media
Claude CodeでVitePressサイトを作る — デッドリンク検出と編集範囲の制限

Claude CodeでVitePressサイトを作る — デッドリンク検出と編集範囲の制限

Claude CodeでVitePressのドキュメントサイトを育てる手順です。vitepress buildのデッドリンク検出をStop hookで完了条件にし、生成物の編集範囲をpermissionsで絞ります。

VitePressのサイトでClaude Codeに任せる範囲

VitePressは、Markdownで書いた原稿にテーマを当てて、静的なHTMLを生成するツールです。初回の表示は事前に作ったHTMLで、その後のページ移動は単一ページアプリケーション(SPA)として動きます。技術ドキュメント向けの標準テーマを持ち、Vue.jsの公式ドキュメントのような大きなサイトも、この仕組みの上にあります。

Claude Codeに任せやすいのは、原稿の追加・改稿と、サイト設定の調整です。ただしドキュメントサイトには、ほかの作業にない落とし穴が2つあります。

  • ページを増やすたびに、ページ同士のリンクが壊れやすい
  • 自動生成されたMarkdownを、Claude Codeが手で直してしまう

前者は vitepress build のデッドリンク検出を「完了の条件」にして防ぎます。後者は、permissionsで編集できる範囲を絞って防ぎます。本記事は、この2点をVitePressの設定、CLAUDE.md、Stop hookにつなぐ手順です。静的サイトジェネレーターの別の例は、Docusaurusの日本語化、Hugo、Astroブログの記事にあります。

土台を作る: initとnpmスクリプト

インストールと雛形の生成

VitePressのGetting Startedは、Node.js 22以上を前提にしています。既存のプロジェクトに入れる場合は、./docs のような入れ子のディレクトリに雛形を作ることが勧められています。

npm add -D vitepress
npx vitepress init

init は対話式のセットアップウィザードです。docs を置き場所に選ぶと、次のような構成になります。

.
├─ docs
│  ├─ .vitepress
│  │  └─ config.js
│  ├─ api-examples.md
│  ├─ markdown-examples.md
│  └─ index.md
└─ package.json

docs がサイトのルート(プロジェクトルート)です。.vitepress は予約されたディレクトリで、設定ファイル、開発サーバーのキャッシュ、ビルド出力、テーマのカスタマイズが入ります。キャッシュは .vitepress/cache、ビルド出力は .vitepress/dist が既定の場所で、Gitを使うなら .gitignore に入れます。

ウィザードでnpmスクリプトの追加を許可すると、package.json に次の3つが入ります。

{
  "scripts": {
    "docs:dev": "vitepress dev docs",
    "docs:build": "vitepress build docs",
    "docs:preview": "vitepress preview docs"
  }
}

以降は docs:build を検査のコマンドとして使います。.vitepress/config.js の拡張子は、package.json に "type": "module" がない場合は .mjs か .mts にします。VitePressはESM専用のパッケージです。

先に確認したいこと

  • Claude Codeに「サイトを作って」と頼む前に、npx vitepress init は自分で実行しておく。対話式のウィザードは、Claude Codeに任せるより手元で答えたほうが速い
  • 生成された docs/ 直下のファイルは、サンプルの api-examples.md と markdown-examples.md を含めてコミットしておく。以降の差分が、Claude Codeの変更だけになる

デッドリンク検出を完了条件にする

vitepress buildの終了コードを実測する

VitePressは、ビルドの途中でページ間のリンクを検査します。サイト設定の ignoreDeadLinks の既定値は false で、true にすると「デッドリンクでビルドを失敗させない」挙動になります。裏返すと、既定ではデッドリンクがあればビルドが失敗します。

挙動を手元で確かめました。VitePress 1.6.4(npm add -D vitepress で入る版)で、存在しないページへのリンクと、存在しない見出しへのリンクを置いてビルドしています。

# Home
 
[ok](/guide/a)
[bad](/guide/missing)
[badanchor](/guide/a#nope)

/guide/a は実在するページで、/guide/missing は存在しません。#nope という見出しは /guide/a にありません。

npx vitepress build docs
echo $?

出力の要点は次のとおりです。

(!) Found dead link /guide/missing in file index.md
 
If this is expected, you can disable this check via config. ...
 
x Build failed in 794ms
build error:
[vitepress] 1 dead link(s) found.

終了コードは 1 でした。Claude Codeは、この「落ちた」という信号と、Found dead link の行にある宛先とファイル名を読んで直せます。

見出しアンカーの壊れは検出されない

注目したいのは、検出された数が 1 dead link(s) だった点です。存在しない見出しへのリンク(/guide/a#nope)は、この版のビルドを止めませんでした。VitePressのデッドリンク検査は、ページの存在を確かめるもので、見出しの存在までは見ないと読めます。

この結果から、完了条件の守備範囲が分かります。

壊れ方vitepress build で止まるか
存在しないページへのリンクvitepress build で止まるか止まる(終了コード1)
存在しない見出しへのリンク(#nope)vitepress build で止まるか止まらない(1.6.4で確認)

見出し名を変更する改稿をClaude Codeに任せるときは、見出しへのリンクを grep で探させる指示をCLAUDE.mdに足します。

ignoreDeadLinksをClaude Codeに触らせない

デッドリンク検査を完了条件にすると、別の抜け道が生まれます。Claude Codeが、ビルドを通すために ignoreDeadLinks: true を設定ファイルに書く可能性です。ビルドは確かに通りますが、検査は無効になります。

ignoreDeadLinks には、除外したいリンクだけを指定する書き方もあります。完全一致のURL文字列、正規表現、関数の配列を渡せます。'localhostLinks' を指定すると、localhost のリンクを除いてビルドを検査します。

// .vitepress/config.ts
export default {
  ignoreDeadLinks: [
    // 開発中のローカルURLは検査から外す
    /^https?:\/\/localhost/,
  ],
}

除外の判断は人が行います。次の節で、設定ファイルの編集に確認を求める権限ルールを書きます。

生成物の編集範囲をpermissionsで絞る

手で直させたくない場所を決める

ドキュメントサイトには、手で直しても次の生成で消えるファイルが混ざります。たとえば、APIリファレンスをスクリプトから書き出した docs/api/ や、ビルド出力の .vitepress/dist です。Claude Codeがこれらを直接編集すると、直した内容は次の生成で消え、気づきにくい巻き戻りになります。

この防ぎ方に、CLAUDE.mdの注意書きは向きません。権限ルールは、モデルではなくClaude Code本体が強制します。プロンプトやCLAUDE.mdの指示は、Claude Codeが何を試みるかに影響しても、許可の判定は変えません。

Editルールを書く

.claude/settings.json に、permissions の allow / ask / deny を書きます。ルールは、deny、ask、allowの順に評価され、最初に一致したものが結果になります。広いdenyは、狭いallowがあっても優先されます。

{
  "permissions": {
    "allow": [
      "Edit(/docs/**)",
      "Bash(npm run docs:build)",
      "Bash(npm run docs:preview)"
    ],
    "ask": [
      "Edit(/docs/.vitepress/config.*)"
    ],
    "deny": [
      "Edit(/docs/api/**)",
      "Edit(/docs/.vitepress/dist/**)",
      "Edit(/docs/.vitepress/cache/**)"
    ]
  }
}

このルールセットの狙いは次のとおりです。

権限

ルールごとの役割

  • allowで原稿を開く

    Edit(/docs/**) で、原稿のMarkdownを確認なしで編集できます。ビルドのコマンドも、完全一致で許可します。

  • askで設定を確認する

    config.* の変更は毎回確認します。ignoreDeadLinks のような検査の抜け道が、黙って入るのを防ぎます。

  • denyで生成物を守る

    docs/api/ とビルド出力、キャッシュは編集不可にします。直したいときは、生成元のスクリプトを直す手順になります。

書き方で気をつける点が3つあります。

  1. パスの書き出しの / は、設定ファイルの置き場所(プロジェクト設定なら作業ディレクトリ)からの相対パスです。Edit(/docs/**) は、そのディレクトリ直下の docs/ を指します。ファイルシステムの絶対パスは // で始めます
  2. 編集の許可ルールは Edit(...) で書きます。Write(...) のパスルールは受理されても、一致しません
  3. allowルールの Edit(src/**) のような書き方は、作業ディレクトリ直下の src にだけ一致します。どの階層でも一致させたい場合は **/ を前に付けます

denyの効きは、ルール単体で確かめられます。/permissions で一覧を開くと、各ルールがどの設定ファイルから来ているかが分かります。一覧の見方は/permissionsコマンドの記事にまとめています。

生成元を直す指示をCLAUDE.mdに添える

denyだけだと、Claude Codeは「編集できない」と立ち止まります。次にやることをCLAUDE.mdに書いておきます。

## VitePressサイトの作業ルール
- 原稿は `docs/` 配下のMarkdownを編集する
- `docs/api/` は自動生成。直接編集せず、生成元のスクリプトを直して再生成する
- 作業の最後に `npm run docs:build` を実行し、終了コードが0になるまで直す
- `ignoreDeadLinks` を設定しない。デッドリンクは宛先を直して解消する
- 見出しを変えたら、その見出しへのリンク(`#...`)を `grep` で探して合わせる

「ignoreDeadLinks を設定しない」はCLAUDE.mdの文脈にすぎません。強制は先ほどのaskルールが担い、CLAUDE.mdはその理由を伝える役目です。

Stop hookでビルドが落ちたら終わらせない

ビルドを編集のたびに走らせると、原稿を1つ直すたびにサイト全体のビルドが走ります。Claudeの応答が終わる時点で1回だけ走らせるのが、Stop イベントの使い方です。

Stop hookは、Claudeが応答を終えようとするときに発火します。hookが終了コード2で終わると、Claudeは止まらずに会話を続け、stderrの内容が理由として渡されます。

.claude/settings.json に、permissions と並べてhooksを足します。

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/check-docs-build.sh"
          }
        ]
      }
    ]
  }
}

スクリプトは、ビルドを実行して、失敗したときだけ末尾をstderrに出します。

#!/bin/bash
# .claude/hooks/check-docs-build.sh
input=$(cat)
 
# hook由来の続行中は通す(直せない失敗で終わらなくなるのを防ぐ)
if echo "$input" | grep -q '"stop_hook_active": *true'; then
  exit 0
fi
 
if ! out=$(npm run docs:build 2>&1); then
  echo "vitepress build が失敗しました:" >&2
  echo "$out" | grep -A2 "dead link" | head -n 30 >&2
  echo "$out" | tail -n 20 >&2
  exit 2
fi

入力JSONの stop_hook_active は、hookによる続行の最中に true になります。この値を見ないと、直らない失敗が残ったときにループが終わりません。さらにClaude Codeには、hookによる連続した続行を8回で打ち切る上限があります。

ビルドが通るときは何も出力せず、終了コード0で終わります。リンク切れが残っていれば、Claude Codeには Found dead link の行が届き、宛先を直してから応答を終えることになります。hookでテストを回す設計の比較は、テスト自動化のhooksにあります。イベントの全体像はHooks完全ガイドを参照してください。

つまずきやすい点

落とし穴

サブパス配信でbaseを忘れると、見た目は動いてもリンクが壊れる

GitHub Pagesの user.github.io/repo/ のようにサブパスで配信するときは、base を '/repo/' のように設定します。base は必ずスラッシュで始まり、スラッシュで終わります。公開後の表示まで確かめたいときは、vitepress preview でビルド結果を開きます。

落とし穴

cleanUrlsをサーバーの対応なしで有効にする

cleanUrls を true にすると、URLから .html が外れます。ただし、サーバーが /foo へのアクセスに対して、リダイレクトなしで /foo.html を返せることが条件です。ホスティング先の対応を確かめずにClaude Codeへ設定させると、ローカルのビルドは通っても、公開先でページが開かなくなります。

落とし穴

README.mdがページとして公開される

docs/ 配下の .md は、すべてページになります。ページにしたくないファイルは、サイト設定の srcExclude にglobで指定します。たとえば srcExclude: ['**/README.md', '**/TODO.md'] です。Claude Codeが作業メモをdocsに置いたときのために、CLAUDE.mdに「メモはdocsの外に置く」と書き添えます。

落とし穴

アンカーの検査を完了条件に含められない

先に実測したとおり、見出しへのリンクの壊れは vitepress build では止まりません。見出しを訳したり改名したりする改稿では、見出しの変更ごとに grep で参照元を探す手順を、CLAUDE.mdに加えます。

まとめ

VitePressのビルドは、ページ間のリンク切れで終了コード1を返します。この信号をStop hookにつなげば、リンクを壊したまま作業が終わることはなくなります。一方で、見出しの壊れは検査の外にあるので、CLAUDE.mdで手当てが要ります。

生成物と設定ファイルはpermissionsで守ります。docs/api/ のような生成物はdeny、config.* はaskにすれば、検査を外す変更も生成物の手直しも、黙っては通りません。

この記事を共有:XはてブLinkedIn