# اصلاحات Nobat724: رفع خطای جستجوی پزشک + بهینه‌سازی SEO صفحات لیست و تخصص ## پروژه `nobat724_front` > این پرامپت جایگزین `fix-search-autocomplete-doctor-name.md` است (تسک ۱ همان باگ را پوشش می‌دهد) و چهار تسک SEO را هم اضافه می‌کند. هر تسک مستقل است؛ می‌توانی جدا کامیت کنی. ## زمینه سایت عمومی نوبت‌دهی چند-دامنه‌ای (هر شهر یک دامنه، مثل `yasuj-nobat.ir`، و دامنهٔ ریشه `nobat724.com`). پنج اصلاح لازم است: یک باگ جستجوی UI و چهار مورد بهینه‌سازی SEO روی صفحات لیست پزشکان و صفحات فرود تخصص. --- ## تسک ۱ — رفع خطای `getOptionLabel` در جستجوی نام پزشک ### مشکل در صفحهٔ اصلی، تایپ «دکتر مهدیه» در باکس جستجو این ارور را می‌دهد: ``` MUI: The `getOptionLabel` method of Autocomplete returned undefined instead of a string for "دکتر مهدیه". ``` علت: `Autocomplete` با `freeSolo` رندر شده و `options={specialtiesData}` (اشیاء تخصص با فیلد `name`). وقتی کاربر متنی تایپ می‌کند که در options نیست، MUI مقدار تایپ‌شده را به‌صورت **رشته** به `getOptionLabel` می‌دهد؛ `"دکتر مهدیه".name` می‌شود `undefined`. دیتای `specialties.json` سالم است (هر ۹۳ آیتم `name` دارند) — منشأ ارور رشتهٔ freeSolo است، نه آیتم بدون name. ### فایل `components/searchHead/smSearch/AutoCompleteSearch.js` ### وضعیت فعلی ```jsx getOptionLabel={(option) => option.name} // خط ۲۷ — روی رشته می‌شکند ... onChange={(_, newValue) => { setSelectedOption && setSelectedOption(newValue); handleSearch(newValue ? newValue.name : ""); // خط ۳۶ — newValue می‌تواند رشته باشد }} ``` ### راه‌حل هر دو نقطه را برای رشتهٔ freeSolo امن کن؛ همیشه string برگردان: ```jsx getOptionLabel={(option) => typeof option === "string" ? option : option?.name ?? "" } ... onChange={(_, newValue) => { setSelectedOption && setSelectedOption(newValue); const label = typeof newValue === "string" ? newValue : newValue?.name ?? ""; handleSearch(label); }} ``` `renderOption` (خط ۳۹–۴۳) فقط برای options واقعی صدا زده می‌شود؛ دست نزن. ### تست - تایپ «دکتر مهدیه» → بدون ارور؛ کلیک جستجو → هدایت به `/doctors?...name=دکتر مهدیه` (منطق redirect در `components/home/search/RedirectLink.js` از قبل درست است: match نشدن با تخصص → `filters.name`). - انتخاب یک تخصص از لیست (مثل «داخلی») → `/doctors?...specialty=داخلی`. - خالی‌کردن ورودی → بدون ارور. --- ## تسک ۲ — `noindex, follow` برای همهٔ صفحات جستجوی `/doctors?...` ### هدف هر `/doctors` که **کوئری‌پارامتر داشته باشد** (`?specialty=`, `?city=`, `?state=`, `?name=`, یا هر ترکیبی) باید `robots: noindex, follow` بگیرد. فقط `/doctors` بدونِ کوئری باید ایندکس شود. دلیل: صفحات جستجو داینامیک و Duplicate/Thin هستند؛ صفحهٔ فرود قابل‌ایندکسِ تخصص باید `/specialties/[slug]` باشد (تسک ۳)، نه `/doctors?specialty=`. ### فایل `lib/listingRobots.js` (توسط `app/doctors/page.js` خط ۲۵ و صفحهٔ `/clinics` مصرف می‌شود). ### وضعیت فعلی ```js const USER_FILTER_KEYS = ["specialty", "gender", "degree", "active", "sort"]; export function listingRobots(params, { matchedCity, matchedState } = {}) { if (params?.name) return { robots: { index: false, follow: true } }; const activeFilters = USER_FILTER_KEYS.filter((key) => params?.[key]); if (params?.city && params.city !== matchedCity?.name) activeFilters.push("city"); if (params?.state && params.state !== matchedState?.name) activeFilters.push("state"); if (activeFilters.length >= 2) return { robots: { index: false, follow: true } }; return {}; } ``` منطق فعلی عمداً «تک‌فیلتر specialty» و «city/state تزریق‌شده از دامنه» را ایندکس‌پذیر نگه می‌داشت. طبق خواستهٔ جدید این رفتار باید عوض شود: **هر کوئری‌پارامتر واقعی → noindex**. ### راه‌حل منطق را ساده و سخت‌گیرانه کن: اگر بعد از حذف کلیدهای خالی، هیچ پارامتری نمانده → `{}` (ایندکس)؛ در غیر این صورت → `noindex, follow`. ```js // هر جستجوی /doctors با کوئری واقعی noindex,follow می‌شود؛ فقط /doctors خام ایندکس. export function listingRobots(params) { const hasQuery = params && Object.entries(params).some( ([, v]) => v !== undefined && v !== null && v !== "" ); return hasQuery ? { robots: { index: false, follow: true } } : {}; } ``` ### نکتهٔ حیاتی — مرز URL vs. فیلترِ سرور در `app/doctors/page.js` روی دامنهٔ شهری، `city`/`state` **در URL نیستند**؛ سرور آن‌ها را فقط داخل fetch تزریق می‌کند (`newSearchParams`)، ولی `generateMetadata` تابع را با `awaitedParams = await searchParams` (پارامترهای واقعی URL) صدا می‌زند. پس `/doctors` خام روی `yasuj-nobat.ir` همچنان بدون کوئری و **ایندکس‌پذیر** می‌ماند. ✅ قبل از پیاده‌سازی این را در همان فایل تأیید کن (خط ۹–۲۵) که `listingRobots` با پارامترهای خام URL صدا زده می‌شود، نه با `newSearchParams`. بعد از تغییر امضا (حذف آرگومان دوم)، فراخوان در `app/doctors/page.js` و هر مصرف‌کنندهٔ دیگر (`/clinics`) را هم‌راستا کن — `grep -rn "listingRobots" app` بگیر و همه را بررسی کن. --- ## تسک ۳ — قابل‌ایندکس‌بودن `/specialties` و صفحات تخصص ### وضعیت فعلی (اکثراً درست) - `app/specialties/page.js` — هیچ override روباتی ندارد → ایندکس‌پذیر. ✅ نیازی به تغییر نیست. - `app/specialties/[slug]/page.js` — فقط وقتی `total === 0` است `noindex,follow` می‌دهد (thin content)، وگرنه ایندکس. ✅ فقط قانون تسک ۴ باید به `[slug]` اضافه شود. کاری اضافه در این تسک لازم نیست جز اطمینان از اینکه بعد از تسک ۲، صفحات `/specialties/[child-slug]` (مثل `endourology`, `gastroenterology`) ایندکس‌پذیر می‌مانند. --- ## تسک ۴ — فقط تخصص‌های **فرزند** ایندکس شوند (والدها noindex) ### هدف تخصص‌های والد (`parent_id === null`، مثل «داخلی») صفحاتشان دسته‌بندی کلی و کم‌محتوا است و با فرزندان در نتایج گوگل رقابت می‌کنند. باید: - `/specialties/internal-medicine` (والد، `parent_id: null`) → `noindex, follow` - `/specialties/gastroenterology` (فرزند، `parent_id: 2`) → `index` ### فایل `app/specialties/[slug]/page.js` — تابع `generateMetadata` (خط ۴۳–۶۹). ### وضعیت فعلی ```js const { total } = await getDoctors(specialty.id, isRoot ? null : matchedCity?.id); return { title, description, ...(total === 0 && { robots: { index: false, follow: true } }), openGraph: { title, description, images: [image] }, twitter: { card: "summary_large_image", title, description, images: [image] }, }; ``` `specialty` از `findSpecialty(slug)` می‌آید و شیء کامل `specialties.json` است، پس `specialty.parent_id` در دسترس است. ### راه‌حل والد (`parent_id === null`) را هم به شرط noindex اضافه کن. توجه: `parent_id` ممکن است `null` باشد — از مقایسهٔ صریح استفاده کن. ```js const isParentCategory = specialty.parent_id == null; // والد یا بدون parent const noindex = isParentCategory || total === 0; return { title, description, ...(noindex && { robots: { index: false, follow: true } }), openGraph: { title, description, images: [image] }, twitter: { card: "summary_large_image", title, description, images: [image] }, }; ``` ### نکته - اگر `sitemap.js` صفحات تخصص را لیست می‌کند، مطمئن شو والدها آنجا هم حذف/فیلتر می‌شوند تا با `noindex` هم‌خوان باشد. `grep -rn "specialties" app/sitemap.js` و در صورت وجود، والدها (`parent_id == null`) را از خروجی sitemap کنار بگذار. --- ## تسک ۵ — اصلاح نگارشی متن SEO سوالات متداول تخصص + برند روی دامنهٔ ریشه ### فایل `lib/specialtyContent.js` — تابع `buildSpecialtyFaq` (خط ۹۸–۱۲۰). مصرف‌کننده: `app/specialties/[slug]/page.js` خط ۸۶. ### وضعیت فعلی ```js export function buildSpecialtyFaq(specialtyName, cityName, doctorCount = 0) { return [ { question: `چطور از متخصص ${specialtyName} در ${cityName} نوبت بگیرم؟`, ... ``` روی دامنهٔ ریشه `cityName` برابر `"ایران"` است (از `resolveCityDisplayName` → `ROOT_DISPLAY_NAME`)، پس سوال می‌شود: «چطور از متخصص اینترونشنال کاردیولوژی در **ایران** نوبت بگیرم؟». ### خواسته ۱. جملهٔ سوال به شکل نگارشیِ روان‌تر تغییر کند: «چطور از … نوبت **بگیرم؟**» → «چطور **می‌توان** از … نوبت **گرفت؟**». ۲. روی دامنهٔ ریشه، به‌جای «ایران» برند «**نوبت724**» استفاده شود (تقویت برند + طبیعی‌تر). روی دامنهٔ شهری همان نام شهر بماند. ### راه‌حل بدون تغییر `ROOT_DISPLAY_NAME` سراسری (که در Titleها استفاده می‌شود و «متخصص X در ایران» آنجا درست است)، فقط داخل FAQ برند را جایگزین کن. یک آرگومان اختیاری `isRoot` به تابع اضافه کن: ```js export function buildSpecialtyFaq(specialtyName, cityName, doctorCount = 0, { isRoot = false } = {}) { const loc = isRoot ? "نوبت724" : cityName; return [ { question: `چطور می‌توان از متخصص ${specialtyName} در ${loc} نوبت گرفت؟`, answer: `از فهرست همین صفحه پزشک موردنظرتان را انتخاب کنید، وارد پروفایل او شوید و از تقویم نوبت‌های خالی، تاریخ و ساعت دلخواه را ثبت کنید. رزرو در همان لحظه قطعی می‌شود و تأییدیه برایتان پیامک می‌شود.`, }, { question: `چند پزشک ${specialtyName} در ${loc} در دسترس است؟`, answer: doctorCount > 0 ? `در حال حاضر ${doctorCount} پزشک ${specialtyName} در ${loc} در این سامانه ثبت شده‌اند. این عدد با افزوده‌شدن پزشکان جدید به‌روزرسانی می‌شود.` : `فهرست پزشکان ${specialtyName} در ${loc} در حال تکمیل است و به‌مرور پزشکان جدید به آن افزوده می‌شوند.`, }, // بقیهٔ سوالات (هزینه ویزیت / لغو نوبت) بدون تغییر ... ]; } ``` سپس در `app/specialties/[slug]/page.js` خط ۸۶، `isRoot` را پاس بده: ```js const faq = buildSpecialtyFaq(specialty.name, cityName, total, { isRoot }); ``` (`isRoot` از قبل در همان تابع از `getStateInfo()` گرفته شده — خط ۷۶.) ### نکات - **فقط FAQ** برند را عوض کند؛ `buildSpecialtyIntro` و Titleها دست‌نخورده بمانند (روی ریشه همچنان «در ایران»). این عمدی است تا فقط متن سوالِ موردِ اشارهٔ کاربر اصلاح شود. - این متن هم در بلوک بصری صفحه و هم در `FAQPage` JSON-LD استفاده می‌شود (منبع مشترک) — پس اصلاح یک‌جا هر دو را می‌پوشاند. --- ## نکات کلی پروژه - App Router / Next.js 15؛ همیشه `await params` و `await searchParams`. - بعد از تغییرِ `listingRobots`، `robots.js`/`sitemap.js` را برای ناسازگاری بررسی کن (صفحات noindex نباید در sitemap باشند). - RTL فارسی، MUI v5 + Tailwind، فونت Vazir؛ استایل جدید اضافه نکن. - بعد از اتمام: `graphify update .` برای به‌روز نگه‌داشتن گراف (بعد از کامیت).