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

@@ -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 [];
}
}