我得坦白:当客户在 2023 年第一次要求我使用 Airtable 作为他们的 CMS 时,我以为他们在开玩笑。一个电子表格应用为生产网站提供动力?但在用这种方式构建了六十多个网站后 — 有些使用 Astro,有些使用 Next.js — 我改变了看法。Airtable 对某些传统无头 CMS 平台完全忽略的项目来说是一个完美的选择。你的营销团队已经知道如何使用它。它足够灵活来建模大多数内容。API 也非常简单。

但它确实有一些尖锐的边缘。速率限制、附件处理、关系数据的怪癖 — 有很多东西是 2023 年的"Airtable 作为 CMS"博客文章从未告诉你的。本指南涵盖了我在 2026 年使用这个堆栈发布真实项目时学到的所有内容。

目录

在 Astro 和 Next.js 中使用 Airtable 作为 CMS(2026 版)

为什么 Airtable 作为 CMS 其实是有意义的

支持 Airtable 的最大论点不是技术性的 — 而是人为的。你的内容编辑已经知道如何使用它。没有入职摩擦,没有新的登录要忘记,没有内容建模 UI 要学习。他们打开类似电子表格的界面,输入内容,它就显示在网站上。

以下是它对某些用例真正好的原因:

  • 编辑的学习曲线为零。 如果有人会使用 Google Sheets,他们就会使用 Airtable。
  • 灵活的架构。 添加新字段只需 5 秒钟。没有迁移,没有架构部署。
  • 内置的视图和筛选器。 编辑可以创建筛选视图、看板、画廊 — 所有这些都无需开发者帮助。
  • 关系数据。 与平面电子表格不同,Airtable 支持链接记录、查找和汇总。
  • 免费套餐足够慷慨。 免费计划中每个 base 1,000 条记录和 1,000 次 API 调用。Team 计划(2026 年每席位每月 $20)将您升级到 50,000 条记录和更高的 API 限制。

我已经使用 Airtable 作为 CMS 用于作品集网站、事件列表、团队目录、产品目录、招聘板和小博客。它对所有这些都出乎意料地有效。

你不应该何时使用 Airtable 作为 CMS

让我为你节省一些痛苦。如果出现以下情况,不要使用 Airtable 作为你的 CMS:

  • 你有超过约 10,000 条内容记录。 它变得迟缓,API 分页在大规模时成为真正的麻烦。
  • 你需要带嵌入式组件的富文本。 Airtable 的长文本字段支持基本的 Markdown,但你不能嵌入 React 组件或自定义块,就像你可以用 Sanity 或 Contentful 那样。
  • 你需要对内容的粒度权限。 Airtable 的权限模型是按 base 和按表的,而不是按记录。如果编辑 A 不应该看到编辑 B 的草稿,你会有个麻烦。
  • 你需要实时预览。 没有内置的草稿/预览工作流。你可以用筛选视图和状态字段来破解它,但它很不稳定。
  • 你需要图像转换。 Airtable 附件 URL 是临时的(它们在约 2 小时后过期)。你需要一个单独的图像管道。

对于超出小型到中型内容网站的任何东西,你可能最好使用专用无头 CMS。

为内容设置你的 Airtable Base

在编写任何代码之前,把你的 Airtable base 弄好。这是我用于典型博客的结构:

Base 结构

创建一个名为 Posts 的表,包含以下字段:

字段名 字段类型 备注
Title 单行文本 主字段
Slug 单行文本 URL 安全,小写
Body 长文本(Markdown) 启用富文本格式
Excerpt 长文本 纯文本,1-2 句
Published 复选框 在此筛选生产内容
Publish Date 日期 按此降序排序
Author 链接到 Authors 表 关系链接
Tags 多选 或链接到 Tags 表
Featured Image 附件 单个图像
SEO Title 单行文本 可选覆盖
SEO Description 长文本 元描述

创建一个名为"Published"的筛选视图,仅显示 Published 被选中的记录。这是你的生产内容。

API 设置

  1. 转到 airtable.com/create/tokens 并创建个人访问令牌。
  2. 给它 data.records:read 范围(如果你需要写访问权限,还要给 data.records:write)。
  3. 将其范围限定为你正在使用的特定 base。
  4. 将令牌存储在你的 .env 文件中。永远不要提交它。
# .env
AIRTABLE_TOKEN=pat_xxxxxxxxxxxxx
AIRTABLE_BASE_ID=appXXXXXXXXXXXXXX

你可以在 Airtable API 文档中或查看你的 base 时在 URL 中找到你的 base ID。

在 Astro 和 Next.js 中使用 Airtable 作为 CMS(2026 版)- 架构

将 Airtable 连接到 Astro

Astro 是我在内容主要是静态的情况下用 Airtable 驱动的网站的首选框架。由于 Astro 默认构建到静态 HTML,你在构建时获取所有 Airtable 数据,这意味着来自访问者的零 API 调用和生产中没有速率限制问题。

安装 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 版本)

我更喜欢在 Next.js 中使用带有 fetch 的 Airtable REST API,而不是 SDK。它让你可以更好地控制与 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'] || [],
  }));
}

使用 App Router 的 ISR 页面

// 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,网站在一分钟内更新。没有 webhook 设置,没有重建触发。

处理图像和附件

这是 Airtable 作为 CMS 最大的陷阱。Airtable 附件 URL 会过期。 它们是在大约 2 小时后变成无效的签名 URL。如果你直接在 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 有严格的速率限制:每个 base 每秒 5 个请求。 那不是很多。以下是如何保持在其下的方法。

策略 框架 工作原理
静态生成 Astro 所有 API 调用都在构建时发生。零运行时调用。
ISR Next.js 缓存的响应,在计时器上重新验证。
内存缓存 两者 缓存 API 响应到 Map 中带有 TTL。
Webhook + 重建 两者 Airtable 自动化触发 Vercel/Netlify 重建。
Redis/KV 缓存 Next.js(Vercel) 将 API 响应存储在 Vercel KV 或 Upstash Redis 中。

对于 Astro 网站,构建时方法意味着你仅在部署期间点击 API。对于带有 ISR 的 Next.js,你最多每个重新验证间隔每页点击一次。

如果你有很多页面和短的重新验证间隔,请考虑一次获取所有记录并缓存整个数据集,而不是进行按页 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,或在需要更多控制的地方使用带有 remark 插件的 unified

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 vs 传统无头 CMS 选项

让我们坦诚地讨论权衡。以下是 Airtable 与 2026 年专用无头 CMS 平台的比较:

功能 Airtable Sanity Contentful Strapi
编辑学习曲线 很低 中等 中等
内容建模 灵活,非正式 优秀 优秀 良好
API 速率限制 每 base 5 req/s 慷慨(CDN) 慷慨(CDN) 自托管
图像处理 过期的 URL 内置 CDN 内置 CDN 自托管
预览/草稿 手动(复选框) 内置 内置 内置
定价(5 人团队) $100/月(Team) 免费层可行 $300/月+ 免费(自托管)
Webhook 支持 通过自动化 内置 内置 内置
富文本质量 基本 Markdown Portable Text 结构化 富文本
关系内容 链接记录 引用 引用 关系

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 每个重新验证每个唯一页面调用一次。
  3. 图像通过 Cloudinary 代理或下载到 CDN。
  4. 编辑在几分钟内看到更新而不触发完整重建。

模式 3:Airtable + 补充 CMS

最适合:某些内容在 Airtable 中而其他内容需要更丰富编辑的网站。

  1. 结构化数据(团队成员、事件、产品)保留在 Airtable 中。
  2. 长形式内容(博客文章、案例研究)进入 Sanity 或 Notion。
  3. 前端在构建时或使用 ISR 从两个来源获取。

这种混合方法比你想象的更常见。我们已经构建了几个这样的网站 — 如果你正在考虑类似的东西。

从 Airtable 触发重建

Airtable 有内置自动化,可以触发 webhook。在你的 Posts 表上设置一个触发器"当记录被更新时",然后向你的部署平台的构建钩子发送 POST 请求:

// Vercel 部署钩子
https://api.vercel.com/v1/integrations/deploy/prj_xxxx/yyyy

// Netlify 构建钩子
https://api.netlify.com/build_hooks/xxxxxxxxxxxx

在自动化中添加 30 秒延迟以批量快速编辑。

常见问题

Airtable 可以免费作为 CMS 使用吗? Airtable 的免费计划包括每个 base 1,000 条记录和每月 1,000 次 API 调用。这足以用于小型网站,但你可能需要 Team 计划(2026 年每席位每月 $20)以获得更多内容。Team 计划为你提供 50,000 条记录和更高的 API 限制。

我如何处理 Airtable 的过期图像 URL? Airtable 附件 URL 在约 2 小时后过期。对于使用 Astro 构建的静态网站,在构建时下载图像。对于使用 ISR 的 Next.js,要么通过 Cloudinary 等 CDN 代理图像,要么将图像 URL 存储在单独的图像托管服务中并在 Airtable 中引用为文本字段。

Airtable 可以处理包含数百篇文章的博客吗? 是的,但有一定的限制。Airtable 很好地处理数百条记录。一旦你进入数千条,API 分页和构建时间开始变得很明显。对于少于 1,000 篇文章的博客,它工作得很好。超过这个,考虑一个专用无头 CMS。

Airtable 比 Notion 作为 CMS 更好吗? 他们解决不同的问题。Airtable 对结构化数据(产品、事件、团队成员)更好,因为它的关系数据库模型。Notion 对长形式书面内容更好,因为它的块编辑器。Airtable 的 API 也比 Notion 的更成熟和更快。

我如何使用 Airtable 设置预览/草稿功能? 添加一个名为"Status"的单选字段,选项如"Draft"、"In Review"和"Published"。为每个状态创建筛选视图。你的生产网站从"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 对每个 base 强制执行 5 个请求/秒的速率限制。超过此返回 429 状态码。在 Team 计划中,你获得更高的月度调用限额,但按秒速率限制保持不变。静态生成和 ISR 是最小化 API 使用的最佳方式。

我可以在同一个项目中同时使用 Airtable 与 Astro 和 Next.js 吗? 不完全在同一个项目中,但你可以有一个共享的 Airtable base 为多个前端提供动力。一些团队使用 Astro 用于他们的营销网站,Next.js 用于他们的 web 应用,两者都从同一个 Airtable base 读取。只需注意所有消费者之间的共享速率限制。