Refactor code structure for improved readability and maintainability

This commit is contained in:
hamed
2026-07-06 15:10:19 +03:30
parent beb050aee2
commit df7fbb7530
12 changed files with 7810 additions and 4041 deletions
@@ -0,0 +1,110 @@
# رفع خطای `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:
```js
// 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 تولید شود:
```js
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 اضافه کن:
```js
const res = await fetch(`${API_URL}${path}?${search.toString()}`, {
next: { revalidate: 3600 },
signal: AbortSignal.timeout(8000), // 8s برای هر صفحه
});
```
`try/catch` موجود (خط ۷۸) خطای timeout را هم می‌گیرد، پس رفتار fail-safe فعلی (بازگشت آرایه‌ی جمع‌شده تا آن لحظه) حفظ می‌شود.
### ۳. (اختیاری) لاگ تمیزتر برای شبکه‌ی در دسترس‌نبودن
پیام خطای فعلی خام است. برای اینکه در آینده گیج‌کننده نباشد، پیام را کمی روشن‌تر کن (فقط پیام، منطق دست‌نخورده):
```js
} 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 نداشته باشد؛ اگر ندارد دست‌نخورده بماند.