Astro 5 + Tailwind CSS v4でブログを作る手順(このブログの実物の設定で解説)
このブログ(clvr.lol)自体がAstro 5 + Tailwind CSS v4製。プロジェクト作成からContent Collections、Cloudflare Pages公開までの手順と、記事180本超を運用して踏んだスキーマ制約の落とし穴を実物の設定で書く。
このブログ(clvr.lol)は Astro 5 + Tailwind CSS v4 で動いている。記事は180本を超えて、Markdown を Content Collections で管理し、Cloudflare Pages に配信している構成だ。「Astroでブログを作る手順」の記事は世の中にいくらでもあるが、たいてい作って終わりで、運用してから何が起きるかが書かれていない。なのでこの記事では、手順そのものは最短で通しつつ、実際にこのサイトを運用して「あの設定にしておいてよかった」「ここで一度ビルドが落ちた」という部分に紙幅を割く。設定ファイルは全部このリポジトリの実物だ。
なぜAstroにしたか
理由は単純で、ブログはほぼ全ページ静的だから。Next.jsでも作れるが、記事を表示するだけのページにReactのランタイムを載せる必要がない。Astroはデフォルトでクライアントに JavaScript を送らないので、何も頑張らなくても表示が速い。このサイトにはブラウザゲームや文字数カウント等のツールみたいにJSが必要なページもあるが、そこだけ個別にスクリプトを持たせればいい。フレームワーク選定で迷った経緯はSSG比較の記事に書いたので、ここでは省く。
セットアップ
プロジェクト作成とTailwindの導入。Tailwind v4はAstro公式インテグレーションではなくViteプラグインで入れる。
npm create astro@latest my-blog
cd my-blog
npm install tailwindcss @tailwindcss/vite
astro.config.mjs はこれだけ。以下はこのブログの実物(サイトマップも最初から入れておくのがおすすめ。後述)。
// astro.config.mjs
import { defineConfig } from 'astro/config';
import tailwindcss from '@tailwindcss/vite';
import sitemap from '@astrojs/sitemap';
export default defineConfig({
site: 'https://clvr.lol',
vite: {
plugins: [tailwindcss()],
},
integrations: [sitemap()],
markdown: {
shikiConfig: {
theme: 'github-dark',
},
},
});
markdown.shikiConfig はコードブロックのシンタックスハイライトのテーマ。ビルド時に色が付くので、ハイライト用のJSをクライアントに送らずに済む。ブログなら設定しておいて損はない。
Tailwind v4は設定ファイルがない。テーマはCSSに書く
v4で一番変わったのがここで、tailwind.config.js が要らない。テーマはCSSファイルの @theme ディレクティブに書く。詳細はv4の新機能まとめに譲るとして、このブログで実際にやっている工夫を1つ紹介する。@theme のトークンを、素のCSS変数に接続しておくやり方だ。
/* src/styles/global.css(このブログの実物を抜粋) */
@import "tailwindcss";
:root {
--c-bg: #150829;
--c-fg: #f3edff;
--c-pink: #ff2ea6;
--c-cyan: #22e4ff;
}
@theme {
--color-bg: var(--c-bg);
--color-fg: var(--c-fg);
--color-pink: var(--c-pink);
--color-cyan: var(--c-cyan);
}
@theme に直接色コードを書かずに :root の変数を参照させておくと、デザインを変えたいときに :root の数行を差し替えるだけで、bg-bg や text-pink みたいなユーティリティクラスを使っている全ページが一斉に追従する。このブログは途中でデザインをシンセウェイブ調に全面リニューアルしたが、マークアップ側のクラス名はほとんど触らずに済んだのはこの構成のおかげだ。
Content Collections。ここが運用の本丸
記事をただの .md ファイルの山にせず、スキーマ付きで管理するのが Content Collections。このブログの src/content.config.ts の実物がこれ。
// src/content.config.ts
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
const posts = defineCollection({
loader: glob({ pattern: '**/*.md', base: './src/content/posts' }),
schema: z.object({
title: z.string(),
description: z.string().max(160),
category: z.enum(['web-development', 'tools', 'freelance', 'ai', 'seo', 'game-dev']),
tags: z.array(z.string()).default([]),
publishedAt: z.coerce.date(),
updatedAt: z.coerce.date().optional(),
featured: z.boolean().default(false),
draft: z.boolean().default(false),
}),
});
export const collections = { posts };
ポイントを実体験ベースで3つ。
description に .max(160) を付けている。 descriptionは検索結果に出るmeta descriptionにそのまま使うので、160字を超えると尻切れになる。最初は「気を付ければいい」と思っていたが、記事が増えると気を付けられない。実際、自動生成した記事のdescriptionが160字を超えていて、その1本のせいでビルド全体が落ちたことがある。一見迷惑な挙動だが、これが正しい。落ちなければ尻切れのdescriptionのまま公開されて、誰も気付かないまま検索結果で損をし続けるからだ。スキーマ違反でビルドごと止まるのは、公開前の最後の検問として機能している。
category は z.enum で固定。 自由な文字列にすると「web-dev」「webdev」「Web開発」みたいな表記ゆれが必ず発生して、カテゴリページが崩壊する。enumにしておけば、ゆれた瞬間にビルドが落ちて教えてくれる。これも実際に規約外のcategoryで記事が作られてビルドが壊れたことがあって、以来enumの存在に何度も救われている。
日付は z.coerce.date()。 YAMLのfrontmatterでは publishedAt: 2026-03-17 がパーサーによってDate型で来たり文字列で来たりする。coerce を挟んでおくとどちらでもDateに揃うので、テンプレート側で場合分けが要らない。
記事の一覧と個別ページは getCollection('posts') で取れる。draft: true の記事を本番から除外するのもここでフィルタするだけだ。
import { getCollection } from 'astro:content';
const posts = (await getCollection('posts'))
.filter((p) => !p.data.draft)
.sort((a, b) => b.data.publishedAt.valueOf() - a.data.publishedAt.valueOf());
SEOまわりで最初から入れておくもの
後から入れると面倒なので、初日に入れておくべきものが2つ。
サイトマップは @astrojs/sitemap を入れて integrations に追加するだけ。ただし site の設定が必須で、これを忘れるとビルド時に怒られる。生成された sitemap-index.xml を Google Search Console に登録して終わり。
RSSは @astrojs/rss で src/pages/rss.xml.ts を作る。読者のためというより、更新を機械的に拾ってもらう入口として置いている。
あとこのブログでは全文検索に Pagefind を使っている。ビルド後の dist に対してインデックスを張る方式なので、検索サーバーが要らない。package.json のビルドスクリプトに繋げるだけで済む。
"scripts": {
"build": "astro build && npx pagefind --site dist"
}
静的サイトに検索を付けたいならこれが一番楽だった。記事180本のインデックスでもビルドに足される時間は気にならない程度だ。
デプロイはCloudflare Pages
このブログは wrangler pages deploy で dist をCloudflare Pagesに上げている。静的ファイルの配信だけなら無料枠で全く問題なく、独自ドメインとSSLも設定画面で数分で終わる。Astro×Cloudflareの詳しい手順は別記事に書いた。
同じ構成でもう1サイト作ったら、半日で公開まで行けた
この構成が手に馴染んだ後、ポートフォリオサイト(cleverlab.lol)をもう1つ、同じ Astro 5 + Tailwind v4 で立ち上げた。2回目は astro.config.mjs も @theme の書き方も分かっているので、環境構築で詰まる時間がゼロ。デザインを考える時間以外はほとんどかからなかった。「一度覚えれば次が速い」のは構成選びの隠れた評価軸だと思う。
まとめると、手順自体は「create astro → Tailwindの2パッケージ → Content Collectionsのスキーマ → sitemap/RSS → デプロイ」で1日あれば公開できる。ただ、長く効いてくるのはスキーマの設計だ。.max(160) とenumは、書いた時点では過剰に見えて、記事が3桁になった今も毎日サイトを守ってくれている。ブログを作るなら、最初の1本を書く前にスキーマを厳しくしておくことを勧めたい。
ちなみにこのサイトの実装作業の大半はClaude Codeにやらせていて、その運用記録は別記事にまとめてある。