# 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` را پیروی کن.