diff --git a/.claude/prompt/seo-performance-rendering-audit.md b/.claude/prompt/seo-performance-rendering-audit.md
new file mode 100644
index 0000000..20bc493
--- /dev/null
+++ b/.claude/prompt/seo-performance-rendering-audit.md
@@ -0,0 +1,276 @@
+# بازبینی کامل Rendering Strategy، SEO، Performance و امنیت (Next.js 15)
+
+## پروژه
+
+`nobat724_front`
+
+## زمینه
+
+این سایت عمومی نوبتدهی (App Router، چند-شهری، RTL فارسی) چندین مشکل ساختاری دارد که هم روی SEO و هم روی performance اثر میگذارد:
+
+1. تمام fetchهای server-side با **axios** انجام میشوند (`lib/req.js` → `fetchReq`/`axiosInstance`)، نه `fetch` بومی Next.js. این یعنی **هیچکدام از قابلیتهای `cache`/`revalidate`/`force-cache`/`no-store` Next.js کار نمیکنند** — همهچیز عملاً همیشه SSR بدون cache است، حتی صفحاتی که میتوانند ISR باشند (مثل `/doctor/[slug]`).
+2. `middleware.js` روی همه مسیرها (`matcher: '/((?!api|_next/static|_next/image|favicon.ico).*)'`) اجرا میشود تا فقط یک هدر (`x-pathname`) ست کند — این کار رندر استاتیک را در سطح کل سایت بهصورت اجباری به dynamic تبدیل میکند.
+3. در `app/doctor/[slug]/page.js`، `generateMetadata` و کامپوننت `Doctor()` هر دو مستقل `GET /api/v1/doctor/${slug}` را صدا میزنند — یعنی هر بار بازدید صفحه پزشک، **۲ بار درخواست یکسان** به backend میرود (هیچ dedup با `React.cache()` وجود ندارد چون axios است نه fetch).
+4. هیچ `loading.js`, `error.js`, `template.js` در کل `app/` وجود ندارد — یعنی هیچ Suspense boundary یا error boundary واقعی در سطح route نیست؛ خطاهای fetch با `catch` خاموش میشوند و صفحه با داده `null` رندر میشود.
+5. `og:image`/`twitter:image` در بسیاری صفحات به یک لوگوی استاتیک ثابت (`https://www.nobat724.com/assets/images/logo.png`) فالبک میکنند یا اصلاً ست نمیشوند (`app/doctors/page.js` فقط `title`/`description` در `openGraph` دارد، بدون `images`).
+6. JSON-LD فقط در `doctor/[slug]`، `clinic/[slug]`، `blog/[slug]` هست؛ هیچ `Organization`, `WebSite`, `BreadcrumbList` در سطح global (`layout.js`) وجود ندارد.
+
+## فایلهای مرتبط
+
+| فایل | نقش |
+|------|-----|
+| `lib/req.js` | `fetchReq`/`axiosInstance` — تمام server fetchها از اینجا رد میشوند |
+| `middleware.js` | روی همه مسیرها اجرا میشود، رندر دینامیک سراسری تحمیل میکند |
+| `app/layout.js` | `generateMetadata` ریشه؛ بدون JSON-LD سراسری (Organization/WebSite) |
+| `app/doctors/page.js` | لیست پزشکان؛ SSR کامل، بدون cache/revalidate، بدون `og:images` |
+| `app/doctor/[slug]/page.js` | دو فراخوانی تکراری به همان endpoint؛ JSON-LD ناقص (بدون `@id`, `url`) |
+| `app/clinic/[slug]/page.js`, `app/blog/[slug]/page.js` | الگوی مشابه `doctor/[slug]` — باید با همان منطق بررسی شوند |
+| `app/specialties/page.js`, `app/about-us/page.js`, `app/blogs/page.js`, `app/clinics/page.js` | از `getStateInfo()` برای متادیتا استفاده میکنند؛ محتوای نسبتاً ایستا اما بهصورت SSR رندر میشوند |
+| `app/dashboard/page.js`, `app/panel/(layout)/layout.js` | پنل کاربری احرازشده — اینها باید SSR/CSR بمانند (داده per-user) |
+| `lib/getStateInfo.js` | تشخیص شهر از subdomain — روی هر درخواست header میخواند، نمیتواند cache شود مگر با segment config درست |
+| `app/sitemap.js`, `app/robots.js` | باید بررسی شوند که `revalidate` و فیلتر صفحات DEV_MODE درست تنظیم شده باشد |
+| `app/globals.css`, `mui/index.js` | فونت Vazir، بررسی `next/font` بهجای `@font-face` دستی |
+
+## وضعیت فعلی
+
+### `lib/req.js` — مشکل اصلی caching
+
+```js
+import axios from "axios";
+import https from "https";
+
+export const axiosInstance = axios.create({
+ ...(process.env.NODE_ENV === "development" && {
+ httpsAgent: new https.Agent({ rejectUnauthorized: false }),
+ }),
+});
+
+export const fetchReq = async (url, headers) => {
+ try {
+ const response = await axiosInstance.get(url, headers);
+ return response.data;
+ } catch (error) {
+ console.error("fetchReq error:", error.message);
+ return null;
+ }
+};
+```
+
+### `app/doctor/[slug]/page.js` — دو فراخوانی تکراری
+
+```js
+export async function generateMetadata({ params }) {
+ const { slug } = await params;
+ // ...
+ const res = await axiosInstance.get(`${API_URL}/api/v1/doctor/${slug}`);
+ // ...
+}
+
+async function Doctor({ params }) {
+ const { slug } = await params;
+ // ...
+ const resDoctor = await axiosInstance.get(`${API_URL}/api/v1/doctor/${slug}`); // همان درخواست، دوباره
+ // ...
+}
+```
+
+### `middleware.js` — اجرا روی همه مسیرها
+
+```js
+export function middleware(request) {
+ const response = NextResponse.next();
+ response.headers.set('x-pathname', request.nextUrl.pathname);
+ return response;
+}
+
+export const config = {
+ matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
+};
+```
+
+### `app/doctors/page.js` — بدون og:images، بدون cache
+
+```js
+export async function generateMetadata() {
+ // ...
+ return {
+ title,
+ description,
+ openGraph: { title, description }, // بدون images
+ };
+}
+
+async function Doctors({ searchParams }) {
+ // ...
+ doctors = await fetchReq(`${API_URL}/api/v1/doctors`, { params }); // هر بار fresh، بدون revalidate
+ // ...
+}
+```
+
+## وظایف
+
+### ۱. جایگزینی fetch لایهی axios با `fetch` بومی Next.js (یا wrapper روی آن) برای فراخوانیهای Server Component
+
+برای صفحاتی که قابل ISR هستند (`doctor/[slug]`, `clinic/[slug]`, `blog/[slug]`, `specialties`, `about-us`)، بهجای `axiosInstance.get`/`fetchReq` از `fetch` با `next.revalidate` استفاده کن:
+
+```js
+async function getDoctor(slug) {
+ const res = await fetch(`${API_URL}/api/v1/doctor/${slug}`, {
+ next: { revalidate: 3600, tags: [`doctor-${slug}`] },
+ });
+ if (!res.ok) return null;
+ const json = await res.json();
+ return json?.data?.data;
+}
+```
+
+- برای صفحاتی که داده per-user/per-session دارند (`dashboard`, `panel/*`, `appointment/[doctorId]` در حالت لاگینشده) از `cache: 'no-store'` یا اصلاً تغییری در رویکرد SSR فعلی نده.
+- چون مسیر axios هنوز برای client-side (`services/api.js`/`services/response.js`) لازم است، **این تغییر را فقط در فایلهای Server Component (`app/.../page.js`) اعمال کن** — axios در client services دست نخورد.
+
+### ۲. حذف فراخوانی تکراری در `doctor/[slug]/page.js` (و الگوی مشابه در `clinic/[slug]`, `blog/[slug]`)
+
+از `React.cache()` برای dedup بین `generateMetadata` و کامپوننت صفحه استفاده کن:
+
+```js
+import { cache } from "react";
+
+const getDoctor = cache(async (slug) => {
+ const res = await fetch(`${API_URL}/api/v1/doctor/${slug}`, {
+ next: { revalidate: 3600 },
+ });
+ if (!res.ok) return null;
+ const json = await res.json();
+ return json?.data?.data ?? null;
+});
+
+export async function generateMetadata({ params }) {
+ const { slug } = await params;
+ const doctor = await getDoctor(slug);
+ // ...
+}
+
+async function Doctor({ params }) {
+ const { slug } = await params;
+ const doctor = await getDoctor(slug); // همان نتیجه cacheشده، بدون درخواست دوم
+ // ...
+}
+```
+
+بررسی کن همین الگو در `app/clinic/[slug]/page.js` و `app/blog/[slug]/page.js` هم تکرار شده یا نه و در صورت وجود اصلاح کن.
+
+### ۳. بازبینی `middleware.js` — محدود کردن matcher یا حذف وابستگی غیرضروری
+
+اگر `x-pathname` فقط برای `getCanonicalUrl()` لازم است، بررسی کن آیا میتوان canonical را بدون middleware (مثلاً از `headers()` در خود `generateMetadata` با `request.url` معادل App Router، یا با محاسبه از `params`/segment) ساخت. اگر middleware واقعاً لازم است، **matcher را به مسیرهایی که واقعاً به canonical نیاز دارند محدود کن** (نه همهی سایت):
+
+```js
+export const config = {
+ matcher: [
+ '/doctor/:path*',
+ '/clinic/:path*',
+ '/blog/:path*',
+ '/doctors',
+ '/clinics',
+ '/blogs',
+ '/specialties',
+ ],
+};
+```
+
+مستندسازی کن که این تغییر چه صفحاتی را از حالت force-dynamic خارج میکند.
+
+### ۴. افزودن JSON-LD سراسری در `app/layout.js`
+
+`Organization` و `WebSite` schema را یکبار در ریشه اضافه کن (نه در هر صفحه):
+
+```jsx
+const orgJsonLd = {
+ "@context": "https://schema.org",
+ "@type": "Organization",
+ name: matchedCity?.site_name || "نوبت 724",
+ url: "https://www.nobat724.com",
+ logo: "https://www.nobat724.com/assets/images/logo.png",
+};
+const websiteJsonLd = {
+ "@context": "https://schema.org",
+ "@type": "WebSite",
+ url: "https://www.nobat724.com",
+ potentialAction: {
+ "@type": "SearchAction",
+ target: "https://www.nobat724.com/doctors?search={search_term_string}",
+ "query-input": "required name=search_term_string",
+ },
+};
+```
+
+و `BreadcrumbList` در صفحات تکآیتمی (`doctor/[slug]`, `clinic/[slug]`, `blog/[slug]`) کنار JSON-LD موجود اضافه کن:
+
+```js
+const breadcrumbJsonLd = {
+ "@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/doctors" },
+ { "@type": "ListItem", position: 3, name: `دکتر ${doctor.name}` },
+ ],
+};
+```
+
+### ۵. تکمیل og:image/twitter:image در همه صفحات
+
+- `app/doctors/page.js`: اضافه کن `images: ["https://www.nobat724.com/assets/images/logo.png"]` (یا تصویر مرتبطتر اگر موجود است) به `openGraph` و `twitter`.
+- `app/doctor/[slug]/page.js`: مقدار `doctor.img` را قبل از استفاده در `images` با `imageUrl()` (از `helper/index.js`) absolute کن — همان helper که در کار قبلی avatar استفاده شد — چون ممکن است relative path باشد و در og:image کرول نشود:
+
+```js
+import { imageUrl } from "@/helper";
+// ...
+images: [imageUrl(doctor.img) || "https://www.nobat724.com/assets/images/logo.png"],
+```
+
+- همین بررسی را برای `app/clinic/[slug]/page.js` و `app/blog/[slug]/page.js` انجام بده.
+
+### ۶. اضافه کردن `loading.js` برای مسیرهای دادهمحور
+
+برای `app/doctors/`, `app/doctor/[slug]/`, `app/clinics/`, `app/clinic/[slug]/`, `app/blogs/`, `app/blog/[slug]/` یک `loading.js` با اسکلت متناسب با `CircularLoading`/`TextLoading`/`CustomLoading` موجود در `app/component/loading/` بساز (این کامپوننتها همین الان هم بهصورت دستی در صفحات استفاده میشوند؛ هدف اینجا یک Suspense boundary واقعی در سطح route است، نه تغییر کامپوننتهای فعلی).
+
+### ۷. اضافه کردن `error.js` در سطح root و برای مسیرهای پرتقاضا
+
+یک `app/error.js` (Client Component با `"use client"`) برای گرفتن خطاهای رندر، و یک `error.js` در `app/doctor/[slug]/` برای حالتی که fetch واقعاً fail میکند (بهجای برگرداندن `null` خاموش):
+
+```jsx
+"use client";
+export default function Error({ error, reset }) {
+ return (
+
+
مشکلی پیش آمد. لطفاً دوباره تلاش کنید.
+
+
+ );
+}
+```
+
+### ۸. بررسی `app/sitemap.js` و `app/robots.js`
+
+تأیید کن:
+- `sitemap.js` از همان axios/fetchReq استفاده نمیکند بدون cache (احتمال timeout زیر بار)؛ در صورت لزوم به fetch بومی با `revalidate` بزرگ (مثلاً ۲۴ ساعت) تغییر بده.
+- صفحات `panel/*` و `dashboard` در sitemap نباشند (نیاز auth دارند).
+- `robots.js` در حالت `DEV_MODE=TRUE` همهچیز را drop میکند (طبق `CLAUDE.md` همین الان این رفتار مستند است) — فقط تأیید کن پیادهسازی با مستندات همخوان است.
+
+### ۹. گزارش نهایی بهصورت جدول
+
+برای هر صفحهی زیر جدول را تکمیل کن — این لیست کامل صفحات `app/` پروژه است (تمام موارد را پوشش بده، هیچکدام را رد نکن):
+
+`/`, `/about-us`, `/contact-us`, `/specialties`, `/blogs`, `/blog/[slug]`, `/clinics`, `/clinic/[slug]`, `/doctors`, `/doctor/[slug]`, `/appointment/[doctorId]`, `/login`, `/login-verify`, `/dashboard`, `/panel/add-doctor`, `/panel/dashboard` (route group), `/panel/turns`, `/panel/user-account`, `/payment/[uuid]`, `/payment/result`
+
+| Page | Current Strategy | Recommended Strategy | Reason | SEO Impact | Performance Impact |
+|------|------------------|----------------------|--------|-------------|---------------------|
+
+## نکات مهم
+
+- **هیچ تغییری در `services/api.js`/`services/response.js` (مسیر axios سمت کلاینت) ندهی** — فقط فراخوانیهای Server Component (`app/.../page.js`) که با `axiosInstance`/`fetchReq` کار میکنند هدف این پرامپت هستند.
+- صفحات `panel/*` و `dashboard` چون نیاز به session/JWT کاربر دارند و داده per-user است، **باید SSR/dynamic بمانند** — این صفحات را به ISR/SSG تبدیل نکن؛ فقط در گزارش جدول توضیح بده چرا.
+- `getStateInfo()` به `host` header وابسته است (تشخیص subdomain چندشهری) — این یعنی صفحاتی که از آن استفاده میکنند (`generateMetadata` همه صفحات public) را نمیتوان بهطور کامل static کرد مگر با `generateStaticParams` محدود به دامنههای شناختهشده در `data/city.json`؛ اگر چنین تغییری پیشنهاد میشود، توضیح بده trade-off چندشهری بودن چیست.
+- پس از هر تغییر در `app/.../page.js`، طبق قانون پروژه (`CLAUDE.md`): «همیشه `await params`» را رعایت کن — این الگو همین الان در همه فایلها هست، نشکن.
+- بعد از تغییرات، حتماً `npm run build` را اجرا کن و خروجی Route را بررسی کن — ستون `Size`/`First Load JS` باید تغییر معنادار (کاهش یا حداقل عدم افزایش) داشته باشد.
+- `npm run lint` در این پروژه به دلیل عدم migrate شدن از `next lint` به ESLint CLI، interactive میپرسد و کار نمیکند (مشکل از قبل موجود، نه نتیجه این تغییرات) — برای validation فقط به `npm run build` تکیه کن.
+- تمام متنهای جدید (پیام خطا، loading text و غیره) باید فارسی و RTL باشند، مطابق بقیهی پروژه.
diff --git a/app/about-us/page.js b/app/about-us/page.js
index 89fa48a..763e2d8 100644
--- a/app/about-us/page.js
+++ b/app/about-us/page.js
@@ -7,10 +7,12 @@ export async function generateMetadata() {
const siteName = matchedCity?.site_name || "نوبت 724";
const title = `درباره ما | ${siteName}`;
const description = `آشنایی با ${siteName}، سیستم آنلاین نوبتدهی پزشکی. هدف ما ارائه خدمات سریع و کارآمد رزرو نوبت پزشکی برای همه مردم است.`;
+ const image = "https://www.nobat724.com/assets/images/logo.png";
return {
title,
description,
- openGraph: { title, description },
+ openGraph: { title, description, images: [image] },
+ twitter: { card: "summary_large_image", title, description, images: [image] },
};
}
diff --git a/app/blog/[slug]/loading.js b/app/blog/[slug]/loading.js
new file mode 100644
index 0000000..0372100
--- /dev/null
+++ b/app/blog/[slug]/loading.js
@@ -0,0 +1,13 @@
+import { Skeleton } from "@mui/material";
+
+export default function Loading() {
+ return (
+