Files
nobat724_front/.claude/prompt/seo-performance-rendering-audit.md
T
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

16 KiB
Raw Blame History

بازبینی کامل Rendering Strategy، SEO، Performance و امنیت (Next.js 15)

پروژه

nobat724_front

زمینه

این سایت عمومی نوبت‌دهی (App Router، چند-شهری، RTL فارسی) چندین مشکل ساختاری دارد که هم روی SEO و هم روی performance اثر می‌گذارد:

  1. تمام fetchهای server-side با axios انجام می‌شوند (lib/req.jsfetchReq/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

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 — دو فراخوانی تکراری

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 — اجرا روی همه مسیرها

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

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 استفاده کن:

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 و کامپوننت صفحه استفاده کن:

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 نیاز دارند محدود کن (نه همه‌ی سایت):

export const config = {
  matcher: [
    '/doctor/:path*',
    '/clinic/:path*',
    '/blog/:path*',
    '/doctors',
    '/clinics',
    '/blogs',
    '/specialties',
  ],
};

مستندسازی کن که این تغییر چه صفحاتی را از حالت force-dynamic خارج می‌کند.

۴. افزودن JSON-LD سراسری در app/layout.js

Organization و WebSite schema را یک‌بار در ریشه اضافه کن (نه در هر صفحه):

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 موجود اضافه کن:

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 کرول نشود:
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 خاموش):

"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 باشند، مطابق بقیه‌ی پروژه.