Claude CodeとCodexを1台で協業させる話から続く「個人開発を量産するための自動化基盤」シリーズです。今回は、フロント実装に入る前にブランドガイドをAIに生成させておく仕組みを作った話を書きます。
なぜガイドを「先出し」するのか
個人開発でUIを作るとき、よくやりがちなパターンがあります。「とりあえず実装してみて、後からデザインを整える」というものです。
その結果どうなるか。Claudeが生成するコードには必ずデフォルトのTailwind感が漂い、シャドウ・角丸・均一カードグリッドで埋め尽くされた、どこかで見たような画面が完成します。「クリーンでミニマル」という指示では、AIが選ぶのは常に最大公約数的なデザインです。
問題は情報の不均衡にあります。Claudeは指示がなければライブラリのデフォルト設定で実装します。コンポーネントに意志がなければ、フレームワークの意志で埋められます。
逆に言えば、実装前に意図を持ったブランドガイドを渡せば、AIの生成物は一変します。どのプロジェクトにも「これしかない」という方向を先に決めて、色・タイポ・モーション・グラデーション・グレインの意図を明示した状態で実装に入ると、生成コードの品質が桁違いに変わります。
その「先出し」を自動化したのが、~/dev/brand-kit/generate.sh です。
brand-kit の全体像
~/dev/brand-kit/ の構成はシンプルです。
brand-kit/
├── generate.sh ← メインスクリプト
├── lib/
│ └── strip_fence.py ← コードフェンス除去ユーティリティ
├── output/
│ └── <slug>/ ← 生成物置き場
└── README.md
generate.sh にスラッグ(プロジェクト識別子)とブリーフ(1〜3行の概要)を渡すと、output/<slug>/ に最大4つのファイルが生成されます。
| ファイル | 内容 |
|---|---|
brand-guideline.md |
世界観・スタイル方向・パレット意図・タイポ戦略・モーション原則・Do&Don't |
tokens.css |
oklch ベースのCSS カスタムプロパティ |
preview.html |
トークンを実際に効かせた単一HTMLのLP断片 |
references.md |
(--refs 時のみ)WebSearch で集めた実在参考URL |
このガイド一式を実装者(Claude Code)に「このガイドに従って実装して」と渡すのが基本の使い方です。ガイドが先にあれば、Claudeは選択を迷わずに済みます。
実際の使い方
bash generate.sh <slug> "<概要・誰向け・何のページか>" [--refs] [--force]
架空の地域工務店案件で動かしてみます。
cd ~/dev/brand-kit
bash generate.sh marui-koumuten \
"板橋区の地域工務店のリニューアルHP。50〜60代施主向け。職人の手仕事と地域実績で問い合わせを取る"
実行すると、順番に3ファイルが生成されます。
[brand-kit] guideline生成 (試行1/3)…
[brand-kit] ✓ brand-guideline.md
[brand-kit] tokens生成 (試行1/3)…
[brand-kit] ✓ tokens.css
[brand-kit] preview生成 (試行1/3)…
[brand-kit] ✓ preview.html
[brand-kit] 完了: ~/dev/brand-kit/output/marui-koumuten
- brand-guideline.md
- tokens.css
- preview.html
→ preview.html をブラウザで確認。方向が違えば概要を変えて --force で再生成。
方向が気に入らなければ概要を書き直して --force を付けると全ファイルを作り直します。--refs を付けると WebSearch で実在の参考URLを集めた references.md も生成されます(その分だけ時間がかかります)。
設計の核心:なぜ3コールに分けるのか
generate.sh を読むと、guideline・tokens・preview をそれぞれ独立したコールで生成していることがわかります。内部の call() ヘルパはこうなっています。
# 1コール実行ヘルパ: $1=ラベル $2=プロンプト $3=allowedTools
call() {
local label="$1" prompt="$2" tools="${3:-}" r=""
local toolflag=(); [ -n "$tools" ] && toolflag=(--allowedTools "$tools")
for attempt in 1 2 3; do
echo "[brand-kit] $label (試行$attempt/3)…" >&2
r=$(timeout "$T" "$CLAUDE" -p "$prompt" "${toolflag[@]}" --model "$MODEL" </dev/null 2>/dev/null)
[ -n "$r" ] && ! printf '%s' "$r" | grep -qiE \
'usage limit|rate limit|session limit|hit your session|request timed out' \
&& { printf '%s' "$r"; return 0; }
sleep 3
done
return 1
}
なぜ1コールで全部出さないのか。理由は明確です。guideline.md だけで十分なボリュームがあり、そこに tokens.css と preview.html まで加えると出力が最大トークン数を超えて途中で切れます。CSSが閉じ括弧のないまま終わっていたり、HTMLが </body> を持たない壊れたファイルになったりします。
分割するもう一つのメリットは、後段のコールが前段の出力を入力として受け取れる点です。
GUIDE=$(head -c 6000 "$GFILE" 2>/dev/null)
# ↑ guideline の先頭6000文字を tokens 生成プロンプトに埋め込む
TOKENS=$(cat "$TFILE" 2>/dev/null)
# ↑ tokens.css 全文を preview 生成プロンプトに埋め込む
ガイドラインの意図がトークンに、トークンの値がプレビューに、一貫して引き継がれます。生成物がバラバラにならず、3ファイルが同じ世界観を共有できます。
また各ファイルはすでに存在すればスキップされます。
if [ "$FORCE" = 1 ] || [ ! -s "$TFILE" ]; then
# tokens を生成
else
echo "[brand-kit] = tokens.css (既存・skip)"
fi
これが冪等性の肝です。途中でタイムアウトしたり失敗したりしても、再実行すれば欠けたファイルだけが補完されます。brand-guideline.md が成功して tokens.css で失敗した場合、次の実行では brand-guideline.md をスキップして tokens.css から再開します。
踏んだ落とし穴
落とし穴①:</dev/null を忘れるとフリーズする
最初に claude -p をシェルスクリプトから呼び出したとき、コマンドが帰ってこない現象に遭遇しました。
原因は stdin です。claude -p はパイプされた入力を読もうとします。スクリプト内で単純に呼ぶと、Claude がターミナルの標準入力を待ったまま止まります。timeout で囲っていても、タイムアウトまでの間ずっとブロックされます。
# NG: stdin が閉じておらず、対話入力を待ってフリーズする
r=$(timeout 240 "$CLAUDE" -p "$prompt" --model sonnet)
# OK: </dev/null で stdin を閉じる
r=$(timeout 240 "$CLAUDE" -p "$prompt" --model sonnet </dev/null 2>/dev/null)
claude -p をシェルスクリプトや launchd から呼ぶときは </dev/null が必須です。対話的なターミナルで手打ちするときは問題ありませんが、バックグラウンドプロセスや自動化スクリプトでは必ず付けてください。Codex CLI のヘッドレス実行でも同じ罠があります(前回の記事参照)。
落とし穴②:セッション上限エラーで壊れたファイルが生成される
claude -p はセッション上限に達すると、CSSやHTMLではなくエラーメッセージを返します。
You've hit your session limit. Please wait before continuing.
このエラー文字列をそのまま tokens.css に書き出すと、後続の preview.html 生成がエラーテキストを「CSSトークン」として受け取り、ゴミHTMLを生成します。さらにそのままファイルに保存されてしまい、次回実行時には「既存・skip」でスキップされて壊れたままになります。
対策として call() ヘルパの中にガードを入れています。
[ -n "$r" ] && ! printf '%s' "$r" | grep -qiE \
'usage limit|rate limit|session limit|hit your session|request timed out' \
&& { printf '%s' "$r"; return 0; }
sleep 3
空でなく、かつエラーパターンを含まない場合のみ出力を返します。エラーが返ってきた場合は3秒待ってリトライします。3回失敗すれば return 1 でコール失敗とし、ファイルへの書き込み自体を行いません。
落とし穴③:コードフェンスが応答に混入する
claude -p の応答には、指示しなくても ```css や ```html などのコードフェンスが混入することがあります。CSSファイルにコードフェンスが入ると、ブラウザにそのまま適用できません。
各コールの出力を lib/strip_fence.py にパイプして前処理しています。
# lib/strip_fence.py(抜粋)
t = sys.stdin.read().strip()
# ```lang ... ``` で囲まれている最大ブロックを優先
m = re.search(r'```[a-zA-Z]*\n(.*?)```', t, flags=re.DOTALL)
if m:
print(m.group(1).strip() + "\n")
sys.exit(0)
# フェンス無し: 最初の構造的な行以降を採用(前置き散文を落とす)
lines = t.splitlines()
start = 0
for i, ln in enumerate(lines):
s = ln.lstrip()
if s.startswith(("# ", "## ", ":root", "<!--", "<!DOCTYPE", "<html", "<header", "/*", "# 参考")):
start = i
break
print("\n".join(lines[start:]).strip() + "\n")
フェンスがあればその内側だけを取り出し、なければ最初の「構造的な行」以降を採用します。# (Markdown見出し)・:root(CSS)・<!--(HTML)・<html が開始行の判定に使われています。前置きの「以下にCSSを示します。」などの散文が自動的に落とされます。
R=$(call "tokens生成" "$P" "") && printf '%s\n' "$R" | strip_fence > "$TFILE"
落とし穴④:1コールで全部出そうとすると出力が切れる
最初のバージョンでは1プロンプトに「guideline と tokens と preview を全部出して」と書いていました。
guideline は出ます。しかし tokens.css が :root { の途中で終わっていたり、preview.html が <section> タグを閉じないまま止まっていたりしました。出力トークン数の上限に引っかかっているためです。
3コールに分けて、各コールが1ファイルだけを返すようにしてから問題は解消しました。コールあたりのタイムアウトは BRAND_KIT_TIMEOUT(デフォルト240秒)で調整できます。分割したことで各コールが短くなり、デフォルトの240秒でも余裕を持って完了します。
実際の出力:brand-guideline.md を見る
実際に生成された brand-guideline.md のスタイル方向セクションがこれです。
## 選んだ方向と理由
### 墨付けエディトリアル(Sumitsuke Editorial)
「墨付け(すみつけ)」とは、大工が刃を入れる前に木材に墨壺で正確な基準線を引く所作——
職人の仕事はこの一線から始まる。このサイトのデザイン言語は、その精度と誠実さを視覚に移植する。
和紙のような温かい紙質のベースに、重量のある明朝体見出しを置く。
グリッドを意図的に破り、写真は枠に入れず断ち切る。
グレインは「刷られた紙」の手触りとして機能する。
参照軸は、活版印刷の日本建築専門誌(新建築・住宅建築)の誌面。
なぜこの方向か(3つの根拠):
1. 「ミニマル」は空虚に見える:50-60代には「余白だらけ=やる気がない会社」と読まれるリスクがある
2. 明るい紙白が読みやすさを担保する:温和紙のような地色が50-60代の読み疲れを防ぐ
3. エディトリアルの非対称が「記憶に残る」:均一カードグリッドはどの工務店も使う。
7:5分割・テキストと写真の重なり・引き出し線が「ここは違う」と感じさせる
「クリーンでミニマル」ではなく「墨付けエディトリアル」という固有名を持ったスタイル方向です。選ばなかった方向(和モダン・ダークラグジュアリー・スイスグリッド)とその理由まで含まれており、方向性の決断が記録として残ります。
実装者がこのガイドを読むと、「角丸8px以上のカードグリッドは使わない」「明朝体見出しの line-height は1.15」「CTAは焼き杉色だけで、緑青アクセントはプルクォートライン専用」という判断が一意に決まります。指示の余白がなくなるぶん、生成物のブレが減ります。
実際の出力:tokens.css を見る
tokens.css の一部です。
:root {
/* ─── カラー ───────────────────────────────────────
素材参照: 漆喰・墨・赤土・緑青の錆 / すべて oklch() */
--color-ground: oklch(96.5% 0.008 82); /* 温和紙・漆喰白 — 全体ベース */
--color-ground-alt: oklch(93.0% 0.012 79); /* 素焼きタイル面 — セクション交互 */
--color-ink: oklch(14.0% 0.006 255); /* 墨 — わずかに冷色 */
--color-accent: oklch(40.0% 0.068 165); /* 緑青 — プルクォートライン・リンク */
--color-cta: oklch(48.0% 0.052 60); /* 焼き杉色 — CTAの唯一の強調色 */
色はすべて oklch() で定義されています。oklch は知覚的に線形な色空間で、明度L(0〜100%)・彩度C・色相H の3成分で色を表します。hsl と異なり、同じ彩度のまま明度だけ変えてもくすまないため、ホバー状態などの派生色を作りやすいです。
流体タイポグラフィは clamp() で実装されます。
/* ─── 流体テキストスケール ──────────────────────────
50-60代ターゲット → ベースやや大きめ */
--text-base: clamp(1rem, 0.95rem + 0.45vw, 1.125rem); /* 16-18px 本文 */
--text-hero: clamp(3.5rem, 1.50rem + 8.93vw, 7.5rem); /* 56-120px ヒーロー */
clamp(最小値, 推奨値, 最大値) の形で、ビューポート幅に応じてなめらかにサイズが変化します。320px で最小値、1440px で最大値に達するように推奨値の vw 係数が計算されています。
グレイン(紙の質感)は SVG の feTurbulence フィルタを data-URI として CSS 変数に直接埋め込んでいます。
/* ─── グレイン ──────────────────────────────────────
刷られた紙の手触り。SVG feTurbulence / 200×200px タイル */
--grain-bg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.75' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23g)' opacity='0.06'/%3E%3C/svg%3E");
外部ファイルの依存なしに「刷られた紙の手触り」を再現できます。feTurbulence で生成したノイズを feColorMatrix でグレースケール化し、opacity='0.06' で薄く重ねます。ガイドラインでは「適用しない箇所」(CTAボタン・入力フォーム・写真の上)まで明示されており、誤用を防ぐコメントも tokens.css に入ります。
グラデーションも3箇所限定でCSS変数化されています。
/* ① ヒーロー背景: 木と陰(杉色 → 墨 / 対角) */
--gradient-hero: linear-gradient(
135deg,
oklch(28% 0.055 68) 0%,
oklch(12% 0.005 255) 100%
);
/* ② ビネット: 写真下端の墨色フェード(テキスト乗せ用) */
--gradient-vignette: linear-gradient(
to bottom,
transparent 45%,
oklch(14% 0.006 255 / 0.88) 100%
);
/* ③ セクション上端: 赤土の気配 */
--gradient-section-top: linear-gradient(
180deg,
oklch(67% 0.082 68 / 0.10) 0%,
transparent 100%
);
グラデーションの使用箇所を「全サイトで3箇所限定」と明示されているため、実装者が不必要に増やしません。
実装への渡し方
生成したガイドの使い方は3パターンあります。
パターン①:Claude Code に直接渡す
output/marui-koumuten/ フォルダを「このガイドに従って実装して」と伝える。
Claude Code にガイドのパスを渡してから実装を依頼すると、色・タイポ・グレインの実装がガイドに沿った形で生成されます。ガイドを読んでいるClaude Codeは「どの色変数を使うか」「ホバー時はどの変数か」「グレインをどこに適用するか」を自律的に判断できます。
パターン②:claude.ai でブラッシュアップ
preview.html と tokens.css を claude.ai に貼り付けて自然言語で調整します。ブラウザで preview.html を開き、気になる箇所を会話で指摘しながらデザインを磨けます。
パターン③:/design-sync でカード化
generate.sh のプロンプトには <!-- @dsCard group="Brand" --> を preview.html の先頭行に出力するよう指示が含まれています。これを /design-sync スキルで claude.ai のデザインシステム Project に同期すると、以後の Claude Design 生成がこのプロジェクトのトークンを継承して一貫します(/design-sync の利用には claude.ai へのログインが必要です)。
モデルとタイムアウトの調整
generate.sh の冒頭に環境変数による設定があります。
CLAUDE="${CLAUDE:-~/.local/bin/claude}"
MODEL="${BRAND_KIT_MODEL:-sonnet}"
T="${BRAND_KIT_TIMEOUT:-240}" # 1コールあたりのタイムアウト(秒)
速度優先なら haiku に切り替えられます。ガイドの思考の深さは落ちますが、生成が速くなります。
# haiku で速度優先
BRAND_KIT_MODEL=haiku bash generate.sh marui-koumuten "..."
# タイムアウトを伸ばす(低速ネット環境など)
BRAND_KIT_TIMEOUT=360 bash generate.sh marui-koumuten "..."
生成コストは claude -p(Claude MAX 枠)で完全無料です。APIキーや課金設定は不要です。
まとめ
- 実装前にガイドを先出しすることで、AI生成コードの脱テンプレ化が実現できます
-
generate.shは guideline → tokens → preview の3コール独立設計。1コールで全部出すと出力が途中で切れます -
claude -pはバックグラウンド実行時に**</dev/nullが必須**です。忘れると stdin 待ちでフリーズします - セッション上限エラーの文字列を
grepで検出し、壊れたファイルへの書き込みをガードします -
strip_fence.pyでコードフェンスと前置き散文を除去し、CSSとHTMLをそのまま使える形で保存します - 各ファイルは存在すればスキップ——冪等性により再実行で欠けた分だけ補完されます
-
tokens.cssはoklch()・clamp()・feTurbulenceグレインをすべてCSS変数として定義します。グラデーションは全サイトで3箇所限定 - 生成コストは Max 枠で完全無料です
Lily(@bokuwalily)― 個人開発者。Claude Code で自動化基盤を組みながら、iOSアプリやWebサービスを量産しています
- 作ったアプリは ポートフォリオ にまとめています📱
- 新着・開発の裏側は X @bokuwalily で発信しています🌍
- OSS: github.com/bokuwalily 🐙
- この仕組みで「作業」じゃなく「環境」を回して月120万に戻した話は noteに無料で 書いています
皆さんの ❤️ やシェアが励みになります!