您的内容 API 执行 47,000 次请求来呈现州级目录,Contentful 的发票金额达到该月 1,200 美元。您完全按照 CMS 文档构建了该网站——查询每个页面的元数据、获取相关条目、组合布局——但没有人警告您 Headless 平台的定价方式就像 SaaS,而不是数据库。数学在 10,000 到 50,000 个程序化页面之间的某个地方会出问题,此时每页的边际成本会上升,而 Postgres 保持不变。Supabase 为您提供相同的结构化内容模型、相同的 REST 和 GraphQL 端点,但去除了 CMS 税。代价呢?您现在负责内容模式、编辑器 UI(如果非开发人员需要的话)和部署管道。这种权衡只对一个特定用例有意义——如果您正在阅读本文,您可能正在盯着它。

这不是「Supabase 是新的 CMS」的宣言。哦不,它比这更微妙。有些特定情况下,具有可靠 API 层的 Postgres 数据库明确胜过 CMS,特别是在程序化 SEO 的大游戏中。请继续阅读,因为我将说明何时进行切换、为什么至关重要以及如何设置一切。

目录

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

程序化 SEO 的真正需求

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

您需要什么来实现杀手级程序化 SEO?

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

Headless CMS?它对博客文章或登录页面等编辑内容很有用。它提供漂亮的 UI、富文本编辑等。当您的「内容」实际上是插入到模板中的数据时,问题就出现了。然后,您将与 CMS 的限制进行搏斗。

Headless CMS 的天花板

去年在一个项目上撞到了 Contentful 的墙。想象一下:一个 SaaS 比较网站,比如说大约 2,000 个软件项目的「工具 A vs 工具 B」。计算一下,您大概要看 200 万个潜在页面。

Headless 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 呢?根据数据库大小收费,而不是行数。当您处理大量数据时,这是一个游戏改变者。

查询限制

这可能是决定因素。Headless 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。它从您的数据库自动生成一个宁静的 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 Headless CMS:何时为程序化 SEO 使用数据库 - 架构

行之有效的架构模式

我已经构建了一些这样的程序化 SEO 网站,几个模式效果很好。让我分享它们:

模式 1:带 ISR 的静态生成

这对于拥有 1,000 到 100,000 页面之间的任何地方的网站来说都很宝贵,这些页面经常更新。

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

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

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

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

  • 框架:Next.js 应用路由器与服务器组件,或 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,
  })) ?? []
}

您何时应该仍使用 Headless CMS

让我们解决房间里的大象:Supabase 并不在所有用例中都击败 Headless CMS。以下是您应该坚持使用 CMS 的时间:

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

在这些情况下,我们通常在我们的 Headless 开发解决方案中推荐 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(`批处理 ${i / 500} 失败:`, error)
}

性能和成本比较

现在,让我们来看成本和速度。以下是我在运行项目后的详细信息:

度量 Headless CMS(Contentful Team) Supabase Pro 自托管 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 处理数百万行吗? Ofcourse!Supabase 构建在 Postgres 的坚实肩膀上。如果您的索引技巧一流,它可以轻松处理数千万行。我已经在专业版上管理了超过 200 万行的程序化 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 通常处理关系数据——想象实体和层级。通过 Supabase 的 Postgres?天然处理它。另外,使用 Firestore 按读取操作收费,当您在构建时生成数千个页面时,您的钱包感受到热量。

我可以将 Supabase 与 Astro 用于程序化 SEO 吗? 绝对的,这是一个非常不错的组合。Astro 的静态网站生成速度闪电般快,其内容集合很好地与从 Supabase 获取的数据配对。在构建时,您将在 getStaticPaths 函数中查询 Supabase 以生成无尽的静态页面。我们在我们的 Astro 项目中做这个时有超级结果。

在没有 CMS 的情况下,我如何处理内容预览? 您将需要里程数来构建这个,但原则是这样的:制作一个预览 API 路由,从 Supabase 中提取草稿数据(使用 status 列如 draftpublished)并呈现页面。简单的身份验证检查可以确保只有您的团队可以访问这些预览。不如 CMS 预览光滑,但嘿,它在大约 50 行 Next.js 代码中完成了工作。

大规模生成元标题和描述的最佳方式是什么? 在您的代码中种植模板字符串,用数据提供它们。也许:${city.name} Cost of Living Guide ${new Date().getFullYear()} | Rent, Food & Transport Costs。对于独特的描述,请尝试通过 Supabase 边缘函数使用 GPT-4o-mini 来为每个页面自动生成和存储元描述。每百万输入令牌 $0.15,制作 100K 元描述成本不到 $5。

大型程序化 SEO 项目的 Supabase 成本是多少? 专业版 $25/月将满足大多数需求。有 8GB 的存储、250GB 的带宽和 500MB 边缘函数调用的空间。如果您的数据集超过 8GB,它每月只需 $0.125/GB。一个 50GB 数据库?大约 $30.25/月。与大公司 CMS 定价相比?甚至不接近。需要更多详情?如果您对完整构建的成本感到好奇,请弹出到我们的 定价页面