Files
nobat724_front/.claude/prompt/seo-performance-rendering-audit.md
hamed 06a877a725 feat(metadata): enhance SEO by adding Open Graph and Twitter metadata across various pages
feat(blog): implement loading and error handling components for blog pages

feat(clinic): add loading and error handling components for clinic pages

feat(doctor): create loading and error handling components for doctor pages

feat(clinics): add loading component for clinics page

feat(specialties): improve metadata for specialties page with Open Graph and Twitter images

feat(layout): add structured data for Organization and WebSite in layout

fix(middleware): restrict middleware execution to specific routes to improve performance

chore(audit): add comprehensive SEO and performance audit documentation
2026-06-21 11:38:39 +03:30

277 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# بازبینی کامل 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 (
<div className="p-8 text-center" dir="rtl">
<p>مشکلی پیش آمد. لطفاً دوباره تلاش کنید.</p>
<button onClick={() => reset()}>تلاش دوباره</button>
</div>
);
}
```
### ۸. بررسی `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 باشند، مطابق بقیه‌ی پروژه.