小規模サイトのコンテンツ更新どうしてますか?
私は趣味で友達のサイトを作ることがあります。ランディングページとか、お店の紹介サイトとか。受託の会社にいたころも似たような小規模案件はよくありました。
コンテンツの更新頻度は低いけど、こういう依頼がたまに来ます。
- 「料金が変わったので直してほしい」
- 「写真を新しいのに差し替えたい」
管理画面を作るほどでもない。でも依頼のたびに自分が手を動かすのも地味にコストがかかる。
WordPressやmicroCMSという選択肢もありますが——
WordPress:セキュリティ管理、プラグインのアップデート対応が継続的に発生し、作った後の面倒を自分が引き受け続けることになりがちです。頑張って管理画面をつくっても使い方がわからないと言われることも多々あるかと思います。
microCMS:スキーマ設計、APIキーの管理、クライアントへの使い方説明が必要で、初期設定のコストが「たまに変えたいだけ」というニーズに見合わないです。
そこで、GitHubのイシューに書いてもらえばClaudeが勝手に更新してPRを立ててくれる仕組みを作りました。アイデア段階で検証として実装したものですが、せっかくなので公開します。
全体の流れ
1. クライアントがGitHub Issuesのテンプレートに変更内容を記入
2. @claude メンションでコメント
3. Claude Code GitHub Actions が起動
4. Claudeがコンテンツ管理しているYAMLファイルを編集
この時点でPRはできていませんがイメージとして 7.の画像を参照してください。
5. ClaudeがPlaywrightでスクリーンショットを撮影してコミット
この時点でPRはできていませんがイメージとして 7.の画像を参照してください。
6. イシューの案内にしたがって開発者またはクライアントがセルフでPRを作成
7. GitHub PR Files changed でスクリーンショットを確認、見た目に問題なければマージ
8. 本番反映
今回は実装しませんでしたが、お好きなデプロイ方法で。
ClaudeはYAML編集とスクリーンショット撮影までを担当し、PRの作成・確認・マージは人間が行います。
主要技術
- Astro — 静的サイトジェネレーター & Content Collectionsでコンテンツの型をZodで定義する
- Docker — ローカル&CI環境でAstroとPlaywrightをコンテナで動かす
- Playwright — MCPではなくスクリプトとしてスクリーンショットを撮影する
-
Claude Code GitHub Actions — GitHubイシューへの
@claudeメンションを受けてClaude Codeを起動するGitHub Actions -
Claude Code — 開発時にClaude Codeを使う場合の設定やルールを
.claude/やCLAUDE.mdに記述する
画像管理は画像サーバーを用意してURLが発行できていることとします。
(もしくは、ウェブサイトのリポジトリの画像フォルダに保存しておいても対応はできます)
ディレクトリ構成
.
├── astro/ # Astroプロジェクト(Dockerでまるごとマウント)
│ └── src/
│ ├── content/
│ │ ├── config.ts # スキーマ定義
│ │ ├── top/
│ │ │ └── data.yaml # トップページのコンテンツ
│ │ └── gallery/
│ │ └── data.yaml # ギャラリーページのコンテンツ
⋮ ⋮
│ ├── pages/
│ │ ├── index.astro
│ │ └── gallery.astro
│ └── styles/
|
├── docker/
│ ├── astro/
│ │ └── Dockerfile
│ └── playwright/
│ └── Dockerfile
├── tools/
│ └── playwright/
│ ├── screenshot.js # スクリーンショット撮影スクリプト
│ └── screenshots/ # 撮影した画像の出力先
│
├── compose.yml
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ └── content-update.yml # 依頼フォームのテンプレート
│ └── workflows/
│ └── claude.yml # Claudeを起動するワークフロー
├── CLAUDE.md # ClaudeへのコンテキストとルールのMD
└── .claude/
├── settings.json # ツール実行の権限設定
└── skills/
├── add-or-remove-page/ # ページ追加・削除のスキル
├── check-content/ # コンテンツ検証・スクリーンショットのスキル
└── edit-claude-workflow/ # ワークフロー編集のスキル
ポイント① :コンテンツを管理するYAMLを作成する
コンテンツとコードを分けます。コンテンツの構造と画像URLをYAMLに集約しておくことで、Claudeが触る対象を明確にできます。このYAMLは作成時にも自分は定義していません。すべてAIに任せました。
# src/content/top/data.yaml
hero:
catchcopy: キャッチコピーがここに入ります
image: https://example.com/images/hero.jpg
plans:
- name: プランA
price: "¥10,000 / 月"
description: プランの説明
ポイント②:スキーマで構造を守る
Astro Content CollectionsでYAMLのスキーマを定義しておきます。
// src/content/config.ts
const top = defineCollection({
type: 'data',
schema: z.object({
hero: z.object({
catchcopy: z.string(),
image: z.string(),
}),
plans: z.array(z.object({
name: z.string(),
price: z.string(),
description: z.string(),
})),
}),
});
これがあることで、ClaudeがYAMLを編集するときに構造を壊しにくくなります。型が仕様書の役割を兼ねています。
ポイント③:イシューテンプレートを「依頼フォーム」にする
name: コンテンツ更新
description: サイトのテキスト・画像コンテンツを更新する
title: "[コンテンツ更新] "
labels: ["content"]
body:
- type: checkboxes
id: page
attributes:
label: 対象ページ
options:
- label: トップページ (/)
- label: ギャラリーページ (/gallery)
- type: textarea
id: text_changes
attributes:
label: テキスト変更
description: どのページのどの箇所を、どんな内容に変えたいか書いてください。
placeholder: |
例)
トップページのキャッチコピーを「○○○」に変更
ギャラリーページの Interior の説明文を「○○○」に変更
プランAの料金を「¥30,000 / 月」に変更
- type: textarea
id: image_changes
attributes:
label: 画像変更
description: どのページのどの画像を変えたいか、新しい画像の URL とあわせて書いてください。
placeholder: |
例)
トップページのメイン画像を https://example.com/images/hero.jpg に変更
ギャラリーページの Tables の画像を https://example.com/images/table.jpg に変更
- type: markdown
attributes:
value: |
---
準備ができたら、このissueにコメントで `@claude 上記の内容で更新してください` と投稿してください。
GitHubのIssue Forms機能を使って、.github/ISSUE_TEMPLATE/ にテンプレートを置いておくと、クライアントはフォーム感覚で変更内容を書けます。フォームの最後に「準備ができたら @claude 上記の内容で更新してください とコメントしてください」と添えておくと、操作がわかりやすいです。
開発時は「ページ追加・削除のスキル」でメンテするようにしています。
ポイント④:Claudeの権限を絞る
編集できるファイルを制限する
- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
claude_args: "--max-turns 20"
settings: |
{
"permissions": {
"allow": ["Edit(astro/src/content/**)", "Read", "Glob", "Grep", "Bash(git status)", "Bash(docker compose run --rm playwright node screenshot.js:*)"],
"deny": ["WebFetch", "WebSearch"]
}
}
allow に列挙したツールだけが使えるホワイトリストになります。Edit(astro/src/content/**) でコンテンツファイル以外の編集を防ぎ、Bashも許可したコマンドだけに絞っています。
セキュリティの観点でも意味があります。そもそも小規模サイトの運用としてプライベートリポジトリにクライアントだけを招待する形を想定しているため、第三者に勝手に @claude を起動される心配はありませんが、それに加えてこの権限設定が多重の防御(Claude自身の暴走や意図しない操作の防止)になっています。
-
任意のBashコマンドの実行を防ぐ — 許可したコマンド以外は実行できないので、
curlやrmなど危険なコマンドを叩かれる心配がない -
外部への通信を防ぐ —
WebFetchとWebSearchを deny しているので、外部URLへのリクエストによる情報漏洩を防げる -
ファイルの新規作成・削除を防ぐ —
Editのみ許可しているので、任意の場所にファイルを作ったり削除したりできない
deny について(未検証)
公式ドキュメントのサンプルには allow と deny を併記した例が載っています。ただし両者の関係は明記されていません。ドキュメントには「ベースのGitHubツール(ファイル操作・コメント管理など)は常に有効」とあるため、allow は追加ツールを許可するもので完全なホワイトリストではない可能性があります。だとすると deny はベースツールから特定のものを除外するために意味を持つかもしれません。この挙動は未検証です。
ポイント⑤:DockerでAstroを動かす
GitHub Actions上でAstroの開発サーバーをDockerで起動しています。後工程のPlaywrightでスクリーンショット撮影のためにも一時サーバーとして立ち上げておく必要があります。ローカルでも、ワークフローの中でも簡単に環境を立ち上げられるメリットがあります。
- name: Start Astro dev server
run: |
docker compose up -d astro
timeout 120 bash -c 'until curl -sf http://localhost:14322/; do sleep 3; done'
ポイント⑥:Playwright でスクリーンショットをPRに含める
ClaudeがYAMLを編集したあと、PlaywrightをMCPではなくスクリプトとして実行してスクリーンショットを撮影し、コミットに含めます。
import { chromium } from 'playwright';
import { mkdirSync } from 'fs';
const BASE_URL = 'http://astro:4321';
const jst = new Date().toLocaleString('sv-SE', { timeZone: 'Asia/Tokyo' })
.replace(' ', 'T').replace(/:/g, '-');
const OUT_DIR = `./screenshots/${jst}`;
const allPages = [
{ path: '/', name: 'index' },
{ path: '/gallery', name: 'gallery' },
];
const args = process.argv.slice(2);
const pages = args.length > 0
? args.map(p => ({ path: p, name: p.replace(/^\//, '') || 'index' }))
: allPages;
mkdirSync(OUT_DIR, { recursive: true });
const browser = await chromium.launch();
const page = await browser.newPage();
await page.setViewportSize({ width: 1280, height: 800 });
for (const { path, name } of pages) {
await page.goto(`${BASE_URL}${path}`, { waitUntil: 'networkidle' });
const file = `${OUT_DIR}/${name}.png`;
await page.screenshot({ path: file, fullPage: true });
console.log(`saved: ${file}`);
}
await browser.close();
PRの「Files changed」を開くとスクリーンショットが確認できるので、ローカルで環境を立ち上げなくても見た目の確認ができます。Claudeが自分で撮って自分でコミットするので、レビュー側の手間がないです。
スクリーンショットがコミットされることになりますが、これは定期的な削除で対応します。人がやるのも面倒なのでGitHubのcron実行で定期的に削除PRを作ってもらうのがいいと思います。
ポイント⑦:CLAUDE.mdでClaudeへの指示を渡す
リポジトリルートの CLAUDE.md はClaude Codeが読み込むコンテキストファイルで、GitHub Actions上でも参照されます。ここにClaudeへの制約や手順を書いておくことで、動作をコントロールできます。
このリポジトリでは以下のような指示を書いています。
- 編集対象は
src/content/配下のみ - 画像の差し替えはURLをそのままフィールドに書き込むだけでよい(ファイルのダウンロードや
public/への配置は不要) - スクリーンショットは変更したページのみ撮影する
- スクリーンショット撮影後、画像ファイルは読み込まない(コスト削減)
細かい制約をCLAUDE.mdで管理することで、ワークフローのYAMLをシンプルに保てます。
Q&A
コストはどのくらい?

topページの画像を1枚変えてもらって$0.2これくらい。
自分の場合は、自分が修正するくらいならこのくらいのお金でクライアントが依頼したことをやってくれるエージェントがいると楽だと思います。
画像はどうするの?
画像サーバーは別で用意します。私は cloudinaryを使っています。
画像は資産として画像サーバーできちんと管理するほうがクライアントにもメリットがあると思います。
(ファイルが一般公開される場合は十分に説明しておく必要はあると思います)
その他
GitHubアカウントが必要
クライアントにGitHubアカウントを作ってもらう必要があります。最初の説明コストはゼロではないです。
PRのマージは人間がやる
完全自動化ではないので、自分がボトルネックになることはあります。
クライアントがやる場合は、マージの責任をもってもらうことになります。
基本的にcontent以外の変更は差分に含まれないし、動作検証はしている状態です。 CMSを使うにしてもコンテンツを反映する責任はクライアントにあります。
id-token: write について
ワークフローの permissions に id-token: write を含めています。
permissions:
contents: write
pull-requests: write
issues: write
id-token: write
AWS BedrockやGCP Vertex AIなどクラウドプロバイダー経由でClaudeを使う場合はOIDC認証のために必要な設定です。Anthropic APIキーを直接使う今回の構成では必須ではないかもしれませんが、公式のドキュメントに記載があったためそのまま含めています。




