139 lines
4.1 KiB
TypeScript
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>
|
|
);
|
|
}
|