Files
nobat724_front/.claude/prompt/doctor-seo-schema-knowledge-panel.md
T
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

16 KiB
Raw Blame History

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 فعلی (ناقص)

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/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 کن — منتظر آن پرامپت نمان.