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
+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>
</>
);