Astro Content Collections と Markdown で Blog を構築した
私はモジュラーシンセでリアルタイムに生成した作業用 BGM としてアンビエントを、 Modular Synth Ambient という YouTube チャンネルで配信している。
自分の作業用の BGM を生成するついでに配信している側面が強いが、備忘録としてお気に入りの配信アーカイブをまとめたり、機材の紹介などができるように、Blog を開設した。
Blog の最初のこの記事では、モジュラーシンセともアンビエントとも関係ない、Blog の技術スタックを紹介したい。
概要・特徴・やりたかったこと
- aube を使ってみる
- ダークモード / ライトモード対応
- 多言語対応
- Markdown でのコンテンツ管理 / Content Collections を使ってみる
- 広告は載せない1
- 無料 or 低価格で公開する
anime.jsを使ってみる
具体的な技術スタック
使用したフレームワークやサービスは以下の通り(バージョンなどは Blog リリース時のもの)。
- ドメイン : Cloudflare Domains
- ホスティング : Cloudflare Workers
- 静的サイトジェネレータ(SSG) : Astro 6.1.9
- CMS : Astro Content Collections
- CSSフレームワーク : tailwindcss 4.2.4
- パッケージマネージャ : aube
- データベース : Cloudflare KV
- アイコン : Phosphor Icons
それぞれのサービスごとの選定理由は以下にまとめた。
Cloudflare Domains の選定理由
Cloudflare 運営のドメインレジストラ。ほぼ卸値でドメインの登録更新ができるため、かなり低価格。
ホスティングに Cloudflare Workers を使うのと、 .stream ドメインを年間 $5 で使えるのがメインの理由。円安著しいので、なるべく安く済ませたい。
Cloudflare Workers の選定理由
こちらも Cloudflare。静的サイトとして使う分には無料(だったはず)。
Cloudflare には静的サイト専用の Pages というサービスもあるが、最近は静的サイトでも Workers の利用を推奨しているので、おとなしく従うことにした。
デプロイについては、ローカルでビルドしたものを wrangler 経由で行う。
Astro / Content Collections の選定理由
コンテンツの管理はヘッドレス CMS、特に Emdash CMS や Payload CMS などを使うことも検討したが、ささっとリリースしたかったので CMS の利用は見送った。
具体的には…
- リッチな WYSIWYG エディタなどは自分ひとりで管理する場合は不要
- 多言語対応も含め CMS だと、環境構築がヘビーすぎる
Astro の Content Collections で Markdown を管理・描画するのが最も速く環境構築ができ。飽きる前にリリースできるだろうという判断。
tailwindcss の選定理由
class がだらだら長くなったり、複数箇所で同じことを何度も書くのが嫌で、最初は抵抗があった。が、今や熱心なファンです。
冗長な class 問題 はコンポーネントごとにスタイルを記述する Astro では表出しづらく、離れた場所にある HTML と CSS とを往復せずに済むメリットをしっかり享受できる。狭めのスコープでスタイルを管理・記述できる = AI フレンドリーなのも良い2。
デザイントークンの強力さもあり、レイアウトやメディアクエリ回りも思考→アウトプットがすぐ。tailwindcss の記法さえ覚えてしまえば、脳のリソースが節約できる開発体験が得られる印象です。
aube の選定理由
最近は pnpm / bun を使うことが多かったが、 mise の開発者が新しく aube なるパッケージマネージャをリリースしたので、使ってみている。
bun よりも高速との売り込みだが、本当に速い。開発中に、速度面で不満を抱く瞬間は今のところ皆無。導入も簡単。
Cloudflare KV の選定理由
YouTube の最新動画を表示する際、チャンネルの RSS を閲覧者のブラウザから読み込んで動的に HTML を生成するのは CORS という仕組みのため難しいことが判明。
仕方がないので Worker を立ち上げ、 YouTube の RSS を cron で 1 日 1 回取得し、KV に保存。閲覧者のブラウザからは、リバースプロキシを通して Worker > KV にアクセスして、動的に HTML を生成することにした。
KV の書き込み回数、保存するデータの大きさ的にも、余裕で無料枠範囲内なので、気兼ねなく採用。
Phosphor Icons の選定理由
- SVG でも配布してくれている
- MIT ライセンス
- アイコンの意匠やクオリティに統一感がある
- astro-icon ライブラリで簡単に使える
Astro Content Collections
Content Colloctions では、Markdown ファイルを使ったフラットファイル CMS 的にコンテンツを管理することができる。
セットアップ
以下の通り、Astro のセットアップ時に、サンプルの Markdown ファイル付きのテンプレートとして選べる。
# aubeを使ってセットアップ
aube create astro@latest
...
# セットアップウィザード内のテンプレート選択時に、
# `Use blog template`を選択
tmpl How would you like to start your new project?
(o) Use blog template
...
# Tailwindのインストールはコマンドを実行
aube install tailwindcss @tailwindcss/vite
# セットアップ完了後には、以下のように起動
aube run dev
tailwindcss のインストールは公式のドキュメントの Install Tailwind CSS with Astro も参照してください。インストールの他に global.css の配置などが必要です。
ディレクトリ構成
Astro の i18n 機能で多言語対応するつもりだったので、ディレクトリ構成は以下の通りにした。デプロイ先の URL の構造としては、https://example.com/ja/*** みたいな感じになります。
src/
├── assets/
│ ├── icons/ # Phosphor IconsのSVGを格納
│ └── images/ # サイト内の画像を格納
├── components/ # Astroのコンポーネントを格納
├── content/ # MDファイルを格納
│ └── blog/
│ ├── en/
│ └── ja/
├── layouts/ # Astroのレイアウトを格納
├── pages/ # ルーティング用のファイル格納
│ ├── en/
│ │ └── blog/
│ └── ja/
│ └── blog/
└── styles/ # TailwindCSS用のglobal.cssを格納
Astro の設定ファイル
astro.config.mjs の多言語対応部分は以下の通りに編集。
export default defineConfig({
...
i18n: {
defaultLocale: 'ja',
locales: ['ja', 'en'],
routing: {
// デフォルトの言語でも、url に locale を含める設定。
// 含めたくない場合は false に変更。
prefixDefaultLocale: true,
redirectToDefaultLocale: true,
},
},
...
});
ルーティング
プロジェクトテンプレートの状態でルーティングを実装する例を以下に掲載します。ご自分のプロジェクトやディレクトリ構成に合うコードは、適宜 AI エージェントに生成してもらうといいと思います。
---
import { type CollectionEntry, getCollection, render } from 'astro:content';
import BlogPost from '/src/layouts/BlogPost.astro';
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts
.filter((post) => post.id.startsWith('ja/'))
.map((post) => {
const [, slug] = post.id.split('/');
return {
params: { slug },
props: post,
};
});
}
type Props = CollectionEntry<'blog'>;
const post = Astro.props;
const { Content } = await render(post);
---
<BlogPost {...post.data} slug={Astro.params.slug!}>
<Content />
</BlogPost>
あとはセットアップ時に生成されたサンプルをいじって、自分好みのウェブを作ってみてください。