在 Astro 和 Next.js 中使用 Airtable 作为 CMS(2026 版)
我得坦白:当客户在 2023 年第一次要求我使用 Airtable 作为他们的 CMS 时,我以为他们在开玩笑。一个电子表格应用为生产网站提供动力?但在用这种方式构建了六十多个网站后 — 有些使用 Astro,有些使用 Next.js — 我改变了看法。Airtable 对某些传统无头 CMS 平台完全忽略的项目来说是一个完美的选择。你的营销团队已经知道如何使用它。它足够灵活来建模大多数内容。API 也非常简单。
但它确实有一些尖锐的边缘。速率限制、附件处理、关系数据的怪癖 — 有很多东西是 2023 年的"Airtable 作为 CMS"博客文章从未告诉你的。本指南涵盖了我在 2026 年使用这个堆栈发布真实项目时学到的所有内容。
目录
- 为什么 Airtable 作为 CMS 其实是有意义的
- 你不应该何时使用 Airtable 作为 CMS
- 为内容设置你的 Airtable Base
- 将 Airtable 连接到 Astro
- 将 Airtable 连接到 Next.js
- 处理图像和附件
- 缓存、速率限制和性能
- 富文本和 Markdown 内容
- Airtable vs 传统无头 CMS 选项
- 真实世界的架构模式
- 常见问题

为什么 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 设置
- 转到 airtable.com/create/tokens 并创建个人访问令牌。
- 给它
data.records:read范围(如果你需要写访问权限,还要给data.records:write)。 - 将其范围限定为你正在使用的特定 base。
- 将令牌存储在你的
.env文件中。永远不要提交它。
# .env
AIRTABLE_TOKEN=pat_xxxxxxxxxxxxx
AIRTABLE_BASE_ID=appXXXXXXXXXXXXXX
你可以在 Airtable API 文档中或查看你的 base 时在 URL 中找到你的 base ID。

将 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 条记录)。
- Astro 在构建时获取所有 Airtable 数据。
- Airtable 自动化在记录更新时向 Vercel/Netlify 发送 webhook。
- 网站在 30-60 秒内重建。
- 图像在构建时下载 — 没有过期 URL 问题。
模式 2:使用 Next.js 的 ISR
最适合:博客、目录、频繁更新的网站。
- Next.js 使用 ISR(每 60-300 秒重新验证一次)生成页面。
- Airtable API 每个重新验证每个唯一页面调用一次。
- 图像通过 Cloudinary 代理或下载到 CDN。
- 编辑在几分钟内看到更新而不触发完整重建。
模式 3:Airtable + 补充 CMS
最适合:某些内容在 Airtable 中而其他内容需要更丰富编辑的网站。
- 结构化数据(团队成员、事件、产品)保留在 Airtable 中。
- 长形式内容(博客文章、案例研究)进入 Sanity 或 Notion。
- 前端在构建时或使用 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 缓存指令,如 revalidate 和 tags。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 读取。只需注意所有消费者之间的共享速率限制。