概要
こんにちは千株式会社のえふてぃです。この記事はSEN Advent Calendar 2025の21日目の記事になります。
今回はコンテンツ駆動のウェブサイト向けフレームワーク「Astro」でブログを構築した振り返りについて書きます。
この記事では、実際に開発して躓いたポイントや解決策を、以下の内容で振り返ります。
- Astroを選んだ理由と実際の開発体験
- MDXコンポーネントによる表現力の向上
- ダークモード実装で躓いたポイントと解決策
- Mermaid図表の導入とハマったポイント
実際のブログ:https://ft-blog.blog/

使用バージョン: Astro 5.x / Node.js 22.x
1. なぜAstroを選んだのか
ブログを作るにあたり、Next.jsとAstroを比較検討しました。
Next.jsはSSR/SSG/ISRなど様々なレンダリング方式に対応した汎用フレームワークですが、ブログのような静的コンテンツ中心のサイトには機能過多に感じました。一方、AstroはSSG(静的サイト生成)を前提として設計されており、コンテンツ駆動のサイトに特化しています。
また、Next.jsはSPA(シングルページアプリケーション)アーキテクチャですが、AstroはMPA(マルチページアプリケーション)を採用しています。ブログではSPAの恩恵(ページ遷移の高速化など)よりも、各ページの軽量さとシンプルさを重視したかったため、Astroを選択しました。
1-1. Markdownがそのまま記事になる快適さ
Next.jsでブログを構築する場合、標準ではMDXをサポートしていないため、@next/mdxやnext-mdx-remote-clientなどのライブラリを追加でインストールし、設定を行う必要があります。
一方、Astroはオールインワンフレームワークとして最初からMarkdown/MDXに対応しており、追加ライブラリなしですぐに使い始められます。
---
title: '記事のタイトル'
description: '記事の説明'
pubDate: 'Oct 17 2024'
---
## 見出し
本文をMarkdownで書くだけ!
さらに、Content Collections という機能により、型安全に記事を管理できます。Content Collectionsは、Markdownや MDXファイルをスキーマ定義に基づいて管理する仕組みで、TypeScriptの型推論が効くため記事データの取り扱いが安全になります。
// content.config.tsで記事のスキーマを定義
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
const blog = defineCollection({
loader: glob({
base: './src/content/blog',
pattern: '20[0-9][0-9]/**/*.{md,mdx}'
}),
schema: ({ image }) =>
z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
heroImage: image().optional(),
categories: z.array(z.enum(['技術', '映画', '読書', 'グルメ', '日常', 'お酒', 'ショート', 'その他'])).optional(),
}),
});
export const collections = { blog };
スキーマに沿わない記事を書くとビルド時にエラーで教えてくれます。
1-2. 必要な時だけReactが使える柔軟性
「動的な機能が欲しくなったらどうするのか問題」については、Astroはアイランドアーキテクチャを採用しています。ページ全体は静的HTMLとして生成し、インタラクティブな部分だけを「島(Island)」として切り出してJavaScriptを読み込む仕組みです。ReactやVue、Svelteなど好きなフレームワークを必要な部分だけ使用できます。
---
// 静的な部分はAstroコンポーネントで
import Header from '../components/Header.astro';
// 動的な部分だけReactを使う
import CommentSection from '../components/CommentSection.jsx';
---
<Header />
<article>
<!-- 記事の内容 -->
</article>
<CommentSection client:visible />
client:visibleディレクティブを使用すると、コンポーネントが画面に表示されたタイミングでJavaScriptが読み込まれます。
2. MDXカスタムコンポーネントを作ってみた
ブログを書いていく中で、以下のような課題を感じていました。
- 会話形式の説明がしたい(Q&A形式など)
- 重要な情報を視覚的に強調したい
Markdownだけでは表現に限界があるため、カスタムコンポーネントを作成しました。
2-2. ChatBubble(チャット吹き出し)
会話形式で説明できるコンポーネントです。
<ChatBubble type="user">
質問内容をここに書きます
</ChatBubble>
<ChatBubble type="assistant">
回答内容をここに書きます
</ChatBubble>
FAQや対話形式の説明に最適です。アイコンも絵文字でカスタマイズできるようにしました。
2-3. InfoBox(情報ボックス)
重要な情報を目立たせたい時に使用するコンポーネントです。
<!-- ヒント(黄色) -->
<InfoBox type="tip">
読者に役立つヒントやテクニックを書く時に使います。
</InfoBox>
<!-- 注意(オレンジ) -->
<InfoBox type="warning">
ミスしやすいポイントや注意事項を強調したい時に使います。
</InfoBox>
用途に応じて色分けすることで、直感的に情報の種類を判断できるようにしてます。
2-5. 実装時に意識したポイント
- シンプルなAPI: プロパティは必要最小限に抑え、使いやすさを重視
- 視覚的な区別: 色やアイコンで一目で種類が分かるように設計
- レスポンシブ対応: スマホでも見やすいように調整
MDXコンポーネントの導入により、記事内の表現を調整できるように!
3. ダークモード実装の試行錯誤
ダークモードの基本的な実装はTailwind CSSのdarkMode: 'class'設定とCSS変数の定義で対応できます。ここでは実装中に躓いたポイントを中心に紹介します。
3-1. FOUC対策:ページ読み込み前にテーマを適用
FOUC(Flash of Unstyled Content)とは、ページ読み込み時に一瞬だけライトモードが表示されてしまう現象です。
これを防ぐために、is:inlineスクリプトを使用します。
// src/components/BaseHead.astro
<script is:inline>
(function() {
const getThemePreference = () => {
if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
return localStorage.getItem('theme');
}
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
}
return 'light';
};
const theme = getThemePreference();
if (theme === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
if (typeof localStorage !== 'undefined') {
localStorage.setItem('theme', theme);
}
})();
</script>
Astroのis:inlineディレクティブは、スクリプトをバンドルせずにHTMLに直接埋め込みます。これにより、JavaScriptのロード前に実行され、FOUCを防止できます。
3-2. トグルボタンの実装と躓いたポイント
最初に実装した際、スマホでダークモードが切り替わらない問題が発生しました。
原因: PC用とモバイル用で2つのボタンが表示されているのに、IDが重複していたことです。getElementByIdは最初の要素しか取得できないため、モバイル用ボタンにイベントリスナーが付いていない凡ミスをしてしまった。
修正前:
<button id="theme-toggle">
const themeToggle = document.getElementById('theme-toggle');
themeToggle.addEventListener('click', toggleTheme);
修正後:
<button class="theme-toggle">
document.querySelectorAll('.theme-toggle').forEach(button => {
button.addEventListener('click', toggleTheme);
});
基本的なミスでしたが、実際に躓いたポイントとして共有します。
3-3. アクセシビリティへの配慮
ダークモードの実装では、アクセシビリティも重要です。
aria属性の追加:
<button
class="theme-toggle"
aria-label="ダークモードとライトモードを切り替える"
aria-pressed="false"
>
-
aria-label: スクリーンリーダーでボタンの機能を説明 -
aria-pressed: ダークモードのオン/オフ状態を明示
カラーコントラストの確保:
WCAG AA準拠(通常テキスト4.5:1以上)を目指し、以下のコントラスト比を確保しました。
- テキスト:
#e0e0e0/ 背景:#0a1f14→ 13.05:1
4. Mermaid図表の導入
ブログで処理フローやシステム構成を説明する際、図表があると分かりやすいです。Mermaidを導入することで、テキストベースで図表を管理できるようになりました。
4-1. Mermaidとは
Mermaidは、テキストで図表を描けるJavaScriptライブラリです。
```mermaid
graph TD
A[開始] --> B{条件分岐}
B -->|Yes| C[処理1]
B -->|No| D[処理2]
C --> E[終了]
D --> E
```
上記のようなコードブロックを書くと、フローチャートが自動的にSVG形式で生成されます。フローチャート、シーケンス図、クラス図、ガントチャートなど様々な図表が作成可能です。
4-2. 導入に必要なパッケージ
npm install rehype-mermaid unist-util-visit
npm install -D playwright
npx playwright install # ブラウザバイナリのインストール
- rehype-mermaid: MermaidコードをSVGに変換するrehypeプラグイン
- unist-util-visit: ASTを走査するユーティリティ
- Playwright: サーバーサイドでMermaidをレンダリングするために必要
rehype-mermaidはビルド時にMermaid図をSVGに変換します。Mermaid自体はブラウザ上で動くライブラリのため、Node.js環境ではPlaywright(ヘッドレスブラウザ)を使って仮想的にブラウザ環境を作り出しています。
strategy: 'inline-svg'を指定することで、クライアント側にMermaidのJavaScriptライブラリを送る必要がなく、Astroの「Zero JavaScript」の思想を維持できます。
4-3. ハマったポイント:rehype-mermaidがそのまま動かない
問題: rehype-mermaidをインストールして設定に追加しただけでは、Mermaidコードブロックがただの文字列として表示される
原因: rehype-mermaidは<pre>タグにmermaidクラスが付いていることを期待しているが、Astroは自動でそのクラスを付けてくれない
解決策: カスタムrehypeプラグインを作成
// astro.config.mjs
import rehypeMermaid from 'rehype-mermaid';
import { visit, CONTINUE } from 'unist-util-visit';
// カスタムrehypeプラグイン
const addMermaidClass = () => (tree) => {
visit(tree, (node) => {
if (
node.type === 'element' &&
node.tagName === 'pre' &&
node.properties?.dataLanguage === 'mermaid'
) {
node.properties.className = [...(node.properties.className || []), 'mermaid'];
return CONTINUE;
}
});
};
4-4. astro.config.mjsの設定
export default defineConfig({
integrations: [
mdx({
syntaxHighlight: false, // 重要!
rehypePlugins: [
addMermaidClass, // 先にクラスを追加
[rehypeMermaid, { strategy: 'inline-svg' }] // 後でレンダリング
],
}),
],
markdown: {
syntaxHighlight: 'shiki', // Markdown用は有効
},
});
ポイント:
- MDXの
syntaxHighlight: falseでShikiとの競合を回避 - プラグインの順序が重要(クラス追加→Mermaidレンダリング)
- Markdown用のシンタックスハイライトは別途設定可能
まとめ
Astro導入のポイント
- 静的サイト・ブログにはNext.jsよりAstroが適している
- Content Collectionsによる型安全な記事管理が快適
- アイランドアーキテクチャで必要な部分だけ動的にできる
MDXコンポーネントのポイント
- カスタムコンポーネントで表現の幅が大きく広がる
- シンプルなAPIを心がけると使いやすい
- 色分けで情報の種類を直感的に伝えられる
ダークモード実装のポイント
- FOUC対策は
is:inlineスクリプトで必須 - 複数の同一要素には
classを使い、querySelectorAllで取得する - アクセシビリティ(aria属性、カラーコントラスト)も忘れずに
Mermaid導入のポイント
- カスタムrehypeプラグインでmermaidクラスを追加する必要がある
- Playwrightのブラウザバイナリのインストールを忘れずに
- MDXのシンタックスハイライトとの競合に注意
個人ブログを作ろうと思っている方は、ぜひAstro試してみてください。
そして22日の SEN Advent Calendar 2025 を飾るのは、べったけさんになります。どうぞお楽しみに!




