Add structured data for search engines
This commit is contained in:
+184
-56
@@ -55,6 +55,14 @@ export async function generateMetadata({ params }) {
|
|||||||
type: "article",
|
type: "article",
|
||||||
publishedTime: post.frontmatter.date,
|
publishedTime: post.frontmatter.date,
|
||||||
authors: [post.frontmatter.author],
|
authors: [post.frontmatter.author],
|
||||||
|
url: `https://communityrule.com/blog/${slug}`,
|
||||||
|
siteName: "CommunityRule",
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
title: post.frontmatter.title,
|
||||||
|
description: post.frontmatter.description,
|
||||||
|
creator: "@communityrule",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -79,66 +87,186 @@ export default async function BlogPostPage({ params }) {
|
|||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get related articles (for now, just get other posts)
|
// Get related articles with improved algorithm
|
||||||
const allPosts = getAllPosts();
|
const allPosts = getAllPosts();
|
||||||
const relatedArticles = allPosts; // Pass all posts to RelatedArticles component for filtering
|
|
||||||
|
// Simple related articles algorithm based on content similarity
|
||||||
|
const getRelatedArticles = (currentPost, allPosts, limit = 3) => {
|
||||||
|
const otherPosts = allPosts.filter((p) => p.slug !== currentPost.slug);
|
||||||
|
|
||||||
|
// Score posts based on content similarity
|
||||||
|
const scoredPosts = otherPosts.map((post) => {
|
||||||
|
let score = 0;
|
||||||
|
|
||||||
|
// Check for similar keywords in title and description
|
||||||
|
const currentTitle = currentPost.frontmatter.title.toLowerCase();
|
||||||
|
const currentDesc = currentPost.frontmatter.description.toLowerCase();
|
||||||
|
const postTitle = post.frontmatter.title.toLowerCase();
|
||||||
|
const postDesc = post.frontmatter.description.toLowerCase();
|
||||||
|
|
||||||
|
// Common keywords that indicate similarity
|
||||||
|
const keywords = [
|
||||||
|
"community",
|
||||||
|
"conflict",
|
||||||
|
"decision",
|
||||||
|
"governance",
|
||||||
|
"security",
|
||||||
|
"trust",
|
||||||
|
"collaboration",
|
||||||
|
"organization",
|
||||||
|
];
|
||||||
|
|
||||||
|
keywords.forEach((keyword) => {
|
||||||
|
if (currentTitle.includes(keyword) && postTitle.includes(keyword))
|
||||||
|
score += 3;
|
||||||
|
if (currentDesc.includes(keyword) && postDesc.includes(keyword))
|
||||||
|
score += 2;
|
||||||
|
if (currentTitle.includes(keyword) && postDesc.includes(keyword))
|
||||||
|
score += 1;
|
||||||
|
if (currentDesc.includes(keyword) && postTitle.includes(keyword))
|
||||||
|
score += 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
return { ...post, score };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort by score and return top posts
|
||||||
|
return scoredPosts
|
||||||
|
.sort((a, b) => b.score - a.score)
|
||||||
|
.slice(0, limit)
|
||||||
|
.map(({ score, ...post }) => post); // Remove score from final result
|
||||||
|
};
|
||||||
|
|
||||||
|
const relatedArticles = getRelatedArticles(post, allPosts);
|
||||||
|
|
||||||
|
// Generate structured data for search engines
|
||||||
|
const structuredData = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "Article",
|
||||||
|
headline: post.frontmatter.title,
|
||||||
|
description: post.frontmatter.description,
|
||||||
|
author: {
|
||||||
|
"@type": "Person",
|
||||||
|
name: post.frontmatter.author,
|
||||||
|
},
|
||||||
|
publisher: {
|
||||||
|
"@type": "Organization",
|
||||||
|
name: "CommunityRule",
|
||||||
|
url: "https://communityrule.com",
|
||||||
|
logo: {
|
||||||
|
"@type": "ImageObject",
|
||||||
|
url: "https://communityrule.com/assets/Logo.svg",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
datePublished: post.frontmatter.date,
|
||||||
|
dateModified: post.frontmatter.date,
|
||||||
|
mainEntityOfPage: {
|
||||||
|
"@type": "WebPage",
|
||||||
|
"@id": `https://communityrule.com/blog/${post.slug}`,
|
||||||
|
},
|
||||||
|
url: `https://communityrule.com/blog/${post.slug}`,
|
||||||
|
articleSection: "Community Building",
|
||||||
|
keywords: ["community", "governance", "decision making", "collaboration"],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Breadcrumb structured data
|
||||||
|
const breadcrumbData = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "BreadcrumbList",
|
||||||
|
itemListElement: [
|
||||||
|
{
|
||||||
|
"@type": "ListItem",
|
||||||
|
position: 1,
|
||||||
|
name: "Home",
|
||||||
|
item: "https://communityrule.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "ListItem",
|
||||||
|
position: 2,
|
||||||
|
name: "Blog",
|
||||||
|
item: "https://communityrule.com/blog",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "ListItem",
|
||||||
|
position: 3,
|
||||||
|
name: post.frontmatter.title,
|
||||||
|
item: `https://communityrule.com/blog/${post.slug}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-[#F4F3F1] relative overflow-hidden">
|
<>
|
||||||
{/* Content Banner */}
|
{/* Structured Data */}
|
||||||
<ContentBanner post={post} />
|
<script
|
||||||
|
type="application/ld+json"
|
||||||
{/* Decorative Shapes */}
|
dangerouslySetInnerHTML={{
|
||||||
{/* Right Side Shape (3/4 up the page) */}
|
__html: JSON.stringify(structuredData),
|
||||||
<div
|
}}
|
||||||
className="hidden md:block absolute top-1/4 right-0 pointer-events-none z-10"
|
/>
|
||||||
style={{ transform: "translateX(40%)" }}
|
<script
|
||||||
>
|
type="application/ld+json"
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
dangerouslySetInnerHTML={{
|
||||||
<img
|
__html: JSON.stringify(breadcrumbData),
|
||||||
src={getAssetPath(ASSETS.CONTENT_SHAPE_1)}
|
}}
|
||||||
alt=""
|
|
||||||
className="w-auto h-auto max-w-none"
|
|
||||||
style={{
|
|
||||||
width: "clamp(120px, 15vw, 200px)",
|
|
||||||
height: "auto",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Left Side Shape (3/4 down the page) */}
|
|
||||||
<div
|
|
||||||
className="hidden md:block absolute top-1/2 left-0 pointer-events-none z-10"
|
|
||||||
style={{ transform: "translateX(-10%)" }}
|
|
||||||
>
|
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
||||||
<img
|
|
||||||
src={getAssetPath(ASSETS.CONTENT_SHAPE_2)}
|
|
||||||
alt=""
|
|
||||||
className="w-auto h-auto max-w-none"
|
|
||||||
style={{
|
|
||||||
width: "clamp(100px, 12vw, 180px)",
|
|
||||||
height: "auto",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Main Content */}
|
|
||||||
<article className="p-[var(--spacing-scale-024)] sm:py-[var(--spacing-scale-032)]">
|
|
||||||
{/* Article Content */}
|
|
||||||
<div className="post-body -mt-[var(--spacing-scale-048)] text-[var(--color-content-inverse-primary)] text-[16px] leading-[24px] sm:text-[18px] sm:leading-[130%] lg:text-[24px] lg:leading-[32px] xl:text-[32px] xl:leading-[40px] sm:mx-auto sm:max-w-[390px] md:max-w-[472px] lg:max-w-[700px] xl:max-w-[904px]">
|
|
||||||
<div dangerouslySetInnerHTML={{ __html: post.htmlContent }} />
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
{/* Related Articles Section */}
|
|
||||||
<RelatedArticles
|
|
||||||
relatedPosts={relatedArticles}
|
|
||||||
currentPostSlug={post.slug}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Ask Organizer Section */}
|
<div className="min-h-screen bg-[#F4F3F1] relative overflow-hidden">
|
||||||
<AskOrganizer {...askOrganizerData} variant="inverse" />
|
{/* Content Banner */}
|
||||||
</div>
|
<ContentBanner post={post} />
|
||||||
|
|
||||||
|
{/* Decorative Shapes */}
|
||||||
|
{/* Right Side Shape (3/4 up the page) */}
|
||||||
|
<div
|
||||||
|
className="hidden md:block absolute top-1/4 right-0 pointer-events-none z-10"
|
||||||
|
style={{ transform: "translateX(40%)" }}
|
||||||
|
>
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
|
<img
|
||||||
|
src={getAssetPath(ASSETS.CONTENT_SHAPE_1)}
|
||||||
|
alt=""
|
||||||
|
className="w-auto h-auto max-w-none"
|
||||||
|
style={{
|
||||||
|
width: "clamp(120px, 15vw, 200px)",
|
||||||
|
height: "auto",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Left Side Shape (3/4 down the page) */}
|
||||||
|
<div
|
||||||
|
className="hidden md:block absolute top-1/2 left-0 pointer-events-none z-10"
|
||||||
|
style={{ transform: "translateX(-10%)" }}
|
||||||
|
>
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
|
<img
|
||||||
|
src={getAssetPath(ASSETS.CONTENT_SHAPE_2)}
|
||||||
|
alt=""
|
||||||
|
className="w-auto h-auto max-w-none"
|
||||||
|
style={{
|
||||||
|
width: "clamp(100px, 12vw, 180px)",
|
||||||
|
height: "auto",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<article className="p-[var(--spacing-scale-024)] sm:py-[var(--spacing-scale-032)]">
|
||||||
|
{/* Article Content */}
|
||||||
|
<div className="post-body -mt-[var(--spacing-scale-048)] text-[var(--color-content-inverse-primary)] text-[16px] leading-[24px] sm:text-[18px] sm:leading-[130%] lg:text-[24px] lg:leading-[32px] xl:text-[32px] xl:leading-[40px] sm:mx-auto sm:max-w-[390px] md:max-w-[472px] lg:max-w-[700px] xl:max-w-[904px]">
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: post.htmlContent }} />
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
{/* Related Articles Section */}
|
||||||
|
<RelatedArticles
|
||||||
|
relatedPosts={relatedArticles}
|
||||||
|
currentPostSlug={post.slug}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Ask Organizer Section */}
|
||||||
|
<AskOrganizer {...askOrganizerData} variant="inverse" />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user