feat: enhance domain handling for global representatives

- Updated `getStateInfo` to fetch site context for domains not in city.json, returning `repContext` with representative details.
- Implemented caching for site context requests to optimize performance.
- Modified doctor and clinic listing pages to pass the `domain` parameter when fetching data for global representatives.
- Adjusted metadata generation in layout and pages to reflect representative branding based on `repContext`.
- Added documentation for the new functionality in `.claude/prompt/global-rep-domain-site.md`.
This commit is contained in:
hamed
2026-07-09 07:34:09 +03:30
parent a56b7e8de5
commit ab2ab0dea2
12 changed files with 612 additions and 135 deletions
+116
View File
@@ -0,0 +1,116 @@
# سایت دامنه اختصاصی نماینده سراسری — تشخیص دامنه، فیلتر پزشکان/کلینیک‌ها
## پروژه
`nobat724_front`**پیش‌نیاز:** پرامپت backend اول اجرا شود: `clinicpro/.claude/prompt/representation-multi-city-domain-commission.md`. این پرامپت مصرف‌کننده قراردادهای آن است:
- `GET /api/v1/site-context?domain=<host>` (عمومی) → `{ type: "city"|"representation"|"unknown", representation: {uuid, full_name, is_global}|null, city: {...}|null }`
- پارامتر جدید `domain` روی `GET /api/v1/doctors` و `GET /api/v1/clinics` — اگر دامنه متعلق به نماینده سراسری باشد، backend فقط پزشکان/کلینیک‌های همان نماینده را برمی‌گرداند.
## زمینه
سایت multi-domain است و دامنه فقط با `data/city.json` تطبیق داده می‌شود (`lib/getStateInfo.js`). نماینده سراسری دامنه اختصاصی خودش را دارد (مثل `x-nobat.ir`) که در city.json نیست → الان چنین دامنه‌ای مثل «بدون شهر» رفتار می‌کند و همه پزشکان را نشان می‌دهد. باید: دامنه نماینده سراسری تشخیص داده شود و فقط پزشکان/کلینیک‌های ثبت‌شده توسط همان نماینده نمایش یابند. کمیسیون خودش backend-side است (از `frontend_address` پرداخت) — فرانت فقط باید مثل الان دامنه درست را در `frontend_address` بفرستد (بدون تغییر).
## مشکل / هدف
۱. `getStateInfo` برای دامنه‌های خارج از city.json از API زمینه بگیرد (`site-context`) و `repContext` برگرداند.
۲. صفحات لیست پزشکان/کلینیک‌ها روی دامنه نماینده سراسری، پارامتر `domain` را به API پاس بدهند.
۳. متادیتا/برندینگ صفحات روی دامنه نماینده از `full_name` نماینده ساخته شود (fallback «نوبت 724»).
## فایل‌های مرتبط
| فایل | نقش |
|------|-----|
| `lib/getStateInfo.js` | تشخیص دامنه — فقط city.json؛ باید repContext هم بدهد |
| `app/doctors/page.js` | لیست پزشکان — fetch `/api/v1/doctors` |
| `app/clinics/page.js` | لیست کلینیک‌ها — fetch `/api/v1/clinics` |
| `app/layout.js` | متادیتای پایه از matchedCity |
| `components/home/*` (سرچ صفحه اصلی) | روی دامنه rep هم باید `domain` را پاس بدهد |
| `lib/req.js` | `fetchReq` برای server-side |
## وضعیت فعلی
`lib/getStateInfo.js` (کامل — کپی واقعی):
```js
export async function getStateInfo() {
const headersList = await headers();
const host = headersList.get("host") || "";
const subdomain = host.split(".")[0];
const matchedCity = citiesData.find((city) => {
const cityDomain = city.domain.split(".")[0];
return cityDomain === subdomain;
});
const matchedState =
matchedCity && statesData.find((state) => state.id === matchedCity.province_id);
return { matchedCity, matchedState, isRoot: isRootCity(matchedCity) };
}
```
`app/doctors/page.js` (بخش fetch — کپی واقعی):
```js
const { matchedCity, matchedState, isRoot } = await getStateInfo();
// ...
if (cityParams) newSearchParams.city = cityParams;
else if (matchedCity && !isRoot) newSearchParams.city = matchedCity.name;
const params = buildDoctorParams(newSearchParams);
doctors = await fetchReq(`${API_URL}/api/v1/doctors`, { params });
```
## وظایف
### ۱. توسعه `getStateInfo` — repContext
خروجی جدید: `{ matchedCity, matchedState, isRoot, repContext }` که `repContext = { uuid, full_name, is_global } | null`.
```js
export async function getStateInfo() {
// ... منطق فعلی city.json دست‌نخورده ...
let repContext = null;
if (!matchedCity && host) {
repContext = await fetchSiteContext(host); // فقط وقتی city match نشد
}
return { matchedCity, matchedState, isRoot: isRootCity(matchedCity), repContext };
}
```
- `fetchSiteContext(host)`: صدا زدن `GET ${NEXT_PUBLIC_API_URL}/api/v1/site-context?domain=${host}` با `fetchReq`؛ اگر `type === "representation"` → آبجکت representation، وگرنه null. **خطای شبکه هرگز صفحه را نشکند** (try/catch → null) و پاسخ برای هر host **cache شود** (in-memory `Map` در سطح ماژول + `next: { revalidate: 300 }` اگر با fetch native؛ با axios همان Map با TTL ۵ دقیقه کافی است) — این تابع در هر render صدا می‌خورد.
- `localhost` و host خالی → بدون درخواست، null.
- تمام call-siteهای فعلی `getStateInfo` بدون تغییر کار کنند (فیلد اضافه فقط additive است).
### ۲. پاس دادن `domain` در لیست‌ها
در `app/doctors/page.js` و `app/clinics/page.js`:
```js
const { matchedCity, matchedState, isRoot, repContext } = await getStateInfo();
// ...
if (repContext?.is_global) {
params.domain = host; // host از headers — از طریق getStateInfo برگردان یا headers() مستقیم؟
// الگو: getStateInfo مقدار host را هم برگرداند تا صفحات دوباره parse نکنند
delete params.city; delete params.state; // روی دامنه نماینده، فیلتر شهر بی‌معنی است
}
doctors = await fetchReq(`${API_URL}/api/v1/doctors`, { params });
```
- `getStateInfo` فیلد `host` را هم برگرداند (نرمال‌شده) تا هیچ صفحه‌ای خودش `headers()` را برای دامنه parse نکند — هم‌راستا با اصل «سرویس مرکزی دامنه» در backend.
- جستجوی صفحه اصلی (`components/home/search/*`) که client-side به `/api/v1/doctors` می‌زند: از `window.location.hostname` همان پارامتر `domain` را وقتی سایتِ rep است اضافه کند — تشخیص client-side: مقدار repContext از server از طریق props/context (ساده‌ترین راه: `ProvinceProvider` یا prop از layout؛ الگوی موجود client-side پروژه را دنبال کن).
### ۳. متادیتا و برندینگ دامنه نماینده
- `app/layout.js` و `generateMetadata` صفحات doctors/clinics: وقتی `repContext` هست:
- `siteName = repContext.full_name`
- title الگو: `نوبت‌دهی آنلاین پزشکان | ${repContext.full_name}`
- description عمومی (بدون نام شهر).
- Header/Footer: جایی که `matchedCity?.site_name` مصرف می‌شود (`components/layout/*`, `app/component/Logo.js`) fallback به `repContext?.full_name` قبل از «نوبت 724».
- صفحات وابسته به شهر (مثل انتخاب شهر در سرچ): روی دامنه rep رفتار «ریشه» (همه شهرها) بماند — گیت اضافه نزن؛ فقط لیست نتایج فیلتر می‌شود.
## نکات مهم
- **هیچ regression روی دامنه‌های شهری**: مسیر `matchedCity` پیدا شد → `fetchSiteContext` اصلاً صدا زده نشود؛ رفتار فعلی بایت‌به‌بایت حفظ.
- `DEV_MODE=TRUE` مثل قبل noindex — دامنه‌های rep هم مشمول همان robots.
- تست local: `HOST=x-nobat.localhost npm run dev` کار نمی‌کند مگر backend لوکال یک rep با دامنه `x-nobat.localhost` داشته باشد — در گزارش، دستور ساخت rep تستی (از پنل ادمین clinicpro لوکال) را ذکر کن.
- خطای API سایت‌کانتکست → سایت مثل دامنه ناشناخته (رفتار فعلی) — هرگز 500 نشود (درس صفحه contact-us).
- build کامل (`npm run build`) و تست دستی سه حالت: دامنه شهر (yazd-nobat.localhost)، دامنه ریشه، دامنه ناشناخته.
- بعد از پیاده‌سازی: مستندات backend (`clinicpro/docs/api/doctor.md`/`clinic.md`) باید با مصرف واقعی این فرانت هم‌خوان باشد — اگر اختلافی دیدی همان‌جا اصلاح کن.
- **عملیاتی**: هر دامنه نماینده سراسری باید در Coolify به سرویس فرانت و به `ALLOWED_FRONTEND_HOSTS` بک‌اند اضافه شود (CORS/TLS) — در گزارش نهایی یادآوری کن.