# سایت دامنه اختصاصی نماینده سراسری — تشخیص دامنه، فیلتر پزشکان/کلینیک‌ها ## پروژه `nobat724_front` — **پیش‌نیاز:** پرامپت backend اول اجرا شود: `clinicpro/.claude/prompt/representation-multi-city-domain-commission.md`. این پرامپت مصرف‌کننده قراردادهای آن است: - `GET /api/v1/site-context?domain=` (عمومی) → `{ 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) — در گزارش نهایی یادآوری کن.