この記事は、Astro + React + Tailwind CSS + MDX + Vercel を使って、静的サイトをゼロから構築し、公開するまでの一連の手順をまとめたものです(2025年6月時点の構成に対応)。
筆者は1990年代から2010年ごろまでウェブエンジニアとして活動しており、その後は現場を離れました。今回あらためて、モダンなフロントエンド技術が活かせそうな場面があり、手を動かして整理した記録が本構築メモです。
到達目標は下記の2つで、
- サイト構築基盤として Astro を採用し、Vercel 上で稼働させること
- アクセスに応じて、React 製の chart(各種グラフ)を含むコンテンツを、動的ルーティングで返す構成を構築する
記事の執筆時点でサイトは動作し、具体にコンテンツを入る直前の段階です。
本記事では、Astro の Content Collections や MDX、React コンポーネント統合、Tailwind の導入(v4未対応問題)、そして Vercel への公開までを、10のステップに分けて順を追って整理しました。
公式ドキュメントだけでは見落としがちな注意点(そもそも記載がなく試行錯誤したところ)や、バージョン差異による落とし穴にも随時言及し、つまずいたポイントごとに調査・検証結果を付記しています。
十数年ぶりにフロントエンド環境に戻った身としては、GitHub 連携が前提となった Vercel のような CDN インフラが、あまりに手軽に使えるようになっていたことに驚かされました。
このような経緯から、いまの段階では誤解や不備が残っているはずですが、同じように再挑戦を試みる方や、Astro 初体験の方にとって、実際に役立つ内容になれば本望です。
コメントやご指摘の方、いただけると大変助かります。
どうぞよろしくお願いいたします。
この構築メモがカバーする10ステップ
機能 | 実装ステップ |
---|---|
Node.js + Astro セットアップ | 1〜2 |
GitHub + Vercel + Tailwind | 3〜4 |
Content Collections 型管理 | 5 |
.mdx + React チャート統合 |
6〜7 |
記事一覧 + 動的ルート | 8〜9 |
タグフィルタ | 10 |
私のシステム環境:
- モデル名: Mac mini
- モデル識別子: Mac16,10
- モデル番号: MU9D3J/A
- チップ: Apple M4
- メモリ: 16 GB
- システムバージョン: macOS 15.5 (24F74)
ステップ 1:macOS に Node.js をインストールする
nvm
(Node Version Manager)を使う
理由:複数バージョンを切り替えられ、システム汚染が少ない。
1-1. nvm のインストール(1行でOK)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
※ 最新バージョン確認は:https://github.com/nvm-sh/nvm
1-2. .zshrc
に以下が入っているかを確認:
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
1-3. source ~/.zshrc
を実行して反映:
source ~/.zshrc
1-4. Node.js をインストール(安定版)
nvm install --lts
nvm use --lts
node -v
npm -v
ここまで確認できれば、Node.js + npm は zsh
環境でも問題なく使える状態です。
ステップ 2:Astroプロジェクトの作成
Node.js が入ったらすぐに:
cd ~/var/
npm create astro@latest my-project
- インストール中にプロジェクトテンプレートの種別を聞かれるので「minimal」を選択。
- ~/var/your-project-root に展開される
cd my-project
npm install
npm run dev
- ここまで一気にやって大丈夫。初回 npm install で導入されるパッケージは、プロジェクトテンプレートに依存する。
- ローカルで
http://localhost:4321
にアクセスして、Astro の初期ページが表示されたら準備完了。
説明
npm create astro@latest
で行われること:
- ユーザーにプロジェクトテンプレート(minimal / blog / docs など)を選ばせる。
- 選択したテンプレートに基づいて
package.json
を生成。 - その時点では Tailwind の依存関係は含まれていない。
npm install
- 生成された
package.json
に記載されたパッケージのみをインストールします。 - デフォルトでは Astro 本体(
astro
)や統合機能(React など)だけで、Tailwind は含まれません。 - (参考)この段階の package.json
{
"name": "datainstruments",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"astro": "^5.10.1"
}
}
操作
- 起動・停止ともターミナルで操作
- 起動:npm run dev
- 停止:Ctrl + C
この段階で完成するディレクトリ構成
/Users/your-project_root/
├─ public/ ← 画像やJSONなど静的ファイル
├─ src/
│ ├─ pages/
│ └─ index.astro(=サイトのトップ)
│ ├─ content/ ← Markdown記事を追加予定
│ └─ components/ ← Reactチャート部品などもここに
├─ package.json ← npmの依存リスト
├─ astro.config.mjs ← Astro設定
├─ node_modules/ ← npm installで生成されたライブラリ群
ステップ 3:GitHubにpush & Vercelに連携
先に、空のレポジトリ:my-project を作っておく。また、Vercel にアカウントを作成して、GitHub と紐づけておく。この辺は省略。
# GitHub 初期化
git init
git remote add origin git@github.com:yourname/my-project.git
git add .
git commit -m "initial commit"
git push -u origin main
その後:
- https://vercel.com/import/git にアクセス
- GitHubリポジトリを選択(Vercel からみてレポジトリは公開候補)
- デプロイ完了 → 自動で独自URLが割り当てられる(
https://my-project.vercel.app
)
- メモ:https://rokugeisha.com/my-project/ 的な、独自ドメイン配下にコンテンツを階層整理する運用は Vercel と馴染まない。
ステップ 4:Tailwindを導入
💡本来なら「npx astro add tailwind」 だけでサクッと終わるのだが、2025/06/25 現在、(この npx 〜は)バージョンの上で対応しない関係のパッケージを導入してしまうので、手動での導入が求められる。
4-0:(参考)パッケージの導入をやりなおす際の事前の清掃作業
手戻りが生じた場合は、おちついて完全清掃から再開すること。
# 依存パッケージとロックファイルを削除
rm -rf node_modules
rm package-lock.json
# キャッシュ系
rm -rf .astro
ゴミを残すとドツボにハマるので面倒がらずに行うこと。このあと、npm install する。各パッケージのバージョンについての対応関係を修正しようしているのだから、package.json が正確であるかどうかが全て。
4-1:手動でパッケージをインストールする
npm install @astrojs/tailwind
npm install -D tailwindcss@^3.4.1 postcss autoprefixer
@astrojs/tailwind と、tailwindcss は別物だから混乱しないように!
note: 表記構文分解:@astrojs/tailwind
部分 | 意味 |
---|---|
@astrojs |
npm スコープ(=パッケージ名の名前空間) |
tailwind |
スコープ内の個別パッケージ名 |
全体:@astrojs/tailwind
|
npm に公開されている公式の integration パッケージ名 |
@astrojs
とは?
- npm レジストリ上の組織(スコープ)、紛らわしい名称にメーカー名をつけるようなもの、たとえば、@BMW/325 のようなこと
- したがって、これは、Astro チームが管理している公式パッケージ群の意味
- 例:
@astrojs/react → React 統合
@astrojs/mdx → MDX 対応
@astrojs/tailwind → Tailwind CSS 統合
@astrojs/sitemap → サイトマップ自動生成
@astrojs/image → Astro 画像最適化
note: 各パッケージが導入された理由と役割
パッケージ | 導入目的 | install 手段 |
---|---|---|
astro |
フレームワーク本体 |
npm create astro@latest で自動 |
@astrojs/mdx |
.mdx ファイルの扱い |
npx astro add mdx or npm install
|
@astrojs/tailwind |
Astro の Tailwind 統合 | 手動で npm install
|
tailwindcss |
CSS フレームワーク本体 | npm install -D |
postcss |
Tailwind をビルド処理で通す | npm install -D |
autoprefixer |
ブラウザ互換用 prefix 付加 | npm install -D |
- これを踏まえて、あらためて導入コマンをどみると、@astro 印のパッケージと、tailwindcss 本体とがあり、それらを分けて npm install していることがわかる。
npm install @astrojs/tailwind
npm install -D tailwindcss@^3.4.1 postcss autoprefixer
- 結果的に、package.json はこうなっているはず。
{
"name": "my-project",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"astro": "^5.10.1",
"@astrojs/tailwind": "^4.0.0"
},
"devDependencies": {
"tailwindcss": "^3.4.1",
"postcss": "^8.4.38",
"autoprefixer": "^10.4.19"
}
}
2025/06/28 時点で、@astrojs/tailwind が v6 系にアップしているが、後方置換が取れているとみて、このまま受け入れる。
{
"name": "datainstruments",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/tailwind": "^6.0.2",
"astro": "^5.10.1"
},
"devDependencies": {
"autoprefixer": "^10.4.21",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.17"
}
}
参考:astro.config.mjs の初期状態
// @ts-check
import { defineConfig } from 'astro/config';
// https://astro.build/config
export default defineConfig({});
note: そもそも手動インストールする理由
npx astro add tailwind の内部に下記の記述があるらしい。このとき(Astro は) tailwindcss@latest
たる v4 を "正しく" 導入する。ところが、当の Astro 公式プラグイン(@astro/tailwind)は Tailwind v4 に未対応。想像するに、この記述は v4登場前まではワークしていたはずで、v4 登場に対して対応が遅れているものと考えられる。
npm install tailwindcss postcss autoprefixer @astrojs/tailwind
4-2:Tailwind の設定ファイルを生成
npx tailwindcss init -p
これで以下の2つの設定ファイルが生成される:
tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{astro,html,js,jsx,ts,tsx,md,mdx}'],
theme: {
extend: {},
},
plugins: [],
};
この config は、CSS適用対象のスコープを定義している:
-
content
はTailwind CSSが「どのファイル内のHTML/JSX/Astroコードに書かれたクラス名をCSSとして生成すべきか」という適用対象のスコープを定めます。 -
例えば、
content
に指定されていないファイルにTailwindクラスを書いても、それは最終的なCSSには含まれず、スタイルが適用されません。content: ['./src/**/*.{astro,html,js,jsx,ts,tsx,md,mdx}'],
の意味-
./src/
: プロジェクトルートのsrc
ディレクトリ以下を対象とする -
**
: 任意のサブディレクトリを再帰的に含める。(例:src/components/
,src/pages/
など) -
*
: ファイル名にマッチ -
{astro,html,js,jsx,ts,tsx,md,mdx}
: 拡張子が.astro
,.html
,.js
,.jsx
,.ts
,.tsx
,.md
,.mdx
のファイルを対象とするつまり、「
src
ディレクトリ以下の全てのサブディレクトリにある、これらの拡張子を持つファイル内で使用されているTailwindクラスを検出して、最終的なCSSに含めなさい」という指示
-
postcss.config.js
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
2025/06/28 現在下記のように改まっている。mjs 記法になっているので、上の js 記法に戻す
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
解説:
-
tailwindcss
: Tailwind 本体の処理 -
autoprefixer
: ブラウザベンダープレフィックスの自動付与(Safari対策など)
tailwind.config.js
は CommonJS 記法の範疇に収まっているので、新しい方が良いかくらいの意味合いで mjs に変更しないこと(どちらでも動くからどちらでもよいとするガイダンスもある)Astro は プロダクトのジェネレーションとしては、mjs(モダンJavaScript, ESModules)に属するが、本体の Node.js は、CJS/ESM 両対応している。
note: tailwind.config.js についてもう少し:
tailwind.config.js は CLI や PostCSS 経由で実行されるため、Node にとって読みやすい形式が望ましいとする考えがある。つまり、「Astro の外でも使われる可能性がある設定ファイル」だから、Node にとって読みやすい形式(= CommonJS)が望ましいというのが、tailwind.config.js
が (ESModulesでなく)CommonJS スタイルで提供されている理由。
- Tailwind は CLI ツールとして独立して存在している
npx tailwindcss -i input.css -o output.css --watch
このとき Tailwind は、Node.js の CLI として起動され、設定ファイル(tailwind.config.js
)を読み込む。つまり:Astro というフレームワークを経由せず、Node が直接 tailwind.config.js を解釈することもある
- さらに、PostCSS を通して呼ばれるケースもある
// postcss.config.js
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
ここで tailwindcss
プラグインは、**PostCSS のビルドパイプラインの中で tailwind.config.js
を探して読み込む。**このときも Astro ではなく、Node.js(CJS互換性ありきの環境) が直接呼びに行く
だから、CommonJS (module.exports
) のほうが「一番事故らない」
理由 | 詳細 |
---|---|
Node.js のデフォルトが今も CJS 寄り(特に .js 拡張子) |
.js だと import が構文エラーになる可能性 |
多くのツール(Vite, PostCSS, CLI)が .js 前提で探す |
.mjs を自動で探さないものもある |
クロスフレームワーク(Next.js, Nuxt, SvelteKit)間でも使い回せる | CommonJS にしておくと互換性が高い |
note: CSS 処理のフェーズ
フェーズ | 説明 | 関連ツール |
---|---|---|
PreCSS(プリプロセッサ) | CSSを書く前に、より豊かな文法で書く | Sass, Less, Stylus |
PostCSS(ポストプロセッサ) | 書かれたCSSに対して、変換や最適化を行う | PostCSS plugins |
実行時(ランタイム) | ブラウザが解釈・表示 | Chrome, Safari, Firefox 等 |
PostCSS は、「書いたCSSに対して自動変換・補完・最適化したい」
- 例:
- ベンダープレフィックスの自動追加(
autoprefixer
) - future CSS の記法を現在のブラウザで使えるようにする(
postcss-preset-env
) - Tailwind CSS のユーティリティクラス展開もこれ!
- ベンダープレフィックスの自動追加(
PostCSS の動作イメージ
/* 人力で書いた CSS */
body {
display: flex;
}
PostCSS + autoprefixer を通すと:
/* 出力される CSS(対応ブラウザに応じて) */
body {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
}
note: Tailwind 自体が PostCSS の plugin である
@tailwind base;
@tailwind components;
@tailwind utilities;
note: ここでざっくり振り返るSass vs Less の時代感
時期 | 状況 |
---|---|
2010〜2013年頃 | Sass(Ruby製)と Less(JavaScript製)が流行の双璧に |
デザイナー界隈 | Less の方が CSS に近くて馴染みやすい |
エンジニア・Rails界隈 | Sass 派多数(.scss 記法導入で CSS 互換も強化) |
結果 | 互換性・拡張性・ツール連携の強さで Sass が勝利傾向に |
2020年代以降 | PostCSS + Utility-First(Tailwind)で前提が変わる |
CSS ハンドコーディングが無理ゲーになって以降、いまはどうも第2世代らしい
4-3:astro.config.mjs
にプラグインを登録
import { defineConfig } from 'astro/config';
import tailwind from '@astrojs/tailwind';
export default defineConfig({
integrations: [tailwind()],
});
4-4:src/styles/global.css
を作成
@tailwind base;
@tailwind components;
@tailwind utilities;
4-5:使用例
例:src/pages/index.astro
---
import '../styles/global.css';
---
<html>
<body class="bg-slate-100 text-gray-900">
<h1 class="text-blue-600 text-2xl font-bold">Tailwind v3 有効!</h1>
</body>
</html>
4-6:起動して確認
npm run dev
ステップ 5:Astro に Content Collections を導入
5-1. Content Collections を使う準備
Astro の Content Collections は標準機能なので、追加のインストールは不要
5-2. ディレクトリを作成
mkdir -p src/content/articles
articles は任意のコレクション名である。複数カテゴリに分けたい場合は、notes, projects, news など用途別に分ける。
5-3. src/content/config.ts
を作成
import { defineCollection, z } from "astro:content";
const articles = defineCollection({
schema: z.object({
title: z.string(),
description: z.string(),
date: z.string(),
tags: z.array(z.string()).optional(),
}),
});
export const collections = {
articles,
};
ポイント:
-
z.object(...)
は Zod による型スキーマ - Markdown 側の
frontmatter
がこのスキーマに沿っているか検証される(型安全)
5-4. 記事ファイルを作成
touch src/content/articles/first-post.md
src/content/articles/first-post.md
---
title: "初めての投稿"
description: "Astro Content の確認"
date: "2025-06-25"
tags: ["intro"]
---
これは最初のコンテンツファイルです。
5-5. レイアウトファイルを作成(再利用用)
mkdir -p src/layouts
src/layouts/BaseLayout.astro
---
const { title = "デフォルトタイトル" } = Astro.props;
---
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>{title}</title>
</head>
<body>
<slot />
</body>
</html>
5-6. 記事を読み込むページを作成
touch src/pages/test.astro
src/pages/test.astro
---
import BaseLayout from '../layouts/BaseLayout.astro';
import { getEntryBySlug } from 'astro:content';
const entry = await getEntryBySlug('articles', 'first-post');
if (!entry) {
console.error("Content Collection entry not found: articles/first-post");
}
---
<BaseLayout title={entry?.data?.title || '記事が見つかりません'}>
{entry ? (
<>
<h1>{entry.data.title}</h1>
<p>{entry.data.description}</p>
{/* <Fragment set:html={entry.body} /> ← Markdown 本文を使う場合はこの対応が必要 */}
</>
) : (
<p>指定された記事は見つかりませんでした。</p>
)}
</BaseLayout>
.body を HTML としてレンダリングしたい場合、set:html={entry.body} を使う方法があります。.mdx を使って React コンポーネントを埋め込みたい場合は、別途 @astrojs/mdx を導入する必要があります(後述のステップ 6 参照)。
5-7. 実行して確認
npm run dev
ブラウザで以下の URL にアクセス:
http://localhost:4321/test
表示確認ポイント:
-
title
とdescription
が正しく表示される - Markdown 本文が未表示でも、エラーが出ていなければ OK
5-8. VSCode などでの型補完を確認
-
entry.data.
と打つとtitle
,description
,date
,tags
が補完される - YAML の書き間違いがあれば Astro 側でエラーになります
5-9. よくある発展方向(参考)
やりたいこと | 使用する関数 |
---|---|
一覧を取得 | getCollection('articles') |
動的ルート |
[slug].astro + getCollection()
|
タグ別絞り込み | filter(entry => entry.data.tags.includes('タグ名')) |
.mdx 利用 |
@astrojs/mdx 統合済みなのですぐに可 |
ステップ 6:Astro に MDX, React を導入して React + Markdown を統合
.mdx
は「Markdown + JSX」のハイブリッド形式で、Markdown 記事内に React コンポーネントを直接埋め込むことができる。Astro の場合、公式統合プラグイン @astrojs/mdx
を追加することで対応可能。
6-1. @astrojs/mdx
を導入する
npx astro add mdx
このコマンドは以下を自動で行います:
-
@astrojs/mdx
をdependencies
に追加 -
astro.config.mjs
にmdx()
を登録
6-2. @astrojs/react
を導入する
npx astro add react
このコマンドは以下を自動で行います:
-
@astrojs/react
をdependencies
に追加 -
astro.config.mjs
に react()
を登録 - tsconfig.json に、compilerOption を追記
6-2. astro.config.mjs
を確認(または追記)
astro.config.mjs
// @ts-check
import { defineConfig } from 'astro/config';
import tailwind from '@astrojs/tailwind';
import mdx from '@astrojs/mdx';
import react from '@astrojs/react';
// https://astro.build/config
export default defineConfig({
integrations: [tailwind(), mdx(), react()],
});
tsconfig.json
{
"extends": "astro/tsconfigs/strict",
"include": [
".astro/types.d.ts",
"**/*"
],
"exclude": [
"dist"
],
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "react"
}
}
6-3. .mdx
コンテンツファイルを作成
mkdir -p src/content/articles
touch src/content/articles/post-without_chart.mdx
src/content/articles/post-without-chart.mdx
---
title: "グラフなし投稿(MDX 基本表示)"
description: "React を埋め込まない状態での確認"
date: "2025-06-26"
tags: ["mdx"]
---
これは `.mdx` を使った記事です。
通常の Markdown 記法と同じように見出しやリストも使えます:
## サブ見出し
- リスト1
- リスト2
この状態では、まだ React コンポーネントは埋め込んでいません。
6-4. .astro
ページで表示テスト
src/pages/mdxtest.astro
---
import { getEntry } from 'astro:content';
const entry = await getEntry('articles', 'post-without-chart');
// RenderedContent は、Reactコンポーネントとして期待されるものを取得
// ここで entry.render() の結果が直接Reactコンポーネントであれば、そのまま使う
// もし、entry.render() が更にJSXを返す関数であれば、その関数を呼び出す必要がある
// AstroのContent Collectionsでは、通常 await entry.render() で得られるのは
// 既にレンダリング可能なComponentオブジェクトです。
const { Content } = entry?.render ? await entry.render() : { Content: undefined };
---
<html lang="ja">
<head>
<meta charset="utf-8" />
<title>{entry?.data?.title || 'No Title'}</title>
</head>
<body>
<h1>{entry?.data?.title}</h1>
<p>{entry?.data?.description}</p>
<h2>Rendered Content Debug:</h2>
<div style="border: 1px solid red; padding: 10px;">
{/* Content Component をJSXとして描画 */}
{Content ? <Content /> : '(本文がありませんでした)'}
</div>
{/* 通常の表示ロジック */}
{Content ? <Content /> : <p>本文がありません。</p>}
</body>
</html>
6-5. 表示テスト
npm run dev
ブラウザで以下にアクセス:
http://localhost:4321/mdxtest
成功していれば表示されるもの
- 記事タイトルと説明(Frontmatter)
- Markdown 形式で記述された
.mdx
本文(見出し、リストなど) - React コンポーネントは未使用で、純粋な MDX 表示確認
補足
💡 .mdx を .astro ページ内で使う場合、 は import された .mdx コンテンツを React コンポーネントのように扱う仕組み。Frontmatter の型検査・取得には Content Collections の恩恵を受けつつ、HTML 本文は entry.render 経由で JSX 形式で描画される。
ステップ 7:React コンポーネントを .mdx
記事内に埋め込む(20250628現在成功していない)
ここでは、前ステップで確認した .mdx
表示機構に、React コンポーネントを統合し、動的なチャートを記事内に埋め込む例を作成するが、
2025/06/28現在成功していない。
mdxtest.astro から、(内部で react コンポーネントを呼び出している)post-with-chart.mdx を呼び出した時に、描画されない。回避策として SampleChart.jsx をダイレクトに mdxtest.astro から呼び出す(配置する)と問題なく表示できている
7-1. React コンポーネント
src/components/SampleChart.jsx
// src/components/SampleChart.jsx
import {
Chart as ChartJS,
BarElement,
CategoryScale,
LinearScale,
Tooltip,
Legend,
} from 'chart.js';
import { Bar } from 'react-chartjs-2';
ChartJS.register(CategoryScale, LinearScale, BarElement, Tooltip, Legend);
export default function SampleChart() {
const data = {
labels: ['A', 'B', 'C'],
datasets: [
{
label: 'データセット',
data: [12, 19, 3],
backgroundColor: 'rgba(75,192,192,0.5)',
},
],
};
const options = {
responsive: true,
plugins: {
legend: {
position: 'top',
},
},
};
return (
<div>
<h2>サンプルグラフ</h2>
<Bar data={data} options={options} />
</div>
);
}
7-2. 【暫定】.mdx
記事ファイルに React コンポーネントを埋め込まない
src/content/articles/post-with-chart.mdx
---
title: "グラフ付き投稿(React埋め込み)"
description: "MDX 内に React コンポーネントを組み込んだ例"
date: "2025-06-27"
tags: ["mdx", "chart"]
---
これは `.mdx` を使った記事です。
以下にグラフが表示されるはずです。(これはastroファイルで挿入されます)
通常の Markdown 記法と同じように見出しやリストも使えます:
## サブ見出し
- リスト1
- リスト2
以下のように `.mdx` ファイル内に `<SampleChart />` を記述した場合:
<SampleChart />
しかし、これではクライアント側での描画が行われず、空白のままレンダリングされる。
7-3. .astro
ページで React コンポーネントを直接呼び出す
src/pages/mdxchart.astro
---
import { getEntry } from 'astro:content';
import SampleChart from '../components/SampleChart.jsx'; // グラフコンポーネントを直接インポート
const entry = await getEntry('articles', 'post-with-chart');
// MDXの本体(Markdown部分)をHTMLとして取得します
// .render() で Content を取得する代わりに、await entry.render() を呼び出して HTML を取得
// ただし、entry.render() は Content Component を返すので、set:html ではなく
// 実際には Markdown のHTML内容を直接使うことになります。
// ここでは、MDXの本文とチャートを分離してレンダリングする戦略を取ります。
const { Content } = entry?.render ? await entry.render() : { Content: undefined };
---
<html lang="ja">
<head>
<meta charset="utf-8" />
<title>{entry?.data?.title || 'No Title'}</title>
</head>
<body class="prose mx-auto p-6">
<h1>{entry?.data?.title}</h1>
<p>{entry?.data?.description}</p>
{entry ? (
<>
{/* MDXのMarkdown部分(Reactコンポーネントを含まない純粋なMarkdown)をレンダリング */}
{/* ここではContentコンポーネントをそのまま使いますが、
MDXファイルからReactコンポーネントの記述を削除します。 */}
<Content />
{/* ここでSampleChartを直接呼び出し、client:loadを適用 */}
<SampleChart client:load /> // ← client:load を明示的に指定しないと描画されない点がポイント
</>
) : (
<p>本文がありませんでした。</p>
)}
</body>
</html>
補足説明
現在のアプローチは、MDXコンテンツのレンダリングとReactチャートのレンダリングおよびハイドレーションを完全に分離します。
-
MDXは純粋なMarkdownとして機能:
.mdx
ファイルは、実質的にContent
にとっての.md
ファイル(Markdownファイル)として機能します。AstroのMDX統合(を使っているものの、現在は)、MarkdownをHTMLに変換する処理のみを行います。 -
明示的なReactコンポーネント:
SampleChart
をmdxchart.astro
に直接インポートし配置する。これはAstroファイル内の直接的なJSX要素であるため、MDXパーサーの内部動作との曖昧さや競合なしにclient:load
を適用できる。
Astro の MDX 統合機能では、.mdx 内に記述された React コンポーネントのハイドレーションが想定通りに機能しない場合がある。特に client:* ディレクティブの付与が .astro 側でのみ可能であるため、React コンポーネントの描画が失敗する。
そのため、本構成では .mdx ファイルを Markdown 相当として扱い、React チャートは .astro 側に明示的に配置する方法を採っている。
7-4. 動作確認
npm run dev
ブラウザで以下にアクセス:
http://localhost:4321/mdxchart
成功していれば表示されるもの
- 記事のタイトルと説明
- Markdown 本文
<SampleChart />
によるチャート描画(React 経由)
補足:この段階で達成されていること
機能 | 対応済 |
---|---|
.mdx の静的表示 |
ステップ 6 で確認済 |
React 組み込み | 今回のステップで実施 |
.astro → .mdx への props 渡し |
components={{ ... }} で明示的に渡す構成 |
ステップ 8:getCollection()
による記事一覧ページの作成
ここでは、Content Collections(例:articles
)に含まれる全記事を一覧表示する .astro
ページを作成します。
8-1. 複数の .md
/ .mdx
記事を準備
すでに以下のような記事ファイルが存在していれば OK:
src/content/articles/first-post.md
src/content/articles/post-without-chart.mdx
src/content/articles/post-with-chart.mdx
.md も .mdx も、Content Collections のスキーマに沿っていれば一覧取得可能。
8-2. 記事一覧ページを作成
touch src/pages/articles.astro
src/pages/articles.astro
---
import BaseLayout from '../layouts/BaseLayout.astro';
import { getCollection } from 'astro:content';
const articles = await getCollection('articles');
// 公開日降順に並べる(ISO 8601形式の日付を前提)
articles.sort((a, b) => b.data.date.localeCompare(a.data.date));
---
<BaseLayout title="記事一覧">
<h1 class="text-2xl font-bold mb-4">記事一覧</h1>
<ul class="space-y-4">
{articles.map((entry) => (
<li>
<h2 class="text-xl text-blue-600 font-semibold">
<a href={`/articles/${entry.slug}/`}>{entry.data.title}</a>
</h2>
<p class="text-gray-700">{entry.data.description}</p>
<p class="text-sm text-gray-500">公開日: {entry.data.date}</p>
</li>
))}
</ul>
</BaseLayout>
8-3. 動作確認
npm run dev
ブラウザで以下にアクセス:
http://localhost:4321/articles
成功していれば表示されるもの
- 複数の記事が、タイトル・説明・日付付きでリスト表示
-
.md
/.mdx
両方の記事が同列で扱われている
補足:ここで使った主な API の意味
API | 説明 |
---|---|
getCollection('articles') |
src/content/articles/ 以下の全 .md / .mdx を取得 |
entry.data |
Frontmatter の内容(スキーマで定義した構造) |
entry.slug |
ファイル名ベースのスラッグ(動的ルーティングで使用可能) |
entry.render |
.mdx ファイルなら React コンポーネントとしてレンダリング可能 |
ステップ 9:[slug].astro
による動的ルーティングで個別記事ページを生成
9-1. ファイルを作成:src/pages/articles/[slug].astro
mkdir -p src/pages/articles
touch src/pages/articles/\[slug\].astro
// 鉤括弧にエスケープ必要
9-2. .astro
ファイルにスラッグから記事を取得する処理を書く
src/pages/articles/[slug].astro
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import { getCollection } from 'astro:content';
import SampleChart from '../../components/SampleChart.jsx'; // SampleChart をインポート
export async function getStaticPaths() {
const articles = await getCollection('articles');
return articles.map((entry) => ({
params: { slug: entry.slug },
props: { entry },
}));
}
const { entry } = Astro.props;
// entry.render() を呼び出して、Content コンポーネントを取得
// MDX コンテンツのレンダリングは非同期になる可能性があるため、await を使用
const { Content } = entry?.render ? await entry.render() : { Content: undefined };
// post-with-chart.mdx の場合のみチャートを表示するためのフラグ
const showChart = entry.slug === 'post-with-chart';
---
<BaseLayout title={entry.data.title}>
<h1 class="text-2xl font-bold mb-4">{entry.data.title}</h1>
<p class="text-gray-700 mb-2">{entry.data.description}</p>
<p class="text-sm text-gray-500 mb-6">公開日: {entry.data.date}</p>
{/* Content コンポーネントをレンダリング */}
{Content && <Content />}
{/* post-with-chart の場合のみ SampleChart を表示 */}
{showChart && <SampleChart client:load />}
</BaseLayout>
修正のポイント
-
await entry.render()
:entry.render()
はコンポーネント関数をラップしたオブジェクトを返すが、その内部で実際にレンダリングを行うContent
コンポーネントを取得するためには、await
が必要。これにより、MDX やその中の React コンポーネントがレンダリングされる準備が整うまで Astro が待機します。 -
const { Content } = ...
:entry.render()
は{ Content, ... }
のようなオブジェクトを返す。このContent
プロパティが、実際にマークダウンや MDX の本文をレンダリングするコンポーネント。 -
<Content />
: 取得したContent
コンポーネントを JSX 構文でレンダリングする。Content && <Content />
のように条件付きでレンダリングすることで、Content
がundefined
の場合の安全性を高めている(通常は問題ないが、念のため)。
さらに補足
ステップ7の回避策
まず、ステップ7でReactコンポーネントの描画ができなかった問題に対する回避策は、mdx ファイル内に jsx オブジェクトを置かないこと。
-
.mdx
ファイル内に直接Reactコンポーネントを埋め込むとハイドレーションされない問題に対し、それをレンダリングする**.astro
ファイル(mdxtest.astro
)側でSampleChart.jsx
を直接インポートし、client:load
ディレクティブを明示的に付与**することで、Reactコンポーネントの描画とクライアント側でのJavaScript実行(ハイドレーション)を成功させた。
これは、Astroのアイランドアーキテクチャの原則に則った、堅実なアプローチである。MDXコンテンツと動的なReactコンポーネントの役割を分離し、Astroが最適な方法でそれぞれのレンダリングとインタラクティビティを管理できるようにした。
【重要】[slug].astro
での分岐処理の必要性
今回はこの考え方を動的なルーティング ([slug].astro
) に適用した形になる。
-
[slug].astro
は、複数の記事(今回は.md
と.mdx
)を単一のテンプレートでレンダリングします。 - しかし、
SampleChart
のような特定のReactコンポーネントは、特定の条件(例:post-with-chart
というスラッグの記事) でのみ表示したいもの。 - そのため、
entry.slug
を使って現在表示している記事がどの記事なのかを判別し、SampleChart
を表示すべきかどうかをプログラム的に分岐させる必要があった。つまり「内部的には分岐処理が必要」 であって、「この分岐構造を正確に制御しないと、[slug].astro
によるサイト運営は不可能(あるいは意図しない表示になる)」 と言えます。
9-3. 動作確認
npm run dev
アクセス例:
- 一覧:
http://localhost:4321/articles
成功していれば表示される内容
- 各記事のタイトル、説明、日付が
/articles/スラッグ
に展開される
このステップで実現すること
- ファイル名ベースのルーティングに頼らず、Frontmatter に従った動的ルーティング
-
.md
/.mdx
を問わず、Content Collections のslug
に基づいてページ生成 - 今後のフィルタや分類・ページネーションの土台となる構造
ステップ 10:タグによる記事フィルタ表示
10-1. 各記事に tags
を付与しておく
※ステップ 5 で作成した config.ts
のスキーマで tags: z.array(z.string()).optional()
が定義済みであれば OK。
例:first-post.md
---
title: "初めての投稿"
description: "Astro Content の確認"
date: "2025-06-25"
tags: ["intro", "basic"]
---
10-2. タグ一覧ページを作成
mkdir -p src/pages/tags
touch src/pages/tags/index.astro
src/pages/tags/index.astro
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import { getCollection } from 'astro:content';
const articles = await getCollection('articles');
// タグを集計
const tagMap = new Map();
for (const entry of articles) {
const tags = entry.data.tags || [];
for (const tag of tags) {
if (!tagMap.has(tag)) tagMap.set(tag, []);
tagMap.get(tag).push(entry);
}
}
---
<BaseLayout title="タグ一覧">
<h1 class="text-2xl font-bold mb-6">タグ一覧</h1>
<ul class="space-y-2">
{[...tagMap.entries()].map(([tag, entries]) => (
<li>
<a href={`/tags/${tag}/`} class="text-blue-600 hover:underline">
{tag} ({entries.length})
</a>
</li>
))}
</ul>
</BaseLayout>
10-3. タグごとの記事一覧ページを動的生成
touch src/pages/tags/\[tag\].astro
src/pages/tags/[tag].astro
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import { getCollection } from 'astro:content';
export async function getStaticPaths() {
const articles = await getCollection('articles');
const tags = new Set();
for (const entry of articles) {
(entry.data.tags || []).forEach(tag => tags.add(tag));
}
return [...tags].map(tag => ({
params: { tag }
}));
}
const { tag } = Astro.params;
const articles = (await getCollection('articles')).filter(
(entry) => entry.data.tags?.includes(tag)
);
articles.sort((a, b) => b.data.date.localeCompare(a.data.date));
---
<BaseLayout title={`タグ: ${tag}`}>
<h1 class="text-2xl font-bold mb-6">タグ: {tag}</h1>
<ul class="space-y-4">
{articles.map((entry) => (
<li>
<h2 class="text-xl text-blue-600 font-semibold">
<a href={`/articles/${entry.slug}/`}>{entry.data.title}</a>
</h2>
<p class="text-sm text-gray-500">{entry.data.date}</p>
<p class="text-gray-700">{entry.data.description}</p>
</li>
))}
</ul>
</BaseLayout>
10-4. 動作確認
npm run dev
- タグ一覧:
http://localhost:4321/tags
このステップで実現すること
- タグによるクロスコンテンツフィルタ
- 動的パス生成によるスケーラブルな構造
- トップページや記事末尾からタグリンクを張ることも容易に