程序化SEO:如何使用Next.js和Supabase索引253K个页面
程序化SEO:我们如何用Next.js和Supabase索引了253K个页面
去年,我们跨越了一个我真心认为不可能达到的里程碑:在三个生产网站上索引了253,000个程序化页面,所有页面都运行在同一个技术栈上。不是玩具项目。不是演示。真实网站,真实流量,真实收入。
我将带你了解我们确切地如何构建豪华占星术(91,000个页面)、Not Another Sunday(137,000个场地列表)和HostList(25,000个主机公司资料)——包括Supabase查询、Next.js页面架构、数据管道,最重要的是,路上什么东西坏掉了。因为很多东西坏掉了。
大多数网上关于程序化SEO的内容读起来像有人略读了文档就收工了。这不是那样。我们在这些页面上花了一年多时间,盯着Google搜索控制台图表,对爬虫预算限制咒骂,慢慢弄清楚在规模上什么真的有效。
目录
- 程序化SEO在2025年真正意味着什么
- 三个项目:生产数据
- 技术栈:Supabase、Next.js ISR、Vercel Edge
- 数据管道架构
- Google不讨厌的页面模板架构
- 真实代码:从Supabase查询到渲染页面
- 什么坏掉了以及我们如何修复它
- 结果:曲棍球棍和诚实的失败
- 程序化SEO与传统内容:何时使用哪个
- 常见问题

程序化SEO在2025年真正意味着什么
程序化SEO是使用模板从结构化数据按规模生成页面。这是一句话版本。现实要混乱得多。
Google在2025年的立场很清楚但很微妙:他们不会因为内容是程序化的而惩罚它。他们会在内容很薄、重复或无用时惩罚它。Zapier的70,000个索引页面贡献1.4亿美元年经常性收入与dev.to案例研究中287,000个页面几乎零索引之间的区别归结为一件事——每个页面是否真的能回答人类在搜索栏中输入的查询。
Ahrefs数据告诉我们96.55%的所有网页获得零有机流量。如果你只是生成相同内容的变体,程序化SEO会放大这个问题。但如果你的数据真的独特,且你的模板生成意义不同的页面,它也可以以惊人的方式解决它。
对我们有效的心智模型是:每个程序化页面都应该通过"我会书签这个吗?"测试。如果你从Google着陆了它,你会留下吗?你会找到别地找不到的东西吗?如果答案是否定的,就别发布它。
三个项目:生产数据
让我列出我们实际构建了什么以及数据看起来像什么。
| 项目 | 页面 | 内容类型 | 地理范围 | 关键指标 |
|---|---|---|---|---|
| 豪华占星术 | 91,000 | 星座运势、名人资料、天使数字、宇宙币、宝石、瑜伽姿势、名字实验室、占星师目录 | 30种语言 | 91K索引页面 |
| Not Another Sunday | 137,000 | 咖啡厅和烘焙机场地列表,带NRI分数、照片、地图 | 美国、英国、日本 | 137K个独特场地页面 |
| HostList | 25,000 | 主机公司资料,带HostScore算法 | 53个国家 | 25K索引资料 |
| 总计 | 253,000 |
豪华占星术:跨30种语言的91K页面
豪华占星术开始于单语言星座运势网站。规模来自内容类型和语言的交叉。想象一下:如果你有12个黄道星座×365个每日星座运势×30种语言,仅从一种内容类型你已经有131,000个潜在页面。我们很有选择性——不是每个组合都会得到页面——但占星术内容的组合性质非常适合pSEO。
名人资料部分单独有28,840条记录,每条都通过Claude丰富化,包括本命星盘分析、性格分析和兼容性见解。稍后会详细介绍该数据管道。
Not Another Sunday:137K场地列表
Not Another Sunday是一个专业咖啡发现平台。每家咖啡厅和烘焙机都会得到一个独特页面,带有专有的NRI(邻域相关性指数)分数、精选照片、嵌入式地图、营业时间和评论。我们从多个API、用户生成的内容和人工策划中提取数据。
关键见解:没有两个场地页面看起来相同,因为没有两个场地_真的_相同。模板是一致的,但数据每次以不同方式填充它。一家在涩谷有4.8 NRI和拿铁艺术竞赛的咖啡厅看起来与布鲁克林有3.2 NRI和仅批发业务的烘焙机完全不同。
HostList:跨53个国家的25K主机资料
HostList在全球编目主机公司,每个都有HostScore——我们基于正常运行时间数据、定价、支持响应能力和用户评论的算法评级。跨53个国家25,000个资料,每个都有独特的性能数据、定价表和比较部件。
技术栈:Supabase、Next.js ISR、Vercel Edge
我们在所有三个项目中标准化了相同的技术栈。以下是每个部分重要的原因。
Supabase(PostgreSQL + pgvector):我们的整个数据层都在Supabase中。PostgreSQL给我们需要的关系结构用于复杂查询(给我所有在12月出生的射手座名人,他们也是音乐家),pgvector为内容语义搜索提供动力。Supabase免费层处理500MB;我们在Pro上$25/月每个项目用于8GB数据库,无限制API调用。
Next.js与ISR(增量静态再生):每个页面在构建时或首次请求时静态生成,然后在计划上重新验证。这意味着Google的爬虫总是会到达一个快速、预先渲染的HTML页面——不是加载微调等待客户端JavaScript。我们使用带有generateStaticParams的App Router进行路径生成。
Vercel Edge:部署、CDN和边缘中间件全部在一个中。Vercel的Pro计划$20/用户/月给我们1TB带宽,这可以轻松处理来自253K页面的流量。边缘中间件为豪华占星术的30语言设置处理地理路由。
所有三个项目的总基础设施成本运行大约$150–200/月。这是在托管253,000个页面获得数百万次月度爬虫。如果你正在构建程序化网站并考虑我们的Next.js开发能力或需要帮助的无头CMS架构,这是我们会推荐的技术栈。

数据管道架构
数据是制造或破坏程序化SEO的东西。模板很容易。为数万个页面获取真正独特、高质量的数据?这是困难的部分。
我们在项目中使用四种数据源类型:
1. API抓取
Not Another Sunday从Google Places API、Yelp Fusion API和日本的一些地区API中提取场地数据。我们通过Supabase Edge Functions运行夜间同步作业,检查新场地、更新时间和关闭的位置。每个API响应在插入前被规范化到我们的架构中。
2. CSV导入带验证
HostList的初始数据集来自在两年内编译的大规模主机公司CSV。我们构建了一个验证管道,检查重复、规范化公司名称和标记不完整记录。大约30%的初始导入被标记并需要手动审查。
3. Claude AI丰富化
这是事情变得有趣的地方。对于豪华占星术,我们有28,840条名人记录,带有基本传记数据——名字、生日、出生地。这不足以用于有用的页面。我们使用Claude(Anthropic的API)丰富化每条记录,包括本命星盘解释、性格分析、职业兼容性见解和有趣事实。
关键:我们没有使用Claude从无生成_内容。我们使用它来_分析和解释_真实天文数据。每个名人的本命星盘从其出生数据数学计算,然后Claude提供占星术解释。基础数据是独特和可验证的。AI层增加深度,而不是虚构。
以下是我们丰富化管道的简化版本:
import anthropic
from supabase import create_client
client = anthropic.Anthropic()
supabase = create_client(SUPABASE_URL, SUPABASE_KEY)
def enrich_celebrity(record):
natal_chart = calculate_natal_chart(
birth_date=record['birth_date'],
birth_place=record['birth_place']
)
prompt = f"""Given this natal chart data for {record['name']}:
Sun: {natal_chart['sun_sign']} in {natal_chart['sun_house']}
Moon: {natal_chart['moon_sign']} in {natal_chart['moon_house']}
Rising: {natal_chart['ascendant']}
Write a 300-word astrological personality profile focusing on
how these placements manifest in their career as a {record['profession']}.
Include specific aspect interpretations."""
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}]
)
supabase.table('celebrities').update({
'natal_chart': natal_chart,
'ai_profile': response.content[0].text,
'enriched_at': 'now()'
}).eq('id', record['id']).execute()
我们在大约一周内处理了所有28,840条记录,分批请求以保持在速率限制内。成本大约是$180的API信用。不错,为将近29K页面与独特内容丰富化。
4. 用户生成的内容
Not Another Sunday接受用户的评论和照片提交。这个UGC随着时间推移使页面越来越独特,并向Google发出信号表明内容是新鲜的和社区驱动的。
Google不讨厌的页面模板架构
这是大多数程序化SEO项目失败的地方。他们创建一个模板,如:
<h1>{City} {Service} Directory</h1>
<p>Looking for {service} in {city}? Browse our directory of {count} providers.</p>
这是很薄的内容。Google知道它。用户知道它。不要这样做。
我们的模板架构确保每个页面都有五个独特的元素:
独特的H1:不仅仅是
{name}插入到一个模式中。H1结构因内容类型而异,包括上下文修饰符。独特的meta描述:从实际页面数据生成,不是一个用空白填充的模板。
独特的body内容:这是大的。每个页面都有400-2,000字的内容,对该实体特定。对于名人,这是他们的本命星盘分析。对于场地,这是他们的NRI分解、邻域上下文和菜单亮点。对于主机公司,这是他们的HostScore分解,具有具体的正常运行时间百分比和定价。
结构化数据(schema.org):每个页面都获得适合其类型的JSON-LD标记——名人用
Person,场地用LocalBusiness,主机公司用Organization。内部链接:每个页面根据实际数据关系链接到5-15个相关页面。一个名人页面链接到具有相同太阳星座、相同职业或相同出生年份的其他名人。一个场地页面链接到附近的场地和具有相似NRI分数的场地。
内部链接部分成为索引化最单一最重要的因素。更多在修复部分。
真实代码:从Supabase查询到渲染页面
让我为你展示Not Another Sunday场地页面的实际流程。这是生产代码,为了可读性略微简化。
首先,Supabase查询层:
// lib/queries/venues.ts
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
)
export async function getVenueBySlug(slug: string) {
const { data, error } = await supabase
.from('venues')
.select(`
id, name, slug, description, nri_score,
address, city, country, lat, lng,
opening_hours, photos, menu_highlights,
created_at, updated_at,
venue_reviews (
id, rating, body, author_name, created_at
),
venue_tags (
tag:tags ( name, slug )
)
`)
.eq('slug', slug)
.eq('status', 'published')
.single()
if (error) throw error
return data
}
export async function getRelatedVenues(venueId: string, city: string, nriScore: number) {
const { data } = await supabase
.rpc('get_related_venues', {
p_venue_id: venueId,
p_city: city,
p_nri_score: nriScore,
p_limit: 12
})
return data ?? []
}
get_related_venues函数是Supabase中的一个PostgreSQL函数,返回按NRI分数接近度排序的附近场地:
CREATE OR REPLACE FUNCTION get_related_venues(
p_venue_id UUID,
p_city TEXT,
p_nri_score NUMERIC,
p_limit INT DEFAULT 12
)
RETURNS TABLE (
id UUID, name TEXT, slug TEXT,
nri_score NUMERIC, city TEXT, country TEXT
) AS $$
BEGIN
RETURN QUERY
SELECT v.id, v.name, v.slug, v.nri_score, v.city, v.country
FROM venues v
WHERE v.id != p_venue_id
AND v.status = 'published'
AND v.city = p_city
ORDER BY ABS(v.nri_score - p_nri_score) ASC
LIMIT p_limit;
END;
$$ LANGUAGE plpgsql;
现在使用App Router的Next.js页面组件:
// app/venues/[country]/[city]/[slug]/page.tsx
import { getVenueBySlug, getRelatedVenues } from '@/lib/queries/venues'
import { VenueHeader } from '@/components/venue/VenueHeader'
import { NRIScoreCard } from '@/components/venue/NRIScoreCard'
import { VenueMap } from '@/components/venue/VenueMap'
import { ReviewSection } from '@/components/venue/ReviewSection'
import { RelatedVenues } from '@/components/venue/RelatedVenues'
import { venueJsonLd } from '@/lib/schema/venue'
import { notFound } from 'next/navigation'
export const revalidate = 3600 // ISR: revalidate every hour
export async function generateMetadata({ params }: Props) {
const venue = await getVenueBySlug(params.slug)
if (!venue) return {}
const reviewCount = venue.venue_reviews?.length ?? 0
const avgRating = reviewCount > 0
? (venue.venue_reviews.reduce((sum, r) => sum + r.rating, 0) / reviewCount).toFixed(1)
: null
return {
title: `${venue.name} -- Specialty Coffee in ${venue.city} | NRI ${venue.nri_score}`,
description: avgRating
? `${venue.name} in ${venue.city} scores ${venue.nri_score}/10 NRI. Rated ${avgRating}/5 from ${reviewCount} reviews. ${venue.description?.slice(0, 80)}...`
: `${venue.name} in ${venue.city} scores ${venue.nri_score}/10 on our Neighbourhood Relevance Index. ${venue.description?.slice(0, 100)}...`,
alternates: {
canonical: `/venues/${params.country}/${params.city}/${params.slug}`
}
}
}
export default async function VenuePage({ params }: Props) {
const venue = await getVenueBySlug(params.slug)
if (!venue) notFound()
const related = await getRelatedVenues(venue.id, venue.city, venue.nri_score)
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(venueJsonLd(venue)) }}
/>
<article>
<VenueHeader venue={venue} />
<NRIScoreCard score={venue.nri_score} breakdown={venue.nri_breakdown} />
<VenueMap lat={venue.lat} lng={venue.lng} />
<section className="venue-body">
<h2>About {venue.name}</h2>
<p>{venue.description}</p>
{venue.menu_highlights && (
<>
<h3>Menu Highlights</h3>
<ul>
{venue.menu_highlights.map(item => (
<li key={item}>{item}</li>
))}
</ul>
</>
)}
</section>
<ReviewSection reviews={venue.venue_reviews} />
<RelatedVenues venues={related} currentCity={venue.city} />
</article>
</>
)
}
注意revalidate = 3600。这是ISR——页面在首次请求时静态生成并缓存一小时。Google的爬虫总是获得快速的HTML。新数据在下一个重新验证周期流入。这对爬虫预算非常重要。
什么坏掉了以及我们如何修复它
这是大多数案例研究变得不诚实的地方。他们显示结果而不显示数月的调试。我们有三个主要问题。
问题1:豪华占星术——爬虫预算饥荒
我们以91,000个页面和平坦的网站地图结构启动。Google在第一个月索引了大约12,000个页面,然后...停了。GSC覆盖报告显示数万个URL的"已发现——当前未索引"。
问题是两方面的。首先,我们的网站地图是一个有91,000个URL的单一文件。Google建议每个网站地图最多50,000个,但即使在该限制内,单一巨大网站地图也不会发出优先级信号。其次,我们的内部链接很弱——许多页面只能通过网站地图到达,不能通过页面链接。
修复:
网站地图重组:我们将单一网站地图分解为基于类别的网站地图。
sitemap-celebrities.xml、sitemap-horoscopes-en.xml、sitemap-horoscopes-es.xml等。每个在10,000个URL以下。内部链接大修:我们在每个页面上添加了上下文交叉链接。名人页面现在链接到相关名人(相同黄道、相同职业、相同出生年份)。星座运势页面链接到该星座的名人资料。每个页面连接到至少8个其他页面。
薄页面移除:我们杀死了大约4,000个有少于200字独特内容的页面。这些主要是自动生成的组合页面,没有添加价值。更少页面,但更高质量。
在这些更改后,索引从12K升至91K,大约花费10周。内部链接是最大的杠杆。
问题2:HostList——ISR错误配置
HostList使用export const dynamic = 'force-dynamic'在每个页面上启动。这意味着每个请求——包括每个Googlebot爬虫——都实时在Supabase中。Google每天爬虫数千个页面,我们的Supabase实例被击中,响应时间飙升,一些页面在爬虫期间超时。
**修复:**我们切换到export const revalidate = 3600。页面被静态缓存并在100毫秒内服务。Supabase每小时每页只被点击一次,而不是每请求一次。我们的p95响应时间从2.8秒下降到47毫秒。Googlebot开始每天爬虫3倍的页面,因为它不再等待。
问题3:Not Another Sunday——跨国家的重复内容
一些咖啡馆连锁在多个国家运营。东京星巴克保留和伦敦星巴克保留最初有非常相似的页面内容,因为模板强调了品牌信息而不是位置特定的数据。
**修复:**我们权重了位置特定的内容高得多。邻域描述、附近场地比较、本地评论情绪和国家特定的定价现在构成每个页面的70%+。品牌信息是一个小部分。Google停止了标记这些为接近重复。
结果:曲棍球棍和诚实的失败
跨所有三个项目的合并GSC数据显示经典曲棍球棍曲线——几周持平,然后指数增长当Google的爬虫获得了对我们域的信心。
| 指标 | 第1个月 | 第3个月 | 第6个月 | 第12个月 |
|---|---|---|---|---|
| 总索引页面 | 18,200 | 67,000 | 189,000 | 253,000 |
| 日有机点击 | 340 | 2,100 | 8,400 | 19,600 |
| 平均位置(所有查询) | 42 | 28 | 16 | 11 |
| 爬虫请求/日(所有网站) | 4,200 | 12,800 | 31,000 | 48,000 |
| 月Supabase成本 | $75 | $75 | $125 | $150 |
| 月Vercel成本 | $40 | $60 | $60 | $60 |
但让我诚实地说关于失败。大约8%的我们的页面在12个月后仍在"已发现——当前未索引"。这些往往是长尾中流量潜力最低的页面——低搜索量语言中的具体天使数字页面,或小市场中的主机公司。我们可能能够用更多内部链接强制索引它们,但ROI不在那里。
我们也有大约第4个月的时期,当豪华占星术的流量在Google核心更新后下降30%。它在6周内恢复,没有我们端的任何改变,但那些是压力周。程序化网站在核心更新期间似乎更不稳定,因为Google一次重新评估整个页面语料库中的质量信号。
如果你正在考虑在这个规模建设某些东西,我们在我们的定价页面详细说明了我们的方法和定价。对于Astro-based静态网站生成——我们也为纯静态pSEO实验了——查看我们的Astro开发能力。
程序化SEO与传统内容:何时使用哪个
程序化SEO不是编辑内容的替代品。这是一个不同工作的不同工具。
| 因素 | 程序化SEO | 传统内容 |
|---|---|---|
| 最好用于 | 数据驱动查询("涩谷最佳咖啡馆","狮子座星座运势今天") | 意图驱动查询("如何冲制pour-over咖啡") |
| 内容独特性 | 来自每个页面的独特数据 | 来自独特视角/研究 |
| 扩展速度 | 每周1,000+页面 | 每周2-5篇文章 |
| 维护负担 | 数据库更新、模板修复 | 定期内容刷新 |
| Google信任建设 | 更慢(需要证明大规模质量) | 更快(每件单独评判) |
| 风险配置文件 | 更高(薄内容处罚影响整个网站) | 更低(一篇坏文章不会击垮域) |
最甜蜜的点是结合两者。Not Another Sunday有137K程序化场地页面_和_200+关于咖啡文化、冲制方法和城市特定咖啡馆爬虫路线的编辑指南。编辑内容建造E-E-A-T信号,提升整个域,这帮助程序化页面更快索引。
常见问题
用程序化SEO可以现实地索引多少页面? 这完全取决于域权限和内容质量。在有强大反向链接配置文件的已建立域上,我们对100K+页面看到90%+索引率。新域争取——dev.to案例研究中287K页面在新域上获得接近零索引是常态,而不是例外。开始1,000-5,000个高质量页面,建立权限,然后扩展。
避免薄内容处罚每个页面的最少内容是什么? 我们针对每个页面至少400字的独特内容,加上结构化数据、图像和内部链接。但字数本身不是指标——这是关于页面是否比已存在的更好地回答用户的查询。一个200字有独特数据表和地图的页面可以超过一个2,000字通用文本的页面。
在Google的2025年有用内容更新后程序化SEO仍然安全吗? 是的,但仅当你真的创建有用的页面。Google的2025年更新特别针对低质量程序化内容,仅存在于捕获搜索流量而不提供价值。Zapier这样的网站(70K页面,1.4亿美元年经常性收入)继续蓬勃发展,因为他们的页面解决真实问题。被处罚的网站是生成"最好的{服务}在{城市}"变体,没有真实数据的那些。
一个具有Supabase和Vercel的程序化SEO技术栈成本多少? 我们的三项目技术栈运行大约$150-200/月总计。Supabase Pro是$25/月每个项目(我们使用三个实例)。Vercel Pro是$20/用户/月。通过Claude的API的AI丰富化是大约$180一次性成本,用于28,840条记录。对于大多数项目在50K页面以下,预期$50-100/月在基础设施成本。
Google需要多长时间索引程序化页面? 预期初始爬虫的网站地图2-4周,但大型页面集的完整索引需要3-6个月。我们的经验显示曲棍球棍模式:最初6-8周缓慢爬虫当Google评估质量,然后快速加速当它决定你的内容值得索引。内部链接和网站地图结构戏剧性地影响这个时间表。
我应该为程序化SEO页面使用Next.js SSR还是ISR?
ISR,几乎总是。SSR(force-dynamic)意味着每个爬虫请求——包括每个Googlebot爬虫——在实时中hit你的数据库,这在规模上创建性能问题并在缓慢响应上浪费爬虫预算。ISR与revalidate = 3600(甚至86400对于日更新)给你静态网站性能与动态数据新鲜。我们用HostList困难方式学到了这个——从force-dynamic切换到ISR下降我们的响应时间从2.8秒到47毫秒。
你如何处理跨100K+页面的内部链接? 数据库驱动的相关内容查询。每个页面运行一个查询,找到8-15个相关页面,基于实际数据关系——相同类别、相似分数、地理接近、共享属性。不仅仅随机链接到页面。链接需要对用户和Google两者有上下文意义。我们在Supabase中使用PostgreSQL函数高效计算这些关系。
人们用程序化SEO犯的最大错误是什么? 专注于页面计数而不是页面质量。生成数据的每个可能组合是诱人的,但10,000个优秀页面将超过100,000个平庸的。我们在豪华占星术上杀死了4,000个薄页面,看到索引在剩余页面上_增加_。Google将薄页面解释为一个信号,你的整个网站可能是低质量的。如果你准备好以正确的方式构建程序化页面,联系我们的团队——我们已经学过这些教训,所以你不必。