feat(metadata): enhance SEO fields for blog and blogs pages
This commit is contained in:
+61
-7
@@ -37,25 +37,43 @@ export async function generateMetadata({ params }) {
|
|||||||
const blogCity = findCityById(extractEntityCityId(blog));
|
const blogCity = findCityById(extractEntityCityId(blog));
|
||||||
const brand = blogCity?.site_name || siteName;
|
const brand = blogCity?.site_name || siteName;
|
||||||
|
|
||||||
const title = `${blog.title} | ${brand}`;
|
// meta_title از فیلد اختصاصی SEO (اگر پایپلاین/ادمین ست کرده باشد) وگرنه عنوان.
|
||||||
|
const title = `${blog.meta_title || blog.title} | ${brand}`;
|
||||||
const rawText = (blog.summary || blog.body || "")
|
const rawText = (blog.summary || blog.body || "")
|
||||||
.replace(/<[^>]*>/g, "")
|
.replace(/<[^>]*>/g, "")
|
||||||
.trim();
|
.trim();
|
||||||
const baseDescription = rawText.slice(0, 160) || blog.title;
|
const baseDescription = rawText.slice(0, 160) || blog.title;
|
||||||
// نام شهر فقط برای پست شهریافته؛ پست سراسری هیچ شهری نمیگیرد.
|
// فیلد meta_description اختصاصی مقدم است؛ وگرنه از خلاصه/متن استخراج میشود.
|
||||||
const description = blogCity
|
const description =
|
||||||
? `${baseDescription}`.slice(0, 150) + ` | ${blogCity.name}`
|
blog.meta_description ||
|
||||||
: baseDescription;
|
(blogCity
|
||||||
const image = blog.image_url ? imageUrl(blog.image_url) : FALLBACK_IMG;
|
? `${baseDescription}`.slice(0, 150) + ` | ${blogCity.name}`
|
||||||
|
: baseDescription);
|
||||||
|
// کلیدواژهها از فیلدهای SEO (اصلی + فرعی) + تگها.
|
||||||
|
const keywords = [
|
||||||
|
blog.primary_keyword,
|
||||||
|
...(Array.isArray(blog.secondary_keywords) ? blog.secondary_keywords : []),
|
||||||
|
...(Array.isArray(blog.tags)
|
||||||
|
? blog.tags.map((t) => (typeof t === "string" ? t : t?.name))
|
||||||
|
: []),
|
||||||
|
].filter(Boolean);
|
||||||
|
const image = blog.og_image
|
||||||
|
? imageUrl(blog.og_image)
|
||||||
|
: blog.image_url
|
||||||
|
? imageUrl(blog.image_url)
|
||||||
|
: FALLBACK_IMG;
|
||||||
|
|
||||||
// C1-b — پست شهریافته به دامنهٔ همان شهر canonical میشود؛
|
// C1-b — پست شهریافته به دامنهٔ همان شهر canonical میشود؛
|
||||||
// پست سراسری (بدون شهر) روی دامنهٔ جاری self-canonical میماند.
|
// پست سراسری (بدون شهر) روی دامنهٔ جاری self-canonical میماند.
|
||||||
|
// فیلد canonical_url اختصاصی (مثلاً نسخهٔ شهریِ پایپلاین) مقدم است.
|
||||||
const origin = await getEntityOrigin(extractEntityCityId(blog));
|
const origin = await getEntityOrigin(extractEntityCityId(blog));
|
||||||
|
const canonical = blog.canonical_url || `${origin}/blog/${slug}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
alternates: { canonical: `${origin}/blog/${slug}` },
|
...(keywords.length && { keywords }),
|
||||||
|
alternates: { canonical },
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
@@ -63,6 +81,9 @@ export async function generateMetadata({ params }) {
|
|||||||
...(blog.created_at && {
|
...(blog.created_at && {
|
||||||
publishedTime: new Date(blog.created_at * 1000).toISOString(),
|
publishedTime: new Date(blog.created_at * 1000).toISOString(),
|
||||||
}),
|
}),
|
||||||
|
...(blog.updated_at && {
|
||||||
|
modifiedTime: new Date(blog.updated_at * 1000).toISOString(),
|
||||||
|
}),
|
||||||
images: [image],
|
images: [image],
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
@@ -97,16 +118,27 @@ async function Blog({ params }) {
|
|||||||
.slice(0, 4)
|
.slice(0, 4)
|
||||||
.map(normalizeBlog);
|
.map(normalizeBlog);
|
||||||
|
|
||||||
|
const brand = blogCity?.site_name || "نوبت 724";
|
||||||
|
const articleDescription =
|
||||||
|
blog?.meta_description ||
|
||||||
|
(blog?.body?.value || blog?.summary || "").replace(/<[^>]*>/g, "").trim().slice(0, 160);
|
||||||
|
|
||||||
const jsonLd = blog
|
const jsonLd = blog
|
||||||
? {
|
? {
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
"@type": "Article",
|
"@type": "Article",
|
||||||
headline: blog.title,
|
headline: blog.title,
|
||||||
|
...(articleDescription && { description: articleDescription }),
|
||||||
...(blog.images?.[0]?.url && { image: imageUrl(blog.images[0].url) }),
|
...(blog.images?.[0]?.url && { image: imageUrl(blog.images[0].url) }),
|
||||||
|
mainEntityOfPage: { "@type": "WebPage", "@id": `${origin}/blog/${slug}` },
|
||||||
...(blog.created && {
|
...(blog.created && {
|
||||||
datePublished: new Date(blog.created * 1000).toISOString(),
|
datePublished: new Date(blog.created * 1000).toISOString(),
|
||||||
}),
|
}),
|
||||||
|
...(blog.updated_at && {
|
||||||
|
dateModified: new Date(blog.updated_at * 1000).toISOString(),
|
||||||
|
}),
|
||||||
...(blog.author && { author: { "@type": "Person", name: blog.author } }),
|
...(blog.author && { author: { "@type": "Person", name: blog.author } }),
|
||||||
|
publisher: { "@type": "Organization", name: brand },
|
||||||
// پست شهریافته حوزهٔ جغرافیاییاش را اعلام میکند؛ پست سراسری این فیلد را ندارد.
|
// پست شهریافته حوزهٔ جغرافیاییاش را اعلام میکند؛ پست سراسری این فیلد را ندارد.
|
||||||
...(blogCity && {
|
...(blogCity && {
|
||||||
spatialCoverage: { "@type": "Place", name: blogCity.name },
|
spatialCoverage: { "@type": "Place", name: blogCity.name },
|
||||||
@@ -114,6 +146,22 @@ async function Blog({ params }) {
|
|||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
// FAQPage از فیلد faq (سؤال/جواب) — فقط اگر مقاله FAQ داشته باشد.
|
||||||
|
const faqItems = Array.isArray(blog?.faq)
|
||||||
|
? blog.faq.filter((f) => f?.q && f?.a)
|
||||||
|
: [];
|
||||||
|
const faqJsonLd = faqItems.length
|
||||||
|
? {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "FAQPage",
|
||||||
|
mainEntity: faqItems.map((f) => ({
|
||||||
|
"@type": "Question",
|
||||||
|
name: f.q,
|
||||||
|
acceptedAnswer: { "@type": "Answer", text: f.a },
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
|
||||||
const breadcrumbJsonLd = blog
|
const breadcrumbJsonLd = blog
|
||||||
? {
|
? {
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
@@ -140,6 +188,12 @@ async function Blog({ params }) {
|
|||||||
dangerouslySetInnerHTML={{ __html: safeJsonLd(breadcrumbJsonLd) }}
|
dangerouslySetInnerHTML={{ __html: safeJsonLd(breadcrumbJsonLd) }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{faqJsonLd && (
|
||||||
|
<script
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: safeJsonLd(faqJsonLd) }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Layout>
|
<Layout>
|
||||||
<BlogPage blog={blog} blogs={relatedBlogs} cityName={blogCity?.name} />
|
<BlogPage blog={blog} blogs={relatedBlogs} cityName={blogCity?.name} />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
@@ -8,9 +8,17 @@ export async function generateMetadata() {
|
|||||||
const title = `مقالات و اخبار پزشکی | ${siteName}`;
|
const title = `مقالات و اخبار پزشکی | ${siteName}`;
|
||||||
const description = `جدیدترین مقالات، اخبار و راهنماهای پزشکی. اطلاعات تخصصی در حوزه سلامت و پزشکی از متخصصان ${siteName}.`;
|
const description = `جدیدترین مقالات، اخبار و راهنماهای پزشکی. اطلاعات تخصصی در حوزه سلامت و پزشکی از متخصصان ${siteName}.`;
|
||||||
const image = "/assets/images/og-image.png";
|
const image = "/assets/images/og-image.png";
|
||||||
|
// کلیدواژههای شهر از city.json (رشته یا آرایه).
|
||||||
|
const rawKeywords = matchedCity?.keywords;
|
||||||
|
const keywords = Array.isArray(rawKeywords)
|
||||||
|
? rawKeywords
|
||||||
|
: typeof rawKeywords === "string" && rawKeywords.trim()
|
||||||
|
? rawKeywords.split(",").map((k) => k.trim()).filter(Boolean)
|
||||||
|
: undefined;
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
|
...(keywords?.length && { keywords }),
|
||||||
// C1-a — لیست بلاگ per-domain است (پستهای همان شهر + سراسری)؛
|
// C1-a — لیست بلاگ per-domain است (پستهای همان شهر + سراسری)؛
|
||||||
// canonical لایهٔ layout (self روی همان دامنه) درست است.
|
// canonical لایهٔ layout (self روی همان دامنه) درست است.
|
||||||
openGraph: { title, description, images: [image] },
|
openGraph: { title, description, images: [image] },
|
||||||
|
|||||||
Reference in New Issue
Block a user