# بازگرداندن صفحهٔ اصلی، رفع ۴۰۴ اسلاگ فارسی، سه اصلاح صفحهٔ مقاله، و دو اصلاح «درباره ما» ## پروژه `nobat724_front` — سایت عمومی. بدون تغییر بک‌اند. ## زمینه پنج مورد جدا که کاربر روی محیط محلی دید. یکی بازگرداندن تغییری است که در تسک قبلی اضافه شد، یکی باگ ۴۰۴ با ریشهٔ اثبات‌شده، و سه اصلاح کوچک روی صفحهٔ مقاله. ## مشکل / هدف ### ۱. بخش تازهٔ صفحهٔ اصلی برداشته شود در تسک قبلی برای رفع `Duplicate, Google chose different canonical` یک بخش شهرمحور به صفحهٔ اصلی اضافه شد: متن مقدمه، «تخصص‌های پرمراجعه در …» و «پزشکان …». کاربر آن را نمی‌خواهد. صفحهٔ اصلی به حالت قبل برگردد. ### ۲. اسلاگ فارسی ۴۰۴ می‌دهد ``` /specialties/%D8%AC%D8%B1%D8%A7%D8%AD-%DA%AF%D9%88%D8%A7%D8%B1%D8%B4 → 404 /specialties/gastroenterology → 200 ``` **ریشه با ابزارگذاری روی build واقعی اثبات شد** — `generateMetadata` و بدنهٔ صفحه دو مقدار متفاوت می‌گیرند: ``` generateMetadata → slug = "جراح-گوارش" found: true بدنهٔ صفحه → slug = "%D8%AC%D8%B1%D8%A7%D8%AD-%DA%AF%D9%88%D8%A7%D8%B1%D8%B4" found: false → notFound() ``` یعنی Next در بدنهٔ صفحه segment را decode نمی‌کند. رشته‌ها بایت‌به‌بایت یکی‌اند (هر دو NFC، بدون تفاوت codepoint)، پس مشکل نرمال‌سازی نیست. **این باگ فقط مخصوص صفحهٔ تخصص نیست.** slug مقاله‌ها هم فارسی است (`آلرژی-دارویی-…-58d2e880`)، پس `app/blog/[slug]/page.js` هم همین را دارد و امروز با uuid تست می‌شود که ASCII است و مشکل را پنهان می‌کند. ### ۳. برچسب «مخصوص شهر» از صفحهٔ مقاله حذف شود فقط همین جمله. منطق scope شهریِ مقاله‌ها دست‌نخورده می‌ماند. ### ۴. تگ‌های مقاله لینک شوند الان چیپ‌های بدون لینک‌اند. کامنت کد می‌گوید «صفحهٔ فیلتر تگ وجود ندارد» — این کامنت کهنه است؛ `components/blogs/index.js` از قبل `?tag=` را می‌خواند و اعمال می‌کند. ### ۵. عکس «مطالب مرتبط» دفرمه است عکس داخل `
  • ` است و ظرفش `shrink-0` ندارد. با تیتر بلند، عرض عکس فشرده می‌شود و ارتفاعش ثابت می‌ماند. ### ۶. صفحهٔ «درباره ما» راه عضویت پزشک را نمی‌گوید مسیر صفحه: ``` /about-us ``` متن فعلی فقط از دید بیمار نوشته شده. پزشکی که بخواهد به سامانه اضافه شود هیچ راهنمایی‌ای نمی‌بیند. باید بند تازه‌ای اضافه شود که او را به این سایت بفرستد: ``` https://clinic-pro.ir/ ``` ### ۷. لوگوی هدر «درباره ما» غلط است هدر بنفش صفحهٔ «درباره ما» فایل قدیمی را نشان می‌دهد: ``` /assets/images/logo-2.png ``` هیچ‌جای دیگر سایت این فایل را مصرف نمی‌کند. لوگوی واقعی سایت این است: ``` /nobat724.svg ``` که هدر اصلی سایت در `app/component/Logo.js` از آن استفاده می‌کند. **دو نکته که ساده‌ترین راه‌حل را می‌شکنند:** اول، رنگ stroke لوگو `#5559CE` است و پس‌زمینهٔ هدر `#5559C2`. اگر همان svg را مستقیم بگذارید، لوگو تقریباً نامرئی می‌شود. باید نسخهٔ روشن رندر شود. دوم، `SiteLogo` از قبل همین کار را می‌کند: هم `city.logo_url` را در نظر می‌گیرد و هم `stroke` را پارامتری گرفته. مسیرش: ``` components/doctor/poster/SiteLogo.js ``` ## معیار پذیرش - ✅ موفق: `/specialties/%D8%AC%D8%B1%D8%A7%D8%AD-%DA%AF%D9%88%D8%A7%D8%B1%D8%B4` کد `200` بدهد و تیتر «متخصص جراح گوارش در …» را نشان دهد. - ✅ موفق: `/specialties/gastroenterology` همچنان `200` — اسلاگ لاتین نشکند. - ✅ موفق: صفحهٔ مقاله دیگر «مخصوص شهر:» ندارد، ولی نویسنده و تاریخ سر جایشان‌اند. - ✅ موفق: هر تگ مقاله لینک به `/blogs?tag=<نام تگ>` است و آن صفحه همان فیلتر را اعمال می‌کند. - ✅ موفق: HTML صفحهٔ اصلی هیچ‌کدام از «تخصص‌های پرمراجعه در»، «پزشکان <شهر>» را ندارد. - ❌ خطا: اسلاگ ناموجود — چه فارسی چه لاتین — همچنان `404` بدهد، نه `200` یا `500`. - ❌ خطا: مقاله‌ای بدون تگ، بلوک تگ را اصلاً رندر نکند. - ⚠️ مرزی: اسلاگ فارسی که با `%` شروع نمی‌شود ولی حرف فارسی خام دارد هم کار کند. - ⚠️ مرزی: تگ حاوی فاصله یا `&` باید در URL درست encode شود. - ⚠️ مرزی: عکس مطالب مرتبط با تیتر بسیار بلند نباید باریک شود؛ نسبت ابعاد ثابت بماند. - ✅ موفق: صفحهٔ `/about-us` بندی دارد که پزشک را به `https://clinic-pro.ir/` می‌فرستد، و آن آدرس لینک واقعی است نه متن ساده. - ✅ موفق: لینک عضویت پزشک `target="_blank"` و `rel="noopener noreferrer"` دارد. - ✅ موفق: هیچ ارجاعی به `logo-2.png` در کد نماند و لوگوی هدر «درباره ما» روی پس‌زمینهٔ بنفش دیده شود. - ❌ خطا: نام سایت در بند تازه هاردکد نشود؛ همان `siteName` که بقیهٔ صفحه استفاده می‌کند. - ⚠️ مرزی: شهری که `logo_url` دارد باید لوگوی خودش را در هدر ببیند، نه لوگوی پیش‌فرض. ## فایل‌های مرتبط | فایل | نقش | |------|-----| | `components/home/index.js` | چیدمان صفحهٔ اصلی — `CityHighlights` از اینجا حذف شود | | `components/home/CityHighlights.js` | کامپوننتی که باید برداشته شود | | `components/home/CityHighlights.test.js` | تستش هم با آن می‌رود | | `lib/listingIntro.js` | `buildCityIntro` و `CITY_INTROS` — فقط مصرف‌کننده‌شان صفحهٔ اصلی بود | | `lib/cityIntro.test.js` | تست‌های `buildCityIntro` | | `app/specialties/[slug]/page.js` | ۴۰۴ اسلاگ فارسی | | `app/blog/[slug]/page.js` | همان باگ، پنهان چون با uuid تست می‌شود | | `components/blog/head/index.js` | برچسب «مخصوص شهر» و چیپ‌های تگ | | `components/blogs/index.js` | خوانندهٔ `?tag=` — تأیید می‌کند لینک تگ کار می‌کند | | `components/blog/relatedContent/Item.js` | عکس دفرمه | | `components/aboutUs/index.js` | متن «درباره ما» — بند تازهٔ پزشک اینجا اضافه شود | | `components/aboutUs/Head.js` | هدر بنفش با لوگوی غلط | | `components/doctor/poster/SiteLogo.js` | لوگوی درست، `stroke` پارامتری، پشتیبانی `logo_url` | | `app/component/Logo.js` | لوگوی هدر اصلی — مرجع اینکه لوگوی واقعی کدام است | | `public/assets/images/logo-2.png` | فایل یتیم — بعد از اصلاح هیچ مصرف‌کننده‌ای ندارد | ## وضعیت فعلی ### صفحهٔ اصلی ```jsx // components/home/index.js
    ``` ### صفحهٔ تخصص — دو مصرف متفاوت از یک param ```jsx // app/specialties/[slug]/page.js:18 const findSpecialty = (slug) => specialtiesData.find((item) => item.slug === slug && item.status === 1) ?? null; // :45 generateMetadata — اینجا decode شده می‌رسد const { slug } = await params; const specialty = findSpecialty(slug); if (!specialty) notFound(); // :78 بدنهٔ صفحه — اینجا encode شده می‌رسد const { slug } = await params; const specialty = findSpecialty(slug); if (!specialty) notFound(); ``` ### برچسب شهر ```jsx // components/blog/head/index.js:49 {cityName && (

    مخصوص شهر:

    {cityName}

    )} ``` ### تگ‌های بدون لینک ```jsx // components/blog/head/index.js:90 {/* فهرست کامل تگ‌ها — تا این تغییر فقط تگ اول در بردکرامب دیده می‌شد. صفحهٔ فیلتر تگ وجود ندارد، پس چیپ‌ها بدون لینک رندر می‌شوند. */} {tags.length > 0 && ( )} ``` ولی فیلتر تگ از قبل هست: ```js // components/blogs/index.js:16 const selectedTag = searchParams.get("tag") || null; // :32 params.tag = selectedTag; ``` ### عکس مطالب مرتبط ```jsx // components/blog/relatedContent/Item.js
  • {data.title ``` ### هدر «درباره ما» ```jsx // components/aboutUs/Head.js
    logo

    درباره {siteName}

    {slogan &&

    {slogan}

    }
    ``` `Head` الان فقط `siteName` و `slogan` می‌گیرد؛ `matchedCity` به آن پاس نمی‌شود. ### متن «درباره ما» ```jsx // components/aboutUs/index.js — دو بند، هر دو از دید بیمار

    {siteName} یک سامانه‌ی آنلاین نوبت‌دهی پزشکی است که …

    در {siteName} می‌توانید بر اساس تخصص، نام پزشک یا مرکز درمانی جستجو کنید …

    ``` ## وظایف ### ۱. برداشتن بخش شهرمحور صفحهٔ اصلی - `` و import‌ش از `components/home/index.js` حذف شود. - فایل `components/home/CityHighlights.js` و تستش پاک شوند. - `buildCityIntro` و `CITY_INTROS` در `lib/listingIntro.js` تنها مصرف‌کننده‌شان همین کامپوننت بود؛ با آن پاک شوند. تست‌های مربوطه در `lib/cityIntro.test.js` هم بروند. **کد مرده نگه ندارید** — همین اشتباه قبلاً با `buildDoctorsIntro` تکرار شد. - `buildClinicsIntro` و `CLINIC_INTROS` بمانند؛ `/clinics` هنوز مصرفشان می‌کند. **نحوه تست:** ```bash grep -rn "CityHighlights\|buildCityIntro" app components lib # باید خالی باشد npm run test && npm run lint NODE_TLS_REJECT_UNAUTHORIZED=0 npm run build && npm run start & curl -s -H "Host: yasuj-nobat.ir" http://localhost:3000/ | grep -cE "تخصص‌های پرمراجعه|پزشکان یاسوج" # باید 0 ``` ### ۲. رفع ۴۰۴ اسلاگ فارسی یک helper مشترک بسازید — این باگ در دو صفحه هست و فردا در صفحهٔ سوم هم می‌آید: ```js // lib/routeParams.js /** * segment مسیر را به شکل decode‌شده برمی‌گرداند. * * Next در `generateMetadata` مقدار decode‌شده می‌دهد ولی در بدنهٔ همان صفحه مقدار * خام؛ روی اسلاگ‌های فارسی این یعنی متادیتا درست ساخته می‌شود و بلافاصله بعدش * صفحه ۴۰۴ می‌دهد. اثبات‌شده روی build واقعی، نه فرض. * * decode شکست‌خورده (درصدِ ناقص در ورودی) نباید صفحه را بشکند؛ همان مقدار خام * برمی‌گردد تا نتیجه‌اش ۴۰۴ طبیعی باشد نه ۵۰۰. */ export function decodeRouteParam(value) { if (typeof value !== "string" || !value.includes("%")) return value; try { return decodeURIComponent(value); } catch { return value; } } ``` سپس در هر دو صفحه، بلافاصله بعد از `await params`: ```js const { slug: rawSlug } = await params; const slug = decodeRouteParam(rawSlug); ``` در `app/specialties/[slug]/page.js` هر دو جا (`generateMetadata` و بدنه) و در `app/blog/[slug]/page.js` هم هر دو جا. **مواظب `slug` که به کامپوننت پاس می‌شود باشید:** در صفحهٔ تخصص، `slug` به `SpecialtyDetailPage` می‌رود و آنجا در breadcrumb به `/specialties/${slug}` تبدیل می‌شود. مقدار decode‌شده آنجا درست است چون `next/link` خودش encode می‌کند. **نحوه تست:** ```bash NODE_TLS_REJECT_UNAUTHORIZED=0 npm run build && npm run start & for u in "/specialties/%D8%AC%D8%B1%D8%A7%D8%AD-%DA%AF%D9%88%D8%A7%D8%B1%D8%B4" \ "/specialties/gastroenterology" "/specialties/does-not-exist"; do echo -n "$u → "; curl -s -o /dev/null -w "%{http_code}\n" -H "Host: yasuj-nobat.ir" "http://localhost:3000$u" done # انتظار: 200، 200، 404 ``` به‌علاوه unit test برای `decodeRouteParam` در `lib/routeParams.test.js`: مقدار encode‌شده، مقدار خام فارسی، مقدار لاتین، رشتهٔ با `%` ناقص (نباید throw کند)، ورودی `undefined`. ### ۳. حذف برچسب «مخصوص شهر» کل بلوک `{cityName && (...)}` در `components/blog/head/index.js` برداشته شود. اگر `cityName` بعد از آن هیچ مصرفی ندارد، محاسبه‌اش هم پاک شود تا متغیر بلااستفاده نماند. منطق scope شهریِ مقاله‌ها (`domainScopeCityId` و `city_id` در fetch) **دست نخورد** — کاربر فقط همین جمله را خواست. **نحوه تست:** `curl` صفحهٔ مقاله و `grep -c "مخصوص شهر"` که باید صفر باشد، در حالی که `grep -c "نویسنده:"` همچنان یک است. ### ۴. لینک‌کردن تگ‌ها هر چیپ به `/blogs?tag=<نام>` لینک شود. `next/link` خودش encode می‌کند، ولی چون تگ ممکن است فاصله یا `&` داشته باشد، از شکل شیئی استفاده کنید تا مطمئن شوید: ```jsx {tag} ``` کامنت کهنهٔ بالای بلوک («صفحهٔ فیلتر تگ وجود ندارد») حذف یا اصلاح شود؛ همان کامنت باعث شد این قابلیت جا بماند. استایل چیپ عوض نشود، فقط `hover` مناسب اضافه شود چون حالا کلیک‌شدنی است. **نحوه تست:** تست کامپوننتی روی `components/blog/head` — مقاله با دو تگ، دو `role="link"` با `href` شامل `/blogs?tag=`؛ مقاله بدون تگ، هیچ لیستی رندر نشود. سپس دستی: کلیک روی تگ باید به `/blogs?tag=…` برود و همان فیلتر اعمال شود. ### ۵. رفع دفرمگی عکس مطالب مرتبط به ظرف عکس `shrink-0` اضافه شود تا در flex فشرده نشود: ```jsx ``` اگر `CustomLoading` خودش ظرف flex می‌سازد، `shrink-0` باید روی همان بیرونی‌ترین عنصرِ داخل `
  • ` بنشیند — اول ساختار رندرشده را ببینید، بعد کلاس را جای درست بگذارید. **نحوه تست:** تست کامپوننتی که کلاس `shrink-0` روی ظرف عکس هست. به‌علاوه بررسی چشمی در مرورگر با یک تیتر بسیار بلند — jsdom چیدمان را اندازه نمی‌گیرد، پس این مورد را **صادقانه به‌عنوان «بررسی چشمی» گزارش کنید**، نه به‌عنوان تست خودکار. ### ۶. بند عضویت پزشک در «درباره ما» یک بند سوم بعد از دو بند فعلی در `components/aboutUs/index.js` اضافه شود. متن پیشنهادی: ```jsx

    اگر پزشک هستید و می‌خواهید در {siteName} نوبت‌دهی آنلاین داشته باشید، ثبت‌نام از طریق سامانه‌ی کلینیک‌پرو انجام می‌شود. کافی است به{" "} clinic-pro.ir {" "} مراجعه کنید و حساب مطب خود را بسازید. پس از تأیید، پروفایل و برنامه‌ی نوبت‌دهی شما روی {siteName} نمایش داده می‌شود.

    ``` قواعد: - نام سایت هاردکد نشود؛ همان `siteName` بالای فایل استفاده شود. - لینک خارجی است، پس `next/link` لازم نیست؛ `` با `target="_blank"` و `rel="noopener noreferrer"` درست است. - استایل بند از بقیه جدا نشود؛ همان ظرف `flex flex-col gap-[16px]` را می‌گیرد. **نحوه تست:** ```bash NODE_TLS_REJECT_UNAUTHORIZED=0 npm run build && npm run start & curl -s -H "Host: yasuj-nobat.ir" http://localhost:3000/about-us | grep -c "clinic-pro.ir" # باید ≥ 1 ``` به‌علاوه تست کامپوننتی: لینکی با `href="https://clinic-pro.ir/"` رندر شود و `rel` شامل `noopener` باشد. ### ۷. اصلاح لوگوی هدر «درباره ما» `` با `SiteLogo` جایگزین شود: ```jsx ``` - `matchedCity` باید از `components/aboutUs/index.js` به `Head` پاس شود؛ الان فقط `siteName` و `slogan` می‌رود. - `stroke="#FAFAFA"` لازم است چون رنگ پیش‌فرض روی پس‌زمینهٔ بنفش گم می‌شود. - `crossOrigin="anonymous"` داخل `SiteLogo` برای حالت poster گذاشته شده و اینجا ضرری ندارد؛ دست نزنید. - اگر `SiteLogo` جای بهتری لازم دارد چون دیگر فقط مال poster نیست، جابه‌جایی‌اش به `components/common/` قابل قبول است — ولی آن‌وقت هر دو مصرف‌کننده باید به‌روز شوند و تست‌ها سبز بمانند. - بعد از اصلاح، `public/assets/images/logo-2.png` هیچ مصرف‌کننده‌ای ندارد و پاک می‌شود. **اول با grep ثابت کنید یتیم است، بعد پاک کنید.** **نحوه تست:** ```bash grep -rn "logo-2" app components public --include="*.js" --include="*.json" # باید خالی باشد ``` به‌علاوه تست کامپوننتی روی `components/aboutUs/Head`: شهر بدون `logo_url` باید svg درون‌خطی بدهد، شهر با `logo_url` باید `` با همان آدرس بدهد. بررسی چشمی هم لازم است — دیده‌شدن لوگو روی بنفش را jsdom نمی‌سنجد. آن را **صادقانه به‌عنوان «بررسی چشمی» گزارش کنید**. ## نکات مهم - **۴۰۴ اسلاگ فارسی، مهم‌ترین بخش این تسک است.** اگر فقط صفحهٔ تخصص درست شود و صفحهٔ مقاله جا بماند، همان باگ با اولین مقالهٔ فارسی‌اسلاگ برمی‌گردد. هر دو صفحه در همین تسک اصلاح شوند. - **دلیل helper مشترک به‌جای دو تکه کد:** دو مصرف‌کنندهٔ فعلی و یک الگوی تکرارشونده. این abstraction «برای آینده» نیست؛ همین حالا دو جا لازم است. - **حذف کد مرده جزو کار است.** `buildCityIntro` بعد از برداشتن `CityHighlights` بلااستفاده می‌شود. نگه‌داشتنش همان وضعیتی را می‌سازد که `buildDoctorsIntro` ساخته بود: تابعی که تست دارد ولی هیچ‌جا رندر نمی‌شود. - **سنجهٔ شباهت صفحهٔ اصلی برمی‌گردد به حدود ۹۰٪.** تسک قبلی آن را به ۷۱٫۵٪ رسانده بود. این عقب‌گرد خواستهٔ کاربر است و باید در گزارش پایانی صریح ذکر شود، نه بی‌صدا. مشکل `Duplicate, Google chose different canonical` روی صفحهٔ اصلی برمی‌گردد. - **دو مورد «درباره ما» مستقل از پنج مورد قبلی‌اند.** اگر یکی از آن‌ها گیر کرد، بقیه را کامل کنید و همان یکی را صریح گزارش دهید. - **لوگو مسئلهٔ کنتراست است، نه فقط عوض‌کردن فایل.** stroke لوگو `#5559CE` و پس‌زمینه `#5559C2` است. اگر بدون `stroke` روشن جایگزین کنید، تست‌ها سبز می‌شوند ولی کاربر لوگو را نمی‌بیند. - خط پایهٔ ریپو پیش از این تسک: `npm run test` چهار شکستِ ازقبل‌موجود در `lib/lib.test.js` و `lib/getStateInfo.test.js`؛ `npm run lint` سه خطا در فایل‌های بی‌ربط. این‌ها رگرسیون نیستند. - `npm run start` هشدار `"next start" does not work with "output: standalone"` می‌دهد. صفحات رندر می‌شوند و برای این تست‌ها کافی است، ولی اگر رفتار عجیبی دیدید اول همین را در نظر بگیرید.