0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Astro + React + Tailwind + MDX + Vercel 構築メモ(2025/06版)

Last updated at Posted at 2025-06-29

この記事は、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

その後:

  1. https://vercel.com/import/git にアクセス
  2. GitHubリポジトリを選択(Vercel からみてレポジトリは公開候補)
  3. デプロイ完了 → 自動で独自URLが割り当てられる(https://my-project.vercel.app

ステップ 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つの設定ファイルが生成される:

  1. 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に含めなさい」という指示

  1. 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 スタイルで提供されている理由。

  1. Tailwind は CLI ツールとして独立して存在している
npx tailwindcss -i input.css -o output.css --watch

このとき Tailwind は、Node.js の CLI として起動され、設定ファイル(tailwind.config.js)を読み込む。つまり:Astro というフレームワークを経由せず、Node が直接 tailwind.config.js を解釈することもある

  1. さらに、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. と打つと titledescriptiondatetags が補完される
  • 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コンポーネント: SampleChartmdxchart.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>

修正のポイント

  1. await entry.render()entry.render() はコンポーネント関数をラップしたオブジェクトを返すが、その内部で実際にレンダリングを行う Content コンポーネントを取得するためには、await が必要。これにより、MDX やその中の React コンポーネントがレンダリングされる準備が整うまで Astro が待機します。
  2. const { Content } = ...entry.render() は { Content, ... } のようなオブジェクトを返す。この Content プロパティが、実際にマークダウンや MDX の本文をレンダリングするコンポーネント。
  3. <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

このステップで実現すること

  • タグによるクロスコンテンツフィルタ
  • 動的パス生成によるスケーラブルな構造
  • トップページや記事末尾からタグリンクを張ることも容易に
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?