بناء خريطة موقع ديناميكية لـ 91,000 صفحة باستخدام Next.js و Supabase
في الشهر الماضي وصلنا إلى 91,000 صفحة في Deluxe Astrology. خرائط الولادة للمشاهير، منشورات المدونة، المحتوى المحلي عبر ست لغات -- نما الموقع بشكل كبير جداً عما يمكن لملف sitemap واحد أن يتعامل معه. بروتوكول sitemap من Google يقيدك بـ 50,000 URL لكل ملف و 50 ميجابايت غير مضغوط. احتجنا إلى فهرس sitemap مع ملفات sitemap فرعية مقسمة، جميعها يتم توليدها ديناميكياً من Supabase، مخبأة باستخدام ISR على Vercel، وتُرسل إلى Google Search Console كـ URL فهرس واحد.
هذا هو التطبيق الدقيق الذي أطلقناه. ليس مجرد شرح نظري -- كود إنتاجي فعلي يتعامل مع 91K URL اليوم وسيتوسع إلى 500K بدون تغييرات.
جدول المحتويات
- فهم حدود البنية المعمارية للخريطة
- بنية Sitemap لـ Deluxe Astrology
- إعداد استعلامات Supabase مع ترقيم الصفحات بالإزاحة
- بناء مسار فهرس Sitemap
- بناء ملفات Sitemap المقسمة الفردية
- Sitemap الصفحات الثابتة
- Sitemaps المحلية مع Hreflang
- استراتيجية إعادة التحقق من ISR
- الأولوية وتكرار التغيير حسب نوع المحتوى
- تقديم Google Search Console
- تصحيح الأخطاء عندما لن تفهرس Google صفحاتك
- مؤشرات الأداء والتكلفة
- الأسئلة الشائعة

فهم حدود البنية المعمارية للخريطة
إليك الحدود الصعبة التي تحتاج لمعرفتها:
| القيد | الحد | المصدر |
|---|---|---|
| عدد URLs لكل ملف sitemap | 50,000 | بروتوكول sitemaps.org |
| حجم الملف لكل sitemap | 50 ميجابايت غير مضغوط | بروتوكول sitemaps.org |
| Sitemaps لكل فهرس sitemap | 50,000 | بروتوكول sitemaps.org |
الحد الأقصى لـ .range() في Supabase لكل استعلام |
1,000 صف (الافتراضي) | إعدادات Supabase PostgREST |
| مهلة دالة Vercel بدون خادم (Pro) | 60 ثانية | وثائق Vercel 2025 |
| حد حجم جسم الاستجابة في Vercel | 10 ميجابايت | التخزين المؤقت للحافة في Vercel |
بالنسبة لـ 91,000 URL، تحتاج على الأقل إلى ملفي sitemap. لكننا لا ننسكب فقط كل شيء في دلاويين بـ 50K URL. نقسم حسب نوع المحتوى -- المشاهير، منشورات المدونة، الصفحات الثابتة، الصفحات المحلية -- لأن كل نوع له changefreq، priority، وأنماط تحديث مختلفة. هذا يعطينا تحكماً أفضل ويجعل تصحيح الأخطاء في GSC أسهل بكثير عندما يحدث خطأ ما.
بنية Sitemap لـ Deluxe Astrology
إليك ما تبدو عليه البنية المعمارية للخريطة النهائية:
/sitemap.xml → Sitemap Index (يشير إلى جميع ملفات sitemap الفرعية)
/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 → صفحات محلية باللغة اليابانية
كل ملف sitemap فرعي هو معالج مسار App Router في Next.js يستعلم Supabase في وقت التشغيل، يولد XML، ويخزن مؤقتاً عبر ISR مع revalidate = 3600 (كل ساعة). فهرس sitemap نفسه هو أيضاً معالج مسار.
إعداد استعلامات Supabase مع ترقيم الصفحات بالإزاحة
إليك الجزء الحاسم الذي معظم البرامج التعليمية تخطئ فيه: لا يمكنك فقط فعل supabase.from('celebrities').select('*') والتوقع الحصول على 91,000 صف في المقابل. طبقة PostgREST في Supabase تقتصر على إرجاع 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! // استخدم service role للجانب الخادم
);
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 لـ ${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 }> {
// أولاً احصل على العدد الإجمالي لفهرس sitemap
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 التي تبطئ استعلامات sitemap الخاصة بنا. تجلب دالة fetchSlugsChunked فقط القسم المحدد المطلوب لملف sitemap معين، وليس مجموعة البيانات بأكملها. هذا يهم عندما تعمل على مهلة 60 ثانية لدالة Vercel بدون خادم.

بناء مسار فهرس Sitemap
فهرس sitemap هو URL واحد تقدمه إلى Google. يشير إلى جميع ملفات sitemap الفرعية الخاصة بك.
// 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 = '';
// sitemap الصفحات الثابتة
sitemaps += `
<sitemap>
<loc>${SITE_URL}/sitemap-pages.xml</loc>
<lastmod>${now}</lastmod>
</sitemap>`;
// sitemaps المدونة
for (let i = 0; i < blogChunks; i++) {
sitemaps += `
<sitemap>
<loc>${SITE_URL}/sitemap-blog-${i}.xml</loc>
<lastmod>${now}</lastmod>
</sitemap>`;
}
// sitemaps المشاهير
for (let i = 0; i < celebrityChunks; i++) {
sitemaps += `
<sitemap>
<loc>${SITE_URL}/sitemap-celebrities-${i}.xml</loc>
<lastmod>${now}</lastmod>
</sitemap>`;
}
// sitemaps المحلية
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 ترجع العدد فقط بدون أي بيانات صف. هذا يجعل توليد فهرس sitemap سريعاً جداً.
بناء ملفات Sitemap المقسمة الفردية
إليك معالج sitemap المشاهير مع ترقيم الصفحات الكامل:
// 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('فهرس القسم غير صحيح', { 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',
},
});
}
يتبع sitemap المدونة نفس النمط لكن بأولوية وتكرار تغيير مختلفة:
// 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
إذا كان نهج الأقواس في اسم المجلد يسبب لك مشاكل مع نظام الملفات أو IDE (أحياناً يحدث)، استخدم إعادة توجيه المسارات في 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;
Sitemap الصفحات الثابتة
لـ sitemap الصفحات الثابتة، نرمز URLs لأنها نادراً ما تتغير:
// 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',
},
});
}
Sitemaps المحلية مع Hreflang
هنا تصبح الأمور مثيرة للاهتمام. للمحتوى متعدد اللغات، تحتاج إلى عناصر xhtml:link مع سمات hreflang. كل sitemap محلي يشير إلى جميع الإصدارات اللغوية البديلة لكل صفحة:
// 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('محلية غير صحيحة', { 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 على جميع مسارات sitemap. هذا يعني أن 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: 'غير مصرح' }, { status: 401 });
}
// أعد التحقق من مسارات sitemap محددة
const targetPaths = paths || ['/sitemap.xml'];
for (const path of targetPaths) {
revalidatePath(path);
}
return NextResponse.json({ revalidated: true, paths: targetPaths });
}
ثم قم بإعداد Supabase Database Webhook (أو trigger Postgres عبر pg_net) لاستدعاء هذه النقطة النهائية كلما تم تحديث جداول 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 الفهرس فقط) - انقر فوق Submit
هذا كل شيء. لا تقدم ملفات sitemap فردية. تقرأ Google الفهرس وتكتشف جميع الأطفال تلقائياً. يجب أن ترى الحالة "Success" في غضون ساعات قليلة، وستتزايد عدادات URL المفهرسة خلال الأسابيع 2-4 التالية.
بالنسبة لـ 91K URL، توقع أن تفهرس Google 70-90٪ في الشهر الأول. عادةً ما تحتوي الصفحات المتبقية على محتوى رقيق أو مشاكل محتوى مكرر، أو هي ببساطة منخفضة الأولوية في تخصيص ميزانية الزحف في Google.
أضف أيضاً sitemap الخاص بك إلى robots.txt:
# robots.txt
User-agent: *
Allow: /
Sitemap: https://deluxeastrology.com/sitemap.xml
تصحيح الأخطاء عندما لن تفهرس Google صفحاتك
هنا حيث يعلق معظم الناس. لقد قدمت 91K URL لكن GSC تظهر فقط 40K مفهرسة. إليك قائمة فحص تصحيح الأخطاء المنهجية التي نتابعها:
تحقق من وجود وسوم Noindex العرضية
هذا هو السبب الأول. قم بفحص موضعي:
curl -s https://deluxeastrology.com/celebrities/some-slug | grep -i 'noindex'
تحقق أيضاً من بيانات تعريف Next.js الخاصة بك في layout أو page. خطأ شائع هو تعيين noindex في layout ينطبق على آلاف الصفحات:
// سيء: هذا يضع noindex على جميع الصفحات التي تستخدم هذا layout
export const metadata = {
robots: { index: false, follow: true },
};
تحقق من أن robots.txt لا يحجب الزحف
تحقق من https://yourdomain.com/robots.txt في المتصفح. تأكد من أنك لا تحجب مساراتك الديناميكية عن غير قصد. على Vercel، تحقق أيضاً من أي middleware قد يعيد 403 إلى Googlebot.
افحص أخطاء الزحف في 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": أضف وسوم canonical صحيحة.
أصلح الصفحات المعزولة بالربط الداخلي
هذا مهم جداً للمواقع الكبيرة. إذا كانت صفحات المشاهير الخاصة بك قابلة للاكتشاف فقط من خلال sitemap وليس لديها روابط داخلية تشير إليها، ستقلل Google من أولويتها في الزحف. أضف:
- صفحات الفئة/القائمة التي تربط إلى مجموعات صفحات المشاهير
- روابط المشاهير المرتبطة على كل صفحة مشهور
- أقسام "Trending" أو "Recently Updated" على الصفحات ذات حركة المرور العالية
- التنقل بالفتات الخبز مع البيانات المنظمة
تحقق من URLs الفردية
استخدم أداة فحص URL في GSC على صفحات محددة لم يتم فهرستها. يُظهر لك بالضبط ما ترى Google -- HTML مرسلة، أي أخطاء، مشاكل سهولة الاستخدام على الهاتف المحمول، وحالة الفهرسة.
تحقق من رؤوس استجابة Sitemap
تأكد من أن مسارات sitemap الخاصة بك ترجع رؤوسًا مناسبة:
curl -I https://deluxeastrology.com/sitemap.xml
يجب أن ترى Content-Type: application/xml وحالة 200. إذا كنت تحصل على استجابات 304 Not Modified من الذاكرة المؤقتة القديمة، فقد يؤدي ذلك إلى قيام Google بتخطي إعادة قراءة sitemap الخاص بك.
مؤشرات الأداء والتكلفة
إليك الأرقام الفعلية من نشرنا الإنتاجي اعتباراً من أوائل 2025:
| المقياس | القيمة |
|---|---|
| إجمالي URLs في sitemap | 91,247 |
| وقت توليد فهرس sitemap | ~120ms (استعلامات العدد فقط) |
| توليد sitemap الفردي (50K URL) | ~4.2 ثانية |
| تكلفة استعلام Supabase لكل تجديد sitemap | ~$0.01 |
| إجمالي حجم XML لـ sitemap (جميع الملفات) | ~8.4MB غير مضغوط |
| نطاق الترددات في Vercel للخرائط شهرياً | ~2.1GB (في الغالب Googlebot) |
| تكلفة خطة Vercel Pro | $20/user/month |
| تكلفة خطة Supabase Pro | $25/month |
| معدل فهرسة GSC بعد 30 يوماً | 84% من URLs المُرسلة |
| الوقت من نشر المحتوى إلى تحديث sitemap | ≤1 ساعة (ISR) أو ~5 ثانية (عند الطلب) |
الوجبة الكبيرة: هذا الإعداد بأكمله يكلف فعلياً لا شيء للتشغيل. توليد sitemap هو خطأ تقريبي في فواتير Vercel و Supabase الخاصة بك.
إذا كنت تبني مشروعاً كبير النطاق مماثلاً وتريد مساعدة بالبنية المعمارية، فقد فعلنا هذا عبر مواقع عملاء متعددة. تحقق من قدرات تطوير Next.js أو عمل تطوير headless CMS الخاص بنا. بالنسبة للمواقع المبنية على Astro بمتطلبات مقياس مماثل، بنينا حلول قابلة للمقارنة باستخدام نهج نقطة نهاية Astro.
الكود العامل الكامل متاح كـ GitHub gist: جميع معالجات المسار، مكتبة استعلام Supabase، وإعادة توجيه next.config.ts. إذا احتاج مشروعك إلى شيء أكثر تخصيصاً -- خرائط متعددة المستأجرين، إعادة تحقق في الوقت الفعلي، أو خرائط لـ 1M+ صفحة -- تواصل معنا وسننتقل من خلاله.
الأسئلة الشائعة
كم عدد URLs التي يمكن أن يحتويها ملف sitemap واحد؟ يسمح بروتوكول sitemap بحد أقصى 50,000 URL لكل ملف وحجم ملف غير مضغوط 50 ميجابايت. بالنسبة للمواقع التي تحتوي على أكثر من 50K صفحة، تحتاج إلى فهرس sitemap يشير إلى ملفات sitemap مقسمة متعددة. في الممارسة العملية، معظم مولدات sitemap تقسم بـ 45,000-50,000 URL لترك هامش أمان.
هل يجب أن أستخدم next-sitemap أو بناء معالجات مسار مخصصة؟ next-sitemap (v4+) رائع للإعدادات الأبسط ويتعامل مع التقسيم التلقائي بشكل جيد. لكن بالنسبة لـ 91K+ صفحة ديناميكية مع أولويات خاصة بنوع المحتوى، خرائط محلية مع hreflang، والتحكم الدقيق في ISR، معالجات المسار المخصصة تعطيك تحكماً أكثر. ذهبنا للتخصيص لأننا احتجنا إلى فترات إعادة تحقق مختلفة لكل نوع محتوى أردنا أن تطابق بنية sitemap سير عمل تصحيح الأخطاء الخاص بنا في GSC.
هل أقدم كل ملف sitemap فردي إلى Google Search Console؟
لا. قدم فقط URL فهرس sitemap (مثل https://yourdomain.com/sitemap.xml). تقرأ Google الفهرس وتكتشف وتعالج جميع ملفات sitemap المرجعية تلقائياً. تقديم الملفات الفردية غير ضروري ويجعل لوحة تحكم GSC مزدحمة.
كم مرة يجب تجديد الخرائط الموقع للمواقع الديناميكية الكبيرة؟
بالنسبة لمعظم المواقع التي تحتوي على محتوى كثير، إعادة إنشاء كل ساعة عبر ISR (revalidate = 3600) هي قيمة افتراضية جيدة. إذا كنت تنشر محتوى بتكرار جداً، قم بدمجه مع إعادة تحقق عند الطلب مشغلة بـ database webhooks. لا تعيد إنشاء في كل طلب -- يهزم هذا التخزين المؤقت ويزيد من حمل Supabase غير الضروري.
لماذا Google لا تفهرس جميع URLs في sitemap الخاص بي؟ الأسباب الأكثر شيوعاً هي: وسوم meta noindex العرضية، حجب robots.txt، محتوى رقيق/مكرر، صفحات معزولة بدون روابط داخلية، وقيود ميزانية الزحف. تحقق من تقرير GSC "Pages" ضمن "Why pages aren't indexed" للأسباب المحددة. بالنسبة للمواقع الكبيرة، ركز على تحسين الربط الداخلي إلى الصفحات المعزولة -- هذا غالباً ما يكون أكبر رافعة واحدة.
هل قيمة priority في الخرائط الموقع تؤثر فعلاً على ترتيب Google؟
قالت Google علناً أنها تتجاهل في الغالب قيم priority و changefreq. ومع ذلك، يستخدمها Bing وأحرف البحث الأخرى. حقل lastmod هو أهم إشارة sitemap -- تأكد من أنه يعكس التغييرات الفعلية للمحتوى من قاعدة البيانات الخاصة بك، وليس الطابع الزمني الحالي.
كيف أتعامل مع حد Supabase البالغ 1,000 صف لاستعلامات sitemap؟
استخدم طريقة .range(offset, offset + batchSize - 1) من Supabase للصفحات في دفعات من 1,000. كرر حتى تجلب جميع الصفوف لقسم sitemap الحالي. بالنسبة لاستعلامات العدد فقط (المستخدمة في فهرس sitemap)، استخدم .select('*', { count: 'exact', head: true }) التي ترجع فقط العدد بدون نقل أي بيانات صف.
هل يمكن لهذا النهج التعامل مع 500K أو مليون صفحة؟ نعم، مع تعديلات طفيفة. تتسع البنية المعمارية المقسمة خطياً -- مليون صفحة ستنتج حوالي 20 ملف sitemap فرعي. الاهتمام الرئيسي يصبح مهلة 60 ثانية من Vercel لدالة بدون خادم لتوليد ملفات sitemap بـ 50K URL. إذا اصطدمت بهذا الحد، قلل حجم القطعة إلى 25,000 أو 10,000 URL لكل ملف. يسمح بروتوكول sitemap بـ 50,000 sitemap في فهرس واحد، لذا لن تصطدم بقيود مستوى الفهرس في أي وقت قريب.