Files
nobat724_front/.claude/prompt/fix-sitemap-build-fetch-failed.md
T

6.7 KiB

رفع خطای TypeError: fetch failed در sitemap هنگام دیپلوی (Docker build)

پروژه

nobat724_front

زمینه

هنگام دیپلوی روی سرور (Coolify، Docker multi-stage build) در لاگ build این خطا ظاهر می‌شود:

#17 192.1 Error fetching sitemap data from /api/v1/clinics: TypeError: fetch failed

#17 همان مرحله‌ی builder در Dockerfile است (RUN npm run build، خط ۵۵). یعنی این خطا در زمان build رخ می‌دهد، نه زمان اجرا.

مشکل / هدف

app/sitemap.js یک metadata route است. Next.js هنگام next build تلاش می‌کند این route را prerender کند و در همان لحظه تابع sitemap() اجرا می‌شود؛ این تابع از طریق fetchAllPages() به NEXT_PUBLIC_API_URL (یعنی api.clinic-pro.ir) درخواست fetch می‌زند.

کانتینر build در Coolify به این API دسترسی شبکه‌ای ندارد (شبکه build ایزوله است / DNS در دسترس نیست) → fetch با TypeError: fetch failed شکست می‌خورد.

خطا داخل try/catch تابع fetchAllPages گرفته می‌شود (خط ۷۸–۸۰)، پس build کرش نمی‌کند ولی نتیجه‌اش این است که sitemap تولیدشده خالی است (فقط صفحات استاتیک، بدون هیچ URL پزشک/کلینیک/بلاگ). این هم لاگ خطای آزاردهنده می‌دهد و هم SEO را خراب می‌کند.

هدف: sitemap در زمان build اصلاً به API وصل نشود؛ داده‌ها در زمان اجرا (request-time) روی سرور production گرفته شوند — جایی که کانتینر runtime به API دسترسی دارد.

فایل‌های مرتبط

فایل نقش
app/sitemap.js تولید sitemap؛ محل fetch در زمان build
Dockerfile مرحله‌ی builder خط ۵۵ (RUN npm run build) جایی که خطا رخ می‌دهد — فقط برای درک، تغییر نمی‌کند
app/robots.js مشابه sitemap؛ ولی fetch ندارد — احتمالاً نیاز به تغییر نیست، فقط چک شود

وضعیت فعلی

app/sitemap.js هیچ export const dynamic یا revalidate ندارد، پس Next آن را کاندید static prerender در build می‌داند. تابع fetch:

// app/sitemap.js — خط 56
async function fetchAllPages(path, extraParams = {}) {
    if (!API_URL) return [];
    const results = [];
    try {
        for (let page = 1; page <= MAX_PAGES; page++) {
            const search = new URLSearchParams({
                ...extraParams,
                page: String(page),
                limit: String(PAGE_LIMIT),
            });
            const res = await fetch(`${API_URL}${path}?${search.toString()}`, {
                next: { revalidate: 3600 },
            });
            if (!res.ok) break;
            // ...
        }
    } catch (error) {
        console.error(`Error fetching sitemap data from ${path}:`, error);
    }
    return results;
}

// خط 140
export default async function sitemap() { /* ... */ }

وظایف

۱. اجبار sitemap به رندر در زمان اجرا (نه build)

بالای app/sitemap.js (بعد از importها) این دو خط را اضافه کن تا Next هرگز sitemap را در build فچ نکند و همیشه per-request روی سرور production تولید شود:

export const dynamic = 'force-dynamic';
export const revalidate = 3600; // کش ۱ ساعته در لایه‌ی سرور

نکته: تابع فعلاً از await headers() استفاده می‌کند که باید route را dynamic کند، اما لاگ build ثابت می‌کند که همچنان در build اجرا می‌شود. force-dynamic این را قطعی می‌کند. بعد از افزودن آن، بلوک next: { revalidate: 3600 } داخل fetch را نگه‌دار — با force-dynamic هم بی‌ضرر است.

۲. مقاوم‌سازی fetch با timeout

اگر در زمان اجرا API کند یا موقتاً down باشد، هر صفحه‌ی sitemap نباید بی‌نهایت منتظر بماند. به fetch یک timeout اضافه کن:

const res = await fetch(`${API_URL}${path}?${search.toString()}`, {
    next: { revalidate: 3600 },
    signal: AbortSignal.timeout(8000), // 8s برای هر صفحه
});

try/catch موجود (خط ۷۸) خطای timeout را هم می‌گیرد، پس رفتار fail-safe فعلی (بازگشت آرایه‌ی جمع‌شده تا آن لحظه) حفظ می‌شود.

۳. (اختیاری) لاگ تمیزتر برای شبکه‌ی در دسترس‌نبودن

پیام خطای فعلی خام است. برای اینکه در آینده گیج‌کننده نباشد، پیام را کمی روشن‌تر کن (فقط پیام، منطق دست‌نخورده):

} catch (error) {
    console.error(`[sitemap] failed to fetch ${path} (page skipped):`, error?.message || error);
}

نکات مهم

  • ریشه‌ی واقعی شبکه build: NEXT_PUBLIC_API_URL در Dockerfile به‌عنوان build arg پاس داده می‌شود چون برای inline شدن در bundle کلاینت لازم است — این درست است و نباید حذف شود. مشکل صرفاً این است که در حین build نباید به آن fetch زده شود. راه‌حل بالا همین را حل می‌کند؛ نیازی به تغییر Dockerfile نیست.
  • بعد از این تغییر، sitemap فقط زمانی که خزنده یا کاربر /sitemap.xml را روی سرور production باز کند تولید می‌شود؛ آنجا کانتینر runtime به api.clinic-pro.ir دسترسی دارد.
  • معماری multi-domain حفظ شود: sitemap() از await headers() برای تشخیص host/شهر استفاده می‌کند — با force-dynamic این هدرها در زمان اجرا در دسترس‌اند (در build نبودند). این یک دلیل اضافه برای درست بودن force-dynamic است.
  • output: 'standalone' در next.config.js فعال است؛ route داینامیک در سرور standalone بدون مشکل کار می‌کند.
  • تست محلی: npm run build باید بدون خطای fetch failed تمام شود؛ سپس npm run start و باز کردن http://yazd-nobat.localhost:3000/sitemap.xml باید URLهای پزشک/کلینیک را نشان دهد (با API در دسترس).
  • app/robots.js را چک کن که fetch نداشته باشد؛ اگر ندارد دست‌نخورده بماند.