Files
nobat724_front/.claude/prompt/clinic-seo-schema-knowledge-panel.md

259 lines
9.3 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.
# Schema.org کامل و Knowledge Panel برای صفحه کلینیک
## پروژه
`nobat724_front` — پیش‌نیاز: `clinicpro/.claude/prompt/clinic-social-media-field.md` باید اجرا شده باشد تا فیلد `social_media` در API کلینیک موجود باشد.
## زمینه
صفحه `/clinic/[slug]/page.js` در حال حاضر فقط یک `MedicalClinic` ساده با `name` و `image` دارد. هدف این است که با افزودن Schema.org کامل، گوگل بتواند Knowledge Panel برای هر کلینیک نمایش دهد — شامل آدرس، تلفن، ساعات کاری، امتیاز، شبکه‌های اجتماعی و لوکیشن.
برخلاف Doctor که آدرس‌ها در entity جدا (`DoctorAddress`) بودند و نیاز به fetch جداگانه داشتند، **کلینیک آدرس/تلفن/مختصات جغرافیایی را مستقیم روی خودش دارد** — پس نیازی به endpoint اضافه نیست.
## مشکل / هدف
Schema.org فعلی کلینیک بسیار ناقص است:
```js
// وضعیت فعلی — فقط name و image
const jsonLd = clinic ? {
"@context": "https://schema.org",
"@type": "MedicalClinic",
name: clinic.title,
...(clinic.images_clinic?.[0]?.url && { image: imageUrl(clinic.images_clinic[0].url) }),
} : null;
```
موارد غایب:
- `twitter:card` در `generateMetadata`
- `telephone`, `address` (PostalAddress)
- `geo` (GeoCoordinates) از `latitude`/`longitude`
- `openingHours` / `openingHoursSpecification` از `is247`
- `aggregateRating` از میانگین امتیاز پزشکان کلینیک
- `sameAs` از `social_media` (پس از اجرای پرامپت backend)
- `medicalSpecialty` از تخصص پزشکان
- `hasMap` لینک گوگل مپ
- `ImageObject` کامل به جای رشته ساده
## فایل‌های مرتبط
| فایل | نقش |
|------|-----|
| `app/clinic/[slug]/page.js` | صفحه اصلی — اینجا همه تغییرات اعمال می‌شود |
| `helper/index.js` | `imageUrl()` موجود است |
| `app/doctor/[slug]/page.js` | الگوی مرجع: `getDoctorAddresses`، `computeRating`، `sameAs`، `Review` |
## وضعیت فعلی API کلینیک
براساس `clinicpro/docs/api/clinic.md`، پاسخ `GET /api/v1/clinic/{uuid}` (داخل `data.data`) این فیلدها را دارد:
```json
{
"id": 1,
"uuid": "...",
"title": "کلینیک پارسیان",
"address": "یزد، خیابان امام خمینی، ...",
"telephone": "035-12345678",
"latitude": "31.8974",
"longitude": "54.3569",
"working_days": "شنبه تا چهارشنبه ۸ تا ۱۴",
"is247": false,
"images_clinic": [{ "url": "/uploads/clinic/...", "fid": 1 }],
"insurance": ["بیمه ایران", "تامین اجتماعی"],
"doctors": 5
}
```
پس از اجرای پرامپت backend، فیلد `social_media` هم اضافه می‌شود:
```json
"social_media": {
"instagram": "https://instagram.com/clinic.example",
"telegram": null,
"aparat": null,
"youtube": null,
"linkedin": null
}
```
لیست پزشکان کلینیک از endpoint جداگانه (که قبلاً در صفحه fetch می‌شود):
```js
const reqDoctors = await fetchReq(`${API_URL}/api/v1/clinic/doctor-list/${slug}?page=${page}&limit=${limit}`);
const doctors = reqDoctors?.data?.data || [];
// هر doctor دارای: point (امتیاز عددی)، specialties (آرایه)، ...
```
## وظایف
### ۱. بهبود `generateMetadata`
`twitter:card` اضافه شود و `openGraph.type` مشخص شود:
```js
return {
title,
description,
openGraph: {
title,
description,
type: "website",
images: clinic.images_clinic?.[0]?.url
? [imageUrl(clinic.images_clinic[0].url)]
: ["https://www.nobat724.com/assets/images/logo.png"],
},
twitter: {
card: "summary_large_image",
title,
description,
images: clinic.images_clinic?.[0]?.url
? [imageUrl(clinic.images_clinic[0].url)]
: ["https://www.nobat724.com/assets/images/logo.png"],
},
};
```
### ۲. تابع کمکی `computeClinicRating`
این تابع را بالای کامپوننت `Clinic` (یا در `helper/index.js`) تعریف کن. از آرایه پزشکانی که قبلاً fetch می‌شوند استفاده می‌کند:
```js
function computeClinicRating(doctors) {
const rated = doctors.filter((d) => d.point && Number(d.point) > 0);
if (rated.length === 0) return null;
const avg = rated.reduce((sum, d) => sum + Number(d.point), 0) / rated.length;
return {
"@type": "AggregateRating",
ratingValue: avg.toFixed(1),
ratingCount: rated.length,
bestRating: "5",
worstRating: "1",
};
}
```
### ۳. ساخت JSON-LD کامل
`jsonLd` را در تابع `Clinic` جایگزین کن. از داده‌هایی که **قبلاً** در صفحه موجودند استفاده کن (`clinic` و `doctors`):
```js
const aggregateRating = computeClinicRating(doctors);
// تخصص‌های یکتا از پزشکان کلینیک
const specialties = [
...new Set(
doctors.flatMap((d) => d.specialties?.map((s) => s.name) ?? [])
),
].slice(0, 5);
// sameAs از social_media (پس از اجرای پرامپت backend موجود می‌شود)
const sameAsLinks = clinic?.social_media
? Object.values(clinic.social_media).filter(Boolean)
: [];
const jsonLd = clinic
? {
"@context": "https://schema.org",
"@type": "MedicalClinic",
name: clinic.title,
// تصویر
...(clinic.images_clinic?.[0]?.url && {
image: {
"@type": "ImageObject",
url: imageUrl(clinic.images_clinic[0].url),
name: clinic.title,
},
}),
// اطلاعات تماس و آدرس
...(clinic.telephone && { telephone: clinic.telephone }),
...(clinic.address && {
address: {
"@type": "PostalAddress",
streetAddress: clinic.address,
addressCountry: "IR",
},
}),
// مختصات جغرافیایی
...(clinic.latitude && clinic.longitude && {
geo: {
"@type": "GeoCoordinates",
latitude: clinic.latitude,
longitude: clinic.longitude,
},
hasMap: `https://maps.google.com/?q=${clinic.latitude},${clinic.longitude}`,
}),
// ساعات کاری — فقط اگر ۲۴ ساعته باشد (working_days متن آزاد است)
...(clinic.is247 && {
openingHoursSpecification: {
"@type": "OpeningHoursSpecification",
dayOfWeek: [
"Monday", "Tuesday", "Wednesday", "Thursday",
"Friday", "Saturday", "Sunday",
],
opens: "00:00",
closes: "23:59",
},
}),
// امتیاز
...(aggregateRating && { aggregateRating }),
// تخصص‌های پزشکی
...(specialties.length > 0 && {
medicalSpecialty: specialties,
}),
// شبکه‌های اجتماعی
...(sameAsLinks.length > 0 && { sameAs: sameAsLinks }),
// URL صفحه
url: `https://www.nobat724.com/clinic/${clinic.uuid}`,
}
: null;
```
### ۴. به‌روزرسانی Breadcrumb
Breadcrumb فعلی آیتم سوم بدون `item` دارد که اشتباه است. آیتم آخر (`position: 3`) نباید `item` داشته باشد (صفحه جاری است):
```js
const breadcrumbJsonLd = clinic
? {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: [
{
"@type": "ListItem",
position: 1,
name: "خانه",
item: "https://www.nobat724.com",
},
{
"@type": "ListItem",
position: 2,
name: "کلینیک‌ها",
item: "https://www.nobat724.com/clinics",
},
{
"@type": "ListItem",
position: 3,
name: clinic.title,
// بدون item — صفحه جاری
},
],
}
: null;
```
## نکات مهم
- **`computeClinicRating` فقط از `doctors` استفاده می‌کند** — این آرایه قبلاً در صفحه fetch شده و نیازی به endpoint جدید نیست.
- **`working_days` متن آزاد است** (مثلاً "شنبه تا چهارشنبه ۸ تا ۱۴") — نمی‌توان آن را به `OpeningHoursSpecification` مپ کرد. فقط برای کلینیک‌های `is247=true` این فیلد را ساختارمند اضافه کن.
- **`social_media` اختیاری** — اگر فیلد در API وجود نداشت (قبل از اجرای پرامپت backend)، `sameAsLinks` آرایه خالی می‌شود و `sameAs` به JSON-LD اضافه نمی‌شود.
- **`doctors` از `page=1&limit=50` فعلی** — برای محاسبه rating همین کافی است؛ endpoint جداگانه‌ای اضافه نکن.
- **بعد از تغییر، `npm run build` اجرا کن** تا خطاهای TypeScript/ESLint آشکار شوند.
- **`DEV_MODE=TRUE` روی سرور dev** — برای بررسی JSON-LD از Google Rich Results Test یا `application/ld+json` در DevTools استفاده کن، نه از ایندکس واقعی.
- الگوی مرجع: `app/doctor/[slug]/page.js` — همان ساختار `sameAs`، `aggregateRating`، و `ImageObject` را پیروی کن.