使用 Next.js 和 Supabase 為 91,000 頁面構建動態網站地圖
上個月我們在 Deluxe Astrology 突破了 91,000 個頁面。名人出生圖表、部落格文章、跨六種語言的在地化內容 -- 該網站已經遠超單一網站地圖檔案的處理能力。Google 的網站地圖協議限制每個檔案最多 50,000 個 URL 和 50MB 未壓縮大小。我們需要一個網站地圖索引,包含分塊的子網站地圖,從 Supabase 動態生成,在 Vercel 上使用 ISR 快取,並作為單一索引 URL 提交到 Google Search Console。
這正是我們發布的實現方式。不是理論上的演練 -- 實際生產程式碼,今天處理 91K 個 URL,不做任何改變就能擴展到 500K。
目錄
- 理解網站地圖限制和架構
- Deluxe Astrology 的網站地圖結構
- 使用偏移分頁設置 Supabase 查詢
- 構建網站地圖索引路由
- 構建單個分塊網站地圖
- 靜態頁面網站地圖
- 使用 Hreflang 的在地化網站地圖
- ISR 重新驗證策略
- 按內容類型的優先級和更改頻率
- Google Search Console 提交
- 當 Google 不願意索引您的頁面時進行除錯
- 性能和成本基準
- 常見問題

理解網站地圖限制和架構
以下是您需要知道的硬性限制:
| 約束 | 限制 | 來源 |
|---|---|---|
| 每個網站地圖檔案的 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。我們按內容類型分割 -- 名人、部落格文章、靜態頁面、在地化頁面 -- 因為每種類型都有不同的 changefreq、priority 和更新模式。這使我們能夠更好地控制,並使當出現問題時在 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 秒函數超時時間內運行時,這很重要。

構建網站地圖索引路由
網站地圖索引是您提交給 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 觸發器)在每次您的 celebrities 或 blog_posts 表被更新時調用此端點。
按內容類型的優先級和更改頻率
以下是我們使用的優先級矩陣。Google 已經說他們大多忽略 priority 和 changefreq,但其他爬蟲(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 中:
- 在左側邊欄中轉到 Sitemaps
- 輸入
https://yourdomain.com/sitemap.xml(僅索引 URL) - 點擊提交
就這樣。不要提交個別子網站地圖。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 中檢查爬蟲錯誤
轉到 Pages → Why 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 已公開表示他們主要忽略 priority 和 changefreq 值。但是,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 個網站地圖,所以您不會遇到索引級限制。