Files
community-rule/app/(marketing)/blog/[slug]/page.tsx
T
2026-05-20 23:01:55 -06:00

273 lines
7.7 KiB
TypeScript

import { notFound } from "next/navigation";
import type { Metadata } from "next";
import dynamic from "next/dynamic";
import type { BlogPost } from "../../../../lib/content";
import {
getBlogPostBySlug,
getAllBlogPosts as getAllPosts,
getRelatedBlogPosts,
} from "../../../../lib/content";
import { logger } from "../../../../lib/logger";
import ContentBanner from "../../../components/sections/ContentBanner";
import AskOrganizer from "../../../components/sections/AskOrganizer";
import { getAssetPath, ASSETS } from "../../../../lib/assetUtils";
import "../blog.css";
// Code split RelatedArticles - blog-specific, below the fold
const RelatedArticles = dynamic(
() => import("../../../components/sections/RelatedArticles"),
{
loading: () => (
<section className="py-[var(--spacing-scale-032)] min-h-[400px]" />
),
ssr: true,
},
);
// AskOrganizer data - same as index page
const askOrganizerData = {
title: "Still have questions?",
subtitle: "Get answers from an experienced organizer",
buttonText: "Ask an organizer",
};
interface PageProps {
params: Promise<{ slug: string }>;
}
/**
* Generate static params for all blog posts
* This enables static generation for all blog posts at build time
*/
export async function generateStaticParams() {
try {
const posts = getAllPosts();
return posts.map((post) => ({
slug: post.slug,
}));
} catch (error) {
logger.error("Error generating static params:", error);
return [];
}
}
/**
* Generate metadata for each blog post
*/
export async function generateMetadata({
params,
}: PageProps): Promise<Metadata> {
try {
const { slug } = await params;
const post = getBlogPostBySlug(slug);
if (!post) {
return {
title: "Post Not Found",
description: "The requested blog post could not be found.",
};
}
return {
title: post.frontmatter.title,
description: post.frontmatter.description,
authors: [{ name: post.frontmatter.author }],
openGraph: {
title: post.frontmatter.title,
description: post.frontmatter.description,
type: "article",
publishedTime: post.frontmatter.date,
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) {
logger.error("Error generating metadata:", error);
return {
title: "Blog Post",
description: "A blog post from our community.",
};
}
}
/**
* Dynamic blog post page
*/
export default async function BlogPostPage({ params }: PageProps) {
// Get the blog post data
const { slug } = await params;
const post = getBlogPostBySlug(slug);
// If post doesn't exist, show 404
if (!post) {
notFound();
}
// Get related articles with improved algorithm
const allPosts = getAllPosts();
const slugOrder = allPosts.map((post) => post.slug);
const relatedArticles = getRelatedBlogPosts(
post.slug,
post.frontmatter.related,
3,
);
// 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/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}`,
},
],
};
// Get article-specific background color from frontmatter
const getBackgroundColor = (post: BlogPost): string => {
if (post.frontmatter?.background?.color) {
return post.frontmatter.background.color;
}
return "#1F2937"; // Default fallback (dark gray)
};
const backgroundColor = getBackgroundColor(post);
return (
<>
{/* Structured Data */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(structuredData),
}}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(breadcrumbData),
}}
/>
<div
className="relative min-h-screen overflow-x-clip"
style={{ backgroundColor }}
>
{/* Content Banner */}
<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 — Figma Content page Template (19003:23305) article body instances */}
<article
data-node-id="19031:10426"
className="
relative z-[2] flex w-full justify-center
p-[var(--spacing-scale-024)]
sm:px-0 sm:py-[var(--spacing-scale-032)]
"
>
<div className="post-body w-full 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: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}
slugOrder={slugOrder}
/>
{/* Ask Organizer Section */}
<AskOrganizer {...askOrganizerData} variant="inverse" />
</div>
</>
);
}