CSS ゼロのブログに、テーマを入れたくなった
Astro で作ったブログに CSS を一切書いていなかった。セマンティック HTML だけで構成されていて、機械には読めるが人間には読みにくい。そろそろスタイリングが必要だ。
Astro にはテーマがある。テーマがあるということは、テーマが前提とする標準的な構造があるはずだ。その構造を知らずに独自の構成でブログを作り込むと、後からテーマを適用しようとしたときにコンテンツの書き直しが発生する。それは避けたい。
テーマを入れる前に、まずデファクト標準を調べることにした。
人気テーマを分析してデファクト標準を特定する
調べたのは、AstroPaper、Astro Cactus、AstroWind、Astro 公式ブログスターターの 4 つ。コミュニティで人気のテーマと公式テンプレートを並べれば、共通パターンが見えてくるはずだ。
結果はこうなった。
| 要素 | AstroPaper | Astro Cactus | AstroWind | 公式スターター |
|---|---|---|---|---|
| Base + Post 二層レイアウト | ✓ | ✓ | ✓ | ✓ |
| 説明文フィールド必須 | ✓ | ✓ | ✓ | ✓ |
コレクション名 blog | ✓ | × | × | ✓ |
| 更新日フィールド(任意) | ✓ | ✓ | ✓ | ✓ |
| 代表画像フィールド(任意) | ✓ | ✓ | ✓ | ✓ |
フィールド名はテーマごとに微妙に異なる(heroImage / ogImage / coverImage など)が、概念としての構造は共通している。ここから互換性の高い構成を抽出した。
- 二層レイアウト: Base レイアウトが HTML シェル(
<html>,<head>,<body>)を担当し、Post レイアウトが Base を包んで記事固有のマークアップを提供する。全テーマがこのパターンを採用している - 説明文フィールド必須: 全テーマが frontmatter の説明文(
description)を必須フィールドとして要求する。OGP やメタタグの生成に使われるため当然だ - コレクション名
blog: AstroPaper と公式スターターがblogを使用。他は異なる名前だが、blogが最も互換性が高い - 更新日と代表画像: ほぼ全テーマが任意フィールドとしてサポート。フィールド名は異なるが概念は共通しており、コストが低いうちに追加しておくべきだ
ギャップ分析と変更
自分のブログの構成とデファクト標準を突き合わせた。
| 項目 | デファクト標準 | 変更前 | 変更後 |
|---|---|---|---|
| レイアウト | Base + Post 二層 | ページごとに HTML 出力 | Base.astro + PostLayout.astro |
description | 必須 | なし | スキーマに必須フィールドとして追加 |
| コレクション名 | blog | posts | blog |
updatedDate | 任意 | なし | スキーマに任意フィールドとして追加 |
heroImage | 任意 | なし | スキーマに任意フィールドとして追加 |
| ルーティング | /posts/[slug] | /posts/[slug] | 変更不要 |
スキーマは以下の形に落ち着いた。
const blog = defineCollection({
schema: ({ image }) => z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
tags: z.array(z.string()),
draft: z.boolean().default(false),
heroImage: image().optional(),
}),
});
レイアウトの二層構造はこう分離される。
Base.astro: <html><head>...</head><body><slot /></body></html>
PostLayout.astro: <Base><article><Content /></article></Base>
Base がサイト共通の HTML 構造を持ち、PostLayout が記事のメタ情報(タイトル、公開日、タグ)と本文を配置する。ページを追加するときは Base だけを使い、記事ページでは PostLayout を通して Base を使う。責務が明確に分かれる。
AstroPaper テーマで互換性を検証する
構造を整えたら、実際にテーマが動くか検証する。AstroPaper を選んだ理由は、コミュニティで最も人気があり、コレクション名 blog とルーティング /posts/[slug] がそのまま一致するからだ。
AstroPaper は Tailwind CSS に依存しているため、依存関係として追加した。テーマのレイアウトとコンポーネントを移植し、既存のコンテンツがそのまま表示されることを確認した。
興味深かったのは、フィールド名の差異の吸収方法だ。AstroPaper は内部的に pubDatetime というフィールド名を使っているが、自分のスキーマでは pubDate を使っている。同様に画像フィールドもテーマごとに heroImage、ogImage、coverImage と名前が異なる。これらの差異はコンテンツのスキーマを変更するのではなく、コンポーネント側で吸収した。コンテンツは共通の最小構成のまま、テーマ側のコンポーネントが自分のスキーマに合わせて読み替える。
これが標準構造の価値だ。テーマを変えるとき、変更するのはレイアウトとコンポーネントであって、コンテンツではない。 コンテンツが標準構造に従っていれば、テーマの差し替えはコンポーネントの差し替えで完結する。
まとめ
Astro ブログのテーマ互換性を確保するための構造は、シンプルだ。
- Base + Post の二層レイアウトにする
descriptionをスキーマの必須フィールドにする- コレクション名は
blogが最も互換性が高い updatedDateとheroImageはコストが低いうちに追加しておく
テーマを変えるとき、コンテンツを書き直す必要がなくなる。構造が標準に従っていれば、テーマの切り替えはコンポーネントの入れ替えだけで済む。