- 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`.
117 lines
8.5 KiB
Markdown
117 lines
8.5 KiB
Markdown
# سایت دامنه اختصاصی نماینده سراسری — تشخیص دامنه، فیلتر پزشکان/کلینیکها
|
||
|
||
## پروژه
|
||
|
||
`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) — در گزارش نهایی یادآوری کن.
|