程式化 SEO:我們如何用 Next.js 和 Supabase 達到 253K 頁面索引

去年,我們跨越了一個我真正認為不可能的里程碑:在同一個技術堆疊上運行的三個生產網站中,共有 253,000 個程式化頁面被索引。這不是玩具專案。不是演示。真實的網站,有真實的流量和真實的收入。

我將逐步帶你了解我們如何構建 Deluxe Astrology(91,000 頁)、Not Another Sunday(137,000 個場所列表)和 HostList(25,000 個託管公司資料)-- 包括 Supabase 查詢、Next.js 頁面架構、資料管道,最重要的是,沿途什麼出了問題。因為有很多東西出了問題。

網上大多數程式化 SEO 內容讀起來就像有人翻閱了文檔就算完了。這不是那樣。我們已經發佈這些頁面超過一年,盯著 Google Search Console 圖表,對爬取預算限制咒罵,並慢慢找出什麼在規模上實際有效。

目錄

Programmatic SEO: How We Got 253K Pages Indexed with Next.js & Supabase

程式化 SEO 在 2025 年的真正含義

程式化 SEO 是使用範本從結構化資料大規模生成頁面。這是一句話的版本。現實要混亂得多。

Google 在 2025 年的立場很清楚但很微妙:他們不會因為內容是程式化的而懲罰它。當它很薄、重複或無用時,他們會懲罰它。Zapier 的 70,000 個索引頁面貢獻 $140M ARR 和 dev.to 案例研究(287,000 個頁面獲得接近零索引)之間的區別歸結為一件事 -- 每個頁面是否真正回答了人類在搜尋欄中輸入的查詢。

Ahrefs 資料告訴我們,96.55% 的所有網頁獲得零有機流量。如果你只是生成同一內容的變體,程式化 SEO 會放大這個問題。但如果你的資料真正獨特,並且你的範本生成的頁面彼此有意義上不同,它也可以以驚人的方式解決它。

適用於我們的心智模型是:每個程式化頁面都應通過「我會為此加書籤嗎?」測試。如果你從 Google 登陸它,你會停留嗎?你會找到在其他地方找不到的東西嗎?如果答案是否定的,就不要發佈它。

三個專案:生產數字

讓我列出我們實際構建的東西和數字。

專案 頁面 內容類型 地理範圍 關鍵指標
Deluxe Astrology 91,000 星座運勢、名人資料、天使數字、宇宙幣、寶石、瑜伽姿勢、名字實驗室、占星師目錄 30 種語言 91K 索引頁面
Not Another Sunday 137,000 具有 NRI 評分、照片、地圖的咖啡館和烘焙師場所列表 美國、英國、日本 137K 獨特場所頁面
HostList 25,000 具有 HostScore 算法的託管公司資料 53 個國家 25K 索引資料
合計 253,000

Deluxe Astrology:跨 30 種語言的 91K 頁面

Deluxe Astrology 始於單語言的星座運勢網站。規模來自內容類型和語言的交集。想想看:如果你有 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 為我們提供了複雜查詢所需的關聯結構(給我所有在十二月出生的射手座名人,他們也是音樂家),pgvector 支持跨內容的語義搜尋。Supabase 的免費層處理 500MB;我們在 Pro 上,每個專案 $25/月,可獲得 8GB 資料庫和無限 API 呼叫。

Next.js with ISR(增量靜態重新生成):每個頁面都在構建時或第一次請求時靜態生成,然後按計劃重新驗證。這意味著 Google 的爬蟲始終會命中快速、預先呈現的 HTML 頁面 -- 不是等待客戶端 JavaScript 的載入微調。我們使用帶有 generateStaticParams 的應用路由器進行路徑生成。

Vercel Edge:部署、CDN 和邊緣中介軟體都在一個地方。Vercel 的 Pro 計劃是 $20/用戶/月,為我們提供 1TB 頻寬,可輕鬆處理來自 253K 頁面的流量。Edge Middleware 為 Deluxe Astrology 的 30 語言設置處理地理路由。

所有三個專案的總基礎設施成本約為 $150-200/月。這是託管 253,000 頁面的成本,每月獲得數百萬次爬取。如果你正在構建程式化網站並考慮我們的 Next.js 開發功能 或需要幫助進行 無頭 CMS 架構,這是我們會推薦的堆疊。

Programmatic SEO: How We Got 253K Pages Indexed with Next.js & Supabase - architecture

資料管道架構

資料是使或破壞程式化 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 豐富

這變得有趣了。對於 Deluxe Astrology,我們有 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 知道。用戶知道。不要這樣做。

我們的範本架構確保每個頁面都有五個獨特的元素:

  1. 獨特的 H1:不只是 {name} 插入到模式中。H1 結構根據內容類型而變化,並包括上下文修飾符。

  2. 獨特的中繼描述:從實際頁面資料生成,而不是用空白填充的範本。

  3. 獨特的主體內容:這是大的。每個頁面有 400-2,000 字的該實體特定內容。對於名人來說,這是他們的本命盤分析。對於場所,這是他們的 NRI 分解、鄰里背景和菜單亮點。對於託管公司,這是他們的 HostScore 分解,具有特定的正常運行時間百分比和定價。

  4. 結構化資料(schema.org):每個頁面都獲得適合其類型的 JSON-LD 標記 -- 名人的 Person、場所的 LocalBusiness 和託管公司的 Organization

  5. 內部連結:每個頁面根據實際資料關係連結到 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:Deluxe Astrology -- 爬取預算飢荒

我們推出了 91,000 頁和平面網站地圖結構。Google 在第一個月索引了大約 12,000 頁,然後... 停止了。GSC 覆蓋率報告顯示數萬個 URL 的「已發現 -- 目前未索引」。

問題是兩方面的。首先,我們的網站地圖是一個包含 91,000 個 URL 的單個檔案。Google 建議每個網站地圖最多 50,000 個,但即使在該限制內,單個大型網站地圖也不會表示優先級。其次,我們的內部連結很弱 -- 許多頁面只能通過網站地圖訪問,而不是通過頁面連結訪問。

解決方案:

  1. 網站地圖重組:我們將單體網站地圖分解為基於類別的網站地圖。sitemap-celebrities.xmlsitemap-horoscopes-en.xmlsitemap-horoscopes-es.xml 等。每個在 10,000 個 URL 以下。

  2. 內部連結大修:我們在每個頁面上添加了上下文交叉連結。名人頁面現在連結到相關名人(相同黃道、相同職業、相同出生年)。星座運勢頁面連結到該星座的名人資料。每個頁面至少連結到 8 個其他頁面。

  3. 薄頁面移除:我們刪除了大約 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 個月前後也經歷了 Deluxe Astrology 的流量下降 30% 之後的 Google 核心更新。在沒有我們這一端任何更改的情況下,它在 6 週內恢復,但這些是壓力大的週。程式化網站在核心更新期間似乎更具波動性,因為 Google 同時重新評估整個頁面語料庫中的品質信號。

如果你正在考慮構建這個規模的東西,我們已經在 我們的定價頁面 詳細介紹了我們的方法和定價。對於基於 Astro 的靜態網站生成 -- 我們也為純靜態 pSEO 進行了實驗 -- 檢查我們的 Astro 開發功能

程式化 SEO 與傳統內容:何時使用哪種

程式化 SEO 不是編輯內容的替代品。這是一個不同工作的不同工具。

因素 程式化 SEO 傳統內容
最適合 資料驅動查詢(「澀谷最好的咖啡館」、「獅子座今日星座運勢」) 意圖驅動查詢(「如何沖泡倒水咖啡」)
內容獨特性 來自每個頁面的獨特資料 來自獨特觀點/研究
規模速度 每週 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 頁面,$140M ARR)這樣的網站繼續興隆,因為他們的頁面解決真實問題。受到懲罰的網站是那些生成 「{city} 中最好的 {service}」變體而沒有其背後真實資料的網站。

帶有 Supabase 和 Vercel 的程式化 SEO 堆疊成本是多少? 我們的三專案堆疊每月運行約 $150-200。Supabase Pro 是每個專案 $25/月(我們使用三個實例)。Vercel Pro 是 $20/用戶/月。通過 Claude API 的 AI 豐富是針對 28,840 條記錄的一次性成本約 $180。對於大多數 50K 頁面以下的專案,預計基礎設施成本 $50-100/月。

Google 索引程式化頁面需要多長時間? 預計你的網站地圖初始爬取需要 2-4 週,但大型頁面集的完整索引編製需要 3-6 個月。我們的經驗顯示曲棍球棒模式:前 6-8 週緩慢爬取,因為 Google 評估品質,然後一旦它決定你的內容值得索引就快速加速。內部連結和網站地圖結構大大影響這個時間線。

我應該為程式化 SEO 頁面使用 Next.js SSR 還是 ISR? 幾乎總是 ISR。SSR(force-dynamic)意味著每個爬蟲請求 -- 包括每個 Googlebot 爬取 -- 都命中你的資料庫,這在大規模時造成性能問題並浪費爬取預算在慢回應上。帶有 revalidate = 3600(甚至 86400 用於每日更新)的 ISR 提供靜態網站性能和動態資料新鮮度。我們通過 HostList 學到了這一點的困難方式 -- 從 force-dynamic 切換到 ISR 將我們的回應時間從 2.8 秒下降到 47 毫秒。

你如何跨 100K+ 頁面處理內部連結? 資料庫驅動的相關內容查詢。每個頁面執行找到 8-15 個相關頁面的查詢,基於實際資料關係 -- 相同類別、相似評分、地理近似、共享屬性。不要只隨意連結到頁面。連結需要對用戶和 Google 都有上下文意義。我們在 Supabase 中使用 PostgreSQL 函式有效地計算這些關係。

人們對程式化 SEO 犯的最大錯誤是什麼? 關注頁面計數而不是頁面品質。很容易被想要生成資料的每個可能組合所誘惑,但 10,000 個優秀頁面將勝過 100,000 個平庸頁面。我們在 Deluxe Astrology 上刪除了 4,000 個薄頁面,看到索引編製跨其餘頁面增加。Google 將薄頁面解釋為一個信號,你的整個網站可能是低品質。如果你準備好以正確的方式構建程式化頁面,聯繫我們的團隊 -- 我們已經學到了這些課程,所以你不必。