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:
@@ -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,9 +93,7 @@ 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} />
|
||||
</>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
139
src/lib/articles/legacy-source.ts
Normal file
139
src/lib/articles/legacy-source.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { createClient } from "@supabase/supabase-js";
|
||||
import type { Article } from "./sampleArticles";
|
||||
import { rewriteLegacyImageUrl, rewriteLegacyImageUrlsInHtml } from "@/lib/legacy-image";
|
||||
|
||||
const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const SUPABASE_ANON = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const SCHEMA = process.env.NEXT_PUBLIC_SUPABASE_SCHEMA || "bb";
|
||||
|
||||
function client() {
|
||||
return createClient(SUPABASE_URL, SUPABASE_ANON, {
|
||||
db: { schema: SCHEMA as "public" },
|
||||
auth: { persistSession: false },
|
||||
});
|
||||
}
|
||||
|
||||
interface ImportedPostRow {
|
||||
wp_id: number;
|
||||
wp_slug: string;
|
||||
title: string;
|
||||
excerpt: string | null;
|
||||
content: string | null;
|
||||
author_name: string | null;
|
||||
category: string | null;
|
||||
tags: string[] | null;
|
||||
featured_image: string | null;
|
||||
featured_image_alt: string | null;
|
||||
status: string;
|
||||
wp_published_at: string | null;
|
||||
}
|
||||
|
||||
const FALLBACK_IMAGE = "/legacy/site/no_image.png";
|
||||
|
||||
function authorSlug(name: string | null): string {
|
||||
if (!name) return "staff-reporter";
|
||||
return name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "")
|
||||
.slice(0, 60) || "staff-reporter";
|
||||
}
|
||||
|
||||
function formatDate(iso: string | null): string {
|
||||
if (!iso) return "";
|
||||
const d = new Date(iso);
|
||||
if (isNaN(d.getTime())) return "";
|
||||
return d.toLocaleDateString("en-US", {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
function readTime(content: string | null): string {
|
||||
if (!content) return "1 min read";
|
||||
const text = content.replace(/<[^>]+>/g, " ");
|
||||
const words = text.trim().split(/\s+/).filter(Boolean).length;
|
||||
const minutes = Math.max(1, Math.round(words / 200));
|
||||
return `${minutes} min read`;
|
||||
}
|
||||
|
||||
function rowToArticle(row: ImportedPostRow): Article {
|
||||
const image = row.featured_image
|
||||
? rewriteLegacyImageUrl(row.featured_image)
|
||||
: FALLBACK_IMAGE;
|
||||
const content = rewriteLegacyImageUrlsInHtml(row.content || "");
|
||||
const author = row.author_name || "Staff Reporter";
|
||||
return {
|
||||
slug: row.wp_slug,
|
||||
section: "news",
|
||||
title: row.title,
|
||||
excerpt: row.excerpt || "",
|
||||
category: (row.category || "NEWS").toUpperCase(),
|
||||
date: formatDate(row.wp_published_at),
|
||||
author,
|
||||
authorSlug: authorSlug(author),
|
||||
authorTitle: "Contributor",
|
||||
authorAvatar: FALLBACK_IMAGE,
|
||||
image,
|
||||
alt: row.featured_image_alt || row.title,
|
||||
readTime: readTime(row.content),
|
||||
tags: row.tags || [],
|
||||
content,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getLegacyArticleBySlug(slug: string): Promise<Article | null> {
|
||||
try {
|
||||
const { data, error } = await client()
|
||||
.from("wp_imported_posts")
|
||||
.select(
|
||||
"wp_id,wp_slug,title,excerpt,content,author_name,category,tags,featured_image,featured_image_alt,status,wp_published_at",
|
||||
)
|
||||
.eq("wp_slug", slug)
|
||||
.eq("status", "published")
|
||||
.maybeSingle();
|
||||
if (error || !data) return null;
|
||||
return rowToArticle(data as ImportedPostRow);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getLegacyRelatedArticles(
|
||||
excludeSlug: string,
|
||||
category: string | null,
|
||||
count: number,
|
||||
): Promise<Article[]> {
|
||||
try {
|
||||
let q = client()
|
||||
.from("wp_imported_posts")
|
||||
.select(
|
||||
"wp_id,wp_slug,title,excerpt,content,author_name,category,tags,featured_image,featured_image_alt,status,wp_published_at",
|
||||
)
|
||||
.eq("status", "published")
|
||||
.neq("wp_slug", excludeSlug)
|
||||
.order("wp_published_at", { ascending: false })
|
||||
.limit(count);
|
||||
if (category) q = q.eq("category", category);
|
||||
const { data, error } = await q;
|
||||
if (error || !data) return [];
|
||||
return (data as ImportedPostRow[]).map(rowToArticle);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function getLegacyRecentSlugs(limit = 200): Promise<string[]> {
|
||||
try {
|
||||
const { data } = await client()
|
||||
.from("wp_imported_posts")
|
||||
.select("wp_slug")
|
||||
.eq("status", "published")
|
||||
.order("wp_published_at", { ascending: false })
|
||||
.limit(limit);
|
||||
return (data || []).map((r: { wp_slug: string }) => r.wp_slug);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user