114 lines
3.5 KiB
TypeScript
114 lines
3.5 KiB
TypeScript
import type { Metadata } from "next";
|
|
import { redirect } from "next/navigation";
|
|
import ArticleDetailPage from "./ArticleDetailClient";
|
|
import type { Article } from "@/lib/articles/types";
|
|
import {
|
|
getLegacyArticleBySlug,
|
|
getLegacyRecentSlugs,
|
|
getLegacyRelatedArticles,
|
|
} from "@/lib/articles/legacy-source";
|
|
|
|
export const revalidate = 3600;
|
|
export const dynamicParams = true;
|
|
|
|
interface PageProps {
|
|
params: Promise<{ slug: string }>;
|
|
}
|
|
|
|
const SITE_URL =
|
|
process.env.NEXT_PUBLIC_SITE_URL || "https://broadcastbeat.com";
|
|
|
|
function toIso(d: string | null | undefined): string {
|
|
if (!d) return new Date().toISOString();
|
|
const parsed = new Date(d);
|
|
return isNaN(parsed.getTime()) ? new Date().toISOString() : parsed.toISOString();
|
|
}
|
|
|
|
function absoluteImage(src: string | undefined | null): string {
|
|
if (!src) return `${SITE_URL}/assets/images/article-placeholder.jpg`;
|
|
if (/^https?:\/\//i.test(src)) return src;
|
|
return `${SITE_URL}${src.startsWith("/") ? "" : "/"}${src}`;
|
|
}
|
|
|
|
async function loadArticle(slug: string): Promise<{
|
|
article: Article | null;
|
|
related: Article[];
|
|
}> {
|
|
const imported = await getLegacyArticleBySlug(slug);
|
|
if (!imported) return { article: null, related: [] };
|
|
const related = await getLegacyRelatedArticles(slug, imported.category, 3);
|
|
return { article: imported, related };
|
|
}
|
|
|
|
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
|
const { slug } = await params;
|
|
const { article } = await loadArticle(slug);
|
|
if (!article) {
|
|
return { title: "Article Not Found", description: "The article you are looking for does not exist." };
|
|
}
|
|
return {
|
|
title: article.title,
|
|
description: article.excerpt,
|
|
alternates: { canonical: `/articles/${article.slug}` },
|
|
openGraph: {
|
|
type: "article",
|
|
title: article.title,
|
|
description: article.excerpt,
|
|
url: `/articles/${article.slug}`,
|
|
images: [{ url: article.image, width: 1200, height: 630, alt: article.alt }],
|
|
authors: [article.author],
|
|
publishedTime: article.date,
|
|
tags: article.tags,
|
|
},
|
|
twitter: {
|
|
card: "summary_large_image",
|
|
title: article.title,
|
|
description: article.excerpt,
|
|
images: [article.image],
|
|
creator: "@Broadcast Beat",
|
|
},
|
|
};
|
|
}
|
|
|
|
export async function generateStaticParams() {
|
|
const recent = await getLegacyRecentSlugs(50);
|
|
return recent.map((slug) => ({ slug }));
|
|
}
|
|
|
|
export default async function ArticlePage({ params }: PageProps) {
|
|
const { slug } = await params;
|
|
const { article, related } = await loadArticle(slug);
|
|
if (!article) {
|
|
// Slug isn't in any of our article tables — send the reader to the news
|
|
// index so a click from the homepage/ticker doesn't dead-end on 404.
|
|
redirect("/news");
|
|
}
|
|
|
|
const articleSchema = {
|
|
"@context": "https://schema.org",
|
|
"@type": "Article",
|
|
headline: article.title,
|
|
description: article.excerpt,
|
|
image: [absoluteImage(article.image)],
|
|
author: { "@type": "Person", name: article.author },
|
|
publisher: {
|
|
"@type": "Organization",
|
|
name: "Broadcast Beat",
|
|
logo: { "@type": "ImageObject", url: `${SITE_URL}/assets/images/logo.png` },
|
|
},
|
|
datePublished: toIso(article.date),
|
|
dateModified: new Date().toISOString(),
|
|
mainEntityOfPage: { "@type": "WebPage", "@id": `${SITE_URL}/articles/${article.slug}` },
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<script
|
|
type="application/ld+json"
|
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleSchema) }}
|
|
/>
|
|
<ArticleDetailPage article={article} relatedArticles={related} />
|
|
</>
|
|
);
|
|
}
|