import Link from "next/link"; import { notFound } from "next/navigation"; import type { ReactNode } from "react"; import type { Metadata } from "next"; import { publicClient } from "@/lib/supabase"; import { SiteShell } from "@/components/site-shell"; import { AdUnit } from "@/components/ad-unit"; import { sanitizeRenderedHtml } from "@/lib/html"; export const dynamic = "force-dynamic"; type PostRow = { slug: string; title: string; excerpt: string | null; html: string; featured_image: { url?: string } | null; published_at: string; updated_at: string; // A guest post syndicated from another blog carries the original URL here. // When set, it is the canonical for search engines and is shown to readers, // so a cross-post never competes with its source for the same words. canonical_url: string | null; // Stored as jsonb: an object like { name, url }, or occasionally a bare // string. Never rendered directly, or an object prints as [object Object]. author: { name?: string; url?: string } | string | null; }; /** A display name from the jsonb author, whatever shape it took. */ function authorName(author: PostRow["author"]): string { if (!author) return ""; if (typeof author === "string") return author; return typeof author.name === "string" ? author.name : ""; } const SITE_URL = (process.env.PUBLIC_URL ?? "https://logicsrc.com").replace(/\/$/, ""); async function loadPost(slug: string): Promise { try { const supabase = publicClient(); const { data } = await supabase .from("blog_posts") .select("slug, title, excerpt, html, featured_image, published_at, updated_at, canonical_url, author") .eq("slug", slug) .eq("status", "published") .maybeSingle(); return (data as PostRow | null) ?? null; } catch { return null; } } export async function generateMetadata({ params, }: { params: Promise<{ slug: string }>; }): Promise { const { slug } = await params; const post = await loadPost(slug); if (!post) return { title: "Not found · LogicSRC" }; return { title: `${post.title} · LogicSRC`, description: post.excerpt ?? undefined, // A guest post points its canonical at the original; an original post is // canonical to itself. alternates: { canonical: post.canonical_url ?? `/blog/${post.slug}` }, openGraph: { title: post.title, description: post.excerpt ?? undefined, type: "article", images: post.featured_image?.url ? [post.featured_image.url] : undefined, }, }; } function formatDate(value: string): string { return new Date(value).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric", }); } export default async function BlogPostPage({ params, }: { params: Promise<{ slug: string }>; }): Promise { const { slug } = await params; const post = await loadPost(slug); if (!post) notFound(); const canonical = post.canonical_url ?? `${SITE_URL}/blog/${post.slug}`; const jsonLd = { "@context": "https://schema.org", "@type": "BlogPosting", headline: post.title, description: post.excerpt ?? undefined, image: post.featured_image?.url ? [post.featured_image.url] : undefined, datePublished: post.published_at, dateModified: post.updated_at, url: `${SITE_URL}/blog/${post.slug}`, // For a guest post this is the original; search engines follow it. mainEntityOfPage: canonical, ...(authorName(post.author) ? { author: { "@type": "Person", name: authorName(post.author) } } : {}), publisher: { "@id": `${SITE_URL}/#organization` }, }; const byline = authorName(post.author); let originHost = ""; if (post.canonical_url) { try { originHost = new URL(post.canonical_url).host; } catch { originHost = ""; } } const html = sanitizeRenderedHtml(post.html); return (