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
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
# 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 کن — منتظر آن پرامپت نمان.
|
||||
Reference in New Issue
Block a user