# دامنهٔ ریشهٔ nobat724.com نباید مثل «شهر» رفتار کند ## پروژه `nobat724_front` (سایت عمومی — Next.js 15، multi-domain) ## زمینه هر شهر یک دامنهٔ اختصاصی دارد (`data/city.json`؛ مثل `yasuj-nobat.ir`) و `lib/getStateInfo.js` شهر را از روی subdomain تشخیص می‌دهد. اما `nobat724.com` یک رکورد **استثنا** در `city.json` است: `{ id: 600, name: "نوبت 724", domain: "nobat724.com", province_id: null }`. این دامنه **شهر نیست** — دامنهٔ ریشه/مخزنِ همهٔ شهرهاست. الان به‌خاطر این رکورد، وقتی `nobat724.com` باز می‌شود سیستم آن را مثل یک «شهر» با `city_id=600` می‌بیند: - درخواست لیست پزشکان: `/api/v1/doctors?city_id=600&page=1&limit=12` (باید بدون `city_id` = سراسری باشد). - هدر انتخاب شهر: `استان / نوبت 724` (باید placeholder `استان / شهر` باشد چون شهری انتخاب نشده). علت فنی: در چند جا محافظ `matchedCity.id !== "600"` نوشته شده ولی `id` **عدد `600`** است و `"600"` **رشته** → `600 !== "600"` همیشه `true` → استثنا هرگز اعمال نمی‌شود. ## مشکل / هدف `nobat724.com` (id 600) به‌عنوان **دامنهٔ ریشه** شناخته شود: - هیچ `city_id`/`state_id` به API تزریق نشود (لیست پزشکان سراسری). - شهر/استان به‌صورت پیش‌فرض «انتخاب‌نشده» باشد (هدر: `استان / شهر`). - برندینگ (`site_name` = «نوبت 724»، slogan) حفظ شود. ## فایل‌های مرتبط | فایل | نقش | |------|-----| | `nobat724_front/lib/getStateInfo.js` | افزودن فلگ `isRoot` | | `nobat724_front/app/doctors/page.js` | حذف تزریق city روی ریشه (باگ `!== "600"`) | | `nobat724_front/components/doctors/index.js` | filter پیش‌فرض بدون شهر روی ریشه (باگ `!== "600"`) | | `nobat724_front/context/ProvinceProvider.js` | `cityId=null` روی ریشه (client) | | `nobat724_front/app/specialties/page.js` | `cityId` روی ریشه = null | | (در صورت وجود) هر جای دیگری که `matchedCity.id` → `city_id` می‌شود | ## وضعیت فعلی ### `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) => city.domain.split(".")[0] === subdomain); const matchedState = matchedCity && statesData.find((s) => s.id === matchedCity.province_id); return { matchedCity, matchedState }; } ``` ### `app/doctors/page.js` (باگ نوع) ```js // Conditional City if (cityParams) newSearchParams.city = cityParams; else if (matchedCity && matchedCity.id !== "600") // ← 600 عدد است، "600" رشته → همیشه true newSearchParams.city = matchedCity.name; ``` ### `components/doctors/index.js` (باگ نوع در defaultFilter) ```js const defaultCity = (matchedCity?.name && matchedCity?.id !== "600" // ← همان باگ ? matchedCity?.name : "") /* ... */; ``` ### `context/ProvinceProvider.js` ```js const isProvinceInclude = citiesData.find((city) => hostName.includes(city.domain.split(".")[0])); // ... // روی nobat724.com → cityId = 600 ``` ### `app/specialties/page.js` ```js <... cityId={matchedCity?.id ?? null} ... /> // روی ریشه → 600 ``` ### `services/response.js` (بدون تغییر — فقط برای مرجع) ```js getDoctors: (params) => api.get("api/v1/doctors", { params, ... }), getSpecialtyDoctorCounts: (cityId) => api.get("api/v1/specialties/doctor-counts", { params: cityId ? { city_id: cityId } : {}, ... }), ``` `helper/index.js` → `buildDoctorParams` فقط وقتی `searchParams.city` مقدار دارد `city_id` می‌سازد؛ پس اگر روی ریشه city ست نشود، `city_id` هم نمی‌رود. ## وظایف ### ۱. تعریف مرکزی ریشه + `isRoot` در `getStateInfo` یک ثابت مشترک بساز و در `getStateInfo` فلگ برگردان: ```js // lib/getStateInfo.js export const ROOT_CITY_ID = 600; // رکورد nobat724.com در city.json (شهر نیست) export const isRootCity = (c) => c != null && Number(c.id) === ROOT_CITY_ID; export async function getStateInfo() { // ... مثل قبل ... const isRoot = isRootCity(matchedCity); return { matchedCity, matchedState, isRoot }; } ``` > `matchedCity` را برای برندینگ (site_name/slogan/description) نگه دار؛ فقط منطقِ **فیلتر شهر** باید روی ریشه غیرفعال شود. ### ۲. `app/doctors/page.js` — روی ریشه city تزریق نشود ```js const { matchedCity, matchedState, isRoot } = await getStateInfo(); // ... // Conditional City if (cityParams) newSearchParams.city = cityParams; else if (matchedCity && !isRoot) newSearchParams.city = matchedCity.name; ``` (اگر `matchedState` روی ریشه هم null است مشکلی نیست؛ چون province_id=null. ولی برای اطمینان می‌توانی `else if (matchedState && !isRoot)` بگذاری.) ### ۳. `components/doctors/index.js` — defaultFilter بدون شهر روی ریشه `isRoot` را از صفحهٔ سرور به‌عنوان prop بده یا محلی محاسبه کن (`Number(matchedCity?.id) === 600`)، و در `defaultCity`/`defaultFilter` استفاده کن: ```js const isRoot = Number(matchedCity?.id) === 600; const defaultCity = (matchedCity?.name && !isRoot) ? matchedCity.name : ""; const defaultState = (matchedState?.name && !isRoot) ? matchedState.name : ""; ``` با این کار `filter.city`/`filter.state` روی ریشه خالی می‌مانند و `FilterLocate` هدر را `استان / شهر` نشان می‌دهد (نه «نوبت 724»). ### ۴. `context/ProvinceProvider.js` — cityId روی ریشه null ```js const isRoot = Number(isProvinceInclude?.id) === 600; const cityId = isRoot ? null : (isProvinceInclude?.id ?? null); // اگر مصرف‌کننده‌ها به isProvinceInclude برای «شهر فعلی» تکیه دارند، روی ریشه آن را هم null بده: const currentCity = isRoot ? null : isProvinceInclude; // value={{ isProvinceInclude: currentCity, cityId }} ``` (بررسی کن مصرف‌کننده‌های `useProvince()` با `null` درست کار کنند — یعنی «شهری انتخاب نشده».) ### ۵. `app/specialties/page.js` (و هر `getSpecialtyDoctorCounts`/شمارش مبتنی بر شهر) ```js const { matchedCity, matchedState, isRoot } = await getStateInfo(); // ... cityId={isRoot ? null : (matchedCity?.id ?? null)} ``` ### ۶. جستجوی باقی‌ماندهٔ الگوی باگ کل `nobat724_front` را برای `!== "600"` / `=== "600"` / `id === 600`/`city_id` مبتنی بر `matchedCity`/`isProvinceInclude` بگرد و همه را با `isRoot`/`ROOT_CITY_ID` یکدست کن: ```bash grep -rn '"600"\|=== 600\|!== 600\|matchedCity?.id\|isProvinceInclude?.id' app components context lib services helper ``` ## نکات مهم - **type-safety:** id در `city.json` عدد است؛ هرگز با رشتهٔ `"600"` مقایسه نکن. از `Number(x) === ROOT_CITY_ID` یا `isRootCity()` استفاده کن. - **برندینگ حفظ شود:** روی ریشه، `matchedCity.site_name`/`slogan`/عنوان متا («نوبت 724») درست است؛ فقط فیلتر شهر و هدر انتخاب‌شهر نباید ۶۰۰ را به‌عنوان شهرِ انتخاب‌شده نشان دهند. - **canonical:** در `app/layout.js` از قبل `matchedCity.domain !== 'nobat724.com'` هست؛ دست نزن، فقط اگر خواستی با `isRoot` یکدست کن. - **رفتار موردانتظار روی nobat724.com:** لیست پزشکان بدون `city_id` (سراسری)، هدر `استان / شهر`، انتخاب شهر توسط کاربر باعث ست‌شدن فیلتر و رفتن `city_id` واقعی می‌شود. - **رفتار دامنه‌های شهری تغییر نکند:** روی `yasuj-nobat.ir` و بقیه باید دقیقاً مثل قبل `city_id` شهر برود. - backend تغییری ندارد؛ فقط رفتار کلاینت. بعد از تغییر: `npm run build` و `npm run lint` بدون خطا؛ سپس با host `nobat724.com` (یا mock header) بررسی کن که `city_id` نمی‌رود و هدر `استان / شهر` است، و با یک دامنهٔ شهری بررسی کن `city_id` همچنان می‌رود.