feat: add canonical URL handling and entity quality checks

- Implemented canonical URL strategies for city-specific domains and entities.
- Added helper functions for domain and city resolution.
- Created tests for canonical URL generation and domain resolution.
- Introduced entity quality checks for doctors and clinics to ensure meaningful content.
- Developed unique introductory texts for listing pages to avoid duplicate content.
- Established robots.txt policies for listing pages to manage indexing based on user filters.
- Enhanced specialty content with dynamic introductions and FAQs to improve SEO.
This commit is contained in:
hamed
2026-07-19 07:52:50 +03:30
parent 36816eded2
commit fd48b48613
48 changed files with 3951 additions and 733 deletions
-23
View File
@@ -1,23 +0,0 @@
import { Skeleton } from "@mui/material";
export default function Loading() {
return (
<div className="padding-responsive pt-[84px] sm:pt-[110px] md:pt-[140px] lg:pt-[168px]">
<Skeleton variant="rounded" width={300} height={16} />
<Skeleton variant="rounded" width={400} height={28} className="mt-[24px] sm:mt-[29px] md:mt-[35px] lg:mt-[40px]" />
<div className="flex items-center gap-[35px] mt-[16px] sm:mt-[18px] md:mt-[21px] lg:mt-[24px]">
<Skeleton variant="rounded" width={120} height={16} />
<Skeleton variant="rounded" width={140} height={16} />
</div>
<div className="flex flex-col gap-y-[32px] lg:flex-row items-start justify-center gap-[24px] mt-[16px] sm:mt-[19px] md:mt-[21px] lg:mt-[24px]">
<div className="w-full flex flex-col gap-3">
<Skeleton variant="rounded" height={280} className="!rounded-lg" />
<Skeleton variant="rounded" height={20} className="!w-full" />
<Skeleton variant="rounded" height={20} className="!w-full" />
<Skeleton variant="rounded" height={20} width={300} />
</div>
<Skeleton variant="rounded" width={320} height={300} className="!rounded-lg shrink-0" />
</div>
</div>
);
}
+18 -6
View File
@@ -4,7 +4,8 @@ import Layout from "@/components/layout/StLayout";
import { notFound } from "next/navigation";
import { fetchReq } from "@/lib/req";
import { getStateInfo } from "@/lib/getStateInfo";
import { getRequestOrigin } from "@/lib/getCanonicalUrl";
import { getEntityOrigin } from "@/lib/getCanonicalUrl";
import { extractEntityCityId, findCityById } from "@/lib/domainHelpers";
import { safeJsonLd } from "@/lib/sanitize";
import { normalizeBlog, imageUrl } from "@/helper";
@@ -38,11 +39,14 @@ export async function generateMetadata({ params }) {
const description = rawText.slice(0, 160) || blog.title;
const image = blog.image_url ? imageUrl(blog.image_url) : FALLBACK_IMG;
// C1-b — پست شهریافته به دامنهٔ همان شهر canonical می‌شود؛
// پست سراسری (بدون شهر) روی دامنهٔ جاری self-canonical می‌ماند.
const origin = await getEntityOrigin(extractEntityCityId(blog));
return {
title,
description,
// محتوای بلاگ روی همه‌ی دامنه‌های شهری یکسان است — تجمیع اعتبار روی دامنه‌ی اصلی
alternates: { canonical: `https://nobat724.com/blog/${slug}` },
alternates: { canonical: `${origin}/blog/${slug}` },
openGraph: {
title,
description,
@@ -59,18 +63,22 @@ export async function generateMetadata({ params }) {
images: [image],
},
};
} catch {
} catch (error) {
console.error(`[blog/${slug}] generateMetadata failed:`, error);
return {};
}
}
async function Blog({ params }) {
const { slug } = await params;
const origin = await getRequestOrigin();
const blog = normalizeBlog(await getBlog(slug));
if (!blog) notFound();
const blogCityId = extractEntityCityId(blog);
const blogCity = findCityById(blogCityId);
const origin = await getEntityOrigin(blogCityId);
let relatedBlogs = [];
const relatedResponse = await fetchReq(
`${API_URL}/api/v1/blogs?page=1&limit=6`
@@ -90,6 +98,10 @@ async function Blog({ params }) {
datePublished: new Date(blog.created * 1000).toISOString(),
}),
...(blog.author && { author: { "@type": "Person", name: blog.author } }),
// پست شهریافته حوزهٔ جغرافیایی‌اش را اعلام می‌کند؛ پست سراسری این فیلد را ندارد.
...(blogCity && {
spatialCoverage: { "@type": "Place", name: blogCity.name },
}),
}
: null;
@@ -120,7 +132,7 @@ async function Blog({ params }) {
/>
)}
<Layout>
<BlogPage blog={blog} blogs={relatedBlogs} />
<BlogPage blog={blog} blogs={relatedBlogs} cityName={blogCity?.name} />
</Layout>
</>
);