Astro 2025 - コンテンツファーストの次世代Webフレームワーク

2026.01.12

公式ドキュメント

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の「島」を配置する設計です。これにより圧倒的なパフォーマンスを実現します。

ページの大部分を高速な静的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 class="discounted">
 {(price * (1 - discount / 100)).toLocaleString()}<span class="badge">-{discount}%
 </>
 ) : (
 <span>{price.toLocaleString()}円
 )}
 </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>
 );
}
<!-- src/components/vue/Counter.vue -->
<script setup>
import { ref } from 'vue';

const props = defineProps({
 initialCount: {
 type: Number,
 default: 0,
 },
});

const count = ref(props.initialCount);
</script>

<template>
 <div class="counter vue-counter">
 <p>Vue Counter: {{ count }}</p>
 <button @click="count--">-</button>
 <button @click="count++">+</button>
 </div>
</template>
<!-- src/components/svelte/Counter.svelte -->
<script>
 export let initialCount = 0;
 let count = initialCount;
</script>

<div class="counter svelte-counter">
 <p>Svelte Counter: {count}</p>
 <button on:click={() => count--}>-</button>
 <button on:click={() => count++}>+</button>
</div>
---
// 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初期JSLCPTBTCLS
Astro 5.x99.20KB (デフォルト)0.8s0ms0.00
Next.js 1585.495KB1.4s120ms0.02
Nuxt 482.1110KB1.6s150ms0.03
SvelteKit 291.525KB1.0s30ms0.01
Gatsby 678.3180KB2.1s280ms0.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といった革新的な機能により、開発者は最高のユーザー体験を提供できるようになりました。

参考リソース

← 一覧に戻る
PR
PR
PR
PR