我使用过不少内容站点——用过 Contentful、Sanity、Strapi,以及大约半打其他无头 CMS 平台。它们相当可靠,直到它们不再可靠。当你需要,比如说,50,000 个位置页面或从结构化数据快速生成的目录时,标准 CMS 开始感觉像用胶带粘合在一起。这就是我转向 Supabase 的信号。

这不是"Supabase 是新 CMS"宣言。哦不,它比这更微妙。在特定情况下,带有可靠 API 层的 Postgres 数据库会明显胜过 CMS,特别是在程序化 SEO 这场大游戏中。继续跟我来,我会列出何时进行切换、为什么至关重要,以及如何设置。

目录

Supabase vs 无头 CMS:何时为程序化 SEO 使用数据库

程序化 SEO 实际需要什么

程序化 SEO 就像创建一个网页工厂。你生成大量页面,每一个都针对非常特定的长尾关键词。想想 Zapier 的应用页面、Nomadlist 无尽的城市比较或来自 Wise 的有用货币页面。这些页面?它们是用模板构建的,充满了独特数据,每个都针对自己的搜索查询。

要进行出色的程序化 SEO,你需要什么?

  • 数量:我们说的是数百、数千,也许甚至数万页。
  • 结构化数据:内容需要遵循可预测的模式,但具有可变的数据点。
  • 关系:你有相互关联的数据——比如与邻域关联的城市或插入类别的产品。
  • 频繁更新:价格变化、统计更新、新东西出现。
  • 查询灵活性:你需要以你过去没有预见到的方式过滤和切割数据。

无头 CMS?它非常适合博客文章或登陆页面等编辑内容。它提供漂亮的 UI、富文本编辑等。问题出现在你的"内容"实际上是插入模板的数据时。那样,你就在与 CMS 的限制相抗争。

无头 CMS 的天花板

去年在一个项目中与 Contentful 撞上了一堵墙。想象一下:一个 SaaS 比较网站,比如"工具 A 对 工具 B",约有 2,000 个软件项目。计算一下,你要面对约两百万个潜在页面。

无头 CMS 系统从哪里开始摇晃?

API 速率限制

Contentful 的免费限制是每秒 200 个 API 请求。团队计划?相同限制。尝试构建数千个页面,限制会直接打击你。Sanity 也没好多少——每月限制 500K API 请求。达到规模——这些数字会狠狠咬你一口。

条目限制和定价

大多数平台根据条目或记录数收费。所以当你处理,比如说,50,000 条记录时,突然,定价变成......让我们这样说,令人不适:

平台 免费层记录 50K 记录成本 100K 记录成本
Contentful 25,000 条条目 ~$489/月(高级版) 自定义定价
Sanity 100K 文档(免费) 免费(但 API 限制) 免费(但 API 限制)
Strapi Cloud 无限(自托管) ~$99/月 + 托管 ~$99/月 + 托管
Supabase 500MB(无限行) $25/月(专业版) $25/月(专业版)

Sanity 在文档数量上相当慷慨,但 API 使用偷偷增加会更不友好。另一方面,Supabase?根据数据库大小收费,而不是行数。当你处理大量数据时,那是一个游戏改变者。

查询限制

这可能是决胜负因素。无头 CMS 的查询语言——Contentful 的 API 或 Sanity 的 GROQ——是为更简单的请求而构建的。但复杂的联接、聚合、带排名的全文搜索等等?它不足。进入 Supabase。完整的 Postgres。所有那些 SQL 魔法都在你的指尖。

-- 祝你在 CMS 查询语言中能做到
SELECT 
  t1.name AS tool_a,
  t2.name AS tool_b,
  t1.pricing - t2.pricing AS price_difference,
  array_agg(DISTINCT f.name) FILTER (WHERE ft1.tool_id IS NOT NULL AND ft2.tool_id IS NULL) AS unique_to_a,
  array_agg(DISTINCT f.name) FILTER (WHERE ft2.tool_id IS NOT NULL AND ft1.tool_id IS NULL) AS unique_to_b
FROM tools t1
CROSS JOIN tools t2
LEFT JOIN features_tools ft1 ON ft1.tool_id = t1.id
LEFT JOIN features_tools ft2 ON ft2.tool_id = t2.id AND ft2.feature_id = ft1.feature_id
LEFT JOIN features f ON f.id = COALESCE(ft1.feature_id, ft2.feature_id)
WHERE t1.id < t2.id
GROUP BY t1.id, t2.id;

尝试用 GROQ 或在 Contentful 的 API 内做到这一点。你会被埋在 API 调用和手动在代码中重新组装数据中。

为什么 Supabase 适合程序化 SEO

Supabase 就像带有一些花哨功能的托管 Postgres。它从你的数据库自动生成 RESTful API,包括实时订阅、身份验证、边缘函数和仪表板——本质上将你的所有任务包装在一个整洁的包中。

PostgREST API

使用 Supabase,你获得一个直接从数据库表倒出的 RESTful API。每个表的 CRUD。你可以排序、过滤、分页——你想要的一切。非常适合在 Next.js 或 Astro 中拉取构建时数据。

// 在 Next.js 中为程序化 SEO 页面获取数据
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_ANON_KEY!)

export async function generateStaticParams() {
  const { data: cities } = await supabase
    .from('cities')
    .select('slug')
  
  return cities?.map(city => ({ slug: city.slug })) ?? []
}

export default async function CityPage({ params }: { params: { slug: string } }) {
  const { data: city } = await supabase
    .from('cities')
    .select(`
      *,
      neighborhoods (*),
      cost_of_living (*),
      coworking_spaces (count)
    `)
    .eq('slug', params.slug)
    .single()

  // 用真实数据渲染你的模板
}

数据库函数用于复杂逻辑

当 REST API 不够时,Postgres 函数是你新的最好朋友。你可以创建函数通过 RPC 调用来处理所有那些复杂的计算、生成数据和汇总详情。

CREATE OR REPLACE FUNCTION get_city_comparison(city_a_slug TEXT, city_b_slug TEXT)
RETURNS JSON AS $$
  SELECT json_build_object(
    'city_a', (SELECT row_to_json(c) FROM cities c WHERE c.slug = city_a_slug),
    'city_b', (SELECT row_to_json(c) FROM cities c WHERE c.slug = city_b_slug),
    'cost_difference', (
      SELECT a.cost_index - b.cost_index
      FROM cities a, cities b
      WHERE a.slug = city_a_slug AND b.slug = city_b_slug
    )
  )
$$ LANGUAGE sql;

用于公共数据的行级安全

你的大部分数据将变成公开,特别是对于 SEO 项目。Supabase 有这个行级安全功能,可以保护你的数据安全但可访问——让你在不失眠的情况下共享表和列,担心数据泄露。

用于数据充实的边缘函数

你可能需要来自外部 API 的数据,或者也许你在筛选 CSV。Supabase 的边缘函数在你的数据库旁边无服务器运行。我已经将这些用于数据导入、AI 驱动的记录充实,甚至是计划的更新。很方便!

Supabase vs 无头 CMS:何时为程序化 SEO 使用数据库 - 架构

可行的架构模式

我已经构建这些程序化 SEO 站点有一段时间了,几个模式效果非常好。让我与你分享:

模式 1:带 ISR 的静态生成

对于在 1,000 到 100,000 页之间、经常更新的站点来说,这是黄金。

  • 框架:使用 generateStaticParams 的 Next.js 或带静态输出的 Astro
  • 数据源:Supabase Postgres
  • 构建策略:静态生成前 1,000 页,对其余页面使用 ISR(增量静态再生)。
  • 更新机制:Supabase webhook 触发 Vercel 部署钩子以进行完整重建或按需页面重新验证。

我们经常在我们的 Next.js 项目 中使用这个。扩展得很好!

模式 2:混合静态 + 服务器

非常适合有 100K+ 页面的大型站点或数据经常变化的情况。

  • 框架:Next.js App Router 带服务器组件,或 Astro 带服务器端渲染
  • 数据源:Supabase(使用连接池如 Supavisor)
  • 构建策略:在构建时创建站点地图,按需动态渲染页面并进行激进缓存。
  • 缓存:使用 Vercel 的数据缓存或 Cloudflare 的缓存与 stale-while-revalidate 标头。

模式 3:数据库驱动的站点地图

你不想在程序化 SEO 中忘记你的站点地图。直接从数据库生成这个:

// app/sitemap.ts (Next.js)
import { createClient } from '@supabase/supabase-js'

export default async function sitemap() {
  const supabase = createClient(
    process.env.SUPABASE_URL!,
    process.env.SUPABASE_SERVICE_ROLE_KEY!
  )

  const { data: cities } = await supabase
    .from('cities')
    .select('slug, updated_at')
    .order('updated_at', { ascending: false })

  return cities?.map(city => ({
    url: `https://example.com/cities/${city.slug}`,
    lastModified: city.updated_at,
    changeFrequency: 'weekly' as const,
    priority: 0.8,
  })) ?? []
}

何时仍应使用无头 CMS

让我们解决房间里的大象:Supabase 不会在所有用例中击败无头 CMS。以下是你想坚持使用 CMS 的情况:

  • 编辑内容:博客、案例研究或需要富格式的长文章?CMS,请——作家会感谢你。
  • 营销页面:那些需要开发人员不进行调整的页面?带有可视化编辑器的 CMS 是你需要的。
  • 小规模内容:主要是基于文本的 500 页以下?CMS 设置要简单得多。
  • 非技术团队:如果 SQL 对你的团队来说听起来像是刑讯逼供,一个 CMS 更友好。
  • 内容工作流:批准链、版本控制、发布计划——坚持使用 CMS。

在这些情况下,我们通常在我们的 无头开发解决方案 中推荐 Sanity、Contentful 或 Storyblok 等平台。

混合方法:CMS + Supabase 一起使用

说实话,这是我对大多数项目的首选:混合两者。让 CMS 处理编辑内容,同时 Supabase 处理程序化数据。

一个真实例子:我们构建了一个房地产平台,其中:

  • Sanity 管理博客内容、代理配置文件和关于页面
  • Supabase 处理 80,000+ 个房产清单、邻域数据、价格历史和学校评级。
  • Next.js 在构建和运行时从两个来源拉取。

结果?编辑团队不必担心数据库,数据管道从不与 CMS 纠缠在一起。每个工具在自己的角色中闪闪发光。

// 从两个来源拉取的页面
import { sanityClient } from '@/lib/sanity'
import { supabase } from '@/lib/supabase'

export default async function NeighborhoodPage({ params }) {
  // 来自 Sanity 的编辑内容
  const editorial = await sanityClient.fetch(
    `*[_type == "neighborhoodGuide" && slug.current == $slug][0]`,
    { slug: params.slug }
  )

  // 来自 Supabase 的结构化数据
  const { data: stats } = await supabase
    .from('neighborhood_stats')
    .select('*, schools(*), listings(count)')
    .eq('slug', params.slug)
    .single()

  return <NeighborhoodTemplate editorial={editorial} stats={stats} />
}

这个设置让你两全其美,没有妥协。

为程序化 SEO 设置 Supabase

让我们卷起袖子。这是为程序化 SEO 项目设置 Supabase 的细节。我们将使用一个假设的"城市指南"站点。

步骤 1:设计你的模式

想象实体及其关系,而不仅仅是内容类型:

CREATE TABLE countries (
  id SERIAL PRIMARY KEY,
  name TEXT NOT NULL,
  slug TEXT UNIQUE NOT NULL,
  continent TEXT,
  currency_code TEXT
);

CREATE TABLE cities (
  id SERIAL PRIMARY KEY,
  country_id INTEGER REFERENCES countries(id),
  name TEXT NOT NULL,
  slug TEXT UNIQUE NOT NULL,
  population INTEGER,
  latitude DECIMAL(10, 8),
  longitude DECIMAL(11, 8),
  cost_index DECIMAL(5, 2),
  safety_score DECIMAL(3, 2),
  internet_speed_mbps INTEGER,
  meta_title TEXT,
  meta_description TEXT,
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE city_monthly_weather (
  id SERIAL PRIMARY KEY,
  city_id INTEGER REFERENCES cities(id),
  month INTEGER CHECK (month BETWEEN 1 AND 12),
  avg_temp_celsius DECIMAL(4, 1),
  avg_rainfall_mm DECIMAL(5, 1),
  sunshine_hours INTEGER,
  UNIQUE(city_id, month)
);

-- 常见查询模式的索引
CREATE INDEX idx_cities_country ON cities(country_id);
CREATE INDEX idx_cities_slug ON cities(slug);
CREATE INDEX idx_cities_cost ON cities(cost_index);

步骤 2:设置 RLS 策略

-- 启用 RLS
ALTER TABLE cities ENABLE ROW LEVEL SECURITY;
ALTER TABLE countries ENABLE ROW LEVEL SECURITY;

-- 允许公开读取访问
CREATE POLICY "Public read access" ON cities
  FOR SELECT USING (true);

CREATE POLICY "Public read access" ON countries
  FOR SELECT USING (true);

步骤 3:为 SEO 数据创建数据库函数

CREATE OR REPLACE FUNCTION get_similar_cities(target_slug TEXT, match_count INTEGER DEFAULT 5)
RETURNS SETOF cities AS $$
  SELECT c2.*
  FROM cities c1, cities c2
  WHERE c1.slug = target_slug
    AND c2.id != c1.id
  ORDER BY 
    ABS(c2.cost_index - c1.cost_index) + 
    ABS(c2.safety_score - c1.safety_score) * 10
  LIMIT match_count
$$ LANGUAGE sql;

步骤 4:批量导入你的数据

虽然 Supabase 的仪表板让你可以导入 CSV,但对于更大的数据集,通过客户端库或直接通过 Postgres 进行:

import { createClient } from '@supabase/supabase-js'
import { parse } from 'csv-parse/sync'
import { readFileSync } from 'fs'

const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY)

const cities = parse(readFileSync('./data/cities.csv', 'utf-8'), {
  columns: true,
  cast: true,
})

// 以 500 个为一批分批插入
for (let i = 0; i < cities.length; i += 500) {
  const chunk = cities.slice(i, i + 500)
  const { error } = await supabase.from('cities').upsert(chunk, {
    onConflict: 'slug',
  })
  if (error) console.error(`Batch ${i / 500} failed:`, error)
}

性能和成本对比

现在,让我们讨论成本和速度。这是 2025 年运行项目后的低层信息:

指标 无头 CMS(Contentful 团队) Supabase 专业版 自托管 Strapi
月度成本(50K 记录) $489/月 $25/月 ~$20-50/月(托管)
API 响应时间(平均) 80-150ms(CDN) 30-80ms(直接) 50-120ms
构建时间(10K 页) 15-25 分钟(速率限制) 3-8 分钟 5-12 分钟
查询灵活性 有限过滤 完整 SQL 有限(REST/GraphQL)
最大记录数(实际) ~100K 数百万 取决于托管
内置全文搜索 基础 Postgres FTS 需要插件
实时更新 仅 Webhook 原生 websockets 仅 Webhook
非开发人员的管理 UI 优秀 基础(仪表板) 不错

成本节约?引人注目。对于有 50K+ 数据记录的大型 SEO 项目,仅通过选择 Supabase 而不是高级 CMS,你就节省 $400+/月。超过 12 个月,那将近 $5,000。

速度?将构建时间从 20 分钟减少到 5 分钟?是的,它从根本上改变了你迭代和开发的方式。

常见问题

Supabase 能为程序化 SEO 处理数百万行吗? 当然!Supabase 建立在 Postgres 的坚实基础上。如果你的索引游戏得当,它可以轻松处理数千万行。我已经在专业版上管理了超过两百万行的程序化 SEO 项目,一路顺风顺水。只要避免在页面生成期间陷入那些 N+1 查询陷阱。

如果页面是服务器渲染的,Supabase 对 SEO 有好处吗? Supabase 本身不会搞乱 SEO。它只是你的数据层,仅此而已。真正重要的是你如何出版这些页面——静态(SSG)或服务器端(SSR)是使它们可爬取的。Supabase 只是比 CMS API 更快、更灵活地提供这些数据。Google 不在乎你的数据来自何处。

非技术团队成员如何在 Supabase 中编辑数据? 这是痛处——这是 Supabase 与 CMS 相比的一个弱点。仪表板就像电子表格编辑器,对简单更改很好。但为了更友好的体验,用 Retool、Appsmith 或甚至基础 Next.js 管理路由构建轻量级管理面板很聪明。一些团队使用无服务器函数将 Google Sheets 与 Supabase 同步。出乎意料的有效用于数据调整。

对于程序化 SEO,我应该使用 Supabase 还是 Firebase? Supabase,没有竞争。Firebase 的 Firestore 是一个 NoSQL 文档数据库,使关系查询成为苦差事。程序化 SEO 通常处理关系数据——想想实体和层级。Postgres 通过 Supabase?自然处理它。另外,使用 Firestore 按读操作收费,当你在构建时生成数千个页面时,你的钱包会感到热度。

我可以在 Astro 中与程序化 SEO 一起使用 Supabase 吗? 绝对可以,这是一个相当甜蜜的组合。Astro 的静态站点生成闪电般快速,其内容集合与从 Supabase 获取的数据配合良好。在构建时,你将在 getStaticPaths 函数中查询 Supabase 以生成无穷无尽的静态页面。我们在我们的 Astro 项目 中做过非常好的结果。

如何在没有 CMS 的情况下处理内容预览? 你需要付出努力来构建这个,但原理如下:创建一个预览 API 路由,从 Supabase 拉取草稿数据(使用 draftpublishedstatus 列)并渲染页面。简单的身份验证检查可以确保只有你的团队可以访问这些预览。不如 CMS 预览光滑,但嘿,它可以在大约 50 行 Next.js 代码中完成工作。

大规模生成元标题和描述的最佳方式是什么? 将模板字符串植入你的代码,用数据填充它们。也许:${city.name} Cost of Living Guide ${new Date().getFullYear()} | Rent, Food & Transport Costs。对于独特的描述,尝试通过 Supabase Edge Function 使用 GPT-4o-mini 自动生成并存储每个页面的元描述。在 $0.15 每百万输入令牌(那些聪明的 2025 价格!),生成 100K 元描述成本不到 $5。

一个大型程序化 SEO 项目的 Supabase 成本是多少? 专业版本 $25/月将满足大多数需求。有 8GB 存储、250GB 带宽和 500MB 边缘函数调用空间。如果你的数据集超过 8GB,每 GB 只需 $0.125/月。50GB 数据库?约 $30.25/月。与大狗 CMS 定价相比?甚至不接近。想要更多细节?请访问我们的 定价页面,如果你对完整构建看起来像什么很好奇。