From a8c3a57114ecbbfd61c85e74294dbbc55a60a5e6 Mon Sep 17 00:00:00 2001
From: hamed <15238-genius.ha@users.noreply.drupalcode.org>
Date: Sun, 21 Jun 2026 12:35:15 +0330
Subject: [PATCH] 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
---
.../doctor-seo-schema-knowledge-panel.md | 258 ++++++++++++++++++
app/blog/sitemap.js | 49 ++++
app/clinic/sitemap.js | 43 +++
app/doctor/[slug]/page.js | 63 ++++-
app/doctor/sitemap.js | 43 +++
app/robots.js | 2 +-
app/sitemap.js | 92 +------
components/doctor/detailDoctor/Title.js | 33 +++
components/icons/Aparat.js | 27 ++
components/icons/Youtube.js | 27 ++
10 files changed, 540 insertions(+), 97 deletions(-)
create mode 100644 .claude/prompt/doctor-seo-schema-knowledge-panel.md
create mode 100644 app/blog/sitemap.js
create mode 100644 app/clinic/sitemap.js
create mode 100644 app/doctor/sitemap.js
create mode 100644 components/icons/Aparat.js
create mode 100644 components/icons/Youtube.js
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;