前の記事でこのブログの作り方をざっと書きました。今回はもう少し踏み込んで、 なぜこの構成なのかと、 この構成を AI に触らせるために何を書いているかの話をします。
このリポジトリは最初から、自分と AI(Claude Code)の両方が触る前提で組んでいます。 構成を決めるときに「人間が書きやすいか」だけでなく「AI に説明しきれるか」を 条件に入れると、選ぶものが少し変わる、というのが書きたいことです。
3つの案を比べた
記事の持ち方には候補が3つありました。
| 案 | 表現の自由度 | 執筆の手数 | 依存 |
|---|---|---|---|
| Markdown + content collections | 低い | 1ファイル | なし |
| MDX + content collections | 中くらい | 1ファイル | @astrojs/mdx |
| 記事ごとの .astro ページ | 制限なし | 2ファイル | なし |
MDX なら本文にコンポーネントを埋め込めます。ただ「この記事だけの実験的な表現」をやろうとすると、
結局その記事専用のコンポーネントを別ファイルに切り出すことになる。
それなら最初からページごと .astro でいいだろう、と考えて3案目にしました。
ここでいちばん欲しかったのは、記事ファイルの中に
<style> と <script> と SVG をそのまま書けることです。
構成 — 依存が一方向であること
3案目を選ぶと、一覧ページに出すメタデータをどこから集めるかという問題が出てきます。
ここは素直に、メタデータだけを src/data/blog.ts に切り出しました。
大事なのは矢印の向きです。
一覧ページから記事ページへの矢印を作らない、というのがこの構成のいちばんの肝です。
メタデータを記事ファイル側に置いて import.meta.glob で集める書き方もありますが、
それをやると一覧ページが全記事コンポーネントを import することになり、
記事ごとに書いた <script> が一覧ページのバンドルに入ってきます。
記事の中で好きなだけスクリプトを書きたいのに、それが一覧を重くするのでは本末転倒です。
結果として何が載っているか
ビルド後の HTML を実際に測ってみます。各ページに含まれる
<script> の合計バイト数です。
一覧がいちばん大きいのは絞り込み UI のぶんです。そして 記事が何本増えてもこの 1985B は変わりません。 記事ページ側の 788B にも、絞り込みのコードは1バイトも入っていません。 矢印を一方向にした効果がそのまま出ています。
代償は「2ファイル管理」
いいことばかりではありません。この構成では記事1本につき レジストリとページの2ファイルを触ることになります。 片方だけ足して忘れる、という事故が構造的に起こりえます。
しかもこの手のミスは、ビルドが通ってしまうぶんタチが悪い。 ページだけ作った場合は URL は生きているのに一覧に出てこないし、 レジストリだけ足した場合は一覧のリンクが 404 になります。
3層で守る
なので、この「知らないと事故る」を3つの層に分けて書いています。 上にいくほど意図の説明で、下にいくほど強制力があります。
1層目 — 機械全体の方針
このマシンには他にもアプリが載っていて、共通の方針を
~/.claude/CLAUDE.md に書いています。
「アーキテクチャの決定は ADR に残す」「テストしやすさとガードレールは
最初のセットアップに含める、後付けにしない」といった内容です。
今回 decisions/0001 と構造テストが最初から入っているのは、これが効いているからです。
2層目 — この repo での書き方
リポジトリの中に .claude/skills/blog-post/SKILL.md を置いています。
AI 向けの、記事の書き方の説明書です。
description にどういうときに読むかを書いておくと、
「ブログ書きたい」と言うだけで拾ってくれます。
.claude/skills/blog-post/SKILL.md(冒頭)
---
name: blog-post
description: mimitab-portforio のブログ記事(/blog 配下)を新規作成・編集する
ときの手順。「ブログ書きたい」「記事追加して」「/blog に〜の記事を」など、
このポートフォリオの記事に関わる作業で必ず使う。記事は1本 = 1つの .astro
ページで、メタデータのレジストリとの2ファイル管理になっているので、
この手順を踏まないと CI が落ちる。
--- 中身で分量を割いているのは、手順そのものより ソースを読んだだけでは分からないことです。 実際、このブログを作っている最中に自分で踏んだものをそのまま書いています。
落とし穴の節(抜粋)
## CSS の落とし穴
- src/styles/blog.css は Tailwind のレイヤー外なので、ユーティリティより強い。
.chart-bar .step-line .reading-progress は blog.css 側で transform を
持っているので、これらの要素に -translate-x-1/2 などを足しても効かない
- 本文は .prose-mimi で囲まれていて h2 h3 p ul ol a blockquote table code に
スタイルが当たる。コンポーネント内部でリスト風の見た目を作るときは
ul/ol ではなく div を使う(StepFlow がそうしている)
上は「Tailwind のユーティリティが効かない場所がある」という話です。
blog.css は Tailwind のレイヤーの外にあるので、
レイヤー内のユーティリティより強く効きます。
アニメーション用に transform を持っている要素に
-translate-x-1/2 を足しても、黙って無視される。
こういうものはコードを読んでも気づけないので、明示的に書いておく価値があります。
3層目 — 機械的に止める
とはいえ、文章はあくまで読んでもらう前提のものです。最後は機械で止めます。
src/data/blog.structure.test.ts(抜粋)
const pageIds = readdirSync(BLOG_PAGES_DIR, { withFileTypes: true })
.filter((e) => e.isFile() && e.name.endsWith('.astro') && e.name !== 'index.astro')
.map((e) => e.name.replace(/\.astro$/, ''))
.sort()
const registryIds = posts.map((p) => p.id).sort()
it('ページがあるのに src/data/blog.ts に登録されていない記事が無い', () => {
const missing = pageIds.filter((id) => !registryIds.includes(id))
expect(missing, `src/data/blog.ts に登録してください: ${missing.join(', ')}`).toEqual([])
}) src/pages/blog/ のファイル名の集合と、レジストリの id の集合を突き合わせるだけです。
ズレたら、どちらに何を足せばいいかをメッセージに出して落ちます。
日付の形式やカテゴリの妥当性も同じテストで見ています。
記事1本を書く流れ
ここまでを踏まえると、実際の手順はこれだけになります。 この記事自体も、この流れで書かれています。
スキルが読まれる
「ブログ書きたい」と言うと description が引っかかって SKILL.md が読み込まれる。id の命名規則や既存カテゴリ、日付は推測せず date +%F で取ること、などがここに書いてある。
src/data/blog.ts の先頭に1件足す
id・タイトル・description・日付・カテゴリ。新しい順に並べる規約なので配列の先頭に入れる。
src/pages/blog/{id}.astro を書く
getPost(id) を引いて BlogArticle に渡したら、あとは本文。図が足りなければ Figure に SVG を直接書く。
test / typecheck / build
2ファイルのどちらかを忘れていればここで落ちる。通れば一覧にもカテゴリページにも自動で並ぶ。
思ったこと
構成を決めるときに「AI に説明しきれるか」を条件に足すと、 暗黙になっている部分がそのままコストとして見えてきます。 今回でいうと「2ファイル管理」がそれで、人間ひとりなら慣れで済ませていたと思います。 説明しないといけないと分かった時点で、テストで守る側に倒しました。
これは QA の仕事で感じることと似ています。 仕様が曖昧なまま動いているものは、テストを書こうとした瞬間に曖昧さが表に出る。 AI への指示書も同じで、書こうとすると自分が何を分かっていないかが分かります。
指示書のほうも、記事を書きながら足りないものを足していく予定です。 このブログはそのためにあります。