- Removed the newly added city-focused section from the homepage. - Fixed 404 error for Persian slugs in specialties and blog pages by implementing a decode helper. - Removed the "مخصوص شهر" label from article headers. - Linked article tags to their respective filter pages. - Adjusted related content images to prevent distortion. - Added a section in the "About Us" page for physician registration guidance. - Updated the logo in the "About Us" header to the correct version. - Added tests for the new functionality and ensured existing tests are updated accordingly.
23 KiB
بازگرداندن صفحهٔ اصلی، رفع ۴۰۴ اسلاگ فارسی، سه اصلاح صفحهٔ مقاله، و دو اصلاح «درباره ما»
پروژه
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= را میخواند و اعمال میکند.
۵. عکس «مطالب مرتبط» دفرمه است
عکس داخل <li className="flex"> است و ظرفش 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 |
فایل یتیم — بعد از اصلاح هیچ مصرفکنندهای ندارد |
وضعیت فعلی
صفحهٔ اصلی
// components/home/index.js
<div className="padding-responsive z-20 min-h-screen py-[120px] flex items-center justify-center flex-col gap-[40px]">
<TextHeader />
<SearchBar />
<FrequentSearches />
<CityHighlights />
</div>
صفحهٔ تخصص — دو مصرف متفاوت از یک param
// 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();
برچسب شهر
// components/blog/head/index.js:49
{cityName && (
<div className="flex items-center justify-start gap-1">
<p className="text-[#9B9B9B] text-[11px] md:text-[13px] lg:text-[14px] font-normal">
مخصوص شهر:
</p>
<p className="text-[#9B9B9B] text-[11px] md:text-[14px] lg:text-[16px] font-medium">
{cityName}
</p>
</div>
)}
تگهای بدون لینک
// components/blog/head/index.js:90
{/* فهرست کامل تگها — تا این تغییر فقط تگ اول در بردکرامب دیده میشد.
صفحهٔ فیلتر تگ وجود ندارد، پس چیپها بدون لینک رندر میشوند. */}
{tags.length > 0 && (
<ul className="flex flex-wrap items-center justify-start gap-2 mt-[12px] md:mt-[16px]">
{tags.map((tag, idx) => (
<li
key={`${tag}-${idx}`}
className="bg-[#F5F5F5] text-[#525252] rounded-full px-[12px] py-[4px] text-[11px] md:text-[13px] font-medium"
>
{tag}
</li>
))}
</ul>
)}
ولی فیلتر تگ از قبل هست:
// components/blogs/index.js:16
const selectedTag = searchParams.get("tag") || null;
// :32
params.tag = selectedTag;
عکس مطالب مرتبط
// components/blog/relatedContent/Item.js
<li className="flex w-full relative items-start justify-start gap-[8px] py-[12px]">
<CustomLoading width={80} height={72} loading={loading}>
<Link href={`/blog/${data.uuid}`}>
<img
className=" w-[64px] sm:w-[73px] md:w-[85px] lg:w-[97px] h-[56px] sm:h-[64px] md:h-[75px] lg:h-[88px] rounded-[8px] overflow-hidden object-cover "
src={cover}
alt={data.title || "article"}
/>
</Link>
</CustomLoading>
هدر «درباره ما»
// components/aboutUs/Head.js
<div className="bg-[#5559C2] rounded-none md:rounded-lg w-full py-[24px] flex flex-col items-center justify-center gap-[13px] md:gap-[16px]">
<Image
src="/assets/images/logo-2.png"
height={46}
width={46}
alt="logo"
/>
<h1 className="...">درباره {siteName}</h1>
{slogan && <p className="...">{slogan}</p>}
</div>
Head الان فقط siteName و slogan میگیرد؛ matchedCity به آن پاس نمیشود.
متن «درباره ما»
// components/aboutUs/index.js — دو بند، هر دو از دید بیمار
<p>{siteName} یک سامانهی آنلاین نوبتدهی پزشکی است که …</p>
<p>در {siteName} میتوانید بر اساس تخصص، نام پزشک یا مرکز درمانی جستجو کنید …</p>
وظایف
۱. برداشتن بخش شهرمحور صفحهٔ اصلی
<CityHighlights />و importش ازcomponents/home/index.jsحذف شود.- فایل
components/home/CityHighlights.jsو تستش پاک شوند. buildCityIntroوCITY_INTROSدرlib/listingIntro.jsتنها مصرفکنندهشان همین کامپوننت بود؛ با آن پاک شوند. تستهای مربوطه درlib/cityIntro.test.jsهم بروند. کد مرده نگه ندارید — همین اشتباه قبلاً باbuildDoctorsIntroتکرار شد.buildClinicsIntroوCLINIC_INTROSبمانند؛/clinicsهنوز مصرفشان میکند.
نحوه تست:
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 مشترک بسازید — این باگ در دو صفحه هست و فردا در صفحهٔ سوم هم میآید:
// 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:
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 میکند.
نحوه تست:
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 میکند، ولی چون تگ
ممکن است فاصله یا & داشته باشد، از شکل شیئی استفاده کنید تا مطمئن شوید:
<Link href={{ pathname: "/blogs", query: { tag } }} className="...">
{tag}
</Link>
کامنت کهنهٔ بالای بلوک («صفحهٔ فیلتر تگ وجود ندارد») حذف یا اصلاح شود؛ همان کامنت باعث شد این قابلیت جا بماند.
استایل چیپ عوض نشود، فقط hover مناسب اضافه شود چون حالا کلیکشدنی است.
نحوه تست: تست کامپوننتی روی components/blog/head — مقاله با دو تگ، دو
role="link" با href شامل /blogs?tag=؛ مقاله بدون تگ، هیچ لیستی رندر نشود.
سپس دستی: کلیک روی تگ باید به /blogs?tag=… برود و همان فیلتر اعمال شود.
۵. رفع دفرمگی عکس مطالب مرتبط
به ظرف عکس shrink-0 اضافه شود تا در flex فشرده نشود:
<CustomLoading width={80} height={72} loading={loading}>
<Link href={`/blog/${data.uuid}`} className="block shrink-0">
<img className="... shrink-0 ..." ... />
</Link>
</CustomLoading>
اگر CustomLoading خودش ظرف flex میسازد، shrink-0 باید روی همان بیرونیترین
عنصرِ داخل <li> بنشیند — اول ساختار رندرشده را ببینید، بعد کلاس را جای درست بگذارید.
نحوه تست: تست کامپوننتی که کلاس shrink-0 روی ظرف عکس هست. بهعلاوه بررسی
چشمی در مرورگر با یک تیتر بسیار بلند — jsdom چیدمان را اندازه نمیگیرد، پس این مورد
را صادقانه بهعنوان «بررسی چشمی» گزارش کنید، نه بهعنوان تست خودکار.
۶. بند عضویت پزشک در «درباره ما»
یک بند سوم بعد از دو بند فعلی در components/aboutUs/index.js اضافه شود. متن
پیشنهادی:
<p>
اگر پزشک هستید و میخواهید در {siteName} نوبتدهی آنلاین داشته باشید، ثبتنام
از طریق سامانهی کلینیکپرو انجام میشود. کافی است به{" "}
<a
href="https://clinic-pro.ir/"
target="_blank"
rel="noopener noreferrer"
className="text-[#5559C2] font-medium underline underline-offset-4"
>
clinic-pro.ir
</a>{" "}
مراجعه کنید و حساب مطب خود را بسازید. پس از تأیید، پروفایل و برنامهی
نوبتدهی شما روی {siteName} نمایش داده میشود.
</p>
قواعد:
- نام سایت هاردکد نشود؛ همان
siteNameبالای فایل استفاده شود. - لینک خارجی است، پس
next/linkلازم نیست؛<a>باtarget="_blank"وrel="noopener noreferrer"درست است. - استایل بند از بقیه جدا نشود؛ همان ظرف
flex flex-col gap-[16px]را میگیرد.
نحوه تست:
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 باشد.
۷. اصلاح لوگوی هدر «درباره ما»
<Image src="/assets/images/logo-2.png" /> با SiteLogo جایگزین شود:
<SiteLogo city={matchedCity} stroke="#FAFAFA" className="w-[46px] h-[46px]" />
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 ثابت کنید یتیم است، بعد پاک کنید.
نحوه تست:
grep -rn "logo-2" app components public --include="*.js" --include="*.json" # باید خالی باشد
بهعلاوه تست کامپوننتی روی components/aboutUs/Head: شهر بدون logo_url باید
svg درونخطی بدهد، شهر با logo_url باید <img> با همان آدرس بدهد.
بررسی چشمی هم لازم است — دیدهشدن لوگو روی بنفش را 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"میدهد. صفحات رندر میشوند و برای این تستها کافی است، ولی اگر رفتار عجیبی دیدید اول همین را در نظر بگیرید.