feat: Revert homepage changes, fix Persian slug 404, and make various article and about us adjustments
- 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.
This commit is contained in:
+1
-67
@@ -1,71 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import cityData from "@/data/city.json";
|
||||
import {
|
||||
buildCityIntro,
|
||||
buildClinicsIntro,
|
||||
} from "@/lib/listingIntro";
|
||||
|
||||
const CITIES = ["بهبهان", "یاسوج", "یزد", "تبریز", "اصفهان"];
|
||||
|
||||
describe("buildCityIntro", () => {
|
||||
it("دو شهر متن یکسان نمیگیرند", () => {
|
||||
const a = buildCityIntro("بهبهان", 12, "جراحی عمومی");
|
||||
const b = buildCityIntro("یاسوج", 80, "جراحی عمومی");
|
||||
|
||||
expect(a).not.toBe(b);
|
||||
});
|
||||
|
||||
it("یک شهر در دو فراخوانی همان متن را میدهد", () => {
|
||||
// ناپایداری یعنی محتوای صفحهٔ اصلی با هر rebuild عوض شود.
|
||||
expect(buildCityIntro("بهبهان", 12, "جراحی عمومی")).toBe(
|
||||
buildCityIntro("بهبهان", 12, "جراحی عمومی")
|
||||
);
|
||||
});
|
||||
|
||||
it("نام شهر و عدد و تخصص در متن میآیند", () => {
|
||||
const text = buildCityIntro("بهبهان", 12, "جراحی عمومی");
|
||||
|
||||
expect(text).toContain("بهبهان");
|
||||
expect(text).toContain("12");
|
||||
expect(text).toContain("جراحی عمومی");
|
||||
});
|
||||
|
||||
it("شمار صفر جمله را نمیشکند", () => {
|
||||
const text = buildCityIntro("شهر تازه", 0, "");
|
||||
|
||||
expect(text).toContain("شهر تازه");
|
||||
expect(text).not.toContain("undefined");
|
||||
expect(text).not.toContain("NaN");
|
||||
});
|
||||
|
||||
it("ورودی نامعتبر به صفر و تخصص پیشفرض میافتد", () => {
|
||||
const text = buildCityIntro("شهر", "abc", null);
|
||||
|
||||
expect(text).not.toContain("NaN");
|
||||
expect(text).toContain("پزشکی عمومی");
|
||||
});
|
||||
|
||||
it("متنها روی شهرهای واقعی به یک قالب واحد نمیچسبند", () => {
|
||||
// اگر همهٔ شهرها یک variant بگیرند، دوباره همان duplicate ساخته میشود.
|
||||
const texts = CITIES.map((c) => buildCityIntro(c, 10, "قلب و عروق"));
|
||||
|
||||
expect(new Set(texts).size).toBeGreaterThan(1);
|
||||
});
|
||||
|
||||
it("متن به قدر کافی بلند هست که سیگنال محتوایی باشد", () => {
|
||||
for (const city of CITIES) {
|
||||
expect(buildCityIntro(city, 10, "قلب").length).toBeGreaterThan(200);
|
||||
}
|
||||
});
|
||||
|
||||
it("روی همهٔ شهرهای واقعی city.json بدون خطا اجرا میشود", () => {
|
||||
for (const city of cityData) {
|
||||
const text = buildCityIntro(city.name, 5, "داخلی");
|
||||
expect(typeof text).toBe("string");
|
||||
expect(text).not.toContain("undefined");
|
||||
}
|
||||
});
|
||||
});
|
||||
import { buildClinicsIntro } from "@/lib/listingIntro";
|
||||
|
||||
describe("جملهٔ آماری متن صفحات لیست", () => {
|
||||
it("دو شهرِ همvariant با آمار متفاوت، متن یکسان نمیگیرند", () => {
|
||||
|
||||
+2
-35
@@ -1,4 +1,4 @@
|
||||
// متن مقدمهٔ یکتای صفحات لیست (/doctors و /clinics).
|
||||
// متن مقدمهٔ یکتای صفحهٔ لیست /clinics.
|
||||
// canonical درست، محتوای یکسان را غیرتکراری نمیکند — این متن همان نقش مکمل را دارد.
|
||||
// مثل صفحات تخصص، ساختار جملهبندی هر شهر با هش نام شهر انتخاب میشود تا ۳۵ دامنه
|
||||
// ۳۵ متن با ساختار متفاوت داشته باشند، نه یک قالب با نام جایگذاریشده.
|
||||
@@ -21,38 +21,6 @@ const CLINIC_INTROS = (city) => [
|
||||
`مرکز درمانی مناسب در ${city} را از این فهرست پیدا کنید. هر مرکز صفحهٔ اختصاصی دارد و آنچه آنجا میبینید از رکورد خودِ مرکز میآید: خدمات، تخصصها، بیمههای طرف قرارداد و پزشکانی که پذیرش دارند. موقعیت روی نقشه هم هست تا پیش از مراجعه مسیر و زمان رسیدن را بدانید. نوبتگیری از صفحهٔ مرکز و از طریق پزشک انجام میشود؛ هزینهٔ خدمت پیش از تأیید نمایش داده میشود. لغو نوبت از حساب کاربری ممکن است و بازهٔ لغوشده همان لحظه به فهرست نوبتهای آزاد برمیگردد. برای محدود کردن فهرست میتوانید از فیلتر تخصص استفاده کنید تا فقط مراکزی بمانند که خدمت موردنظرتان را ارائه میدهند. نشانی و شمارهٔ تماس هر مرکز دورهای بازبینی میشود.`,
|
||||
];
|
||||
|
||||
|
||||
// مقدمهٔ صفحهٔ اصلی هر دامنهٔ شهری.
|
||||
//
|
||||
// صفحهٔ اصلی تا پیش از این فقط `site_name` و `slogan` را از city.json شهری میکرد و
|
||||
// بقیهٔ صفحه — بنر، کادر جستجو، جستجوهای پرتکرار، فوتر — روی همهٔ دامنهها یکسان بود.
|
||||
// اندازهگیری: شباهت متن رندرشدهٔ بهبهان و یاسوج ۹۹٫۰٪. گوگل canonicalِ درستِ ما را رد
|
||||
// میکرد و صفحهٔ اصلی شهر را با دامنهٔ دیگری تجمیع میکرد
|
||||
// (Duplicate, Google chose different canonical than user).
|
||||
//
|
||||
// عدد پزشک و تخصص داخل متن میآید تا تمایز فقط لفظی نباشد؛ هر شهر آمار خودش را دارد.
|
||||
const CITY_INTROS = (city, doctorCount, topSpecialty) => [
|
||||
`${doctorCount} پزشک از ${city} روی این سامانه پروفایل دارند. بیشترینشان ${topSpecialty}اند. نام پزشک یا نام تخصص را در کادر بالا بزنید؛ هر دو جستوجو میشوند. اگر هنوز نمیدانید سراغ کدام تخصص بروید، فهرست تخصصهای همین شهر پایینتر آمده و از هر کدام مستقیم به پزشکانش میرسید.`,
|
||||
`نوبت پزشکان ${city} را از همینجا میگیرید، در هر ساعت شبانهروز و بدون تماس با مطب. الان ${doctorCount} پزشک ثبت شدهاند و ${topSpecialty} پرمراجعهترین حوزه است. روی پروفایل هر پزشک، محل پذیرش و بازههای خالی همان هفته را میبینید. انتخاب کنید و تأیید بگیرید؛ پیامک تأییدیه همان لحظه میآید.`,
|
||||
`اینجا نقطهٔ شروع برای پیدا کردن پزشک در ${city} است. ${doctorCount} پروفایل ثبت شده و ${topSpecialty} بیشترین سهم را دارد. هر پروفایل تخصص، نشانی محل پذیرش و امتیاز بیمارانی که قبلاً از همینجا نوبت گرفتهاند را نشان میدهد. رزرو، تغییر و لغو نوبت همه از حساب کاربری خودتان انجام میشود.`,
|
||||
`${city} روی این سامانه ${doctorCount} پزشک دارد و ${topSpecialty} از بقیه پرمراجعهتر است. کادر جستوجو نام پزشک و نام تخصص را با هم میگیرد. اگر ترجیح میدهید اول مرکز درمانی را انتخاب کنید، فهرست کلینیکها جداست و پزشکان هر مرکز را کنار هم نشان میدهد.`,
|
||||
`دنبال پزشک در ${city} میگردید؟ ${doctorCount} پزشک ثبت شدهاند، بیشترشان در حوزهٔ ${topSpecialty}. نتیجهٔ جستوجو نشانی مطب، ساعات کاری و اولین نوبت آزاد هر پزشک را نشان میدهد. پزشکی که فعلاً نوبتدهیاش خاموش است هم میماند، فقط با برچسب مشخص، تا بدانید گزینه هست.`,
|
||||
`فهرست پزشکان ${city} الان ${doctorCount} پروفایل دارد و ${topSpecialty} صدرنشین مراجعههاست. اطلاعات هر پزشک — تخصص، محل پذیرش، ساعت کاری — دورهای بازبینی میشود تا با وضعیت واقعی مطب بخواند. نوبت با تأیید شما قطعی میشود و اگر برنامهتان عوض شد، لغوش از همان حساب ممکن است.`,
|
||||
];
|
||||
|
||||
/**
|
||||
* مقدمهٔ یکتای صفحهٔ اصلی یک شهر.
|
||||
*
|
||||
* seed از نام شهر میآید، نه تصادفی: متن باید بین شهرها واگرا شود ولی برای یک شهر
|
||||
* در هر rebuild ثابت بماند، وگرنه محتوای صفحهٔ اصلی مدام عوض میشد.
|
||||
*/
|
||||
export function buildCityIntro(cityName, doctorCount = 0, topSpecialty = "") {
|
||||
const count = Number(doctorCount) > 0 ? Number(doctorCount) : 0;
|
||||
const specialty = topSpecialty || "پزشکی عمومی";
|
||||
|
||||
return pick(CITY_INTROS(cityName, count, specialty), hash(cityName) >>> 5);
|
||||
}
|
||||
|
||||
/**
|
||||
* جملهٔ آماری که به انتهای متن میچسبد.
|
||||
*
|
||||
@@ -60,8 +28,7 @@ export function buildCityIntro(cityName, doctorCount = 0, topSpecialty = "") {
|
||||
* میشوند. برای آن جفتها متن تقریباً یکسان میشد و دو صفحه دوباره شبیه هم — سنجیده
|
||||
* شد: بهبهان و یاسوج روی /clinics از ۶۸٫۹٪ به ۸۴٫۷٪ شباهت رفتند.
|
||||
*
|
||||
* عدد واقعیِ هر شهر این را میشکند، چون هیچ دو شهری آمار یکسان ندارند. همان الگویی
|
||||
* که در متن صفحهٔ اصلی جواب داد.
|
||||
* عدد واقعیِ هر شهر این را میشکند، چون هیچ دو شهری آمار یکسان ندارند.
|
||||
*/
|
||||
const countSentence = (count, cityName, noun) =>
|
||||
Number(count) > 0
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* segment مسیر را به شکل decodeشده برمیگرداند.
|
||||
*
|
||||
* Next در `generateMetadata` مقدار decodeشده میدهد ولی در بدنهٔ همان صفحه مقدار
|
||||
* خام؛ روی اسلاگهای فارسی این یعنی متادیتا درست ساخته میشود و بلافاصله بعدش
|
||||
* صفحه ۴۰۴ میدهد. با build واقعی سنجیده شد:
|
||||
*
|
||||
* [meta] "جراح-گوارش" found: true
|
||||
* [body] "%D8%AC%D8%B1%D8%A7%D8%AD-%DA%AF..." found: false
|
||||
*
|
||||
* decode شکستخورده (درصدِ ناقص در ورودی) نباید صفحه را بشکند؛ همان مقدار خام
|
||||
* برمیگردد تا نتیجهاش ۴۰۴ طبیعی باشد نه ۵۰۰.
|
||||
*/
|
||||
export function decodeRouteParam(value) {
|
||||
if (typeof value !== "string" || !value.includes("%")) return value;
|
||||
try {
|
||||
return decodeURIComponent(value);
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { decodeRouteParam } from "@/lib/routeParams";
|
||||
|
||||
describe("decodeRouteParam", () => {
|
||||
it("اسلاگ درصدی را به فارسی برمیگرداند", () => {
|
||||
expect(
|
||||
decodeRouteParam("%D8%AC%D8%B1%D8%A7%D8%AD-%DA%AF%D9%88%D8%A7%D8%B1%D8%B4")
|
||||
).toBe("جراح-گوارش");
|
||||
});
|
||||
|
||||
it("فارسی خام دستنخورده میماند", () => {
|
||||
expect(decodeRouteParam("جراح-گوارش")).toBe("جراح-گوارش");
|
||||
});
|
||||
|
||||
it("اسلاگ لاتین دستنخورده میماند", () => {
|
||||
expect(decodeRouteParam("gastroenterology")).toBe("gastroenterology");
|
||||
});
|
||||
|
||||
it("درصد ناقص throw نمیکند و ورودی را برمیگرداند", () => {
|
||||
// نتیجهاش باید ۴۰۴ طبیعی باشد، نه ۵۰۰.
|
||||
expect(decodeRouteParam("%D8%")).toBe("%D8%");
|
||||
expect(decodeRouteParam("100%-natural")).toBe("100%-natural");
|
||||
});
|
||||
|
||||
it("ورودی غیر رشتهای همانطور برمیگردد", () => {
|
||||
expect(decodeRouteParam(undefined)).toBeUndefined();
|
||||
expect(decodeRouteParam(null)).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user