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
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
"use client";
|
||||
|
||||
export default function Error({ error, reset }) {
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col items-center justify-center gap-4 min-h-[60vh] p-8 text-center"
|
||||
dir="rtl"
|
||||
>
|
||||
<p className="text-[#3B3B3B] text-[16px] font-bold">
|
||||
اطلاعات این پزشک در حال حاضر در دسترس نیست.
|
||||
</p>
|
||||
<p className="text-[#616161] text-[14px]">لطفاً دوباره تلاش کنید.</p>
|
||||
<button
|
||||
onClick={() => reset()}
|
||||
className="px-6 py-2 rounded-full bg-[#3B3B3B] text-white text-[14px] font-medium"
|
||||
>
|
||||
تلاش دوباره
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Skeleton } from "@mui/material";
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="p-4 flex flex-col gap-6" dir="rtl">
|
||||
<div className="flex flex-col lg:flex-row items-center gap-4">
|
||||
<Skeleton variant="circular" width={164} height={164} />
|
||||
<div className="flex flex-col gap-3 w-full">
|
||||
<Skeleton variant="rounded" width={200} height={24} />
|
||||
<Skeleton variant="rounded" width={160} height={20} />
|
||||
<Skeleton variant="rounded" width={120} height={20} />
|
||||
</div>
|
||||
</div>
|
||||
<Skeleton variant="rounded" height={300} className="!rounded-lg" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+54
-18
@@ -1,17 +1,32 @@
|
||||
import { cache } from "react";
|
||||
import DoctorPage from "@/components/doctor";
|
||||
import Layout from "@/components/layout/StLayout";
|
||||
import { getStateInfo } from "@/lib/getStateInfo";
|
||||
import { axiosInstance } from "@/lib/req";
|
||||
import { imageUrl } from "@/helper";
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
const FALLBACK_IMG = "https://www.nobat724.com/assets/images/logo.png";
|
||||
|
||||
const getDoctor = cache(async (slug) => {
|
||||
try {
|
||||
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 ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
export async function generateMetadata({ params }) {
|
||||
const { slug } = await params;
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
const { matchedCity } = await getStateInfo();
|
||||
const siteName = matchedCity?.site_name || "نوبت 724";
|
||||
|
||||
try {
|
||||
const res = await axiosInstance.get(`${API_URL}/api/v1/doctor/${slug}`);
|
||||
const doctor = res.data?.data?.data;
|
||||
const doctor = await getDoctor(slug);
|
||||
if (!doctor) return {};
|
||||
|
||||
const specialtyNames = doctor.specialties?.map((s) => s.name).join(" و ") || "";
|
||||
@@ -19,6 +34,7 @@ export async function generateMetadata({ params }) {
|
||||
const description =
|
||||
doctor.detail ||
|
||||
`رزرو نوبت آنلاین دکتر ${doctor.name}${specialtyNames ? " متخصص " + specialtyNames : ""}${doctor.address ? " | " + doctor.address : ""}`.trim();
|
||||
const image = imageUrl(doctor.img) || FALLBACK_IMG;
|
||||
|
||||
return {
|
||||
title,
|
||||
@@ -27,13 +43,13 @@ export async function generateMetadata({ params }) {
|
||||
title,
|
||||
description,
|
||||
type: "profile",
|
||||
images: doctor.img ? [doctor.img] : ["https://www.nobat724.com/assets/images/logo.png"],
|
||||
images: [image],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary",
|
||||
title,
|
||||
description,
|
||||
images: doctor.img ? [doctor.img] : ["https://www.nobat724.com/assets/images/logo.png"],
|
||||
images: [image],
|
||||
},
|
||||
};
|
||||
} catch {
|
||||
@@ -43,23 +59,25 @@ export async function generateMetadata({ params }) {
|
||||
|
||||
async function Doctor({ params }) {
|
||||
const { slug } = await params;
|
||||
let doctor = null;
|
||||
let comments = null;
|
||||
let rateAggregate = { point: 0, satisfaction: 0, averages: [] };
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
try {
|
||||
const resDoctor = await axiosInstance.get(`${API_URL}/api/v1/doctor/${slug}`);
|
||||
doctor = resDoctor.data?.data?.data;
|
||||
if (doctor) {
|
||||
const doctor = await getDoctor(slug);
|
||||
|
||||
if (doctor) {
|
||||
try {
|
||||
const [resComments, resRate] = await Promise.all([
|
||||
axiosInstance.get(`${API_URL}/api/v1/comments/${doctor.uuid}`),
|
||||
axiosInstance.get(`${API_URL}/api/v1/rate/${doctor.uuid}`),
|
||||
fetch(`${API_URL}/api/v1/comments/${doctor.uuid}`, { cache: "no-store" }),
|
||||
fetch(`${API_URL}/api/v1/rate/${doctor.uuid}`, { cache: "no-store" }),
|
||||
]);
|
||||
comments = resComments?.data?.data?.data;
|
||||
rateAggregate = resRate?.data?.data?.data ?? rateAggregate;
|
||||
}
|
||||
} catch (error) {}
|
||||
const [jsonComments, jsonRate] = await Promise.all([
|
||||
resComments.ok ? resComments.json() : null,
|
||||
resRate.ok ? resRate.json() : null,
|
||||
]);
|
||||
comments = jsonComments?.data?.data;
|
||||
rateAggregate = jsonRate?.data?.data ?? rateAggregate;
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
const specialtyNames = doctor?.specialties?.map((s) => s.name).join(" و ") || "";
|
||||
const jsonLd = doctor
|
||||
@@ -78,6 +96,18 @@ async function Doctor({ params }) {
|
||||
}
|
||||
: null;
|
||||
|
||||
const breadcrumbJsonLd = doctor
|
||||
? {
|
||||
"@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}` },
|
||||
],
|
||||
}
|
||||
: null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{jsonLd && (
|
||||
@@ -86,6 +116,12 @@ async function Doctor({ params }) {
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||
/>
|
||||
)}
|
||||
{breadcrumbJsonLd && (
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbJsonLd) }}
|
||||
/>
|
||||
)}
|
||||
<Layout>
|
||||
<DoctorPage
|
||||
doctor={doctor}
|
||||
|
||||
Reference in New Issue
Block a user