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 نداشته باشد؛ اگر ندارد دست‌نخورده بماند.
+7 -1
View File
@@ -4,6 +4,11 @@ import citiesData from '@/data/city.json';
import statesData from '@/data/state.json';
import { isRootCity } from '@/lib/rootCity';
// sitemap باید در زمان اجرا (per-request) روی سرور production تولید شود، نه در build.
// در build کانتینر به API دسترسی شبکه ندارد → fetch failed. force-dynamic این را قطعی می‌کند.
export const dynamic = 'force-dynamic';
export const revalidate = 3600;
const MAIN_DOMAIN = 'nobat724.com';
const API_URL = process.env.NEXT_PUBLIC_API_URL;
const PAGE_LIMIT = 500;
@@ -65,6 +70,7 @@ async function fetchAllPages(path, extraParams = {}) {
});
const res = await fetch(`${API_URL}${path}?${search.toString()}`, {
next: { revalidate: 3600 },
signal: AbortSignal.timeout(8000),
});
if (!res.ok) break;
const json = await res.json();
@@ -76,7 +82,7 @@ async function fetchAllPages(path, extraParams = {}) {
if (items.length < PAGE_LIMIT || (total && results.length >= Number(total))) break;
}
} catch (error) {
console.error(`Error fetching sitemap data from ${path}:`, error);
console.error(`[sitemap] failed to fetch ${path} (page skipped):`, error?.message || error);
}
return results;
}
+3 -1
View File
@@ -363,5 +363,7 @@
"361": "Community 361",
"362": "Community 362",
"363": "Community 363",
"364": "Community 364"
"364": "Community 364",
"365": "Community 365",
"366": "Community 366"
}
+1 -1
View File
@@ -1 +1 @@
nobat724_front
.
+48 -42
View File
@@ -1,16 +1,16 @@
# Graph Report - nobat724_front (2026-07-06)
## Corpus Check
- 592 files · ~534,127 words
- 593 files · ~534,940 words
- Verdict: corpus is large enough that graph structure adds value.
## Summary
- 2012 nodes · 1996 edges · 365 communities (336 shown, 29 thin omitted)
- 2024 nodes · 2007 edges · 367 communities (337 shown, 30 thin omitted)
- Extraction: 95% EXTRACTED · 5% INFERRED · 0% AMBIGUOUS · INFERRED: 98 edges (avg confidence: 0.8)
- Token cost: 0 input · 0 output
## Graph Freshness
- Built from commit: `fd5935a1`
- Built from commit: `beb050ae`
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
- Run `graphify update .` after code changes (no API cost).
@@ -140,6 +140,8 @@
- [[_COMMUNITY_Community 339|Community 339]]
- [[_COMMUNITY_Community 347|Community 347]]
- [[_COMMUNITY_Community 348|Community 348]]
- [[_COMMUNITY_Community 365|Community 365]]
- [[_COMMUNITY_Community 366|Community 366]]
## God Nodes (most connected - your core abstractions)
1. `getStateInfo()` - 23 edges
@@ -154,6 +156,8 @@
10. `Architecture Overview` - 10 edges
## Surprising Connections (you probably didn't know these)
- `Dashboard()` --calls--> `safeJsonParse()` [INFERRED]
app/dashboard/page.js → lib/sanitize.js
- `AboutUsPage()` --calls--> `getStateInfo()` [INFERRED]
components/aboutUs/index.js → lib/getStateInfo.js
- `ContactUsPage()` --calls--> `getStateInfo()` [INFERRED]
@@ -162,29 +166,27 @@
components/home/search/index.js → lib/getStateInfo.js
- `StLayout()` --calls--> `getStateInfo()` [INFERRED]
components/layout/StLayout.js → lib/getStateInfo.js
- `generateMetadata()` --calls--> `getStateInfo()` [INFERRED]
app/about-us/page.js → lib/getStateInfo.js
## Import Cycles
- None detected.
## Communities (365 total, 29 thin omitted)
## Communities (367 total, 30 thin omitted)
### Community 0 - "Community 0"
Cohesion: 0.05
Nodes (27): BgGray(), ButtonMenu(), Col(), ModalLogout(), Footer(), head, LogoNamad(), Namads (+19 more)
### Community 1 - "Community 1"
Cohesion: 0.07
Nodes (23): Head(), Head(), HeadTab(), convertTimestampToJalali(), convertTimestampToTime(), ButtonData(), DetailLg(), DetailSm() (+15 more)
Cohesion: 0.24
Nodes (8): Head(), convertTimestampToJalali(), convertTimestampToTime(), ButtonData(), DetailLg(), DetailSm(), Head(), Card()
### Community 2 - "Community 2"
Cohesion: 0.04
Nodes (45): dependencies, aos, axios, @casl/ability, @casl/react, date-fns, dayjs, @emotion/cache (+37 more)
### Community 3 - "Community 3"
Cohesion: 0.06
Nodes (21): ClinicsPage(), ProvinceContext, ProvinceProvider(), Probe(), useProvince(), Content(), Form(), SubmitData() (+13 more)
Cohesion: 0.10
Nodes (12): ProvinceContext, ProvinceProvider(), Probe(), useProvince(), Content(), Form(), SubmitData(), EditField() (+4 more)
### Community 4 - "Community 4"
Cohesion: 0.07
@@ -195,8 +197,8 @@ Cohesion: 0.08
Nodes (18): AppointmentList(), ItemAppointment(), AboutDcotor(), DetailDoctor(), Link(), listLink, List(), VAZIR_WEIGHTS (+10 more)
### Community 6 - "Community 6"
Cohesion: 0.07
Nodes (16): Dashboard(), metadata, defineAbilitiesFor(), getUser(), formatToman(), rialToToman(), buildPatientUser(), safeJsonParse() (+8 more)
Cohesion: 0.15
Nodes (8): Dashboard(), metadata, defineAbilitiesFor(), getUser(), buildPatientUser(), getServerAccessToken(), LogIn(), metadata
### Community 8 - "Community 8"
Cohesion: 0.07
@@ -231,8 +233,8 @@ Cohesion: 0.14
Nodes (10): Chart(), ProgressAll(), fallbackLabels, ProgressDetail(), Form(), Content(), Rates(), numberToArStyle() (+2 more)
### Community 16 - "Community 16"
Cohesion: 0.14
Nodes (10): Head(), AboutUsPage(), ourServices, Services(), SearchBar(), TickOrangeA(), UnderlineLG(), ButtonFilter() (+2 more)
Cohesion: 0.25
Nodes (6): Head(), AboutUsPage(), ourServices, Services(), TickOrangeA(), UnderlineLG()
### Community 17 - "Community 17"
Cohesion: 0.11
@@ -347,12 +349,12 @@ Cohesion: 0.14
Nodes (13): `hours/List.js`, آبجکت اسلات (از `adaptSlots` / API), تأیید و سخت‌سازی غیرفعال‌بودن اسلات‌های گذشته در صفحه‌ی نوبت, زمینه, فایل‌های مرتبط, مشکل / هدف, نکات مهم, وضعیت فعلی (کد واقعی) (+5 more)
### Community 45 - "Community 45"
Cohesion: 0.26
Nodes (11): generateMetadata(), generateMetadata(), RootLayout(), imageUrl(), normalizeBlog(), safeJsonLd(), Blog(), Clinic() (+3 more)
Cohesion: 0.31
Nodes (9): generateMetadata(), generateMetadata(), imageUrl(), normalizeBlog(), Blog(), Doctor(), getBlog, getDoctor (+1 more)
### Community 46 - "Community 46"
Cohesion: 0.22
Nodes (6): Messages(), Message(), Head(), listTab, UserAccountPage(), NotfoundDashboard()
Cohesion: 0.10
Nodes (15): Messages(), Message(), Card(), PAYMENT_STATUS, TYPE_LABELS, Head(), listTab, Transactions() (+7 more)
### Community 47 - "Community 47"
Cohesion: 0.15
@@ -371,8 +373,8 @@ Cohesion: 0.15
Nodes (12): زمینه, فایل‌های مرتبط, مشکل / هدف, نمایش تعداد پزشکان هر تخصص در صفحه /specialties, نکات مهم, وضعیت فعلی (کد واقعی), وظایف, پروژه (+4 more)
### Community 51 - "Community 51"
Cohesion: 0.19
Nodes (9): Card(), PAYMENT_STATUS, TYPE_LABELS, Head(), listTab, Transactions(), List(), PAYMENT_STATUS (+1 more)
Cohesion: 0.13
Nodes (11): ClinicsPage(), DoctorsPage(), AutoComplete(), ButtonFilter(), Content(), ModalSearchCity(), AutoSearch(), FilterButton() (+3 more)
### Community 52 - "Community 52"
Cohesion: 0.24
@@ -471,28 +473,28 @@ Cohesion: 0.29
Nodes (7): Authentication & Authorization, Development Tools, Frontend Framework, State Management & Data Fetching, UI/UX, 🏗️ معماری و تکنولوژی, کتابخانه‌های تخصصی
### Community 76 - "Community 76"
Cohesion: 0.40
Nodes (3): Allergies(), ItemAllergie(), ModalAddAlergie()
Cohesion: 0.07
Nodes (20): Allergies(), ItemAllergie(), HeadTab(), setNewData(), DetailUser(), Disease(), FamilyHistory(), ItemHistory() (+12 more)
### Community 79 - "Community 79"
Cohesion: 0.40
Nodes (3): FamilyHistory(), ItemHistory(), ModalAddHistory()
Cohesion: 0.17
Nodes (5): clearAccessToken(), api, handleSessionExpired(), request, removeToken()
### Community 81 - "Community 81"
Cohesion: 0.40
Nodes (3): Medications(), ItemMedication(), ModalAddMedications()
Cohesion: 0.24
Nodes (5): Head(), Article(), LatestArticles(), Title(), ItemTitle()
### Community 82 - "Community 82"
Cohesion: 0.40
Nodes (3): ModalAddRelatives(), Relatives(), ItemRelatives()
Cohesion: 0.17
Nodes (11): رفع خطای `TypeError: fetch failed` در sitemap هنگام دیپلوی (Docker build), زمینه, فایل‌های مرتبط, مشکل / هدف, نکات مهم, وضعیت فعلی, وظایف, پروژه (+3 more)
### Community 83 - "Community 83"
Cohesion: 0.40
Nodes (3): ModalAddSurgeries(), Surgeries(), ItemSurgeries()
Cohesion: 0.24
Nodes (6): STATUS_LABELS, Head(), listTab, Turns(), List(), STATUS_LABELS
### Community 85 - "Community 85"
Cohesion: 0.53
Nodes (3): setNewData(), DetailUser(), Disease()
Cohesion: 0.36
Nodes (4): SearchBar(), ButtonFilter(), Fields(), RedirectLink()
### Community 86 - "Community 86"
Cohesion: 0.40
@@ -502,10 +504,6 @@ Nodes (5): Doctors(), FILTER_KEYS, generateMetadata(), listingRobots(), buildDoc
Cohesion: 0.33
Nodes (6): 1. افزودن صفحه جدید, 2. افزودن API Endpoint جدید, 3. افزودن کامپوننت جدید, 4. کار با تاریخ شمسی, 5. استفاده از Context, 📝 نکات توسعه
### Community 89 - "Community 89"
Cohesion: 0.70
Nodes (4): generateMetadata(), Doctor(), getDoctor, getDoctorAddresses
### Community 90 - "Community 90"
Cohesion: 0.50
Nodes (4): buildDays(), InlineJalaliMonth(), MONTHS, WEEKDAYS
@@ -554,25 +552,33 @@ Nodes (4): Metadata داینامیک بر اساس شهر, ساختار دامن
Cohesion: 0.67
Nodes (3): Feature Components, Layout Components, 🧩 کامپوننت‌های اصلی
### Community 365 - "Community 365"
Cohesion: 0.70
Nodes (4): generateMetadata(), Clinic(), computeClinicRating(), getClinic
### Community 366 - "Community 366"
Cohesion: 0.50
Nodes (3): RootLayout(), safeJsonLd(), safeJsonParse()
## Knowledge Gaps
- **668 isolated node(s):** `metadata`, `FILTER_KEYS`, `fallbackLabels`, `MaterialUISwitch`, `fixedIconData` (+663 more)
- **677 isolated node(s):** `metadata`, `FILTER_KEYS`, `fallbackLabels`, `MaterialUISwitch`, `fixedIconData` (+672 more)
These have ≤1 connection - possible missing edges or undocumented components.
- **29 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
- **30 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
## Suggested Questions
_Questions this graph is uniquely positioned to answer:_
- **Why does `getStateInfo()` connect `Community 29` to `Community 0`, `Community 67`, `Community 3`, `Community 70`, `Community 6`, `Community 72`, `Community 45`, `Community 14`, `Community 16`, `Community 86`, `Community 89`?**
- **Why does `getStateInfo()` connect `Community 29` to `Community 0`, `Community 67`, `Community 70`, `Community 6`, `Community 72`, `Community 365`, `Community 45`, `Community 366`, `Community 16`, `Community 14`, `Community 51`, `Community 85`, `Community 86`?**
_High betweenness centrality (0.040) - this node is a cross-community bridge._
- **Why does `imageUrl()` connect `Community 45` to `Community 0`, `Community 1`, `Community 99`, `Community 4`, `Community 7`, `Community 55`, `Community 56`, `Community 89`?**
- **Why does `imageUrl()` connect `Community 45` to `Community 0`, `Community 99`, `Community 4`, `Community 7`, `Community 365`, `Community 81`, `Community 55`, `Community 56`, `Community 89`?**
_High betweenness centrality (0.039) - this node is a cross-community bridge._
- **Why does `Dashboard()` connect `Community 6` to `Community 29`, `Community 70`?**
- **Why does `Dashboard()` connect `Community 6` to `Community 3`, `Community 70`, `Community 366`, `Community 79`, `Community 29`?**
_High betweenness centrality (0.015) - this node is a cross-community bridge._
- **Are the 22 inferred relationships involving `getStateInfo()` (e.g. with `generateMetadata()` and `AboutUsPage()`) actually correct?**
_`getStateInfo()` has 22 INFERRED edges - model-reasoned connections that need verification._
- **Are the 14 inferred relationships involving `imageUrl()` (e.g. with `generateMetadata()` and `generateMetadata()`) actually correct?**
_`imageUrl()` has 14 INFERRED edges - model-reasoned connections that need verification._
- **What connects `metadata`, `FILTER_KEYS`, `fallbackLabels` to the rest of the system?**
_668 weakly-connected nodes found - possible documentation gaps or missing edges._
_677 weakly-connected nodes found - possible documentation gaps or missing edges._
- **Should `Community 0` be split into smaller, more focused modules?**
_Cohesion score 0.05263157894736842 - nodes in this community are weakly interconnected._
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+4220 -3990
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff