Files
nobat724_front/.claude/prompt/nobat724-root-domain-not-city.md

8.7 KiB
Raw Permalink Blame History

دامنهٔ ریشهٔ 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.idcity_id می‌شود

وضعیت فعلی

lib/getStateInfo.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 (باگ نوع)

// 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)

const defaultCity =
  (matchedCity?.name && matchedCity?.id !== "600"     // ← همان باگ
    ? matchedCity?.name
    : "") /* ... */;

context/ProvinceProvider.js

const isProvinceInclude = citiesData.find((city) => hostName.includes(city.domain.split(".")[0]));
// ...
<ProvinceContext.Provider value={{ isProvinceInclude, cityId: isProvinceInclude?.id ?? null }}>
// روی nobat724.com → cityId = 600

app/specialties/page.js

<... cityId={matchedCity?.id ?? null} ... />   // روی ریشه → 600

services/response.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.jsbuildDoctorParams فقط وقتی searchParams.city مقدار دارد city_id می‌سازد؛ پس اگر روی ریشه city ست نشود، city_id هم نمی‌رود.

وظایف

۱. تعریف مرکزی ریشه + isRoot در getStateInfo

یک ثابت مشترک بساز و در getStateInfo فلگ برگردان:

// 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 تزریق نشود

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 استفاده کن:

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

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/شمارش مبتنی بر شهر)

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 یکدست کن:

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 همچنان می‌رود.