Files
nobat724_front/.claude/prompt/doctor-seo-schema-knowledge-panel.md
hamed a8c3a57114 feat(doctor): enhance doctor schema with addresses, ratings, and social media links
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
2026-06-21 12:35:15 +03:30

259 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` فعلاً فقط schema `Physician` ساده (نام، تخصص، عکس، آدرس متنی) و یک `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()`) — می‌تواند منبع `Review` schema باشد.
- مقالات/پرسش‌و‌پاسخ پزشک در حال حاضر در هیچ‌جای کد دیده نشد — این پروژه فعلاً **مقاله یا 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`) موارد زیر اضافه/تکمیل شود:
1. Schema.org کامل‌تر برای پزشک (Physician + AggregateRating + Review + LocalBusiness برای هر مطب + ImageObject + sameAs)
2. Sitemap تفکیک‌شده (پزشکان، شهرها، تخصص‌ها، مقالات) به‌جای فایل واحد فعلی
3. ساختار URL/Internal Linking برای افزایش شانس Sitelinks
4. متادیتای دقیق‌تر هماهنگ با 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 فعلی (ناقص)
```js
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` — فعلاً یک فایل واحد
```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`):
```js
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 را به این شکل بازنویسی کن:
```js
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 چندتایی):
```js
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`
```js
return {
rules: {
userAgent: '*',
allow: '/',
disallow: ['/dashboard', '/panel'],
},
sitemap: `${baseUrl}/sitemap.xml`,
};
```
### ۴. کارت شبکه‌های اجتماعی در UI (مشروط به وجود فیلد backend)
در `components/doctor/detailDoctor/` (بررسی فایل‌های موجود مثل `Share.js`) یک بخش کوچک برای نمایش آیکون‌های شبکه‌اجتماعی پزشک اضافه کن، فقط اگر `doctor.socialMedia` مقدار غیر-null داشت:
```jsx
{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`/`BlogPosting` per-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 کن — منتظر آن پرامپت نمان.