wire /news/[slug] and /articles/[slug] to read bb.wp_imported_posts via ISR

- new src/lib/articles/legacy-source.ts: server-side Supabase reader
  (bb schema, anon key, RLS public_read policy) that maps imported rows
  to the existing Article shape, applies rewriteLegacyImageUrl for the
  featured image and rewriteLegacyImageUrlsInHtml across the body.
- both [slug]/page.tsx routes now: try seed sampleArticles first; on
  miss fall back to getLegacyArticleBySlug. Related articles match via
  same-category recent posts when imported.
- generateStaticParams pre-renders seed slugs + 50 most recent imported
  posts. dynamicParams=true means anything else server-renders on first
  request and ISR caches it for 1h (revalidate=3600). Avoids a 4577-page
  build.
- Hardcoded builtwithrocket.new URLs in JSON-LD replaced with
  process.env.NEXT_PUBLIC_SITE_URL.
This commit is contained in:
Ryan Salazar
2026-05-08 06:25:29 +00:00
parent 54154e2d3c
commit 08df1ad4c1
3 changed files with 233 additions and 94 deletions

View File

@@ -1,91 +1,89 @@
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import ArticleDetailPage from './ArticleDetailClient';
import { sampleArticles, getRelatedArticles } from '@/lib/articles/sampleArticles';
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import ArticleDetailPage from "./ArticleDetailClient";
import { sampleArticles, getRelatedArticles, type Article } from "@/lib/articles/sampleArticles";
import {
getLegacyArticleBySlug,
getLegacyRecentSlugs,
getLegacyRelatedArticles,
} from "@/lib/articles/legacy-source";
export const revalidate = 3600;
export const dynamicParams = true;
// DO NOT OVERRIDE — article slug generation and metadata
interface PageProps {
params: Promise<{ slug: string }>;
}
const SITE_URL =
process.env.NEXT_PUBLIC_SITE_URL || "https://bb-staging.onsethost.com";
async function loadArticle(slug: string): Promise<{
article: Article | null;
related: Article[];
}> {
const seed = sampleArticles.find((a) => a.slug === slug);
if (seed) {
return { article: seed, related: getRelatedArticles(slug, 3) };
}
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 = sampleArticles.find((a) => a.slug === slug);
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 Not Found", description: "The article you are looking for does not exist." };
}
return {
title: article.title,
description: article.excerpt,
alternates: {
canonical: `/articles/${article.slug}`,
},
alternates: { canonical: `/articles/${article.slug}` },
openGraph: {
type: 'article',
type: "article",
title: article.title,
description: article.excerpt,
url: `/articles/${article.slug}`,
images: [
{
url: article.image,
width: 1200,
height: 630,
alt: article.alt,
type: 'image/jpeg',
},
],
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',
card: "summary_large_image",
title: article.title,
description: article.excerpt,
images: [article.image],
creator: '@BroadcastBeat',
creator: "@BroadcastBeat",
},
};
}
export async function generateStaticParams() {
return sampleArticles.map((article) => ({
slug: article.slug,
}));
const seedSlugs = sampleArticles.map((a) => a.slug);
const recent = await getLegacyRecentSlugs(50);
return Array.from(new Set([...seedSlugs, ...recent])).map((slug) => ({ slug }));
}
export default async function ArticlePage({ params }: PageProps) {
const { slug } = await params;
const article = sampleArticles.find((a) => a.slug === slug);
if (!article) {
notFound();
}
const related = getRelatedArticles(slug, 3);
const { article, related } = await loadArticle(slug);
if (!article) notFound();
const articleSchema = {
'@context': 'https://schema.org',
'@type': 'Article',
"@context": "https://schema.org",
"@type": "Article",
headline: article.title,
description: article.excerpt,
image: article.image,
author: {
'@type': 'Person',
name: article.author,
},
author: { "@type": "Person", name: article.author },
publisher: {
'@type': 'Organization',
name: 'BroadcastBeat',
logo: {
'@type': 'ImageObject',
url: 'https://broadcastb5322.builtwithrocket.new/assets/images/logo.png',
},
"@type": "Organization",
name: "BroadcastBeat",
logo: { "@type": "ImageObject", url: `${SITE_URL}/legacy/site/broadcastbeat-logo.png` },
},
datePublished: article.date,
dateModified: new Date().toISOString(),
@@ -95,11 +93,9 @@ export default async function ArticlePage({ params }: PageProps) {
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(articleSchema),
}}
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleSchema) }}
/>
<ArticleDetailPage article={article} relatedArticles={related} />
</>
);
}
}

View File

@@ -1,43 +1,57 @@
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { sampleArticles, getRelatedArticles } from "@/lib/articles/sampleArticles";
import { sampleArticles, getRelatedArticles, type Article } from "@/lib/articles/sampleArticles";
import {
getLegacyArticleBySlug,
getLegacyRecentSlugs,
getLegacyRelatedArticles,
} from "@/lib/articles/legacy-source";
import NewsArticleDetailClient from "./NewsArticleDetailClient";
// ISR: serve cached pages but revalidate hourly. New imported posts surface
// without a redeploy.
export const revalidate = 3600;
export const dynamicParams = true;
interface PageProps {
params: Promise<{ slug: string }>;
}
const SITE_URL =
process.env.NEXT_PUBLIC_SITE_URL || "https://bb-staging.onsethost.com";
async function loadArticle(slug: string): Promise<{
article: Article | null;
related: Article[];
source: "seed" | "imported";
}> {
// Seed (sampleArticles) wins for routes already curated.
const seed = sampleArticles.find((a) => a.slug === slug && a.section === "news");
if (seed) {
return { article: seed, related: getRelatedArticles(slug, 6), source: "seed" };
}
const imported = await getLegacyArticleBySlug(slug);
if (!imported) return { article: null, related: [], source: "imported" };
const related = await getLegacyRelatedArticles(slug, imported.category, 6);
return { article: imported, related, source: "imported" };
}
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { slug } = await params;
const article = sampleArticles.find((a) => a.slug === slug && a.section === "news");
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 Not Found", description: "The article you are looking for does not exist." };
}
return {
title: `${article.title} — BroadcastBeat`,
description: article.excerpt,
alternates: {
canonical: `/news/${article.slug}`,
},
alternates: { canonical: `/news/${article.slug}` },
openGraph: {
type: "article",
title: article.title,
description: article.excerpt,
url: `/news/${article.slug}`,
images: [
{
url: article.image,
width: 1200,
height: 630,
alt: article.alt,
type: "image/jpeg",
},
],
images: [{ url: article.image, width: 1200, height: 630, alt: article.alt }],
authors: [article.author],
publishedTime: article.date,
tags: article.tags,
@@ -53,20 +67,19 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
}
export async function generateStaticParams() {
return sampleArticles
// Build-time: only seed articles + the most-recent imported slugs (cap to keep
// build fast). Anything else is on-demand ISR via dynamicParams=true.
const seedSlugs = sampleArticles
.filter((a) => a.section === "news")
.map((article) => ({ slug: article.slug }));
.map((a) => a.slug);
const recent = await getLegacyRecentSlugs(50);
return Array.from(new Set([...seedSlugs, ...recent])).map((slug) => ({ slug }));
}
export default async function NewsArticlePage({ params }: PageProps) {
const { slug } = await params;
const article = sampleArticles.find((a) => a.slug === slug && a.section === "news");
if (!article) {
notFound();
}
const related = getRelatedArticles(slug, 6);
const { article, related } = await loadArticle(slug);
if (!article) notFound();
const articleSchema = {
"@context": "https://schema.org",
@@ -74,24 +87,15 @@ export default async function NewsArticlePage({ params }: PageProps) {
headline: article.title,
description: article.excerpt,
image: article.image,
author: {
"@type": "Person",
name: article.author,
},
author: { "@type": "Person", name: article.author },
publisher: {
"@type": "Organization",
name: "BroadcastBeat",
logo: {
"@type": "ImageObject",
url: "https://broadcastb5322.builtwithrocket.new/assets/images/logo.png",
},
logo: { "@type": "ImageObject", url: `${SITE_URL}/legacy/site/broadcastbeat-logo.png` },
},
datePublished: article.date,
dateModified: new Date().toISOString(),
mainEntityOfPage: {
"@type": "WebPage",
"@id": `https://broadcastb5322.builtwithrocket.new/news/${article.slug}`,
},
mainEntityOfPage: { "@type": "WebPage", "@id": `${SITE_URL}/news/${article.slug}` },
};
return (