AllAi/apps/web/app/[locale]/blog/[slug]/page.tsx
2025-11-14 21:54:04 +03:00

139 lines
4.1 KiB
TypeScript

import { notFound } from "next/navigation";
import type { Metadata } from "next";
import { loadDictionary } from "@allai/i18n/server";
import { locales, resolveLocale } from "@/config/i18n";
import { blogPosts } from "@/features/marketing/blogData";
import { absoluteUrl, buildCanonical, buildLocaleAlternates, buildOpenGraph, buildTwitterCard } from "@/seo/seoUtils";
const PATH_PREFIX = "/blog" as const;
type PageProps = {
params: { locale: string; slug: string };
};
export function generateStaticParams() {
return locales.flatMap((locale) => blogPosts.map((post) => ({ locale, slug: post.slug })));
}
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const locale = resolveLocale(params.locale);
if (!locales.includes(locale)) {
return {
title: "Article not found"
};
}
const dictionary = await loadDictionary(locale);
const post = blogPosts.find((item) => item.slug === params.slug);
if (!post) {
return {
title: `Article not found | ${dictionary.common.brandLong ?? dictionary.common.brandShort}`
};
}
const title = `${post.title} | ${dictionary.common.brandLong ?? dictionary.common.brandShort}`;
const description = post.excerpt;
const path = `${PATH_PREFIX}/${post.slug}`;
return {
title,
description,
alternates: {
canonical: buildCanonical(locale, path),
languages: buildLocaleAlternates(path)
},
openGraph: {
...buildOpenGraph({
locale,
title,
description,
path,
type: "article"
}),
publishedTime: post.publishedAt,
section: post.category
},
twitter: buildTwitterCard({
title,
description
})
};
}
export default async function BlogArticlePage({ params }: PageProps) {
const locale = resolveLocale(params.locale);
if (!locales.includes(locale)) {
notFound();
}
const dictionary = await loadDictionary(locale);
const post = blogPosts.find((item) => item.slug === params.slug);
if (!post) {
notFound();
}
const canonical = buildCanonical(locale, `${PATH_PREFIX}/${post.slug}`);
const publisher = {
"@type": "Organization",
name: dictionary.common.brandLong ?? dictionary.common.brandShort,
logo: {
"@type": "ImageObject",
url: absoluteUrl("/favicon.ico")
}
};
const structuredData = {
"@context": "https://schema.org",
"@type": "BlogPosting",
headline: post.title,
description: post.excerpt,
author: publisher,
publisher,
datePublished: post.publishedAt,
dateModified: post.publishedAt,
articleSection: post.category,
mainEntityOfPage: canonical,
url: canonical,
inLanguage: locale,
wordCount: post.content.replace(/<[^>]+>/g, " ").trim().split(/\s+/).length
};
return (
<main style={{ maxWidth: 960, margin: "0 auto", padding: "72px 24px", display: "grid", gap: 32 }}>
<script
type="application/ld+json"
suppressHydrationWarning
dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }}
/>
<header style={{ display: "grid", gap: 12 }}>
<span style={{ fontSize: 14, textTransform: "uppercase", color: "var(--color-muted)", fontWeight: 600 }}>
{post.category}
</span>
<h1 style={{ fontSize: 46, fontWeight: 700, margin: 0 }}>{post.title}</h1>
<div style={{ display: "flex", gap: 12, fontSize: 14, color: "var(--color-muted)" }}>
<span>{post.published}</span>
<span>{`\u2022`}</span>
<span>{post.readingTime}</span>
</div>
</header>
<article
style={{ color: "var(--color-foreground)", lineHeight: 1.75, fontSize: 18, display: "grid", gap: 24 }}
dangerouslySetInnerHTML={{ __html: post.content }}
/>
<footer style={{ borderTop: "1px solid var(--surface-border)", paddingTop: 24 }}>
<p style={{ color: "var(--color-muted)" }}>
Enjoyed this article? Explore more insights on the <a href={`/${locale}/blog`}>blog</a> or reach out to us at
<a href="mailto:hello@allai.studio"> hello@allai.studio</a>.
</p>
</footer>
</main>
);
}