毎日1本の公開を「台帳+ゲート」で担保する:claude_web の daily-publication 実装
「毎日1本公開する」と決めたサイトで、いちばん起きやすい失敗は公開し忘れではなく、公開したつもりです。ファイルは作った、ビルドも通った、デプロイもした。けれど一覧ページにカードが入っていない、sitemap に URL が無い、canonical が別ページを指している。この状態は見た目には成功していて、あとから数えたときにだけ穴として現れます。
フリートの1サイトである Claude Code ガイド(claude_web)は、この問題を「公開の事実を台帳に書き、デプロイ前にその台帳と実ファイルを突き合わせる」形で潰しています。実装は tools/daily-publication.js の1ファイルで、Hexo のようなビルド工程を持たない静的 HTML 直置きのサイトでも使える構成です。以下、実際のコードに基づいて中身を見ます。
台帳に何を書くか
記録の単位は「その日に公開した1記事」です。台帳 data/daily-publications.json の1エントリは次の形をしています。
1 | |
注目すべきは file / url / title を人が書かないことです。これらは記録コマンドが実ファイルから読み取って埋めます。人が書くのは日付・slug・題材・出典と、その出典をいつ確認したかだけ。つまり台帳は申告ではなく検査結果になっています。
localValidation と liveValidation が分かれているのも意図的で、前者は「ローカルの HTML が条件を満たしている」、後者は「本番の URL が実際に応答した」を別々に持ちます。記録直後の liveValidation は pending から始まります。
記録コマンドが実際に見ている5つのこと
記録は1コマンドです。
1 | |
この record が通るためには、validateLocalPublication() の検査を全部抜ける必要があります。実装を読むと、見ているのは次の5点です。
- canonical の一致 — 記事の canonical が
${origin}/tutorials/${slug}.htmlと完全一致すること。ずれていればCanonical mismatch for <slug>で停止 - 構造化データの URL と日付 —
TechArticleのurlが同じ期待値であること、datePublishedが申告したjstDateと一致すること - 出典へのリンクが本文にあること —
sourceUrlsの各 URL が、記事の HTML から実際にリンクされていること。1つでも欠ければArticle must link to primary source <url>で停止 - 一覧からの到達性 —
public/tutorials/index.htmlにhref="/tutorials/<slug>.html"が含まれること。含まれなければTutorial index does not link to <slug> - sitemap の登録と lastmod —
sitemap.xmlに当該 URL の<url>ブロックがあり、その中の<lastmod>がjstDateと一致すること
3 は地味ですが効きます。「出典を調べた」と台帳に書きながら、記事本文からはその出典にリンクしていない、という状態を機械的に禁止しているからです。筆者は実際にこのゲートに落ちました。台帳の sourceUrls に2本書いたのに、本文からリンクしていたのは1本だけで、Article must link to primary source https://… が返って記録できませんでした。直し方は台帳から URL を消すことではなく、本文にリンクを足すことです。ゲートが「調べたなら読者にも辿らせろ」という方向へ倒してくれます。
重複を「slug 以外」で止める
同じ題材を別の slug で2回公開する事故は、slug の一意性検査では止まりません。ここでは3段構えになっています。
1 | |
slug と topicKey の完全一致で弾いたあと、最後に題名の類似度を見ます。
1 | |
しきい値は 0.78 で、既存の全チュートリアル題名が比較対象です。超えると Near-duplicate topic: <slug> is 83% similar to <既存slug> のように、どの記事に何パーセント似ているかを出して止まります。
ここで重要なのは、完全一致(slug / topicKey)と類似(0.78)を両方持っていることです。完全一致だけだと表記ゆれで素通りします。実際、同じフリートの別サイトでは oomugi と omugi というローマ字のゆれだけで同一題材が2本公開される事故が起きており、キーの完全一致検査は原理的にこれを拾えません。類似度の層はその保険です。
日付は申告ではなく現在時刻で検査する
もう1つ、地味に効く検査があります。
1 | |
台帳の日付を過去に遡って書けないようにしています。「昨日ぶんを今日まとめて記録する」ができないので、記録漏れは記録漏れとして残ります。深夜をまたいで作業していると、23時台に書き始めた記事が0時台の日付で記録されることになりますが、これは正しい挙動です。公開したのは0時台だからです。
実際に試す
手元で動かすなら、記録せずに現状を数えるだけのコマンドから入るのが安全です。
1 | |
coverage は台帳と実ファイルの対応を数えるだけで、何も書き換えません。PR 単位で見たい場合は npm run daily:verify-pr、特定日を見たい場合は npm run daily:verify -- --date 2026-09-22 を使います。
デプロイ側は、この台帳に当日の記録があることを前提条件にしています。記録が無ければ、記事ファイルが存在してもデプロイ手前で止まります。
1 | |
逆に記録が通っていれば、同じ場所が次のように変わります。
1 | |
自分のサイトに持ち込むなら
この仕組みの移植で本質的なのは、ファイル構成ではなく順序です。
- 記事を書く → 台帳に記録しようとする → 落ちたら記事を直す → 通ったらデプロイ
- 「デプロイしてから台帳に書く」にしない(台帳が事後報告になり、検査の意味が消える)
検査項目は自分のサイトのテンプレートに合わせて入れ替えてかまいませんが、canonical・一覧からの到達性・sitemap の3点は外さないほうがよいです。この3つはどれも「見た目は公開できているのに検索エンジンから見ると欠けている」種類の穴で、人の目視でいちばん見落としやすい場所だからです。
実際に生成されている記事の側は Claude Code ガイドのチュートリアル一覧で見られます。1日1本という制約が、ゲートによってどう担保されているかを意識して眺めると、台帳の設計意図が分かりやすいはずです。
このバージョンはその範囲に入るのか
依存の話でいちばん間違えやすいのは ^1.2.3 や ~1.2.3 がどこまでを許すかです(^0.2.3 のようにメジャーが 0 のときは規則が変わります)。範囲とバージョンを貼ると、下限・上限に展開したうえで一致・不一致とその理由を表示するsemver 範囲判定ツールを置いています。ブラウザの中だけで動き、入力はどこにも送信しません。