Astro 2025 - コンテンツ重視の静的サイト生成の決定版
2025年、Astroはコンテンツ駆動型Webサイト構築において圧倒的な存在感を示しています。Islands Architectureの先駆者として登場したAstroは、現在ではView Transitions、Content Layer API、Server Islandsといった革新的な機能を備え、パフォーマンスとデベロッパーエクスペリエンスの両立を実現しています。
本記事では、Astro 4.x/5.xの新機能から実践的なコード例まで、Astroを活用した次世代Web開発の全貌を解説します。
flowchart TB
subgraph Astro2025["Astro 2025 エコシステム"]
subgraph Core["コア機能"]
Islands["Islands Architecture"]
ViewTransitions["View Transitions"]
ContentLayer["Content Layer API"]
ServerIslands["Server Islands"]
end
subgraph Integrations["フレームワーク統合"]
React["React 19"]
Vue["Vue 3.5"]
Svelte["Svelte 5"]
Solid["Solid.js"]
end
subgraph Performance["パフォーマンス"]
ZeroJS["Zero JS Default"]
PartialHydration["Partial Hydration"]
Prerendering["Prerendering"]
end
end
Core --> Performance
Integrations --> Islands
Islands Architecture - Astroの核心
Islands Architecture(アイランドアーキテクチャ)は、Astroが先駆的に導入したフロントエンド設計パターンです。ページの大部分を高速な静的HTMLとしてレンダリングし、インタラクティブ性が必要な箇所にのみJavaScriptの「島」を配置します。
アイランドの基本概念
---
// src/pages/index.astro
// 静的なAstroコンポーネント - JavaScriptは送信されない
import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';
// インタラクティブなReactコンポーネント - 必要時のみハイドレート
import ImageCarousel from '../components/ImageCarousel.jsx';
import NewsletterForm from '../components/NewsletterForm.jsx';
---
<html>
<head>
<title>Astro Islands Demo</title>
</head>
<body>
<!-- 静的HTML - JSなし -->
<Header />
<main>
<article>
<h1>静的コンテンツ</h1>
<p>この部分は純粋なHTMLとして配信されます。</p>
</article>
<!-- Island: ビューポート表示時にハイドレート -->
<ImageCarousel client:visible />
<!-- Island: ブラウザがアイドル時にハイドレート -->
<NewsletterForm client:idle />
</main>
<!-- 静的HTML - JSなし -->
<Footer />
</body>
</html>
クライアントディレクティブの使い分け
Astroは5種類のクライアントディレクティブを提供し、コンポーネント単位でハイドレーション戦略を制御できます。
---
import Counter from '../components/Counter.jsx';
import Analytics from '../components/Analytics.jsx';
import Comments from '../components/Comments.jsx';
import VideoPlayer from '../components/VideoPlayer.jsx';
import HeavyChart from '../components/HeavyChart.jsx';
---
<!-- 即座にハイドレート - ファーストビューのインタラクション向け -->
<Counter client:load />
<!-- ブラウザがアイドル状態になったらハイドレート -->
<Analytics client:idle />
<!-- ビューポートに入ったらハイドレート - 遅延ロード向け -->
<Comments client:visible />
<!-- メディアクエリにマッチしたらハイドレート -->
<VideoPlayer client:media="(min-width: 768px)" />
<!-- 条件式がtrueになったらハイドレート -->
<HeavyChart client:only="react" />
| ディレクティブ | ハイドレーションタイミング | ユースケース |
|---|---|---|
client:load | ページ読み込み直後 | ファーストビューのCTA、ナビゲーション |
client:idle | ブラウザがアイドル時 | アナリティクス、非重要UI |
client:visible | ビューポート表示時 | コメント、下部コンテンツ |
client:media | メディアクエリ一致時 | レスポンシブ対応コンポーネント |
client:only | 指定フレームワークのみ | SSR不要なコンポーネント |
Content Layer API - Astro 5.xの革新
Astro 5.0で導入されたContent Layer APIは、あらゆるデータソースからコンテンツを統一的に管理できる強力な機能です。ローカルのMarkdownファイルだけでなく、CMS、API、データベースからもタイプセーフにコンテンツを取得できます。
コンテンツコレクションの定義
// src/content.config.ts
import { defineCollection, z } from 'astro:content';
import { glob, file } from 'astro/loaders';
// ブログ記事コレクション(ローカルMarkdown)
const blog = defineCollection({
loader: glob({
pattern: '**/*.{md,mdx}',
base: './src/content/blog'
}),
schema: ({ image }) => z.object({
title: z.string().max(100),
description: z.string().max(200),
publishDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
author: z.string(),
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false),
// 画像の最適化もスキーマで定義
heroImage: image().optional(),
heroImageAlt: z.string().optional(),
}),
});
// 著者情報コレクション(JSONファイル)
const authors = defineCollection({
loader: file('./src/data/authors.json'),
schema: z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
avatar: z.string().url(),
bio: z.string(),
social: z.object({
twitter: z.string().optional(),
github: z.string().optional(),
}).optional(),
}),
});
// プロジェクトコレクション(外部API)
const projects = defineCollection({
loader: async () => {
const response = await fetch('https://api.example.com/projects');
const data = await response.json();
return data.map((project: any) => ({
id: project.slug,
...project,
}));
},
schema: z.object({
title: z.string(),
description: z.string(),
status: z.enum(['active', 'completed', 'archived']),
technologies: z.array(z.string()),
url: z.string().url().optional(),
}),
});
export const collections = { blog, authors, projects };
カスタムローダーの実装
// src/loaders/notion-loader.ts
import type { Loader } from 'astro/loaders';
import { Client } from '@notionhq/client';
interface NotionLoaderOptions {
databaseId: string;
auth: string;
}
export function notionLoader(options: NotionLoaderOptions): Loader {
const notion = new Client({ auth: options.auth });
return {
name: 'notion-loader',
async load({ store, logger, parseData }) {
logger.info('Notionデータベースからコンテンツを取得中...');
const response = await notion.databases.query({
database_id: options.databaseId,
filter: {
property: 'Status',
select: { equals: 'Published' },
},
sorts: [
{ property: 'PublishDate', direction: 'descending' },
],
});
for (const page of response.results) {
if (!('properties' in page)) continue;
const props = page.properties;
// Notionのプロパティを変換
const data = await parseData({
id: page.id,
data: {
title: getNotionTitle(props.Title),
description: getNotionRichText(props.Description),
publishDate: getNotionDate(props.PublishDate),
tags: getNotionMultiSelect(props.Tags),
category: getNotionSelect(props.Category),
},
});
store.set(data);
}
logger.info(`${response.results.length}件のエントリを読み込みました`);
},
};
}
// src/content.config.ts での使用
import { notionLoader } from './loaders/notion-loader';
const articles = defineCollection({
loader: notionLoader({
databaseId: import.meta.env.NOTION_DATABASE_ID,
auth: import.meta.env.NOTION_TOKEN,
}),
schema: z.object({
title: z.string(),
description: z.string(),
publishDate: z.coerce.date(),
tags: z.array(z.string()),
category: z.string(),
}),
});
コレクションのクエリと利用
---
// src/pages/blog/index.astro
import { getCollection } from 'astro:content';
import BlogCard from '../../components/BlogCard.astro';
import Pagination from '../../components/Pagination.astro';
// 公開済み記事のみを取得し、日付順にソート
const allPosts = await getCollection('blog', ({ data }) => {
return import.meta.env.PROD ? !data.draft : true;
});
const sortedPosts = allPosts.sort(
(a, b) => b.data.publishDate.valueOf() - a.data.publishDate.valueOf()
);
// タグ別の記事数を集計
const tagCounts = sortedPosts.reduce((acc, post) => {
post.data.tags.forEach(tag => {
acc[tag] = (acc[tag] || 0) + 1;
});
return acc;
}, {} as Record<string, number>);
---
<html>
<head>
<title>ブログ一覧</title>
</head>
<body>
<main>
<h1>ブログ</h1>
<aside>
<h2>タグ</h2>
<ul>
{Object.entries(tagCounts).map(([tag, count]) => (
<li>
<a href={`/blog/tag/${tag}`}>{tag} ({count})</a>
</li>
))}
</ul>
</aside>
<section class="posts-grid">
{sortedPosts.map(post => (
<BlogCard post={post} />
))}
</section>
</main>
</body>
</html>
View Transitions - アプリライクなナビゲーション
Astroは、ブラウザネイティブのView Transitions APIを活用し、SPAのようなスムーズなページ遷移を実現します。JavaScriptフレームワークなしで、アプリライクなユーザー体験を提供できます。
View Transitionsの基本設定
---
// src/layouts/BaseLayout.astro
import { ViewTransitions } from 'astro:transitions';
---
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>{title}</title>
<!-- View Transitionsを有効化 -->
<ViewTransitions />
</head>
<body>
<slot />
</body>
</html>
カスタムアニメーションの定義
---
// src/pages/blog/[slug].astro
import { getCollection } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map(post => ({
params: { slug: post.id },
props: { post },
}));
}
const { post } = Astro.props;
const { Content } = await post.render();
---
<BaseLayout title={post.data.title}>
<article>
<!-- 固有のトランジション名を設定 -->
<img
src={post.data.heroImage}
alt={post.data.heroImageAlt}
transition:name={`hero-${post.id}`}
transition:animate="fade"
/>
<h1 transition:name={`title-${post.id}`}>
{post.data.title}
</h1>
<div transition:animate="slide">
<Content />
</div>
</article>
</BaseLayout>
<style>
/* カスタムアニメーション定義 */
@keyframes slide-in {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slide-out {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(-20px);
}
}
::view-transition-old(slide) {
animation: slide-out 0.3s ease-out;
}
::view-transition-new(slide) {
animation: slide-in 0.3s ease-out;
}
</style>
アイランドの状態保持
---
// ページ遷移後もコンポーネントの状態を維持
import AudioPlayer from '../components/AudioPlayer.jsx';
import ShoppingCart from '../components/ShoppingCart.jsx';
---
<!-- 音楽プレーヤーの再生状態を保持 -->
<AudioPlayer
client:load
transition:persist
transition:name="audio-player"
/>
<!-- カートの中身を保持 -->
<ShoppingCart
client:load
transition:persist
transition:persist-props
/>
Server Islands - 動的コンテンツの部分的SSR
Astro 5.0で導入されたServer Islandsは、静的ページ内に動的なサーバーレンダリングコンポーネントを埋め込む機能です。CDNでキャッシュ可能な静的部分と、パーソナライズされた動的部分を同一ページで共存させることができます。
flowchart TB
subgraph CDN["CDN (Cloudflare/Vercel)"]
StaticHTML["静的HTML<br/>キャッシュ済み"]
end
subgraph Origin["オリジンサーバー"]
ServerIsland1["Server Island 1<br/>ユーザー情報"]
ServerIsland2["Server Island 2<br/>リアルタイム価格"]
end
Browser["ブラウザ"]
Browser -->|1. 初回リクエスト| CDN
CDN -->|2. 静的HTML即時返却| Browser
Browser -->|3. Island fetch| Origin
Origin -->|4. 動的コンテンツ| Browser
Server Islandsの実装
---
// src/components/UserGreeting.astro
export const prerender = false; // Server Islandとして設定
import { getSession } from '../lib/auth';
const session = await getSession(Astro.cookies);
const user = session?.user;
---
{user ? (
<div class="user-greeting">
<img src={user.avatar} alt={user.name} class="avatar" />
<div class="user-info">
<p class="greeting">おかえりなさい、{user.name}さん</p>
<p class="membership">{user.membershipLevel}会員</p>
</div>
<div class="actions">
<a href="/dashboard">ダッシュボード</a>
<a href="/logout">ログアウト</a>
</div>
</div>
) : (
<div class="guest-greeting">
<p>ゲストさん、ようこそ</p>
<a href="/login" class="btn-primary">ログイン</a>
<a href="/register" class="btn-secondary">新規登録</a>
</div>
)}
---
// src/components/RealtimePrice.astro
export const prerender = false;
interface Props {
productId: string;
}
const { productId } = Astro.props;
// リアルタイム価格をAPIから取得
const response = await fetch(`https://api.example.com/prices/${productId}`);
const { price, discount, stock } = await response.json();
---
<div class="realtime-price">
<p class="current-price">
{discount > 0 ? (
<>
<span class="original">{price.toLocaleString()}円</span>
<span class="discounted">
{(price * (1 - discount / 100)).toLocaleString()}円
</span>
<span class="badge">-{discount}%</span>
</>
) : (
<span>{price.toLocaleString()}円</span>
)}
</p>
<p class="stock-status">
{stock > 10 ? '在庫あり' : stock > 0 ? `残り${stock}点` : '在庫切れ'}
</p>
</div>
---
// src/pages/products/[id].astro
import { getProduct } from '../../lib/products';
import BaseLayout from '../../layouts/BaseLayout.astro';
import UserGreeting from '../../components/UserGreeting.astro';
import RealtimePrice from '../../components/RealtimePrice.astro';
export const prerender = true; // 静的生成
const { id } = Astro.params;
const product = await getProduct(id);
---
<BaseLayout title={product.name}>
<header>
<!-- Server Island: ユーザー認証状態に応じた表示 -->
<UserGreeting server:defer>
<div slot="fallback" class="skeleton-user">
読み込み中...
</div>
</UserGreeting>
</header>
<main>
<article class="product-detail">
<!-- 静的コンテンツ - CDNでキャッシュ -->
<h1>{product.name}</h1>
<img src={product.image} alt={product.name} />
<p>{product.description}</p>
<!-- Server Island: リアルタイム価格 -->
<RealtimePrice server:defer productId={id}>
<div slot="fallback" class="skeleton-price">
価格を取得中...
</div>
</RealtimePrice>
<section class="specifications">
<h2>仕様</h2>
<table>
{Object.entries(product.specs).map(([key, value]) => (
<tr>
<th>{key}</th>
<td>{value}</td>
</tr>
))}
</table>
</section>
</article>
</main>
</BaseLayout>
マルチフレームワーク統合
Astroの大きな強みは、React、Vue、Svelte、Solidなど複数のUIフレームワークを同一プロジェクトで使用できることです。既存のコンポーネント資産を活かしながら、最適なツールを選択できます。
フレームワーク統合の設定
// astro.config.mjs
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
import vue from '@astrojs/vue';
import svelte from '@astrojs/svelte';
import solid from '@astrojs/solid-js';
export default defineConfig({
integrations: [
react(),
vue({
appEntrypoint: '/src/vue-app.ts',
}),
svelte(),
solid(),
],
// 各フレームワークのファイルパターンを指定(競合回避)
vite: {
resolve: {
conditions: ['import'],
},
},
});
フレームワーク別コンポーネント例
// src/components/react/Counter.jsx
import { useState } from 'react';
export default function Counter({ initialCount = 0 }) {
const [count, setCount] = useState(initialCount);
return (
<div className="counter react-counter">
<p>React Counter: {count}</p>
<button onClick={() => setCount(c => c - 1)}>-</button>
<button onClick={() => setCount(c => c + 1)}>+</button>
</div>
);
}
Svelte Counter: {count}
---
// src/pages/framework-demo.astro
import ReactCounter from '../components/react/Counter.jsx';
import VueCounter from '../components/vue/Counter.vue';
import SvelteCounter from '../components/svelte/Counter.svelte';
---
<html>
<head>
<title>マルチフレームワークデモ</title>
</head>
<body>
<h1>同一ページで複数フレームワークを使用</h1>
<section>
<h2>React (client:visible)</h2>
<ReactCounter client:visible initialCount={10} />
</section>
<section>
<h2>Vue (client:idle)</h2>
<VueCounter client:idle initialCount={20} />
</section>
<section>
<h2>Svelte (client:load)</h2>
<SvelteCounter client:load initialCount={30} />
</section>
</body>
</html>
パフォーマンス比較と最適化
フレームワーク別ベンチマーク(2025年版)
| フレームワーク | Lighthouse Score | 初期JS | LCP | TBT | CLS |
|---|---|---|---|---|---|
| Astro 5.x | 99.2 | 0KB (デフォルト) | 0.8s | 0ms | 0.00 |
| Next.js 15 | 85.4 | 95KB | 1.4s | 120ms | 0.02 |
| Nuxt 4 | 82.1 | 110KB | 1.6s | 150ms | 0.03 |
| SvelteKit 2 | 91.5 | 25KB | 1.0s | 30ms | 0.01 |
| Gatsby 6 | 78.3 | 180KB | 2.1s | 280ms | 0.05 |
Core Web Vitals最適化のベストプラクティス
---
// src/pages/optimized.astro
import { Image } from 'astro:assets';
import heroImage from '../assets/hero.jpg';
---
<html>
<head>
<!-- クリティカルCSSのインライン化 -->
<style is:inline>
.hero {
aspect-ratio: 16/9;
width: 100%;
}
</style>
<!-- プリロード指定 -->
<link rel="preload" href="/fonts/main.woff2" as="font" crossorigin />
<link rel="preconnect" href="https://api.example.com" />
</head>
<body>
<!-- 最適化された画像(自動srcset生成) -->
<Image
src={heroImage}
alt="ヒーローイメージ"
widths={[400, 800, 1200, 1600]}
sizes="(max-width: 800px) 100vw, 800px"
loading="eager"
class="hero"
/>
<!-- 遅延読み込みの画像 -->
<Image
src={heroImage}
alt="下部の画像"
loading="lazy"
decoding="async"
/>
</body>
</html>
astro:env による型安全な環境変数
// astro.config.mjs
import { defineConfig, envField } from 'astro/config';
export default defineConfig({
env: {
schema: {
// クライアントで利用可能
PUBLIC_SITE_URL: envField.string({
context: 'client',
access: 'public',
default: 'https://example.com',
}),
// サーバーのみ、シークレット
DATABASE_URL: envField.string({
context: 'server',
access: 'secret',
}),
API_KEY: envField.string({
context: 'server',
access: 'secret',
}),
// 数値型
CACHE_TTL: envField.number({
context: 'server',
access: 'public',
default: 3600,
}),
// 列挙型
LOG_LEVEL: envField.enum({
values: ['debug', 'info', 'warn', 'error'],
context: 'server',
access: 'public',
default: 'info',
}),
},
},
});
// 使用例 - 完全な型安全性
import { PUBLIC_SITE_URL } from 'astro:env/client';
import { DATABASE_URL, CACHE_TTL, LOG_LEVEL } from 'astro:env/server';
// 型エラー: DATABASE_URLはクライアントで利用不可
// import { DATABASE_URL } from 'astro:env/client'; // Error!
// 正しい使用
const siteUrl: string = PUBLIC_SITE_URL;
const cacheTtl: number = CACHE_TTL;
const logLevel: 'debug' | 'info' | 'warn' | 'error' = LOG_LEVEL;
実践的なプロジェクト構成
my-astro-project/
├── src/
│ ├── content/
│ │ ├── blog/ # Markdownブログ記事
│ │ └── docs/ # ドキュメント
│ ├── content.config.ts # コレクション定義
│ ├── components/
│ │ ├── astro/ # Astroコンポーネント
│ │ ├── react/ # Reactアイランド
│ │ ├── vue/ # Vueアイランド
│ │ └── common/ # 共有コンポーネント
│ ├── layouts/
│ │ ├── BaseLayout.astro
│ │ └── BlogLayout.astro
│ ├── pages/
│ │ ├── index.astro
│ │ ├── blog/
│ │ │ ├── index.astro
│ │ │ └── [slug].astro
│ │ └── api/ # APIエンドポイント
│ ├── styles/
│ │ └── global.css
│ └── lib/
│ ├── utils.ts
│ └── api.ts
├── public/
│ └── fonts/
├── astro.config.mjs
├── package.json
└── tsconfig.json
まとめ - Astro 2025の選択基準
Astroは以下のようなプロジェクトに最適です。
Astroを選ぶべきケース:
- コンテンツ重視のウェブサイト(ブログ、ドキュメント、マーケティングサイト)
- 極限のパフォーマンスが求められるプロジェクト
- 既存のReact/Vue/Svelteコンポーネントを活用したい場合
- SEOが重要なプロジェクト
- Core Web Vitalsの最適化が必須な場合
他のフレームワークを検討すべきケース:
- 高度にインタラクティブなSPAアプリケーション(Next.js、Nuxt推奨)
- リアルタイム性が全面的に必要なアプリ(SvelteKit、Remix推奨)
- 既存のモノリシックなReact/Vueアプリの拡張
2025年のWeb開発において、Astroはコンテンツファーストのアプローチで圧倒的なパフォーマンスを実現する選択肢として、確固たる地位を築いています。Islands Architecture、Content Layer API、Server Islands、View Transitionsといった革新的な機能により、開発者は最高のユーザー体験を提供できるようになりました。
参考リンク
- Astro 公式ドキュメント
- Astro 5.0 リリースノート
- Islands Architecture 解説
- View Transitions ガイド
- Content Collections API