feat(robots): update disallow rules to include '/panel' feat(sitemap): implement separate sitemaps for doctors, clinics, and blogs feat(icons): add social media icons for doctor profiles
16 KiB
SEO پیشرفته صفحه پزشک: Schema کامل، Sitelinks، Knowledge Panel، Local SEO
پروژه
nobat724_front — این پرامپت برای فعالسازی Sitelinks و Knowledge Panel گوگل برای صفحهی هر پزشک است (هدف: نتیجهی جستجوی نام پزشک شبیه نتایج Zocdoc/Healthgrades با Knowledge Panel، عکس، شبکههای اجتماعی، آدرس، ساعات کاری، امتیاز).
وابستگی cross-repo: فیلد socialMedia پزشک هنوز در backend وجود ندارد — پرامپت همتا در clinicpro/.claude/prompt/doctor-social-media-field.md این فیلد را اضافه میکند. آن پرامپت را اول اجرا کن، سپس این یکی را، چون بخشهای sameAs JSON-LD و کارت شبکههای اجتماعی در UI به آن فیلد نیاز دارند. اگر هنوز اجرا نشده، آن بخشها را با حالت "اگر doctor.socialMedia وجود داشت" مشروط بنویس تا بدون خطا گریسفول fallback شود.
زمینه
بررسی کد واقعی نشان داد:
app/doctor/[slug]/page.jsفعلاً فقط schemaPhysicianساده (نام، تخصص، عکس، آدرس متنی) و یکBreadcrumbListدارد.- دادهی واقعی API (
docs/api/doctor.md) شامل:point/satisfaction(امتیاز و رضایت، میتواندAggregateRatingشود)، چندینaddress[]باclinics[](هرکدامname/address/telephone)، اما این آرایه مستقیماً درgetDoctor()فعلی fetch نمیشود (endpoint جدا:GET /api/v1/clinic-pro/doctor-addresses/{doctorId}کهmap.latitude/map.longitudeهم دارد). - نظرات از
GET /api/v1/comments/{uuid}میآید (commentsدر کامپوننتDoctor()) — میتواند منبعReviewschema باشد. - مقالات/پرسشوپاسخ پزشک در حال حاضر در هیچجای کد دیده نشد — این پروژه فعلاً مقاله یا FAQ مرتبط به پزشک خاص ندارد (فقط
app/blogs/app/blog/[slug]عمومی است، بدون ارتباط به یک پزشک مشخص). بخشهای مرتبط با Article/FAQPage در این پرامپت را فقط برای ساختار سایت عمومی (نه per-doctor) طراحی کن، مگر اینکه در حین اجرا API برای FAQ پزشک پیدا شود.
مشکل / هدف
برای app/doctor/[slug]/page.js (و در حد لازم app/doctors/page.js, app/clinics/page.js, app/sitemap.js, app/robots.js, app/layout.js) موارد زیر اضافه/تکمیل شود:
- Schema.org کاملتر برای پزشک (Physician + AggregateRating + Review + LocalBusiness برای هر مطب + ImageObject + sameAs)
- Sitemap تفکیکشده (پزشکان، شهرها، تخصصها، مقالات) بهجای فایل واحد فعلی
- ساختار URL/Internal Linking برای افزایش شانس Sitelinks
- متادیتای دقیقتر هماهنگ با Knowledge Panel
فایلهای مرتبط
| فایل | نقش فعلی | تغییر لازم |
|---|---|---|
app/doctor/[slug]/page.js |
getDoctor() با cache()، schema Physician ساده، breadcrumb |
افزودن AggregateRating, Review, LocalBusiness(×N مطب), ImageObject, sameAs؛ fetch کردن آدرسهای مطب |
components/doctor/index.js |
رندر breadcrumb بصری (تخصص > نام) - قبلاً به Link تبدیل شده | بدون تغییر مگر اضافهکردن نمایش شبکههای اجتماعی اگر doctor.socialMedia وجود داشت |
app/sitemap.js |
یک فایل واحد، getDoctorUrls/getClinicUrls/getBlogUrls با fetch + revalidate: 3600 |
تفکیک به چند sitemap با app/sitemap/[id]/route.js یا multi-sitemap pattern رسمی Next.js 15 (generateSitemaps) |
app/robots.js |
disallow: '/dashboard' فقط؛ از DEV_MODE میخواند |
اضافهکردن /panel به disallow؛ بدون تغییر در منطق DEV_MODE |
app/layout.js |
JSON-LD سراسری Organization + WebSite با SearchAction |
بررسی کن sameAs سازمانی (لینک شبکههای اجتماعی نوبت724، نه پزشک) باید اینجا اضافه شود اگر data/city.json چنین دادهای دارد |
clinicpro/docs/api/doctor.md |
مرجع شکل دقیق پاسخ API | فقط مرجع خوانده شود، تغییر ندهید (این فایل پروژه backend است) |
وضعیت فعلی
app/doctor/[slug]/page.js — schema فعلی (ناقص)
const jsonLd = doctor
? {
"@context": "https://schema.org",
"@type": "Physician",
name: `دکتر ${doctor.name}`,
...(specialtyNames && { medicalSpecialty: specialtyNames }),
...(doctor.img && { image: doctor.img }),
...(doctor.address && {
address: {
"@type": "PostalAddress",
streetAddress: doctor.address,
},
}),
}
: null;
مشکلات این بلوک:
imageبایدImageObjectکامل (باurl,width,height) باشد، نه رشتهی خام.addressفقط یک متن ساده است؛ مطبهای واقعی پزشک (با مختصات جغرافیایی) اصلاً fetch نمیشوند در این صفحه.- بدون
aggregateRating، بدونreview، بدونsameAs، بدونtelephone. doctor.point/doctor.satisfaction(که در پاسخ API موجود است طبقdocs/api/doctor.md) استفاده نمیشوند.
app/sitemap.js — فعلاً یک فایل واحد
export default async function sitemap() {
try {
const domain = getCurrentDomain();
const baseUrl = getBaseUrl(domain);
const [staticPages, doctorUrls, clinicUrls, blogUrls] = await Promise.all([
Promise.resolve(getStaticPages(baseUrl)),
getDoctorUrls(baseUrl),
getClinicUrls(baseUrl),
getBlogUrls(baseUrl),
]);
const allUrls = [...staticPages, ...doctorUrls, ...clinicUrls, ...blogUrls];
return allUrls.filter(/* dedupe */);
} catch { return []; }
}
این تابع همهچیز را در یک فایل XML واحد میریزد (getDoctorUrls با limit=500). اگر تعداد پزشکان از ۵۰هزار بیشتر شود (محدودیت گوگل برای یک sitemap)، باید چندتایی شود — Next.js 15 از generateSitemaps() پشتیبانی میکند.
وظایف
۱. Schema.org کامل برای صفحه پزشک
در app/doctor/[slug]/page.js، fetch آدرسهای مطب را اضافه کن (مشابه الگوی موجود getDoctor):
const getDoctorAddresses = cache(async (doctorId) => {
try {
const res = await fetch(`${API_URL}/api/v1/clinic-pro/doctor-addresses/${doctorId}`, {
next: { revalidate: 3600, tags: [`doctor-addresses-${doctorId}`] },
});
if (!res.ok) return [];
const json = await res.json();
return json?.data ?? [];
} catch {
return [];
}
});
سپس در کامپوننت Doctor()، schema را به این شکل بازنویسی کن:
const addresses = doctor ? await getDoctorAddresses(doctor.id) : [];
const jsonLd = doctor
? {
"@context": "https://schema.org",
"@type": "Physician",
"@id": `https://www.nobat724.com/doctor/${doctor.uuid}#physician`,
name: `دکتر ${doctor.name}`,
url: `https://www.nobat724.com/doctor/${doctor.uuid}`,
...(specialtyNames && { medicalSpecialty: specialtyNames }),
...(doctor.img && {
image: {
"@type": "ImageObject",
url: imageUrl(doctor.img),
},
}),
...(doctor.point && Number(doctor.point) > 0 && {
aggregateRating: {
"@type": "AggregateRating",
ratingValue: doctor.point,
bestRating: "5",
ratingCount: comments?.length || 1,
},
}),
...(comments?.length > 0 && {
review: comments.slice(0, 5).map((c) => ({
"@type": "Review",
author: { "@type": "Person", name: c.author || "بیمار" },
reviewBody: c.text || c.body,
...(c.rate && {
reviewRating: { "@type": "Rating", ratingValue: c.rate, bestRating: "5" },
}),
})),
}),
...(doctor.socialMedia && {
sameAs: Object.values(doctor.socialMedia).filter(Boolean),
}),
...(addresses.length > 0 && {
workLocation: addresses.map((addr) => ({
"@type": "MedicalClinic",
name: addr.clinic_name || addr.name || `مطب دکتر ${doctor.name}`,
address: {
"@type": "PostalAddress",
streetAddress: addr.address,
},
...(addr.telephone && { telephone: addr.telephone }),
...(addr.map?.latitude && addr.map?.longitude && {
geo: {
"@type": "GeoCoordinates",
latitude: addr.map.latitude,
longitude: addr.map.longitude,
},
}),
})),
}),
}
: null;
نکته مهم: بررسی کن فیلد دقیق rate در comments[] (نام فیلد امتیاز هر نظر) چیست — در docs/api/doctor.md این جزئیات مشخص نشده، باید با console.log یا بررسی docs/api/rating.md (اگر در backend وجود دارد) دقیق کنی، فرضیات بالا (c.rate, c.text, c.author) را verify کن قبل از commit.
۲. Sitemap تفکیکشده
app/sitemap.js فعلی را به ساختار چندتایی Next.js 15 تبدیل کن. ساختار پیشنهادی:
app/
sitemap.js # index — فقط static pages (home, about, contact, specialties)
doctor/sitemap.js # generateSitemaps() برای پزشکان، یکی به ازای هر 1000 پزشک
clinic/sitemap.js # همین الگو برای کلینیکها
blog/sitemap.js # همین الگو برای مقالات
نمونه برای app/doctor/sitemap.js (الگوی رسمی Next.js 15 برای sitemap چندتایی):
export async function generateSitemaps() {
// اگر تعداد پزشکان مشخص نیست، یک تخمین اولیه برگردان (مثلاً بر اساس count از API)
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/doctors?page=1&limit=1`, {
next: { revalidate: 86400 },
});
const json = await res.json();
const total = json?.meta?.totalRecords || 0;
const pageCount = Math.ceil(total / 1000);
return Array.from({ length: pageCount }, (_, i) => ({ id: i }));
}
export default async function sitemap({ id }) {
const API_URL = process.env.NEXT_PUBLIC_API_URL;
const res = await fetch(`${API_URL}/api/v1/doctors?page=${id + 1}&limit=1000`, {
next: { revalidate: 3600 },
});
if (!res.ok) return [];
const json = await res.json();
const doctors = json?.data || [];
return doctors
.filter((d) => d?.uuid)
.map((d) => ({
url: `https://www.nobat724.com/doctor/${d.uuid}`,
lastModified: new Date(),
changeFrequency: "weekly",
priority: 0.8,
}));
}
همین الگو را برای clinic و blog تکرار کن. app/sitemap.js اصلی فقط صفحات ایستا (/, /about-us, /contact-us, /specialties, /doctors, /clinics, /blogs) را برمیگرداند — منطق getStaticPages() موجود قابل reuse است، فقط getDoctorUrls/getClinicUrls/getBlogUrls از این فایل حذف و به فایلهای جدید منتقل میشوند.
نکته مهم: صفحات تخصص (?specialty=) و شهر (subdomain) در حال حاضر URL مجزا با path ندارند (فیلتر با query param است) — اضافهکردن اینها به sitemap بهصورت URL جدا (/doctors?specialty=X) ارزش SEO کمی دارد چون گوگل query-param URLها را با اولویت پایینتر میبیند. اگر میخواهی این بخش (URL Structure برای تخصص/شهر بهصورت path-based مثل /doctors/tehran/cardiology) را هم پیادهسازی کنی، این یک تغییر بزرگتر در routing است که باید جدا و با تأیید قبلی انجام شود — در این پرامپت فقط طراحی پیشنهادی را در بخش «نکات مهم» مستند کن، کد routing را عوض نکن مگر کاربر صریحاً بخواهد.
۳. بهروزرسانی robots.js
return {
rules: {
userAgent: '*',
allow: '/',
disallow: ['/dashboard', '/panel'],
},
sitemap: `${baseUrl}/sitemap.xml`,
};
۴. کارت شبکههای اجتماعی در UI (مشروط به وجود فیلد backend)
در components/doctor/detailDoctor/ (بررسی فایلهای موجود مثل Share.js) یک بخش کوچک برای نمایش آیکونهای شبکهاجتماعی پزشک اضافه کن، فقط اگر doctor.socialMedia مقدار غیر-null داشت:
{doctor?.socialMedia && Object.entries(doctor.socialMedia).some(([, v]) => v) && (
<div className="flex items-center gap-2">
{doctor.socialMedia.instagram && (
<a href={doctor.socialMedia.instagram} target="_blank" rel="noopener noreferrer">
<InstagramIcon />
</a>
)}
{/* تکرار برای telegram, aparat, youtube, linkedin */}
</div>
)}
آیکونهای لازم را در components/icons/ بررسی کن — اگر آیکون اینستاگرام/تلگرام/آپارات/یوتیوب از قبل وجود ندارد، SVG ساده اضافه کن (الگوی فایلهای موجود در همان پوشه را دنبال کن).
نکات مهم
- هیچ تغییری در
services/api.js/services/response.js(مسیر axios سمت کلاینت) ندهی مگر برایsocialMediaکه نیاز به submit از فرم ادمین دارد (آن فرم درclinicproاست، نه اینجا). - چون پروژه فعلاً مقاله/FAQ مرتبط به پزشک خاص ندارد، schemaهای
FAQPageوArticle/BlogPostingper-doctor را در این مرحله پیادهسازی نکن — فقط طراحی پیشنهادی (در صورت افزودن این قابلیت در آینده) را در پایان فایل توضیح بده، کد واقعی برایش ننویس. - صفحه
app/clinic/[slug]/page.jsمیتواند schema مشابه (MedicalClinicباaggregateRating/geo) بگیرد — اگر وقت بود همان الگوی schema پزشک را برای کلینیک هم تکرار کن، اما اولویت اول صفحه پزشک است. - بعد از هر تغییر در
app/doctor/[slug]/page.js، تست کن خروجی JSON-LD نهایی valid باشد — از Google Rich Results Test (بهصورت دستی، نه خودکار در این session) یا حداقلJSON.parse(JSON.stringify(jsonLd))بدون خطا. - پس از تغییرات، حتماً
npm run buildرا اجرا کن — مسیرهای جدید sitemap (app/doctor/sitemap.jsو غیره) باید بدون خطا کامپایل شوند و در خروجی build بهعنوان route مجزا دیده شوند. - اگر در حین اجرا متوجه شدی پرامپت backend (
doctor-social-media-field.md) هنوز اجرا نشده، بخشهایsameAs/کارت شبکههای اجتماعی را با گاردdoctor?.socialMedia &&(که در کد بالا هم رعایت شده) بدون خطا skip کن — منتظر آن پرامپت نمان.