毎日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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"jstDate": "2026-09-22",
"slug": "file-checkpointing-in-the-agent-sdk",
"topic": "Claude Agent SDK のファイルチェックポイントで…",
"topicKey": "file-checkpointing-in-the-agent-sdk",
"sourceUrls": [
"https://code.claude.com/docs/en/agent-sdk/file-checkpointing"
],
"sourceCheckedAt": "2026-09-22T00:06:00+09:00",
"publishedAtJst": "2026-09-22T00:06:00+09:00",
"file": "public/tutorials/file-checkpointing-in-the-agent-sdk.html",
"url": "https://claude-guide.autoarticles.net/tutorials/…",
"title": "…",
"localValidation": { "status": "verified", "checkedAtJst": "…" },
"liveValidation": { "status": "pending", "httpStatus": null }
}

注目すべきは file / url / title人が書かないことです。これらは記録コマンドが実ファイルから読み取って埋めます。人が書くのは日付・slug・題材・出典と、その出典をいつ確認したかだけ。つまり台帳は申告ではなく検査結果になっています。

localValidationliveValidation が分かれているのも意図的で、前者は「ローカルの HTML が条件を満たしている」、後者は「本番の URL が実際に応答した」を別々に持ちます。記録直後の liveValidationpending から始まります。

記録コマンドが実際に見ている5つのこと

記録は1コマンドです。

1
npm run daily:record -- --entry-file /path/to/entry.json

この record が通るためには、validateLocalPublication() の検査を全部抜ける必要があります。実装を読むと、見ているのは次の5点です。

  1. canonical の一致 — 記事の canonical が ${origin}/tutorials/${slug}.html と完全一致すること。ずれていれば Canonical mismatch for <slug> で停止
  2. 構造化データの URL と日付TechArticleurl が同じ期待値であること、datePublished が申告した jstDate と一致すること
  3. 出典へのリンクが本文にあることsourceUrls の各 URL が、記事の HTML から実際にリンクされていること。1つでも欠ければ Article must link to primary source <url> で停止
  4. 一覧からの到達性public/tutorials/index.htmlhref="/tutorials/<slug>.html" が含まれること。含まれなければ Tutorial index does not link to <slug>
  5. sitemap の登録と lastmodsitemap.xml に当該 URL の <url> ブロックがあり、その中の <lastmod>jstDate と一致すること

3 は地味ですが効きます。「出典を調べた」と台帳に書きながら、記事本文からはその出典にリンクしていない、という状態を機械的に禁止しているからです。筆者は実際にこのゲートに落ちました。台帳の sourceUrls に2本書いたのに、本文からリンクしていたのは1本だけで、Article must link to primary source https://… が返って記録できませんでした。直し方は台帳から URL を消すことではなく、本文にリンクを足すことです。ゲートが「調べたなら読者にも辿らせろ」という方向へ倒してくれます。

重複を「slug 以外」で止める

同じ題材を別の slug で2回公開する事故は、slug の一意性検査では止まりません。ここでは3段構えになっています。

1
2
if (ledger.entries.some((entry) => entry.slug === candidate.slug)) fail(`Slug is already recorded: ${candidate.slug}`);
if (ledger.entries.some((entry) => entry.topicKey === candidate.topicKey)) fail(`Topic is already recorded: ${candidate.topicKey}`);

slug と topicKey の完全一致で弾いたあと、最後に題名の類似度を見ます。

1
2
3
4
5
6
7
8
function findNearDuplicate(topic, title, candidates, threshold = 0.78) {
let closest = null;
for (const candidate of candidates) {
const score = Math.max(topicSimilarity(topic, candidate.title), topicSimilarity(title, candidate.title));
if (score >= threshold && (!closest || score > closest.score)) closest = { ...candidate, score };
}
return closest;
}

しきい値は 0.78 で、既存の全チュートリアル題名が比較対象です。超えると Near-duplicate topic: <slug> is 83% similar to <既存slug> のように、どの記事に何パーセント似ているかを出して止まります。

ここで重要なのは、完全一致(slug / topicKey)と類似(0.78)を両方持っていることです。完全一致だけだと表記ゆれで素通りします。実際、同じフリートの別サイトでは oomugiomugi というローマ字のゆれだけで同一題材が2本公開される事故が起きており、キーの完全一致検査は原理的にこれを拾えません。類似度の層はその保険です。

日付は申告ではなく現在時刻で検査する

もう1つ、地味に効く検査があります。

1
2
const today = jstDate(now);
if (candidate.jstDate !== today) fail(`Candidate date ${candidate.jstDate} is not current JST date ${today}`);

台帳の日付を過去に遡って書けないようにしています。「昨日ぶんを今日まとめて記録する」ができないので、記録漏れは記録漏れとして残ります。深夜をまたいで作業していると、23時台に書き始めた記事が0時台の日付で記録されることになりますが、これは正しい挙動です。公開したのは0時台だからです。

実際に試す

手元で動かすなら、記録せずに現状を数えるだけのコマンドから入るのが安全です。

1
2
cd /path/to/claude_web
npm run daily:coverage

coverage は台帳と実ファイルの対応を数えるだけで、何も書き換えません。PR 単位で見たい場合は npm run daily:verify-pr、特定日を見たい場合は npm run daily:verify -- --date 2026-09-22 を使います。

デプロイ側は、この台帳に当日の記録があることを前提条件にしています。記録が無ければ、記事ファイルが存在してもデプロイ手前で止まります。

1
2
daily-ledger:MISSING — 2026-09-22 の記録が無い(台帳の最終記録: 2026-09-20
preflight:fail

逆に記録が通っていれば、同じ場所が次のように変わります。

1
2
3
4
emoji-lint:ok
daily-ledger:ok (2026-09-22 の記録あり)
preflight:ok
deploy:ok

自分のサイトに持ち込むなら

この仕組みの移植で本質的なのは、ファイル構成ではなく順序です。

  • 記事を書く → 台帳に記録しようとする → 落ちたら記事を直す → 通ったらデプロイ
  • 「デプロイしてから台帳に書く」にしない(台帳が事後報告になり、検査の意味が消える)

検査項目は自分のサイトのテンプレートに合わせて入れ替えてかまいませんが、canonical・一覧からの到達性・sitemap の3点は外さないほうがよいです。この3つはどれも「見た目は公開できているのに検索エンジンから見ると欠けている」種類の穴で、人の目視でいちばん見落としやすい場所だからです。

実際に生成されている記事の側は Claude Code ガイドのチュートリアル一覧で見られます。1日1本という制約が、ゲートによってどう担保されているかを意識して眺めると、台帳の設計意図が分かりやすいはずです。


毎日1本の公開を「台帳+ゲート」で担保する:claude_web の daily-publication 実装
https://blog.hashito.biz/2026/09/22/claude-web-daily-publication-ledger-gate/
著者
hashito
作成日
2026年9月22日
著作権

このバージョンはその範囲に入るのか

依存の話でいちばん間違えやすいのは ^1.2.3~1.2.3 がどこまでを許すかです(^0.2.3 のようにメジャーが 0 のときは規則が変わります)。範囲とバージョンを貼ると、下限・上限に展開したうえで一致・不一致とその理由を表示するsemver 範囲判定ツールを置いています。ブラウザの中だけで動き、入力はどこにも送信しません。