在 2026 年使用 Airtable 作為 Astro 和 Next.js 的 CMS
我得老實說:當客戶在 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 與傳統無頭 CMS 選項
- 真實世界架構模式
- 常見問題

為什麼 Airtable 作為 CMS 實際上很有意義
Airtable 最大的論點不是技術性的——它是人為的。你的內容編輯已經知道如何使用它。沒有入門摩擦,沒有新登錄要忘記,沒有內容建模 UI 可以學習。他們打開類似試算表的界面,輸入一些東西,它就會出現在網站上。
以下是使其對某些用例確實很好的原因:
- 編輯者的學習曲線為零。 如果有人可以使用 Google Sheets,他們就可以使用 Airtable。
- 靈活的模式。 添加新字段只需五秒鐘。無需遷移,無需模式部署。
- 內置的視圖和篩選器。 編輯可以創建篩選的視圖、看板、圖庫——所有這些都無需開發者幫助。
- 關係資料。 與平面試算表不同,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。我們在我們的無頭 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 調用以及生產中沒有速率限制問題。
如果你在探索 Astro 用於你的下一個項目,我們對它有深入的經驗——查看我們的 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 網站——這是我們的麵包和黃油。請查看我們的 Next.js 開發能力。
獲取工具(Next.js 版本)
我更喜歡在 Next.js 中直接使用 Airtable REST API 和 fetch,而不是 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 過期。 它們是簽名 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 函數來代理 Airtable 圖像 URL,快取它們,並通過你自己的域提供。這適用於 Astro 和 Next.js。
選項 3:使用單獨的圖像主機
將圖像上載到 Cloudinary、Imgix 或 S3 bucket,並在文本字段中存儲永久 URL,而不是使用 Airtable 的附件字段。這是我對生產網站的建議——它是最可靠的方法。
快取、速率限制和性能
Airtable 的 API 有嚴格的速率限制:每個 base 每秒 5 個請求。 這不多。以下是如何保持在此之下。
| 策略 | 框架 | 工作原理 |
|---|---|---|
| 靜態生成 | Astro | 所有 API 調用在構建時發生。零執行時調用。 |
| ISR | Next.js | 快取的響應,按計時器重新驗證。 |
| 內存快取 | 兩者 | 使用 TTL 在 Map 中快取 API 響應。 |
| 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,或對更多控制使用 unified 和 remark 插件:
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 速率限制 | 每個 base 5 req/s | 慷慨 (CDN) | 慷慨 (CDN) | 自託管 |
| 圖像處理 | 過期 URL | 內置 CDN | 內置 CDN | 自託管 |
| 預覽/草稿 | 手動(複選框) | 內置 | 內置 | 內置 |
| 定價(5 人團隊) | $100/月 (Team) | 免費層級可行 | $300/月+ | 免費(自託管) |
| Webhook 支持 | 通過自動化 | 內置 | 內置 | 內置 |
| 豐富文本質量 | 基本 Markdown | 便攜文本 | 結構化 | 豐富文本 |
| 關係內容 | 連結記錄 | 參考 | 參考 | 關係 |
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 表格中的「當記錄更新時」上設置觸發器,然後向你的部署平台的構建 hook 發送 POST 請求:
// Vercel 部署 hook
https://api.vercel.com/v1/integrations/deploy/prj_xxxx/yyyy
// Netlify 構建 hook
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 讀取。只需留意所有消費者之間的共享速率限制。