正直に言うと、2023年にクライアントが初めて Airtable を CMS として使うように依頼してきたとき、冗談だと思いました。スプレッドシートアプリが本番ウェブサイトを動かす? しかし、この方法でサイトを半ダース以上構築してきた後 — Astro 使用のものもあれば Next.js 使用のものもあります — 考えを改めました。Airtable は、従来のヘッドレス CMS プラットフォームが完全に見落としている特定のプロジェクトに最適な落としどころを提供します。マーケティングチームは既に使い方を知っています。ほとんどのコンテンツモデリングに十分な柔軟性があります。そして API は非常にシンプルです。

しかし鋭い部分がないわけではありません。レート制限、添付ファイルの処理、リレーショナルデータの奇癖 — 2023年の「Airtable を CMS として」ブログ記事が決して教えてくれなかったことがたくさんあります。このガイドは、2026年にこのスタックで本番プロジェクトを配信して学んだすべてをカバーしています。

目次

Using Airtable as a CMS with Astro & Next.js in 2026

Airtable を CMS として実際に機能する理由

Airtable の最大の利点は技術的なものではなく、人間的なものです。コンテンツエディタは既に使い方を知っています。オンボーディングの摩擦がなく、忘れる新しいログインもなく、学ぶコンテンツモデリング UI もありません。スプレッドシートのようなインターフェースを開き、何かを入力すればウェブサイトに表示されます。

特定のユースケースで実際に優れている理由:

  • エディタの学習曲線がゼロ。 Google Sheets を使える人なら、Airtable を使えます。
  • 柔軟なスキーマ。 新しいフィールドを追加するのに 5秒かかります。マイグレーション、スキーマのデプロイはありません。
  • ビューとフィルターが組み込まれています。 エディタは開発者の助けなしにフィルターされたビュー、カンバンボード、ギャラリーを作成できます。
  • リレーショナルデータ。 フラットなスプレッドシートとは異なり、Airtable はリンクされたレコード、ルックアップ、ロールアップをサポートしています。
  • 無料ティアが十分にあります。 無料プランで 1,000 レコード/ベースと 1,000 API 呼び出し/月。Team プラン (2026年で月額$20/ユーザー) では 50,000 レコードと更に高い API 制限になります。

ポートフォリオサイト、イベントリスト、チームディレクトリ、製品カタログ、求人掲示板、小規模ブログで Airtable を CMS として使用してきました。すべてにおいて驚くほどうまく機能します。

Airtable を CMS として使うべきでない場合

痛みを避けてください。次の場合は Airtable を CMS として使用しないでください:

  • コンテンツレコードが ~10,000 を超える場合。 動作が遅くなり、API ページネーションが規模で本当の頭痛になります。
  • 埋め込みコンポーネント付きのリッチテキストが必要な場合。 Airtable の長いテキストフィールドは基本的な Markdown をサポートしていますが、Sanity や Contentful のように React コンポーネントやカスタムブロックを埋め込むことはできません。
  • コンテンツに対するきめ細かいパーミッションが必要な場合。 Airtable のパーミッションモデルはレコード単位ではなく、ベース単位とテーブル単位です。エディタ A がエディタ B の下書きを見るべきでない場合、悪い時間を過ごすことになります。
  • リアルタイムプレビューが必要な場合。 組み込みの下書き/プレビューワークフローはありません。フィルターされたビューとステータスフィールドでハックできますが、不安定です。
  • 画像変換が必要な場合。 Airtable の添付ファイル URL は一時的です (約 2時間後に期限切れになります)。別の画像パイプラインが必要になります。

小〜中規模のコンテンツサイトを超えるものについては、おそらく目的を絞ったヘッドレス CMS の方がいいでしょう。

コンテンツ用の Airtable ベースの設定

コードを書く前に、Airtable ベースを正しく設定してください。一般的なブログに使用する構造は以下の通りです:

ベース構造

Posts というテーブルを作成し、以下のフィールドを使用します:

フィールド名 フィールドタイプ 備考
Title 単一行テキスト プライマリフィールド
Slug 単一行テキスト URL セーフ、小文字
Body 長いテキスト (Markdown) リッチテキストフォーマッティング有効化
Excerpt 長いテキスト プレーンテキスト、1〜2 文
Published チェックボックス これで本番環境をフィルターします
Publish Date 日付 これで降順ソートします
Author Authors テーブルへのリンク リレーショナルリンク
Tags 複数選択 またはタグテーブルへのリンク
Featured Image 添付ファイル 単一画像
SEO Title 単一行テキスト オプションのオーバーライド
SEO Description 長いテキスト メタディスクリプション

Published がチェックされているレコードのみを表示するフィルターされたビュー「Published」を作成します。これが本番環境コンテンツです。

API セットアップ

  1. airtable.com/create/tokens に移動して、個人アクセストークンを作成します。
  2. data.records:read スコープを付与します (書き込みアクセスが必要な場合は data.records:write)。
  3. 使用している特定のベースにスコープを設定します。
  4. .env ファイルにトークンを保存します。コミットしないでください。
# .env
AIRTABLE_TOKEN=pat_xxxxxxxxxxxxx
AIRTABLE_BASE_ID=appXXXXXXXXXXXXXX

ベース ID は Airtable API ドキュメントまたはベースを表示するときの URL で見つけることができます。

Using Airtable as a CMS with Astro & Next.js in 2026 - architecture

Airtable を Astro に接続する

Astro は、コンテンツがほぼ静的な場合、Airtable で動作するサイトに最適なフレームワークです。Astro は デフォルトで静的 HTML にビルドするため、すべての Airtable データをビルド時に取得します。つまり、訪問者からの API 呼び出しがゼロで、本番環境でレート制限の懸念がありません。

Astro の詳細な経験を持っています。

SDK をインストール

npm install airtable

データ取得ユーティリティを作成

// src/lib/airtable.ts
import Airtable from 'airtable';

const base = new Airtable({ apiKey: import.meta.env.AIRTABLE_TOKEN })
  .base(import.meta.env.AIRTABLE_BASE_ID);

export interface Post {
  id: string;
  title: string;
  slug: string;
  body: string;
  excerpt: string;
  publishDate: string;
  featuredImage: { url: string; filename: string } | null;
  tags: string[];
}

export async function getPosts(): Promise<Post[]> {
  const records = await base('Posts')
    .select({
      view: 'Published',
      sort: [{ field: 'Publish Date', direction: 'desc' }],
    })
    .all();

  return records.map((record) => ({
    id: record.id,
    title: record.get('Title') as string,
    slug: record.get('Slug') as string,
    body: record.get('Body') as string,
    excerpt: record.get('Excerpt') as string,
    publishDate: record.get('Publish Date') as string,
    featuredImage: record.get('Featured Image')
      ? {
          url: (record.get('Featured Image') as any[])[0].url,
          filename: (record.get('Featured Image') as any[])[0].filename,
        }
      : null,
    tags: (record.get('Tags') as string[]) || [],
  }));
}

export async function getPostBySlug(slug: string): Promise<Post | undefined> {
  const records = await base('Posts')
    .select({
      view: 'Published',
      filterByFormula: `{Slug} = '${slug}'`,
      maxRecords: 1,
    })
    .all();

  if (records.length === 0) return undefined;
  const record = records[0];

  return {
    id: record.id,
    title: record.get('Title') as string,
    slug: record.get('Slug') as string,
    body: record.get('Body') as string,
    excerpt: record.get('Excerpt') as string,
    publishDate: record.get('Publish Date') as string,
    featuredImage: record.get('Featured Image')
      ? {
          url: (record.get('Featured Image') as any[])[0].url,
          filename: (record.get('Featured Image') as any[])[0].filename,
        }
      : null,
    tags: (record.get('Tags') as string[]) || [],
  };
}

Astro ページで使用する

---
// src/pages/blog/[slug].astro
import { getPosts, getPostBySlug } from '../../lib/airtable';
import Layout from '../../layouts/Layout.astro';

export async function getStaticPaths() {
  const posts = await getPosts();
  return posts.map((post) => ({
    params: { slug: post.slug },
  }));
}

const { slug } = Astro.params;
const post = await getPostBySlug(slug!);

if (!post) return Astro.redirect('/404');
---

<Layout title={post.title}>
  <article>
    <h1>{post.title}</h1>
    <time>{post.publishDate}</time>
    <div set:html={post.body} />
  </article>
</Layout>

それだけです。astro build では、すべてのポストが Airtable から取得され、静的 HTML にレンダリングされます。本番環境サイトは API 呼び出しをしません。

Airtable を Next.js に接続する

Next.js はより多くの柔軟性を提供します。generateStaticParams でビルド時に取得、サーバーコンポーネントでリクエスト時に取得、または ISR (増分静的再生成) を使用して両方の長所を得ることができます。

Next.js サイトを多く構築しています。

Fetch ユーティリティ (Next.js バージョン)

Next.js では SDK ではなく Airtable REST API を fetch で直接使用することを推奨します。Next.js の fetch 拡張機能でキャッシングをコントロールできます。

// lib/airtable.ts
const AIRTABLE_TOKEN = process.env.AIRTABLE_TOKEN!;
const AIRTABLE_BASE_ID = process.env.AIRTABLE_BASE_ID!;

const headers = {
  Authorization: `Bearer ${AIRTABLE_TOKEN}`,
  'Content-Type': 'application/json',
};

export async function fetchPosts() {
  const url = new URL(
    `https://api.airtable.com/v0/${AIRTABLE_BASE_ID}/Posts`
  );
  url.searchParams.set('view', 'Published');
  url.searchParams.set('sort[0][field]', 'Publish Date');
  url.searchParams.set('sort[0][direction]', 'desc');

  const res = await fetch(url.toString(), {
    headers,
    next: { revalidate: 60 }, // ISR: 60秒ごとに再検証
  });

  if (!res.ok) throw new Error(`Airtable API error: ${res.status}`);

  const data = await res.json();
  return data.records.map((record: any) => ({
    id: record.id,
    title: record.fields['Title'],
    slug: record.fields['Slug'],
    body: record.fields['Body'],
    excerpt: record.fields['Excerpt'],
    publishDate: record.fields['Publish Date'],
    tags: record.fields['Tags'] || [],
  }));
}

ISR ページ (App Router 付き)

// app/blog/[slug]/page.tsx
import { fetchPosts } from '@/lib/airtable';
import { notFound } from 'next/navigation';

export async function generateStaticParams() {
  const posts = await fetchPosts();
  return posts.map((post: any) => ({ slug: post.slug }));
}

export default async function BlogPost({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const posts = await fetchPosts();
  const post = posts.find((p: any) => p.slug === slug);

  if (!post) notFound();

  return (
    <article>
      <h1>{post.title}</h1>
      <time>{post.publishDate}</time>
      <div dangerouslySetInnerHTML={{ __html: post.body }} />
    </article>
  );
}

revalidate: 60 の場合、Next.js はキャッシュされたページを提供し、最大 60秒ごとにバックグラウンドで更新します。エディタが Airtable を更新すると、サイトは 1分以内に更新されます。webhook のセットアップはなく、ビルドの再構築トリガーもありません。

画像と添付ファイルの処理

これは Airtable を CMS として使用する際の最大の落とし穴です。Airtable 添付ファイル URL は期限切れになります。 署名された URL で、約 2時間後に無効になります。これらを HTML で直接レンダリングすると、壊れます。

オプションは以下の通りです:

オプション 1: ビルド時にダウンロード (Astro)

静的サイトの場合、ビルド中に画像をダウンロードしてローカルで提供します:

import fs from 'fs/promises';
import path from 'path';

async function downloadImage(url: string, filename: string) {
  const res = await fetch(url);
  const buffer = Buffer.from(await res.arrayBuffer());
  const outputPath = path.join('public', 'images', 'cms', filename);
  await fs.mkdir(path.dirname(outputPath), { recursive: true });
  await fs.writeFile(outputPath, buffer);
  return `/images/cms/${filename}`;
}

オプション 2: CDN 経由でプロキシ

Cloudflare Worker または Vercel Edge Function を設定して、Airtable 画像 URL をプロキシ、キャッシュ、独自のドメイン経由で提供します。Astro と Next.js の両方に対応しています。

オプション 3: 別の画像ホストを使用

Cloudinary、Imgix、または S3 バケットに画像をアップロードし、永続 URL をテキストフィールドに保存します。Airtable の添付ファイルフィールドは使用しません。これは本番サイトにお勧めします — 最も信頼できるアプローチです。

キャッシング、レート制限、パフォーマンス

Airtable の API には厳しいレート制限があります: ベースあたり 1秒あたり 5リクエスト。 これは多くはありません。以下は、これをはるかに下回る方法です。

戦略 フレームワーク 動作
静的生成 Astro すべての API 呼び出しはビルド時に発生します。ランタイム呼び出しはゼロです。
ISR Next.js キャッシュされた応答は、タイマーで再検証されます。
メモリ内キャッシュ 両方 TTL 付きの Map で API 応答をキャッシュします。
Webhook + 再構築 両方 Airtable オートメーションが Vercel/Netlify ビルドをトリガーします。
Redis/KV キャッシュ Next.js (Vercel) Vercel KV または Upstash Redis に API 応答を保存します。

Astro サイトの場合、ビルド時のアプローチは、デプロイ中にのみ API にヒットすることを意味します。ISR 付き Next.js の場合、ページあたり最大 1回再検証間隔でヒットします。

ページが多く再検証間隔が短い場合、ページごとの API 呼び出しではなく、すべてのレコードを一度に取得し、データセット全体をキャッシュすることを検討してください。

ページネーション は重要です

Airtable はリクエストあたり最大 100レコードを返します。SDK の .all() メソッドはページネーションを自動的に処理しますが、fetch を直接使用している場合は、offset トークンを従う必要があります:

async function fetchAllRecords(tableName: string) {
  let allRecords: any[] = [];
  let offset: string | undefined;

  do {
    const url = new URL(
      `https://api.airtable.com/v0/${AIRTABLE_BASE_ID}/${tableName}`
    );
    url.searchParams.set('view', 'Published');
    if (offset) url.searchParams.set('offset', offset);

    const res = await fetch(url.toString(), { headers });
    const data = await res.json();

    allRecords = [...allRecords, ...data.records];
    offset = data.offset;
  } while (offset);

  return allRecords;
}

リッチテキストと Markdown コンテンツ

Airtable の長いテキストフィールドは「リッチテキスト」オプションを有効にしている場合、Markdown を保存できます。しかし、API から得られるのは Markdown でフォーマットされたテキスト、HTML ではありません。

変換する必要があります。シンプルな場合は marked を使用し、より細かくコントロールする場合は unifiedremark プラグインを使用します:

import { marked } from 'marked';

const htmlContent = marked.parse(post.body);

Astro では、組み込みの Markdown 処理も使用できます:

---
import { marked } from 'marked';
const html = marked.parse(post.body);
---
<article set:html={html} />

注意すべき点: Airtable のリッチテキストエディタは独自の Markdown フレーバーを生成します。太字、斜体、リンク、見出し、リストはうまく処理されます。コードブロックとテーブルはサポートされていますが、不安定な場合があります。コンテンツに複雑なフォーマットが必要な場合は、エディタにプレーン Markdown モードで書くよう指示することを検討してください。

Airtable と従来のヘッドレス CMS オプションの比較

トレードオフについて本当のことを言いましょう。2026年の Airtable は目的を絞ったヘッドレス CMS プラットフォームと比較してどのようにスタックされるでしょうか:

機能 Airtable Sanity Contentful Strapi
エディタ学習曲線 非常に低い 中程度 中程度 中程度
コンテンツモデリング 柔軟で非形式的 優れている 優れている 良好
API レート制限 ベースあたり 5 req/s 寛容 (CDN) 寛容 (CDN) 自己ホスト
画像処理 URL の有効期限切れ 組み込み CDN 組み込み CDN 自己ホスト
プレビュー/下書き 手動 (チェックボックス) 組み込み 組み込み 組み込み
価格 (5人チーム) 月額$100 (Team) 無料ティア可能 月額$300+ 無料 (自己ホスト)
Webhook サポート オートメーション経由 組み込み 組み込み 組み込み
リッチテキスト品質 基本 Markdown Portable Text 構造化 リッチテキスト
リレーショナルコンテンツ リンクされたレコード References References Relations

Airtable はエディタ体験と柔軟性に勝ちます。画像処理、プレビューワークフロー、API の信頼性で規模になります。エディタが既に Airtable を使用している小〜中規模のサイトの場合? 固い選択です。コンテンツが多いサイト、複雑なワークフローがある場合? 本物の CMS を使ってください。

実世界のアーキテクチャパターン

ここは本番で使用したパターンです:

パターン 1: Astro を使用した完全静的 + ビルド Webhook

最適: マーケティングサイト、ポートフォリオ、<500 レコードのディレクトリ

  1. Astro はビルド時にすべての Airtable データを取得します。
  2. Airtable オートメーションはレコード更新時に Vercel/Netlify に webhook を送信します。
  3. サイトは 30〜60 秒以内に再構築されます。
  4. ビルド時に画像をダウンロード — URL 期限切れの問題はありません。

パターン 2: Next.js 付き ISR

最適: ブログ、カタログ、頻繁に更新されるサイト

  1. Next.js は ISR を使用してページを生成します (60〜300 秒ごとに再検証)。
  2. Airtable API は再検証ごとに一意のページあたり 1回呼ばれます。
  3. 画像は Cloudinary 経由でプロキシされるか CDN にダウンロードされます。
  4. エディタはフル再構築をトリガーせずに数分以内に更新を表示します。

パターン 3: Airtable + 補足 CMS

最適: 一部のコンテンツが Airtable に存在し、他のコンテンツがより豊富な編集を必要とするサイト

  1. 構造化データ (チームメンバー、イベント、製品) は Airtable に保持されます。
  2. ロングフォームコンテンツ (ブログポスト、ケーススタディ) は Sanity または Notion に移動します。
  3. フロントエンドはビルド時または ISR を使用して両方のソースから取得します。

このハイブリッドアプローチは考えるより一般的です。

Airtable からビルドをトリガー

Airtable には、webhook を発火できる組み込みオートメーションがあります。Posts テーブルの「When a record is updated」トリガーをセットアップし、デプロイプラットフォームのビルドフックに POST リクエストを送信します:

// Vercel デプロイフック
https://api.vercel.com/v1/integrations/deploy/prj_xxxx/yyyy

// Netlify ビルドフック
https://api.netlify.com/build_hooks/xxxxxxxxxxxx

オートメーションに 30秒の遅延を追加して、迅速な編集をバッチ処理します。

FAQ

Airtable を CMS として無料で使用できますか?

Airtable の無料プランには、ベースあたり 1,000レコードと月あたり 1,000 API 呼び出しが含まれています。小規模サイトには十分ですが、本格的な使用には Team プラン (2026年で月額$20/ユーザー) が必要になります。Team プランでは 50,000レコードと更に高い API 制限を取得します。

Airtable の有効期限切れ画像 URL をどのように処理しますか?

Airtable 添付ファイル URL は約 2時間後に期限切れになります。Astro で構築した静的サイトの場合、ビルド時に画像をダウンロードしてください。Next.js を ISR で使用している場合、Cloudinary などの CDN 経由で画像をプロキシするか、別の画像ホストサービスに画像 URL を保存し、Airtable のテキストフィールドとして参照してください。

Airtable は数百のポスト付きブログを処理できますか?

はい、一定の程度まで。Airtable は数百のレコードをうまく処理します。数千に達すると、API ページネーション とビルド時間が目立つようになります。1,000ポスト未満のブログの場合、うまく機能します。それ以上は、専用のヘッドレス CMS を検討してください。

Airtable は Notion より CMS として優れていますか?

彼らは異なる問題を解決します。Airtable はリレーショナルデータベースモデルのため、構造化データ (製品、イベント、チームメンバー) に優れています。Notion はブロックベースのエディタのため、ロングフォーム書き込みコンテンツに優れています。Airtable の API も Notion より成熟で高速です。

Airtable でプレビュー/下書き機能をセットアップするにはどうしますか?

「Draft」、「In Review」、「Published」などのオプションを含む「Status」単一選択フィールドを追加します。各ステータスの フィルターされたビューを作成します。本番サイトは「Published」ビューから取得します。プレビューの場合、認証で保護された「In Review」ビューから取得する別のプレビュートを作成してください。

Airtable SDK または REST API を直接使用すべきですか?

Astro では、ビルド時に取得しているため、公式 airtable npm パッケージが良く機能します。Next.js では、REST API を fetch で直接使用することをお勧めします — Next.js キャッシュディレクティブ (revalidatetags) をコントロールできます。SDK は Next.js の拡張 fetch オプションを理解していません。

Airtable が許可する最大 API 呼び出し数はいくらですか?

Airtable はベースごとに 1秒あたり 5リクエストのレート制限を施行します。これを超えると、429ステータスコードが返されます。Team プランでは、月次呼び出しの許容度が高くなりますが、1秒あたりのレート制限は同じままです。静的生成と ISR は API 使用量を最小化する最良の方法です。

同じプロジェクトで Astro と Next.js の両方で Airtable を使用できますか?

正確には同じプロジェクトではありませんが、複数のフロントエンドを共有の Airtable ベースで動作させることができます。一部のチームは Astro でマーケティングサイトを使用し、同じ Airtable ベースから読み取る Next.js でウェブアプリを実行しています。すべてのコンシューマー間の共有レート制限に注意してください。