7 ヶ月前、運営している Claude Lab のインデックス済みページが 1 週間で 3,500 → 95 まで吹き飛びました。原因は単純で、Claude Code に毎日記事を書かせ続けていたら、Google の Helpful Content System に「テンプレで生成された薄い記事の量産」と判定された、というよくある話です。
廣川政樹です。2014 年から個人開発で iOS/Android アプリ事業を続けていて、累計 5,000 万 DL ほど、現在は Lab 4 サイト(Claude/Gemini/Antigravity/Rork)と Blog 2 サイト(Lacrima/Mystery)を一人で並行運営しています。Claude Code とスケジュールタスクで毎日 16 本の自動投稿を回しながら、上のインデックス崩壊を経験して、品質ゲートを Python で実装し直しました。今は同じ崩壊を二度と起こさない構造に落ち着いています。
このノートでは、月 480 本を回しても運営者が寝ている間に勝手に止まる品質ゲート設計と、GitHub Actions・GitHub PAT・ディスク管理の運用知見を、現役で動いているコードと一緒にまとめます。「個人で複数サイトを並行運営したいけれど、AI 生成の質と量のバランスで悩んでいる」方向けです。
なぜ品質ゲートを後付けで実装する羽目になったか
最初の数ヶ月は、Claude Code に「毎日 4 本記事を生成して push してください」というスケジュールタスクを 4 サイト分組んで終わり、という単純な構成でした。各タスクの中身は、トピックを Claude が自律選定して、MDX を書いて、git push するだけ。当時の自分は「Claude は十分に賢いから、品質の自動検査は要らない」と思っていました。
3 ヶ月ほど回したあと、Google Search Console を開いて凍りました。Claude Lab の登録済みページが 1 週間で 3,500 → 95 まで落ちていました。クリックは月 2,500 → 200 まで急減、表示回数は 16 万 → 4.8 万。手動ペナルティの通知は来ていなかったので、Helpful Content System のアルゴリズム判定だと判断しました。
慌てて記事を読み返すと、確かに「この記事では〜について解説します」「いかがでしたか」「〜の決定版」のようなテンプレ表現が大量に混入していました。1 本ずつ見れば悪くないのですが、3 ヶ月分積み上がるとサイト全体が AI 生成テンプレ集に見えてしまう。Google の品質システムから見ても同じ印象だったのだと思います。
このときの教訓が今のパイプラインに通底しています。Claude が書いた記事を、Claude が判定して、Claude が push する という閉ループは、人間が後から見直さない限り徐々に品質が劣化します。間にもう一つ、機械的で逃げ場のないゲートを挟まないと崩れます。
全体構造 — 4 サイト × 1 日 4 本を寝ている間に回す
現在の構成はこんな形です。
┌─────────────────────────────────────────────────┐
│ Cowork スケジュールタスク (16 タスク, JST 02-17) │
│ - claudelab-premium-thu 02:00 │
│ - gemilab-premium-thu 02:45 │
│ - antigravitylab-premium 04:00 │
│ - rorklab-premium 04:45 │
│ ... (各サイト 4 本/日 = 16 本) │
└──────────────────┬──────────────────────────────┘
│ 各タスクが個別に Claude Code を起動
▼
┌─────────────────────────────────────────────────┐
│ Claude Code (per task) │
│ 1. SKILL.md 読込 │
│ 2. SEO データ + 既存記事から重複なしトピック選定 │
│ 3. MDX (JA+EN) 生成 │
│ 4. article_gate.py に通す ← 機械ゲート │
│ 5. 違反検出時は記事を捨てて再生成 (最大 3 回) │
│ 6. ja/en 件数一致を確認 │
│ 7. git push (永続リポジトリ /tmp/repos/{site}/) │
└──────────────────┬──────────────────────────────┘
│ push をトリガー
▼
┌─────────────────────────────────────────────────┐
│ Cloudflare Pages CI │
│ - prebuild: node generate-content.mjs │
│ (MDX→JSON + 整合性チェック + 内部リンク検証) │
│ - build: next build │
│ - deploy: Cloudflare Workers │
└─────────────────────────────────────────────────┘
ポイントは 3 つです。(1) Claude Code は記事を作るだけでなく、自分が作った記事を捨てる判断もすること。(2) 品質ゲートは Python の独立スクリプト(article_gate.py)に切り出し、Claude のコンテキストとは別プロセスで判定すること。(3) 生成された記事の整合性検証は Cloudflare Pages の prebuild フックで generate-content.mjs がもう一度走ることで、push 後にもう一段ふるいにかけています。
「Claude が自分の出力を Claude に評価させる」のはバイアスがかかります。Python の re モジュールで黙々と検出するだけのスクリプトを間に置くと、Claude が良いと思って書いた記事でも違反になれば容赦なく弾かれます。この「逃げ場のなさ」が崩壊後のパイプラインを支えています。
article_gate.py の核 — 6 種類の実用性シグナル
ゲートの中核は、プレミアム記事に対する「実用性シグナル」の判定です。当初は「文字数 6,000 字未満は違反」というハード閾値で運用していたのですが、これだと「短くても密度の高い実装ガイド」が違反になり、逆に「長くてテンプレで水増しした記事」が通ってしまうという本末転倒な挙動になりました。
3 ヶ月使い込んで分かったのは、Helpful Content System が嫌うのは字数の長短ではなく、**「テンプレ化された AI 解説調」**だということでした。そこで判定軸を 6 種類のシグナルに切り替えました。プレミアム記事は以下のうち 3 つ以上を満たすこと、というルールです。
# article_gate.py の判定ロジック抜粋
practical_signals = []
# (1) 動くコードブロック (5 行以上)
code_blocks = re.findall(r"```[\w-]*\n(.*?)```", body, re.DOTALL)
if any(c.count("\n") >= 5 for c in code_blocks):
practical_signals.append("code")
# (2) 具体的なメトリクス (¥1,000 / 30% / 2.5倍 など)
money_patterns = [
r"[¥¥$]\s*[\d,]+",
r"\d+(?:\.\d+)?\s*[%%]",
r"\d+(?:\.\d+)?\s*倍",
]
if any(re.search(p, body) for p in money_patterns):
practical_signals.append("metrics")
# (3) 構造化された手順 (番号付きリスト ≥3 or H3 ≥3)
numbered_steps = len(re.findall(r"^\s*\d+\.\s+", body, re.MULTILINE))
h3_count = len(re.findall(r"^###\s+", body, re.MULTILINE))
if numbered_steps >= 3 or h3_count >= 3:
practical_signals.append("structure")
# (4) 実装上のハマりどころ
insight_patterns = [
r"(?:エラー|つまずく|ハマる|落とし穴|罠|gotcha)",
r"(?:対処|解決|回避|workaround)",
r"(?:本番(?:環境|運用|デプロイ)|プロダクション)",
]
if sum(1 for p in insight_patterns if re.search(p, body)) >= 2:
practical_signals.append("insights")
# (5) 具体的な推奨・判断
recommendation_patterns = [
r"(?:推奨(?:する|します|されます))",
r"(?:お勧め(?:する|します))",
r"(?:私(?:は|の場合)|個人的に|実体験)",
]
if sum(1 for p in recommendation_patterns if re.search(p, body)) >= 2:
practical_signals.append("recommendations")
# (6) ドメイン特有のメトリクス
domain_metrics = [r"eCPM", r"LTV", r"DAU", r"AdMob", r"RevenueCat", r"Stripe"]
if any(re.search(p, body, re.IGNORECASE) for p in domain_metrics):
practical_signals.append("domain")
if len(practical_signals) < 3:
violations.append(f"premium lacks practical value: {practical_signals}")
このルールに切り替えてから、Claude が「字数のためだけに段落を膨らませる」挙動が消えました。代わりに、コード例を 1 つ追加するか、メトリクスを 1 つ入れるか、ハマりどころを 1 つ書くか、いずれかを選んで密度を上げるようになります。書く側にとっても判定軸が明確で、テンプレ拡張で逃げる余地がありません。
テンプレ表現の機械検出 — Helpful Content の生命線
実用性シグナルと並行して、テンプレ表現を片っ端から正規表現で弾くようにしています。1 本ずつ見ると害がなさそうな表現でも、3 ヶ月分積み上がると Google から見て「AI 生成のテンプレ集」に見えるパターンです。
# 導入文テンプレ (冒頭 800 字以内で検出)
INTRO_TEMPLATES = [
"この記事では", "本記事では", "本ガイドでは", "本稿では",
"について解説します", "について詳しく見ていきましょう",
"を徹底解説", "についてまとめました",
]
# タイトルテンプレ
TITLE_TEMPLATE_REGEX = [
r"完全ガイド", r"徹底解説", r"決定版", r"完全網羅",
r"とは[??]", r"とは何か",
r"\d+選[!!]?$", r"これだけ",
]
# 締めテンプレ
CLOSING_TEMPLATE_REGEX = [
r"いかがでした(?:か|でしょうか)",
r"以上[、,].{0,40}(?:について)?解説しました",
r"ぜひ.{0,40}にチャレンジしてみてください",
]
# 常体混入 (敬体崩れの検出)
JOTAI_REGEX = [
r"(?:^|[^\w])である[。.]",
r"(?:^|[^\w])だった[。.]",
r"[ぁ-んァ-ヶ一-龥]だ[。.]",
]
特に効くのは「常体混入の機械検出」です。Claude に「敬体で書いてください」と指示しても、ポロッと常体(「である」「だった」で終わる文末)が混入することがあり、これが累積すると文体が破綻します。re.findall で全件カウントして 1 件でも出たら違反扱いにしておくと、再生成のフィードバックループでほぼ消えます。
もう一つ、人間が見落としがちなのが「AI 解説調マーカーの多用」です。「することができます」「を活用することで」「と言えるでしょう」のような無味乾燥な表現が 1 本の記事内で 3 回以上出たら違反としています。1 〜 2 回は許容ですが、3 回以上はテンプレ化のサインです。
トークン管理 — 4 サイト分の GitHub PAT を安全に引き回す
4 サイトそれぞれに GitHub PAT が必要です。スケジュールタスクの中に PAT を平文で書くと、ログに残ったりタスク定義のエクスポートで漏れたりするので、ワークスペース内のファイルに分離しています。
# Step 0: ワークスペース取得 + PAT 読み込み
WS="$(ls -d /sessions/*/mnt/Dolice\ Labs 2>/dev/null | head -1)"
GITHUB_TOKEN=$(grep -A1 "Claude Lab" "${WS}/_documents/_github_tokens/github_tokens.txt" \
| tail -1 | tr -d '[:space:]')
# Step 1: 永続リポジトリの準備
WORK="/tmp/repos/claudelab.net"
if [ -d "$WORK/.git" ]; then
cd "$WORK" && git pull --rebase origin main
else
git clone --depth 1 \
"https://${GITHUB_TOKEN}@github.com/masakihirokawa/claudelab.net.git" "$WORK"
fi
ポイントは 2 つです。(1) PAT をスケジュールタスクのプロンプトに書かないこと(テキストファイルに分離してプロンプトからは grep で読む)。(2) /tmp/repos/{site}/ を永続リポジトリとして再利用して、毎回 clone し直さないこと。Cowork のセッションは /tmp を再起動間で再利用するので、git pull --rebase で差分だけ更新したほうがディスクと時間を節約できます。
PAT は 90 日でローテーションする運用にしていて、ローテーションした日は _documents/_github_tokens/github_tokens.txt を更新してその場で git commit して終わりです。スケジュールタスクのプロンプト本体を編集する必要がなく、運用負荷が極端に小さくなります。
ディスク不足時の自動退避
/tmp は Cowork セッション内で他のタスクと共有なので、6 サイト分のリポジトリと npm キャッシュを抱え込むと容量が足りなくなります。Claude Lab で実装した自動退避ロジックを 4 サイトで使い回しています。
# /tmp の残量チェック (MB)
FREE_MB=$(df /tmp --output=avail -m 2>/dev/null | tail -1 | tr -d ' ')
# 500MB を切ったら他サイトのリポジトリを削除
if [ "${FREE_MB:-0}" -lt 500 ]; then
echo "⚠️ Low disk: $FREE_MB MB. Cleaning up other sites..."
for OTHER in /tmp/repos/*; do
if [ "$OTHER" != "$WORK" ]; then
rm -rf "$OTHER" 2>/dev/null
echo " removed: $OTHER"
fi
done
fi
# それでも足りなければ npm キャッシュ削除
FREE_MB=$(df /tmp --output=avail -m 2>/dev/null | tail -1 | tr -d ' ')
if [ "${FREE_MB:-0}" -lt 500 ]; then
npm cache clean --force 2>/dev/null
fi
「他サイトのリポジトリを削除する」のは大胆ですが、git pull --rebase でいつでも復元できるので問題ありません。npm キャッシュも同様です。本当に削っていけないのは自分の作業中リポジトリ($WORK)だけ、と明確に切り分けておくと自律実行で破綻しません。
件数一致チェック — 日英 404 を防ぐ最後の砦
Lab 4 サイトは next-intl v4 で日英 2 言語対応していて、英語版が欠けると言語切替時に 404 が出ます。Cloudflare Pages のビルド時整合性チェックで一応キャッチされますが、push 前に必ず Claude Code 側でも数えています。
# Step 6: 件数一致確認
cd "$WORK"
MDX_JA=$(find content/articles/ja -name "*.mdx" | wc -l)
MDX_EN=$(find content/articles/en -name "*.mdx" | wc -l)
echo "JA=$MDX_JA EN=$MDX_EN"
if [ "$MDX_JA" -ne "$MDX_EN" ]; then
echo "❌ Count mismatch — aborting push"
exit 1
fi
ここで止めると、push されないので Cloudflare のビルドも回らず、サイトに半端な状態が残りません。Claude Code には「件数が一致しなかったら欠けている側を作り直して再 push」と指示してありますが、それでも揃わない場合はその記事ごと諦めます。「無理に push しない」を Claude に許可するのが大事で、何が何でも 1 日 4 本というプレッシャーをかけるとテンプレで埋めようとして崩壊が再発します。
内部リンクの存在検証 — 404 が積み上がるのを防ぐ
MDX 本文中の内部リンク テキスト が、リンク先記事が存在しないとビルドで 404 が量産されます。Claude が記事を書くときに「過去にあったはずの記事」へリンクを張ってしまうことが時々あって、generate-content.mjs の prebuild で機械的に検証しています。
// generate-content.mjs (Cloudflare Pages CI で実行)
const internalLinks = body.matchAll(/\[([^\]]+)\]\(\/articles\/([^/]+)\/([^)#]+)\)/g);
for (const [, text, category, slug] of internalLinks) {
const exists = articleIndex.has(`${category}/${slug}`);
if (!exists) {
console.warn(`⚠️ broken link in ${file}: /articles/${category}/${slug}`);
body = body.replace(/* リンクをプレーンテキストに置換 */);
}
}
存在しないリンクは静かにプレーンテキストに変換するだけで、ビルド自体は止めません。「ビルドを止める」と寝ている間にサイト全体がデプロイされなくなるので、警告 + 修復が個人運営では現実的です。
エラー時の自律リカバリ
Claude Code に「エラーが出たらユーザーに確認を取らずに自律リカバリしてください」と指示しています。具体的には以下のパターンを SKILL.md に書いてあります。
- git push が rejected された場合 → git pull --rebase 後に再 push
- リポジトリが破損していた場合 → /tmp/repos/{site} を削除して再 clone
- article_gate.py が違反を検出した場合 → 記事を捨てて違うトピックで再生成 (最大 3 回)
- npm install が失敗した場合 → そもそも node_modules は不要なので無視 (Cloudflare CI が prebuild で実行)
- 件数一致しない場合 → 欠けている側を生成、それでも揃わなければその回はスキップ
「ユーザーに確認しない」を明示するのは重要で、これがないと Claude Code は親切心で AskUserQuestion を呼び出しがちで、夜間自動実行が止まります。自律実行ポリシーをタスクごとに明文化しておくのと、request_cowork_directory のように許可ダイアログを出す系のツールを呼ばないよう明示禁止リストを置くのが、無人運用のキモです。
月 480 本回しても破綻しない理由 — Push 後のもう一段ゲート
Claude Code 側のゲートを通過した記事は、Cloudflare Pages の prebuild フックで generate-content.mjs がさらに整合性をチェックします。ここで弾かれる主なものは以下です。
premium と level フロントマターの日英不一致 → 日本語版に揃えて自動修正
- MDX 内のコード例で API キーの実フォーマット(
AIzaSy, ghp_, sk_live_ 等)が混入 → ビルド失敗
- Markdown テーブルが remarkGfm 未対応形式 → 検出して警告
- 記事内の OGP 画像参照が欠けている → デフォルト画像にフォールバック
- 内部リンクの存在検証
ここで弾かれた場合は Cloudflare のビルドが失敗してデプロイされません。失敗通知は GitHub Actions のメール通知から飛んでくるので、朝起きて「昨夜の自動投稿が失敗していた」と分かる。Claude Code 側のゲート → Cloudflare 側のゲートと二段構えにすることで、寝ている間に変な記事がインデックスされる事故が消えました。
ロールバック手順 — それでも何かが起きたとき用
「ゲートを 2 段重ねても問題が起きるとき」は、過去に何度かありました。一番怖いのは「ゲートを通過した記事が、結果として既存記事のサイト全体評価を下げる」パターン(典型例: テンプレ化された比較記事を新規追加した結果、その隣のテンプレ化された比較記事も含めて Google に低品質クラスタ判定される)です。これは事前判定が原理的に難しい。
なので、ロールバック手順を Git ベースで標準化してあります。
# 直近 24 時間で追加された記事を一覧
cd /tmp/repos/claudelab.net
git log --since="24 hours ago" --name-only --pretty=format: \
| grep "content/articles/" | sort -u
# 問題ありそうな記事を `_documents/_quality_audit/` に退避してから削除
mkdir -p "_documents/_quality_audit/quarantine/$(date +%Y%m%d)"
for f in $(git log --since="24 hours ago" --name-only --pretty=format: \
| grep "content/articles/" | sort -u); do
cp "$f" "_documents/_quality_audit/quarantine/$(date +%Y%m%d)/"
git rm "$f"
done
git commit -m "Quarantine: 直近24時間生成分の退避"
git push origin main
git rm で消して push するだけだと書いた記事が永久に消えるので、必ず quarantine ディレクトリにコピーしてから消すようにしています。後で「実は良い記事だった」と分かったときに復活できる作りです。
Claude Lab の Helpful Content 崩壊を経験した直後の整理では、この手順で 1 週間で 74 本を退避しました。Gemini Lab では同じスキャンで 183 本を退避。両サイトとも整理後 2 週間で順位指標が +0.5 〜 +0.9 改善し、退避した記事の多くは元々低品質だったことが数字で裏付けられました。
7 ヶ月運用して見えてきたコストと効果
定量的にまとめると以下です。
- 自動投稿本数: 月 480 本(4 サイト × 4 本 × 30 日)
- ヒューマンタイム: 約 30 分/週(PAT ローテーション + ログレビュー + ロールバック判断)
- インフラコスト: Cloudflare Pages 無料枠 + Claude API(Sonnet 4.6 で月 $15 程度)
- 復旧コスト(Helpful Content 崩壊の整理時): 約 8 時間(手動レビュー + 退避スクリプト実行)
崩壊前は「Claude が書いてるからスケールする」と思っていましたが、品質ゲートと運用コストの両方を入れるとスケールは線形ではなく、サイト数 × 品質判定の難しさ で増えていきます。4 サイトくらいまでは個人で回せる上限という感覚です。これ以上増やすなら品質判定の自動化をもう一段強化(記事生成後にもう一回別モデルで品質スコア付け、など)が必要になりそうです。
次に手を入れる予定
現在の品質ゲートでも残課題があって、3 つほど取り組みたいことがあります。
第一に、意味的な重複検出 です。今のゲートは表面的なテンプレ表現は弾けますが、「内容が既存記事と 70% 被っている」のような意味的な重複は弾けません。Embeddings API で記事ベクトル化して類似度判定する仕組みを検討中です。
第二に、GSC データを使った事後評価 です。1 ヶ月後に GSC でクリック 0 のままだった記事を機械的に検出して退避キューに入れる、というループを組めば、生成 → 評価 → 退避の自浄サイクルが自動化できます。
第三に、プレミアム記事の加筆昇格パイプライン です。Free 記事のうち滞在時間が長くエンゲージメントが高いものを Claude に読ませて、実体験エピソードと具体メトリクスを追加してプレミアム化する流れを 2026 年 6 月から実験予定です。
同じように複数サイトを個人運営している方の参考になれば嬉しいです。私自身もまだ運用しながら学んでいる途中なので、見落としや改善案があれば dolice.design 経由で気軽に教えてもらえると助かります。