0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Zennの本が更新されたかを確実に確認する3つのAPI

0
Last updated at Posted at 2026-08-07

Zennには記事(Article)とは別に、複数の章をまとめて出す「本(Book)」がある。自分はその本をGitHub連携で運用していて、原稿をGitHubへpushするとZenn側へ自動で反映される。

章を更新してpushしたら、反映されたかを確認する。

公開ページをcurlで叩いて、追加したはずの文字列をgrepする。0件。届いていない。

そう判断して、デプロイの記録を洗い始めた。判断が間違っていた。 実際はpushの6秒後に反映されていた。

この記事は、その誤判定の中身と、正しい確認手順を残すためのもの。Zennで本(Book)をGitHub連携で運用している人向け。記事(Article)とは挙動が違うので、そこも分けて書く。

未認証のHTMLに、本の章の本文は1文字も入っていない

まず、やってしまった確認方法。

curl -s https://zenn.dev/<user>/books/<book>/viewer/<chapter> | grep -c "追加した文字列"
# → 0

返ってきたHTMLは38,641バイトあった。空ではない。だから「ページは取れている。文字列が無い=届いていない」と読んだ。

実際にこのHTMLに何が入っているかを数えたら、こうなった。

探した語 出現 実体
本のタイトルに含まれる語 20 ヘッダ・パンくず・OGP・サイドバー
章の本文にしかない語(3種類) 0 入っていない

つまり本のタイトル、目次、メタ情報は入る。章の本文は入らない。 Zennの本のビューアは本文をクライアント側で取りに行く作りなので、素のHTMLを取っても本文は付いてこない。

届いていても0が出る。届いていなくても0が出る。この確認方法では、そもそも区別がつかない。

対照を置いたのに、その対照が無効だった

ここが自分でも一番こたえた部分だ。

「取得自体が失敗していないか」を確かめるため、対照になる語を1つ入れていた。ページに必ずあるはずの語をgrepして、それが取れていれば取得は成功している、という判定。

選んだのは「稟議」。本の内容に関わる語で、そのページに必ずある。取れた。だから「取得は成功、文字列は本当に無い」と結論した。

後で確認したら、「稟議」が20回出てきた場所は全部本のタイトルの中だった。

Claude Code チーム/企業導入 安全パック——稟議・ポリシー・権限設計・月次レビューの型

タイトルはHTMLに入る。本文は入らない。タイトルにも含まれる語を対照に選んだ時点で、その対照は「本文が取れているか」を何も検証していなかった。

対照を置くこと自体は正しい。ただ、対照は「検証したい経路を通っているもの」から選ばないと意味がない。本文の到達を測りたいなら、対照も本文にしか出ない語でなければならない。

ちなみに grep -c を使っていたのも効いていた。-c はマッチした行数を返す。ZennのHTMLは実質1行なので、20回出現していても返り値は1になる。「1件ある」と読んで、出現回数を確認しなかった。

正しい確認手順

認証済みのブラウザから、3つのAPIを順に見る。私はCDP(Chrome DevTools Protocol、ブラウザをプログラムから操作する仕組み)経由で叩いているが、ログイン済みのブラウザのコンソールで実行しても同じ。

1. デプロイが成功したか

const r = await fetch('/api/me/github_deployments', {credentials:'include'});
const d = (await r.json()).github_deployments[0];
console.log(d.deploy_status, d.failure_reason, d.notice_list);

deploy_statusSUCCESS かどうか。failure_reasonnotice_list に落ちた理由が日本語で入る。

私のケースではここが SUCCESS だった。この時点で「デプロイされていない」という当初の見立ては崩れている。

2. 何がどこへ着地したか

同じレスポンスの deployed_items に、ファイルと反映先の対応が入っている。

d.deployed_items.map(i => i.repo_path + ' -> ' + i.zenn_path)
// → "books/cc-team-safety-pack/intro.md -> /link/books/cc-team-safety-pack?chapter_slug=intro"

反映先のURLを自分で組み立てて確認しに行くと、推測を間違えたときに「届いていない」と誤読する。ここに書いてあるものを使う。

3. 本文が実際に置き換わったか

章のIDを取ってから、章のAPIを見る。

const book = await (await fetch('/api/books/<book-slug>', {credentials:'include'})).json();
const ch = book.chapters.find(c => c.slug === 'intro');

const chapter = (await (await fetch('/api/chapters/' + ch.id, {credentials:'include'})).json()).chapter;
console.log(chapter.body_updated_at);
console.log(chapter.body_html.includes('追加した文字列'));

body_html に文字列が入っているかで判定する。あわせて body_updated_at を見れば、いつ書き換わったかが分かる。私のケースはこうだった。

body_updated_at: 2026-08-07T22:19:59

pushの6秒後。最初から届いていた。

なお body_markdown というフィールドも存在するが、私の環境では空文字が返った。判定には body_html を使うほうが確実だった。

ついでの注意点

notice_list は遅れて埋まる。デプロイ直後に読むと空で、1分後に読み直すと2件入っていたことがある。空を「問題なし」の根拠にしない。

failure_reasonnull でも notice_list に理由が入ることがある。片方だけ見ない。実際にこれで、章の本文が5万字の上限を超えて保存に失敗していたのを1か月半見逃したことがある。pushは成功し、GitHubも200を返すので、外からは成功と区別がつかない。

記事(articles)は素のcurlで確認できる。本の章と違って本文がHTMLに入るので、公開ページをcurlしてgrepする方法がそのまま使える。同じ感覚で本にも適用したのが今回の失敗だった。

まとめ

  • Zennの本の章は、未認証のHTMLに本文が入らない。curl + grepでは届いていても0が出る
  • 反映の確認は /api/me/github_deploymentsdeploy_statusdeployed_items/api/chapters/<id>body_html で行う
  • 判定に対照を置くときは、検証したい経路を通っている対照を選ぶ。タイトルにも本文にもある語では、本文の到達を検証できない
  • grep -c は行数を返す。1行のHTMLに対して使うと、20回出現していても1になる

自分は「対照を置け」という原則を守ったつもりでいた。守ったのは形だけで、その対照が何を検証しているかを考えていなかった。0という数字を見て原因の調査を始めるより先に、その0が本当に測りたいものを測った結果なのかを疑うべきだった。


Claude Code まわりの「成功に見えるのに、実際には届いていない」は、毎月あたらしい形で出てくる。その月に実際に起きた事故と仕様の変更を、実機で確かめてから月に1本にまとめている(Claude Code 安全運用便・¥500/月・最初の1か月は無料)。無料で足りる人は、cc-safe-setup の hook をそのまま使ってほしい。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?