MDLR.STREAM

Astro Content Collections と Markdown で Blog を構築した

Last updated on

私はモジュラーシンセでリアルタイムに生成した作業用 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 CMSPayload 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 の多言語対応部分は以下の通りに編集。

astro.config.mjs
export default defineConfig({
    ...
    i18n: {
        defaultLocale: 'ja',
        locales: ['ja', 'en'],
        routing: {
			// デフォルトの言語でも、url に locale を含める設定。
			// 含めたくない場合は false に変更。
            prefixDefaultLocale: true,
            redirectToDefaultLocale: true,
        },
	},
	...
});

ルーティング

プロジェクトテンプレートの状態でルーティングを実装する例を以下に掲載します。ご自分のプロジェクトやディレクトリ構成に合うコードは、適宜 AI エージェントに生成してもらうといいと思います。

src/pages/ja/blog/[slug].astro
---
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>

あとはセットアップ時に生成されたサンプルをいじって、自分好みのウェブを作ってみてください。

Notes

  1. その代わり、 Ko-fi での投げ銭を歓迎しています。

  2. ただ、今回のように Contents Collections を使う構成だと、パースされた Markdown に直接 tailwindcss を当てられない。狭いスコープでスタイルを管理できるメリットは薄れる。