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:
@@ -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
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user