diff --git a/.claude/prompt/doctor-seo-schema-knowledge-panel.md b/.claude/prompt/doctor-seo-schema-knowledge-panel.md new file mode 100644 index 0000000..6e5b04a --- /dev/null +++ b/.claude/prompt/doctor-seo-schema-knowledge-panel.md @@ -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) && ( +
+ {doctor.socialMedia.instagram && ( + + + + )} + {/* تکرار برای telegram, aparat, youtube, linkedin */} +
+)} +``` + +آیکون‌های لازم را در `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 کن — منتظر آن پرامپت نمان. diff --git a/app/blog/sitemap.js b/app/blog/sitemap.js new file mode 100644 index 0000000..41c30f3 --- /dev/null +++ b/app/blog/sitemap.js @@ -0,0 +1,49 @@ +const API_URL = process.env.NEXT_PUBLIC_API_URL; +const MAIN_DOMAIN = 'https://nobat724.com'; +const PER_SITEMAP = 1000; + +function toSafeDate(value) { + if (!value) return new Date(); + const d = new Date(value); + return isNaN(d.getTime()) ? new Date() : d; +} + +export async function generateSitemaps() { + if (!API_URL) return [{ id: 0 }]; + + try { + const res = await fetch(`${API_URL}/api/v1/blogs?page=1&limit=1`, { + next: { revalidate: 86400 }, + }); + if (!res.ok) return [{ id: 0 }]; + const json = await res.json(); + const total = json?.meta?.totalRecords || 0; + const pageCount = Math.max(1, Math.ceil(total / PER_SITEMAP)); + return Array.from({ length: pageCount }, (_, id) => ({ id })); + } catch { + return [{ id: 0 }]; + } +} + +export default async function sitemap({ id }) { + if (!API_URL) return []; + + try { + const res = await fetch(`${API_URL}/api/v1/blogs?page=${id + 1}&limit=${PER_SITEMAP}`, { + next: { revalidate: 3600 }, + }); + if (!res.ok) return []; + const json = await res.json(); + const blogs = json?.data || []; + return blogs + .filter((b) => b?.slug || b?.uuid) + .map((b) => ({ + url: `${MAIN_DOMAIN}/blog/${b.slug || b.uuid}`, + lastModified: toSafeDate(b.created), + changeFrequency: 'monthly', + priority: 0.6, + })); + } catch { + return []; + } +} diff --git a/app/clinic/sitemap.js b/app/clinic/sitemap.js new file mode 100644 index 0000000..3855b58 --- /dev/null +++ b/app/clinic/sitemap.js @@ -0,0 +1,43 @@ +const API_URL = process.env.NEXT_PUBLIC_API_URL; +const MAIN_DOMAIN = 'https://nobat724.com'; +const PER_SITEMAP = 1000; + +export async function generateSitemaps() { + if (!API_URL) return [{ id: 0 }]; + + try { + const res = await fetch(`${API_URL}/api/v1/clinics?page=1&limit=1`, { + next: { revalidate: 86400 }, + }); + if (!res.ok) return [{ id: 0 }]; + const json = await res.json(); + const total = json?.meta?.totalRecords || 0; + const pageCount = Math.max(1, Math.ceil(total / PER_SITEMAP)); + return Array.from({ length: pageCount }, (_, id) => ({ id })); + } catch { + return [{ id: 0 }]; + } +} + +export default async function sitemap({ id }) { + if (!API_URL) return []; + + try { + const res = await fetch(`${API_URL}/api/v1/clinics?page=${id + 1}&limit=${PER_SITEMAP}`, { + next: { revalidate: 3600 }, + }); + if (!res.ok) return []; + const json = await res.json(); + const clinics = json?.data || []; + return clinics + .filter((c) => c?.uuid) + .map((c) => ({ + url: `${MAIN_DOMAIN}/clinic/${c.uuid}`, + lastModified: new Date(), + changeFrequency: 'weekly', + priority: 0.7, + })); + } catch { + return []; + } +} diff --git a/app/doctor/[slug]/page.js b/app/doctor/[slug]/page.js index cd38586..b082f44 100644 --- a/app/doctor/[slug]/page.js +++ b/app/doctor/[slug]/page.js @@ -20,6 +20,19 @@ const getDoctor = cache(async (slug) => { } }); +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 []; + } +}); + export async function generateMetadata({ params }) { const { slug } = await params; const { matchedCity } = await getStateInfo(); @@ -79,20 +92,60 @@ async function Doctor({ params }) { } catch (error) {} } + const addresses = doctor ? await getDoctorAddresses(doctor.id) : []; + const specialtyNames = doctor?.specialties?.map((s) => s.name).join(" و ") || ""; 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: doctor.img }), - ...(doctor.address && { - address: { - "@type": "PostalAddress", - streetAddress: doctor.address, + ...(doctor.img && { + image: { + "@type": "ImageObject", + url: imageUrl(doctor.img), }, }), + ...(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?.real_name || "بیمار" }, + reviewBody: c.comment, + datePublished: new Date(c.created * 1000).toISOString(), + })), + }), + ...(doctor.social_media && { + sameAs: Object.values(doctor.social_media).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; diff --git a/app/doctor/sitemap.js b/app/doctor/sitemap.js new file mode 100644 index 0000000..3edabf7 --- /dev/null +++ b/app/doctor/sitemap.js @@ -0,0 +1,43 @@ +const API_URL = process.env.NEXT_PUBLIC_API_URL; +const MAIN_DOMAIN = 'https://nobat724.com'; +const PER_SITEMAP = 1000; + +export async function generateSitemaps() { + if (!API_URL) return [{ id: 0 }]; + + try { + const res = await fetch(`${API_URL}/api/v1/doctors?page=1&limit=1`, { + next: { revalidate: 86400 }, + }); + if (!res.ok) return [{ id: 0 }]; + const json = await res.json(); + const total = json?.meta?.totalRecords || 0; + const pageCount = Math.max(1, Math.ceil(total / PER_SITEMAP)); + return Array.from({ length: pageCount }, (_, id) => ({ id })); + } catch { + return [{ id: 0 }]; + } +} + +export default async function sitemap({ id }) { + if (!API_URL) return []; + + try { + const res = await fetch(`${API_URL}/api/v1/doctors?page=${id + 1}&limit=${PER_SITEMAP}`, { + 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: `${MAIN_DOMAIN}/doctor/${d.uuid}`, + lastModified: new Date(), + changeFrequency: 'weekly', + priority: 0.8, + })); + } catch { + return []; + } +} diff --git a/app/robots.js b/app/robots.js index cd1927b..6f2ee90 100644 --- a/app/robots.js +++ b/app/robots.js @@ -37,7 +37,7 @@ export default function robots() { rules: { userAgent: '*', allow: '/', - disallow: '/dashboard', + disallow: ['/dashboard', '/panel'], }, sitemap: `${baseUrl}/sitemap.xml`, }; diff --git a/app/sitemap.js b/app/sitemap.js index 9bf386f..658e5e4 100644 --- a/app/sitemap.js +++ b/app/sitemap.js @@ -1,12 +1,6 @@ import { headers } from 'next/headers'; import { getBaseUrl } from '../utils/sitemap'; -function toSafeDate(value) { - if (!value) return new Date(); - const d = new Date(value); - return isNaN(d.getTime()) ? new Date() : d; -} - const MAIN_DOMAIN = 'nobat724.com'; function getCurrentDomain() { @@ -33,95 +27,11 @@ function getStaticPages(baseUrl) { ]; } -async function getDoctorUrls(baseUrl) { - const API_URL = process.env.NEXT_PUBLIC_API_URL; - if (!API_URL) return []; - - try { - const res = await fetch(`${API_URL}/api/v1/doctors?page=1&limit=500`, { - next: { revalidate: 3600 }, - }); - if (!res.ok) return []; - const data = await res.json(); - const doctors = data?.data || data?.doctors || data || []; - return doctors - .filter((d) => d?.uuid) - .map((d) => ({ - url: `${baseUrl}/doctor/${d.uuid}`, - lastModified: NOW, - changeFrequency: 'weekly', - priority: 0.8, - })); - } catch { - return []; - } -} - -async function getClinicUrls(baseUrl) { - const API_URL = process.env.NEXT_PUBLIC_API_URL; - if (!API_URL) return []; - - try { - const res = await fetch(`${API_URL}/api/v1/clinics?page=1&limit=500`, { - next: { revalidate: 3600 }, - }); - if (!res.ok) return []; - const data = await res.json(); - const clinics = data?.data || data?.clinics || data || []; - return clinics - .filter((c) => c?.uuid) - .map((c) => ({ - url: `${baseUrl}/clinic/${c.uuid}`, - lastModified: NOW, - changeFrequency: 'weekly', - priority: 0.7, - })); - } catch { - return []; - } -} - -async function getBlogUrls(baseUrl) { - const API_URL = process.env.NEXT_PUBLIC_API_URL; - if (!API_URL) return []; - - try { - const res = await fetch(`${API_URL}/api/v1/blogs?page=1&limit=200`, { - next: { revalidate: 3600 }, - }); - if (!res.ok) return []; - const data = await res.json(); - const blogs = data?.blogs || data?.data || data || []; - return blogs - .filter((b) => b?.slug || b?.uuid) - .map((b) => ({ - url: `${baseUrl}/blog/${b.slug || b.uuid}`, - lastModified: toSafeDate(b.created), - changeFrequency: 'monthly', - priority: 0.6, - })); - } catch { - return []; - } -} - 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( - (item, index, arr) => arr.findIndex((i) => i.url === item.url) === index - ); + return getStaticPages(baseUrl); } catch (error) { console.error('Error generating sitemap:', error); return []; diff --git a/components/doctor/detailDoctor/Title.js b/components/doctor/detailDoctor/Title.js index bfe836f..1c4267b 100644 --- a/components/doctor/detailDoctor/Title.js +++ b/components/doctor/detailDoctor/Title.js @@ -5,6 +5,19 @@ import LikeDI from "@/components/icons/LikeDI"; import StarDI from "@/components/icons/StarDI"; import TickCircle from "@/components/icons/TickCircle"; import DoctorAvatar from "@/app/component/DoctorAvatar"; +import Instagram from "@/components/icons/Instagram"; +import Telegram from "@/components/icons/Telegram"; +import Aparat from "@/components/icons/Aparat"; +import Youtube from "@/components/icons/Youtube"; +import Linkedin from "@/components/icons/Linkedin"; + +const SOCIAL_ICONS = { + instagram: Instagram, + telegram: Telegram, + aparat: Aparat, + youtube: Youtube, + linkedin: Linkedin, +}; function Title({ doctor }) { return ( @@ -67,6 +80,26 @@ function Title({ doctor }) { + {doctor?.social_media && Object.values(doctor.social_media).some(Boolean) && ( +
+ {Object.entries(doctor.social_media).map(([key, url]) => { + if (!url) return null; + const Icon = SOCIAL_ICONS[key]; + if (!Icon) return null; + return ( + + + + ); + })} +
+ )} ); diff --git a/components/icons/Aparat.js b/components/icons/Aparat.js new file mode 100644 index 0000000..8cbb1a3 --- /dev/null +++ b/components/icons/Aparat.js @@ -0,0 +1,27 @@ +function Aparat() { + return ( + + + + + ); +} + +export default Aparat; diff --git a/components/icons/Youtube.js b/components/icons/Youtube.js new file mode 100644 index 0000000..5030d21 --- /dev/null +++ b/components/icons/Youtube.js @@ -0,0 +1,27 @@ +function Youtube() { + return ( + + + + + ); +} + +export default Youtube;