feat(sitemap): optimize doctor URL fetching and add sitemap index threshold warning

This commit is contained in:
hamed
2026-07-19 09:10:04 +03:30
parent 488e5eb0a4
commit 8eb55679b2
+25 -25
View File
@@ -20,6 +20,9 @@ const API_URL = process.env.NEXT_PUBLIC_API_URL;
const PAGE_LIMIT = 50; const PAGE_LIMIT = 50;
const MAX_PAGES = 200; const MAX_PAGES = 200;
// سقف استاندارد هر فایل sitemap ۵۰٬۰۰۰ URL است؛ کمی زودتر هشدار می‌دهیم.
const SITEMAP_INDEX_THRESHOLD = 45000;
function toSafeDate(value) { function toSafeDate(value) {
if (!value) return null; if (!value) return null;
const d = new Date(typeof value === 'number' ? value * 1000 : value); const d = new Date(typeof value === 'number' ? value * 1000 : value);
@@ -106,33 +109,17 @@ function cityFilterParams(scope) {
return params; return params;
} }
// شهرهایی که دامنهٔ اختصاصی دارند — موجودیت‌هایشان canonical روی همان دامنه دارند و // پاسخ لیست پزشکان از GET /api/v1/doctors حالا خودش city دارد، پس تفکیک دامنه با همان
// نباید در sitemap دامنهٔ اصلی تکرار شوند. // فیلتری انجام می‌شود که کلینیک‌ها استفاده می‌کنند. پیش از این، نبودِ city مجبورمان
const CITY_IDS_WITH_DOMAIN = citiesData // می‌کرد روی دامنهٔ اصلی کل لیست را ۳۵ بار (یک‌بار به‌ازای هر شهرِ دامنه‌دار) بگیریم و
.filter((c) => !isRootCity(c) && c.domain) // تفاضل بگیریم — همان کاری که ~۱۳ ثانیه طول می‌کشید.
.map((c) => c.id);
/**
* پاسخ لیست پزشکان شهر ندارد؛ پس تفکیک با فیلتر سمت API انجام می‌شود:
* روی دامنهٔ شهری با city_id، و روی دامنهٔ اصلی «همه منهای پزشکانِ شهرهای دامنه‌دار».
*/
async function getDoctorsForScope(scope) {
if (!scope.isRoot) return fetchAllPages('/api/v1/doctors', cityFilterParams(scope));
const [all, ...perCity] = await Promise.all([
fetchAllPages('/api/v1/doctors'),
...CITY_IDS_WITH_DOMAIN.map((cityId) =>
fetchAllPages('/api/v1/doctors', { city_id: String(cityId) })
),
]);
const ownedByCityDomain = new Set(perCity.flat().map((d) => d?.uuid).filter(Boolean));
return all.filter((d) => !ownedByCityDomain.has(d?.uuid));
}
async function getDoctorUrls(baseUrl, scope) { async function getDoctorUrls(baseUrl, scope) {
const doctors = await getDoctorsForScope(scope); const doctors = await fetchAllPages('/api/v1/doctors', cityFilterParams(scope));
return doctors return doctors
.filter((d) => !isThinDoctor(d)) .filter((d) => !isThinDoctor(d))
// روی دامنهٔ اصلی فقط پزشکانی که شهرشان دامنهٔ اختصاصی ندارد؛ روی دامنهٔ شهری
// فیلتر city_id سمت API قبلاً کار را کرده است.
.filter((d) => !scope.isRoot || !findDomainByCityId(extractEntityCityId(d)))
.map((d) => .map((d) =>
withLastModified( withLastModified(
{ {
@@ -221,11 +208,24 @@ export default async function sitemap() {
]; ];
const seen = new Set(); const seen = new Set();
return allUrls.filter((item) => { const uniqueUrls = allUrls.filter((item) => {
if (seen.has(item.url)) return false; if (seen.has(item.url)) return false;
seen.add(item.url); seen.add(item.url);
return true; return true;
}); });
// sitemap-index عمداً پیاده نشده: سقف استاندارد هر فایل ۵۰٬۰۰۰ URL است و
// بزرگ‌ترین دامنه اکنون حدود ۲٬۵۰۰ URL دارد — یعنی هنوز یک مرتبهٔ بزرگی فاصله.
// پیاده‌سازی‌اش با generateSitemaps ساختار URL را به /sitemap/[id].xml می‌برد و
// ارجاع robots.txt هم باید همراهش عوض شود؛ پس تا نزدیک‌شدن به آستانه نمی‌ارزد.
// این هشدار هست تا وقتی واقعاً لازم شد، در لاگ دیده شود نه در Search Console.
if (uniqueUrls.length > SITEMAP_INDEX_THRESHOLD) {
console.warn(
`[sitemap] ${domain}: ${uniqueUrls.length} URLs — نزدیک سقف ۵۰k، وقت پیاده‌سازی sitemap-index است`
);
}
return uniqueUrls;
} catch (error) { } catch (error) {
console.error('Error generating sitemap:', error); console.error('Error generating sitemap:', error);
return []; return [];