メインコンテンツへスキップ
#Web制作 約10分で読めます

Astro Database で CMS なしにリアルタイムデータを管理する実装ガイド【2026年新機能】

Astro DB の型安全なスキーマ定義からリアルタイムデータ更新まで、CMS なしで動的コンテンツを管理する実装方法を実例付きで解説。

従来の静的サイトジェネレーター(SSG)では、動的なデータ管理にはヘッドレス CMS やマイクロサービスを組み合わせる必要がありました。しかし Astro の公式データベースソリューション「Astro DB」の登場により、CMS なしでリアルタイムにデータを管理し、ビルド時・実行時の両方で活用できる環境が整いました。

本記事では、Astro DB の型安全なスキーマ定義から、ローカル開発環境でのデータ操作、本番環境へのデプロイとリアルタイムデータ更新の実装方法まで、実際のコード例を交えて解説します。

Astro DB とは何か

Astro DB は、Astro 公式が提供する SQL ベースのリレーショナルデータベースです。Turso(libSQL)をベースにしており、以下の特徴があります。

  • 型安全なスキーマ定義: TypeScript でテーブル構造を定義し、コード補完とバリデーションが効く
  • ローカル開発環境: SQLite ベースのローカルデータベースで、npm スクリプトで即座にセットアップ可能
  • Astro Studio との統合: Astro の公式ホスティングプラットフォーム Astro Studio でホストされ、デプロイが簡単
  • ビルド時・実行時の両対応: SSG のビルド時にデータを取得しつつ、サーバーサイドレンダリング(SSR)やサーバーアクションでリアルタイムに更新も可能

従来の Headless CMS(Contentful, Strapi など)と比較した場合、Astro DB は以下の点で優位性があります。

項目Astro DBHeadless CMS
セットアップnpm install 1 コマンド外部サービス登録・API キー設定が必要
型安全性TypeScript でスキーマ定義、完全な型推論API レスポンスは any 型になりがち
コスト無料枠が大きい(Astro Studio Free: 1GB ストレージ)無料枠が限定的
データ移行SQL ダンプで容易ベンダーロックインのリスク

ただし、Astro DB は リレーショナルデータ向けであり、リッチテキストエディタや画像管理 UI などは提供されません。編集者向け UI が必要な場合は Tina CMS などと併用する選択肢もあります。

Astro DB のセットアップ手順

プロジェクトへのインストール

Astro DB を既存プロジェクトに追加するには、公式の CLI を使用します。

npx astro add db

このコマンドは以下の処理を自動で実行します。

  1. @astrojs/db パッケージのインストール
  2. astro.config.mjs へのインテグレーション追加
  3. db/config.tsdb/seed.ts の雛形ファイル生成

スキーマ定義

db/config.ts でテーブル構造を定義します。以下は、ブログ記事と著者を管理する例です。

import { defineDb, defineTable, column } from 'astro:db';

const Author = defineTable({
  columns: {
    id: column.number({ primaryKey: true }),
    name: column.text(),
    email: column.text({ unique: true }),
    createdAt: column.date({ default: new Date() }),
  }
});

const Post = defineTable({
  columns: {
    id: column.number({ primaryKey: true }),
    title: column.text(),
    slug: column.text({ unique: true }),
    content: column.text(),
    published: column.boolean({ default: false }),
    authorId: column.number({ references: () => Author.columns.id }),
    publishedAt: column.date({ optional: true }),
  }
});

export default defineDb({
  tables: { Author, Post }
});

型定義のポイント:

  • column.number(), column.text(), column.date(), column.boolean() などの型が用意されている
  • references: () => Author.columns.id で外部キー制約を定義
  • unique: true, optional: true, default などのオプションで制約を指定

初期データの投入

db/seed.ts で開発用のシードデータを用意します。

import { db, Author, Post } from 'astro:db';

export default async function seed() {
  await db.insert(Author).values([
    { id: 1, name: 'Alice Smith', email: 'alice@example.com' },
    { id: 2, name: 'Bob Johnson', email: 'bob@example.com' },
  ]);

  await db.insert(Post).values([
    {
      id: 1,
      title: 'Getting Started with Astro DB',
      slug: 'astro-db-intro',
      content: '...',
      published: true,
      authorId: 1,
      publishedAt: new Date('2026-04-15'),
    },
    {
      id: 2,
      title: 'Real-time Data with Astro',
      slug: 'astro-realtime-data',
      content: '...',
      published: false,
      authorId: 2,
    },
  ]);
}

ローカル環境でシードを実行するには以下のコマンドを使用します。

npx astro db push --seed

このコマンドは、スキーマをローカルの SQLite データベースに反映し、シードデータを投入します。

ビルド時のデータ取得(SSG)

Astro DB は、静的サイト生成時にデータベースからデータを取得し、ページを事前レンダリングできます。

記事一覧ページの例

---
// src/pages/blog/index.astro
import { db, Post, Author, eq } from 'astro:db';

const posts = await db.select()
  .from(Post)
  .innerJoin(Author, eq(Post.authorId, Author.id))
  .where(eq(Post.published, true))
  .orderBy(Post.publishedAt, 'desc');
---

<html>
<head>
  <title>Blog Posts</title>
</head>
<body>
  <h1>Latest Posts</h1>
  <ul>
    {posts.map(({ Post: post, Author: author }) => (
      <li>
        <a href={`/blog/${post.slug}`}>{post.title}</a>
        <p>By {author.name} on {post.publishedAt?.toLocaleDateString()}</p>
      </li>
    ))}
  </ul>
</body>
</html>

ポイント:

  • db.select().from(Post) で SQL ライクなクエリを記述
  • innerJoin, where, orderBy などの Drizzle ORM ベースの API を使用
  • TypeScript の型推論が効くため、post.title などのフィールドアクセスに補完が効く

動的ルートでの記事詳細ページ

---
// src/pages/blog/[slug].astro
import { db, Post, Author, eq } from 'astro:db';

export async function getStaticPaths() {
  const posts = await db.select().from(Post).where(eq(Post.published, true));
  return posts.map(post => ({
    params: { slug: post.slug },
    props: { post },
  }));
}

const { post } = Astro.props;
const [author] = await db.select().from(Author).where(eq(Author.id, post.authorId));
---

<html>
<head>
  <title>{post.title}</title>
</head>
<body>
  <article>
    <h1>{post.title}</h1>
    <p>By {author.name}</p>
    <div set:html={post.content} />
  </article>
</body>
</html>

ビルド時にすべての記事パスが生成され、完全な静的サイトとして配信されます。

リアルタイムデータ更新の実装(SSR + Server Actions)

Astro DB の真価は、SSR モードと組み合わせてリアルタイムにデータを更新できる点にあります。以下は、記事のいいね数をリアルタイムにカウントする実装例です。

スキーマの拡張

// db/config.ts に Like テーブルを追加
const Like = defineTable({
  columns: {
    id: column.number({ primaryKey: true }),
    postId: column.number({ references: () => Post.columns.id }),
    count: column.number({ default: 0 }),
  }
});

export default defineDb({
  tables: { Author, Post, Like }
});

API エンドポイントの作成

// src/pages/api/like.ts
import type { APIRoute } from 'astro';
import { db, Like, eq } from 'astro:db';

export const POST: APIRoute = async ({ request }) => {
  const { postId } = await request.json();

  const [existing] = await db.select().from(Like).where(eq(Like.postId, postId));

  if (existing) {
    await db.update(Like)
      .set({ count: existing.count + 1 })
      .where(eq(Like.postId, postId));
  } else {
    await db.insert(Like).values({ postId, count: 1 });
  }

  const [updated] = await db.select().from(Like).where(eq(Like.postId, postId));
  return new Response(JSON.stringify({ count: updated.count }), {
    status: 200,
    headers: { 'Content-Type': 'application/json' }
  });
};

フロントエンドからの呼び出し

---
// src/pages/blog/[slug].astro(一部抜粋)
const { post } = Astro.props;
const [likeData] = await db.select().from(Like).where(eq(Like.postId, post.id));
const initialCount = likeData?.count || 0;
---

<article>
  <h1>{post.title}</h1>
  <button id="like-btn" data-post-id={post.id}>
    👍 <span id="like-count">{initialCount}</span>
  </button>
</article>

<script>
  document.getElementById('like-btn')?.addEventListener('click', async (e) => {
    const postId = (e.currentTarget as HTMLElement).dataset.postId;
    const res = await fetch('/api/like', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ postId: Number(postId) })
    });
    const { count } = await res.json();
    document.getElementById('like-count')!.textContent = count;
  });
</script>

このように、API エンドポイントを経由してデータベースを更新し、クライアント側で即座に UI に反映できます。

Astro Studio へのデプロイ

本番環境のセットアップ

Astro Studio は、Astro 公式が提供するホスティングプラットフォームで、Astro DB を本番環境で簡単に利用できます。

  1. Astro Studio アカウント作成: https://studio.astro.build でサインアップ
  2. プロジェクトのリンク:
npx astro login
npx astro link
  1. 本番データベースへのプッシュ:
npx astro db push --remote

このコマンドで、ローカルで定義したスキーマが本番環境のデータベースに反映されます。

環境変数の設定

Vercel や Netlify などのホスティングサービスでデプロイする場合、以下の環境変数を設定します。

ASTRO_STUDIO_APP_TOKEN=your_token_here

この token は astro link 実行時に生成され、.env ファイルに自動で記録されます。

デプロイフロー図

flowchart TD
    A[ローカル開発] -->|astro db push| B[ローカル SQLite DB]
    A -->|astro db push --remote| C[Astro Studio DB]
    C -->|環境変数設定| D[Vercel/Netlify]
    D -->|ビルド時クエリ| C
    D -->|実行時クエリ| C
    E[ユーザー] -->|リクエスト| D
    D -->|静的/動的レスポンス| E

パフォーマンス最適化とキャッシング戦略

ビルド時データ取得のベストプラクティス

静的生成時にデータベースクエリを実行する場合、以下の点に注意します。

  • 必要なカラムのみ取得: db.select({ title: Post.title, slug: Post.slug }) のように明示的に指定
  • インデックスの活用: 頻繁に検索されるカラムには indexed: true を設定(Astro DB は自動的に primary key と unique にインデックスを作成)
  • ページネーション: 大量のデータがある場合は limit()offset() を使用
const pageSize = 10;
const page = 1;
const posts = await db.select()
  .from(Post)
  .where(eq(Post.published, true))
  .limit(pageSize)
  .offset((page - 1) * pageSize);

リアルタイムデータのキャッシング

API エンドポイントでは、頻繁にアクセスされるデータをキャッシュすることで、データベースへの負荷を軽減できます。

// src/pages/api/post/[id].ts
import type { APIRoute } from 'astro';
import { db, Post, eq } from 'astro:db';

const cache = new Map<number, { data: any; expires: number }>();

export const GET: APIRoute = async ({ params }) => {
  const postId = Number(params.id);
  const now = Date.now();

  if (cache.has(postId) && cache.get(postId)!.expires > now) {
    return new Response(JSON.stringify(cache.get(postId)!.data), {
      status: 200,
      headers: { 'Content-Type': 'application/json', 'X-Cache': 'HIT' }
    });
  }

  const [post] = await db.select().from(Post).where(eq(Post.id, postId));
  cache.set(postId, { data: post, expires: now + 60000 }); // 1分キャッシュ

  return new Response(JSON.stringify(post), {
    status: 200,
    headers: { 'Content-Type': 'application/json', 'X-Cache': 'MISS' }
  });
};

本番環境では、Cloudflare Workers KV や Redis などの外部キャッシュストアを使用することも検討してください。

まとめ

Astro DB を活用することで、以下のメリットが得られます。

  • CMS 不要: 外部サービスに依存せず、コード内で完結したデータ管理が可能
  • 型安全性: TypeScript でスキーマを定義し、エディタの補完とバリデーションが効く
  • 柔軟性: SSG とリアルタイムデータ更新の両方に対応し、用途に応じて使い分けられる
  • 低コスト: Astro Studio の無料枠が充実しており、小規模プロジェクトなら運用コストがほぼゼロ

ただし、以下の点には注意が必要です。

  • 編集 UI なし: 非技術者向けの管理画面が必要な場合は別途用意する必要がある
  • スケーラビリティ: 大規模トラフィックには Astro Studio の有料プランや別のデータベースへの移行が必要
  • バックアップ戦略: 定期的な SQL ダンプとバージョン管理が推奨される

Astro DB は、ブログ・ドキュメントサイト・ポートフォリオなど、中小規模のデータ駆動型サイトに最適なソリューションです。従来の Headless CMS と比較して、セットアップの手軽さと型安全性で大きなアドバンテージがあります。

参考リンク

#Astro #Astro DB #データベース #SSG #リアルタイムデータ
シェア