上個月我們在 Deluxe Astrology 突破了 91,000 個頁面。名人出生圖表、部落格文章、跨六種語言的在地化內容 -- 該網站已經遠超單一網站地圖檔案的處理能力。Google 的網站地圖協議限制每個檔案最多 50,000 個 URL 和 50MB 未壓縮大小。我們需要一個網站地圖索引,包含分塊的子網站地圖,從 Supabase 動態生成,在 Vercel 上使用 ISR 快取,並作為單一索引 URL 提交到 Google Search Console。

這正是我們發布的實現方式。不是理論上的演練 -- 實際生產程式碼,今天處理 91K 個 URL,不做任何改變就能擴展到 500K。

目錄

Building a Dynamic Sitemap for 91,000 Pages with Next.js and Supabase

理解網站地圖限制和架構

以下是您需要知道的硬性限制:

約束 限制 來源
每個網站地圖檔案的 URL 數量 50,000 sitemaps.org 協議
每個網站地圖的檔案大小 50MB 未壓縮 sitemaps.org 協議
每個網站地圖索引的網站地圖數量 50,000 sitemaps.org 協議
Supabase .range() 每個查詢最大值 1,000 行(預設) Supabase PostgREST 配置
Vercel 無伺服器函數超時時間(Pro) 60 秒 Vercel docs 2025
Vercel 回應主體大小限制 10MB Vercel 邊界快取

對於 91,000 個 URL,您至少需要兩個網站地圖檔案。但我們不只是將所有內容傾倒到兩個 50K URL 的 bucket。我們按內容類型分割 -- 名人、部落格文章、靜態頁面、在地化頁面 -- 因為每種類型都有不同的 changefreqpriority 和更新模式。這使我們能夠更好地控制,並使當出現問題時在 GSC 中的除錯變得容易得多。

Deluxe Astrology 的網站地圖結構

以下是最終網站地圖架構的外觀:

/sitemap.xml                    → 網站地圖索引(指向所有子網站地圖)
  /sitemap-pages.xml            → 靜態頁面(~30 個 URL)
  /sitemap-blog-0.xml           → 部落格文章分塊 0(最多 50K)
  /sitemap-blog-1.xml           → 部落格文章分塊 1(溢出)
  /sitemap-celebrities-0.xml    → 名人頁面分塊 0(最多 50K)
  /sitemap-celebrities-1.xml    → 名人頁面分塊 1(溢出)
  /sitemap-locale-es.xml        → 西班牙語在地化頁面
  /sitemap-locale-fr.xml        → 法語在地化頁面
  /sitemap-locale-de.xml        → 德語在地化頁面
  /sitemap-locale-pt.xml        → 葡萄牙語在地化頁面
  /sitemap-locale-ja.xml        → 日語在地化頁面

每個子網站地圖都是一個 Next.js App Router 路由處理程序,在運行時查詢 Supabase,生成 XML,並通過 ISR 快取,revalidate = 3600(每小時)。網站地圖索引本身也是一個路由處理程序。

使用偏移分頁設置 Supabase 查詢

這是大多數教程弄錯的關鍵部分:您不能只是做 supabase.from('celebrities').select('*') 並期望獲得 91,000 行。Supabase 的 PostgREST 層預設最多返回 1,000 行。您需要分頁。

我們使用批次大小為 1,000 的基於範圍的偏移分頁:

// lib/supabase-sitemap.ts
import { createClient } from '@supabase/supabase-js';

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY! // 使用服務角色以供伺服器端使用
);

const BATCH_SIZE = 1000;

export interface SitemapEntry {
  slug: string;
  updated_at: string;
}

export async function fetchAllSlugs(
  table: string,
  selectColumns: string = 'slug, updated_at'
): Promise<SitemapEntry[]> {
  const allRows: SitemapEntry[] = [];
  let offset = 0;
  let hasMore = true;

  while (hasMore) {
    const { data, error } = await supabase
      .from(table)
      .select(selectColumns)
      .order('updated_at', { ascending: false })
      .range(offset, offset + BATCH_SIZE - 1);

    if (error) {
      console.error(`Sitemap fetch error for ${table}:`, error.message);
      break;
    }

    if (data && data.length > 0) {
      allRows.push(...data);
      offset += BATCH_SIZE;
      hasMore = data.length === BATCH_SIZE;
    } else {
      hasMore = false;
    }
  }

  return allRows;
}

export async function fetchSlugsChunked(
  table: string,
  chunkIndex: number,
  chunkSize: number = 50000
): Promise<{ entries: SitemapEntry[]; totalCount: number }> {
  // 首先獲取網站地圖索引的總計數
  const { count } = await supabase
    .from(table)
    .select('*', { count: 'exact', head: true });

  const totalCount = count || 0;
  const startOffset = chunkIndex * chunkSize;
  const entries: SitemapEntry[] = [];
  let offset = startOffset;
  const endOffset = Math.min(startOffset + chunkSize, totalCount);

  while (offset < endOffset) {
    const batchEnd = Math.min(offset + BATCH_SIZE - 1, endOffset - 1);
    const { data, error } = await supabase
      .from(table)
      .select('slug, updated_at')
      .order('updated_at', { ascending: false })
      .range(offset, batchEnd);

    if (error || !data || data.length === 0) break;
    entries.push(...data);
    offset += data.length;
  }

  return { entries, totalCount };
}

export function getChunkCount(totalCount: number, chunkSize: number = 50000): number {
  return Math.ceil(totalCount / chunkSize);
}

這裡有幾點要注意。我們使用 SUPABASE_SERVICE_ROLE_KEY -- 而不是 anon 金鑰 -- 因為這些路由處理程序在伺服器端運行,我們不希望 RLS 策略減慢我們的網站地圖查詢速度。fetchSlugsChunked 函數僅獲取給定網站地圖檔案所需的特定分塊,而不是整個資料集。在 Vercel 的 60 秒函數超時時間內運行時,這很重要。

Building a Dynamic Sitemap for 91,000 Pages with Next.js and Supabase - architecture

構建網站地圖索引路由

網站地圖索引是您提交給 Google 的單一 URL。它引用您的所有子網站地圖。

// app/sitemap.xml/route.ts
import { NextResponse } from 'next/server';
import { createClient } from '@supabase/supabase-js';

export const revalidate = 3600; // ISR:每小時重新生成一次

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
);

const CHUNK_SIZE = 50000;
const SITE_URL = 'https://deluxeastrology.com';
const LOCALES = ['es', 'fr', 'de', 'pt', 'ja'];

async function getTableCount(table: string): Promise<number> {
  const { count } = await supabase
    .from(table)
    .select('*', { count: 'exact', head: true });
  return count || 0;
}

export async function GET() {
  const blogCount = await getTableCount('blog_posts');
  const celebrityCount = await getTableCount('celebrities');

  const blogChunks = Math.ceil(blogCount / CHUNK_SIZE);
  const celebrityChunks = Math.ceil(celebrityCount / CHUNK_SIZE);

  const now = new Date().toISOString();

  let sitemaps = '';

  // 靜態頁面網站地圖
  sitemaps += `
    <sitemap>
      <loc>${SITE_URL}/sitemap-pages.xml</loc>
      <lastmod>${now}</lastmod>
    </sitemap>`;

  // 部落格網站地圖
  for (let i = 0; i < blogChunks; i++) {
    sitemaps += `
    <sitemap>
      <loc>${SITE_URL}/sitemap-blog-${i}.xml</loc>
      <lastmod>${now}</lastmod>
    </sitemap>`;
  }

  // 名人網站地圖
  for (let i = 0; i < celebrityChunks; i++) {
    sitemaps += `
    <sitemap>
      <loc>${SITE_URL}/sitemap-celebrities-${i}.xml</loc>
      <lastmod>${now}</lastmod>
    </sitemap>`;
  }

  // 語言環境網站地圖
  for (const locale of LOCALES) {
    sitemaps += `
    <sitemap>
      <loc>${SITE_URL}/sitemap-locale-${locale}.xml</loc>
      <lastmod>${now}</lastmod>
    </sitemap>`;
  }

  const xml = `<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${sitemaps}
</sitemapindex>`;

  return new NextResponse(xml, {
    headers: {
      'Content-Type': 'application/xml',
      'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=600',
    },
  });
}

注意我們在這裡只進行 count 查詢 -- head: true 表示 Supabase 只返回計數,不返回任何行資料。這使網站地圖索引生成幾乎瞬間完成。

構建單個分塊網站地圖

以下是具有完整分頁的名人網站地圖處理程序:

// app/sitemap-celebrities-[chunk].xml/route.ts
import { NextResponse } from 'next/server';
import { fetchSlugsChunked } from '@/lib/supabase-sitemap';

export const revalidate = 3600;

const SITE_URL = 'https://deluxeastrology.com';

export async function GET(
  request: Request,
  { params }: { params: Promise<{ chunk: string }> }
) {
  const { chunk } = await params;
  const chunkIndex = parseInt(chunk, 10);

  if (isNaN(chunkIndex) || chunkIndex < 0) {
    return new NextResponse('Invalid chunk index', { status: 400 });
  }

  const { entries } = await fetchSlugsChunked('celebrities', chunkIndex);

  const urls = entries
    .map(
      (entry) => `
  <url>
    <loc>${SITE_URL}/celebrities/${entry.slug}</loc>
    <lastmod>${new Date(entry.updated_at).toISOString()}</lastmod>
    <changefreq>monthly</changefreq>
    <priority>0.6</priority>
  </url>`
    )
    .join('');

  const xml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${urls}
</urlset>`;

  return new NextResponse(xml, {
    headers: {
      'Content-Type': 'application/xml',
      'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=600',
    },
  });
}

部落格網站地圖遵循相同的模式,但具有不同的優先級和 changefreq:

// app/sitemap-blog-[chunk].xml/route.ts
import { NextResponse } from 'next/server';
import { fetchSlugsChunked } from '@/lib/supabase-sitemap';

export const revalidate = 3600;

const SITE_URL = 'https://deluxeastrology.com';

export async function GET(
  request: Request,
  { params }: { params: Promise<{ chunk: string }> }
) {
  const { chunk } = await params;
  const chunkIndex = parseInt(chunk, 10);

  const { entries } = await fetchSlugsChunked('blog_posts', chunkIndex);

  const urls = entries
    .map(
      (entry) => `
  <url>
    <loc>${SITE_URL}/blog/${entry.slug}</loc>
    <lastmod>${new Date(entry.updated_at).toISOString()}</lastmod>
    <changefreq>weekly</changefreq>
    <priority>0.8</priority>
  </url>`
    )
    .join('');

  const xml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${urls}
</urlset>`;

  return new NextResponse(xml, {
    headers: {
      'Content-Type': 'application/xml',
      'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=600',
    },
  });
}

您需要配置 Next.js 路由以處理動態段。在 App Router 中,資料夾名稱使用括號:

app/
  sitemap.xml/
    route.ts
  sitemap-pages.xml/
    route.ts
  sitemap-blog-[chunk].xml/
    route.ts
  sitemap-celebrities-[chunk].xml/
    route.ts
  sitemap-locale-[lang].xml/
    route.ts

如果括號中的資料夾名稱方法給您帶來麻煩(有時確實會),請改在 next.config.ts 中使用路由重寫:

// next.config.ts
const nextConfig = {
  async rewrites() {
    return [
      {
        source: '/sitemap-blog-:chunk(\\d+).xml',
        destination: '/api/sitemap-blog/:chunk',
      },
      {
        source: '/sitemap-celebrities-:chunk(\\d+).xml',
        destination: '/api/sitemap-celebrities/:chunk',
      },
      {
        source: '/sitemap-locale-:lang.xml',
        destination: '/api/sitemap-locale/:lang',
      },
    ];
  },
};

export default nextConfig;

靜態頁面網站地圖

對於靜態頁面網站地圖,我們硬編碼 URL,因為它們很少改變:

// app/sitemap-pages.xml/route.ts
import { NextResponse } from 'next/server';

export const revalidate = 86400; // 每天一次對靜態頁面很好

const SITE_URL = 'https://deluxeastrology.com';

const staticPages = [
  { path: '/', priority: '1.0', changefreq: 'daily' },
  { path: '/about', priority: '0.7', changefreq: 'monthly' },
  { path: '/solutions/birth-chart', priority: '0.9', changefreq: 'weekly' },
  { path: '/solutions/compatibility', priority: '0.9', changefreq: 'weekly' },
  { path: '/solutions/transit-report', priority: '0.9', changefreq: 'weekly' },
  { path: '/blog', priority: '0.8', changefreq: 'daily' },
  { path: '/celebrities', priority: '0.8', changefreq: 'daily' },
  { path: '/contact', priority: '0.5', changefreq: 'yearly' },
  { path: '/pricing', priority: '0.7', changefreq: 'monthly' },
];

export async function GET() {
  const now = new Date().toISOString();

  const urls = staticPages
    .map(
      (page) => `
  <url>
    <loc>${SITE_URL}${page.path}</loc>
    <lastmod>${now}</lastmod>
    <changefreq>${page.changefreq}</changefreq>
    <priority>${page.priority}</priority>
  </url>`
    )
    .join('');

  const xml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${urls}
</urlset>`;

  return new NextResponse(xml, {
    headers: {
      'Content-Type': 'application/xml',
      'Cache-Control': 'public, s-maxage=86400, stale-while-revalidate=3600',
    },
  });
}

使用 Hreflang 的在地化網站地圖

這是變得有趣的地方。對於多語言內容,您需要 xhtml:link 元素的 hreflang 屬性。每個在地化網站地圖引用每個頁面的所有替代語言版本:

// app/sitemap-locale-[lang].xml/route.ts
import { NextResponse } from 'next/server';
import { fetchAllSlugs } from '@/lib/supabase-sitemap';

export const revalidate = 3600;

const SITE_URL = 'https://deluxeastrology.com';
const ALL_LOCALES = ['en', 'es', 'fr', 'de', 'pt', 'ja'];

export async function GET(
  request: Request,
  { params }: { params: Promise<{ lang: string }> }
) {
  const { lang } = await params;

  if (!ALL_LOCALES.includes(lang)) {
    return new NextResponse('Invalid locale', { status: 404 });
  }

  const entries = await fetchAllSlugs('localized_pages');
  // 篩選具有此語言環境的頁面
  const localeEntries = entries.filter((e: any) => e.locale === lang);

  const urls = localeEntries
    .map((entry: any) => {
      const alternates = ALL_LOCALES.map(
        (loc) =>
          `    <xhtml:link rel="alternate" hreflang="${loc}" href="${SITE_URL}/${loc}/${entry.slug}" />`
      ).join('\n');

      return `
  <url>
    <loc>${SITE_URL}/${lang}/${entry.slug}</loc>
    <lastmod>${new Date(entry.updated_at).toISOString()}</lastmod>
    <changefreq>monthly</changefreq>
    <priority>0.6</priority>
${alternates}
  </url>`;
    })
    .join('');

  const xml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
        xmlns:xhtml="http://www.w3.org/1999/xhtml">${urls}
</urlset>`;

  return new NextResponse(xml, {
    headers: {
      'Content-Type': 'application/xml',
      'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=600',
    },
  });
}

ISR 重新驗證策略

我們在所有網站地圖路由上設置 revalidate = 3600。這意味著 Vercel 可以緩存 XML 長達一小時,然後在下一個請求時在背景中重新生成它。對於 91K 個頁面,這是最佳點 -- 足夠頻繁,新內容在同一天內出現,但不那麼激進,我們不會對 Supabase 施加很大的力量。

對於內容發布時的按需重新驗證,請添加重新驗證端點:

// app/api/revalidate-sitemap/route.ts
import { revalidatePath } from 'next/cache';
import { NextResponse } from 'next/server';

export async function POST(request: Request) {
  const { secret, paths } = await request.json();

  if (secret !== process.env.REVALIDATION_SECRET) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  // 重新驗證特定的網站地圖路徑
  const targetPaths = paths || ['/sitemap.xml'];
  for (const path of targetPaths) {
    revalidatePath(path);
  }

  return NextResponse.json({ revalidated: true, paths: targetPaths });
}

然後設置一個 Supabase 資料庫 Webhook(或通過 pg_net 的 Postgres 觸發器)在每次您的 celebritiesblog_posts 表被更新時調用此端點。

按內容類型的優先級和更改頻率

以下是我們使用的優先級矩陣。Google 已經說他們大多忽略 prioritychangefreq,但其他爬蟲(Bing、Yandex)仍然使用它們,它們不會造成傷害:

內容類型 優先級 更改頻率 理由
首頁 1.0 每天 最高重要性,經常更新
解決方案/功能 0.9 每週 核心產品頁面
部落格清單 0.8 每天 定期新增文章
部落格文章 0.8 每週 內容偶爾更新
名人頁面 0.6 每月 創建後幾乎不改變
在地化頁面 0.6 每月 翻譯更新不頻繁
聯絡/法律 0.5 每年 幾乎從不改變

lastmod 值至關重要,應始終來自您資料庫的 updated_at 列 -- 絕不要硬編碼為 new Date()。Google 使用 lastmod 來優先重新爬行,如果每頁都說它是現在修改的,Google 最終將完全忽略您的 lastmod。

Google Search Console 提交

這是直接的部分。在 GSC 中:

  1. 在左側邊欄中轉到 Sitemaps
  2. 輸入 https://yourdomain.com/sitemap.xml(僅索引 URL)
  3. 點擊提交

就這樣。不要提交個別子網站地圖。Google 讀取索引並自動發現所有子項。您應該在幾小時內看到狀態"成功",索引的 URL 計數將在接下來的 2-4 週內增加。

對於 91K 個 URL,預計 Google 在第一個月內索引 70-90%。其餘頁面通常具有內容薄弱、重複內容問題,或者根本就是 Google 爬蟲預算分配中的低優先級。

也將您的網站地圖添加到 robots.txt

# robots.txt
User-agent: *
Allow: /

Sitemap: https://deluxeastrology.com/sitemap.xml

當 Google 不願意索引您的頁面時進行除錯

這是大多數人陷入困境的地方。您已提交 91K 個 URL,但 GSC 只顯示 40K 個索引。以下是我們遵循的系統除錯檢查清單:

檢查意外的 Noindex 標籤

這是 #1 原因。運行現場檢查:

curl -s https://deluxeastrology.com/celebrities/some-slug | grep -i 'noindex'

也檢查您的 Next.js 佈局或頁面元資料。常見的錯誤是在應用於數千個頁面的佈局中設置 noindex

// 不好:這會取消索引所有使用此佈局的頁面
export const metadata = {
  robots: { index: false, follow: true },
};

驗證 robots.txt 未阻止爬蟲

在瀏覽器中檢查 https://yourdomain.com/robots.txt。確保您沒有意外阻止動態路由。在 Vercel 上,也檢查任何可能向 Googlebot 返回 403s 的中介軟體。

在 GSC 中檢查爬蟲錯誤

轉到 PagesWhy pages aren't indexed。常見問題:

  • "Crawled - currently not indexed":Google 看到了頁面,但決定不索引它。通常內容薄弱。
  • "Discovered - currently not indexed":Google 知道 URL 存在,但尚未爬蟲。爬蟲預算問題。
  • "Excluded by noindex tag":不言而喻。修復標籤。
  • "Duplicate without canonical":添加適當的規範標籤。

使用內部連結修復孤立頁面

這對大型網站來說是巨大的。如果您的名人頁面只能通過網站地圖發現,並且沒有零個內部連結指向它們,Google 將降低爬蟲它們的優先級。添加:

  • 鏈接到名人頁面組的類別/清單頁面
  • 每個名人頁面上的相關名人連結
  • 高流量頁面上的"趨勢"或"最近更新"部分
  • 具有結構化資料的麵包屑導航

驗證單個 URL

在未索引的特定頁面上使用 GSC 的 URL 檢查工具。它顯示 Google 看到的確切內容 -- 呈現的 HTML、任何錯誤、行動可用性問題和索引狀態。

檢查網站地圖回應頭

確保您的網站地圖路由返回正確的頭:

curl -I https://deluxeastrology.com/sitemap.xml

您應該看到 Content-Type: application/xml 和 200 狀態。如果您從陳舊快取獲得 304 Not Modified 回應,這可能導致 Google 跳過重新閱讀您的網站地圖。

性能和成本基準

以下是截至 2025 年初我們生產部署的真實數字:

指標
網站地圖中的總 URL 數 91,247
網站地圖索引生成時間 ~120ms(僅計數查詢)
單個網站地圖生成(50K URL) ~4.2 秒
每個網站地圖重新生成的 Supabase 查詢成本 ~$0.01
所有檔案合併的總網站地圖 XML 大小 ~8.4MB 未壓縮
每月網站地圖的 Vercel 帶寬 ~2.1GB(主要是 Googlebot)
Vercel Pro 計畫成本 $20/使用者/月
Supabase Pro 計畫成本 $25/月
30 天後的 GSC 索引率 提交的 URL 的 84%
內容發布到網站地圖更新的時間 ≤1 小時(ISR)或 ~5 秒(按需)

大的要點:這整個設置基本上沒有任何成本。網站地圖生成是您 Vercel 和 Supabase 帳單上的四捨五入錯誤。

如果您正在構建類似的大規模項目,並希望在架構方面獲得幫助,我們已經跨多個客戶網站進行過此操作。查看我們的 Next.js 開發能力 或我們的 headless CMS 開發工作。對於具有類似規模要求的基於 Astro 的網站,我們使用 Astro 端點方法 構建了可比較的解決方案。

完整的工作程式碼可作為 GitHub gist 使用:所有路由處理程序、Supabase 查詢庫和 next.config.ts 重寫。如果您的項目需要更自訂的內容 -- 多租戶網站地圖、即時重新驗證或 1M+ 頁面的網站地圖 -- 與我們聯絡,我們將進行範圍界定。

常見問題

單個網站地圖檔案可以包含多少個 URL? 網站地圖協議允許每個檔案最多 50,000 個 URL 和 50MB 未壓縮檔案大小。對於有超過 50K 頁面的網站,您需要一個網站地圖索引,引用多個分塊的網站地圖檔案。實際上,大多數網站地圖生成器在 45,000-50,000 個 URL 進行分塊,以保留安全邊界。

我應該使用 next-sitemap 還是構建自訂路由處理程序? next-sitemap(v4+)對於更簡單的設置很好,並且可以很好地處理自動分塊。但對於 91K+ 個具有內容類型特定優先級的動態頁面、具有 hreflang 的在地化網站地圖和細粒度 ISR 控制,自訂路由處理程序提供了更多控制。我們走上自訂路線,因為我們需要每種內容類型的不同重新驗證間隔,並且想要網站地圖結構來匹配我們的 GSC 除錯工作流程。

我是否應該將每個單獨的網站地圖檔案提交到 Google Search Console? 不。只提交網站地圖索引 URL(例如 https://yourdomain.com/sitemap.xml)。Google 讀取索引並自動發現和處理所有引用的子網站地圖。提交個別檔案是不必要的,會使您的 GSC 儀表板變得混亂。

應該多久重新生成一次大型動態網站的網站地圖? 對於大多數內容豐富的網站,通過 ISR (revalidate = 3600) 每小時重新生成是一個很好的預設值。如果您發布內容非常頻繁,請將其與由資料庫 webhook 觸發的按需重新驗證配對。不要在每個請求時重新生成 -- 這會打敗快取並不必要地增加 Supabase 負載。

為什麼 Google 不索引我的所有網站地圖 URL? 最常見的原因是:意外的 noindex 元標籤、robots.txt 阻止、內容薄弱/重複、沒有內部連結的孤立頁面以及爬蟲預算限制。在 GSC 的"Pages"報告下檢查"Why pages aren't indexed"以了解具體原因。對於大型網站,專注於改進孤立頁面的內部連結 -- 這通常是單一最大的槓桿。

網站地圖中的 priority 值是否確實影響 Google 排名? Google 已公開表示他們主要忽略 prioritychangefreq 值。但是,Bing 和其他搜尋引擎確實使用它們。lastmod 欄位是最重要的網站地圖信號 -- 確保它反映來自您資料庫的實際內容更改,而不是當前的時間戳。

如何處理 Supabase 的 1,000 行限制以進行網站地圖查詢? 使用 Supabase 的 .range(offset, offset + batchSize - 1) 方法以批次大小為 1,000 進行分頁。循環直到您獲取了當前網站地圖分塊的所有行。對於僅計數查詢(在網站地圖索引中使用),使用 .select('*', { count: 'exact', head: true }),它只返回計數而不傳輸任何行資料。

此方法能否處理 500K 或 100 萬個頁面? 是的,需要進行小的調整。分塊架構可線性擴展 -- 100 萬個頁面將產生大約 20 個子網站地圖。主要關注點是 Vercel 的 60 秒函數超時時間,用於生成單個 50K URL 網站地圖。如果您達到該限制,將分塊大小減少到 25,000 或 10,000 個 URL。網站地圖協議允許單個索引中最多 50,000 個網站地圖,所以您不會遇到索引級限制。