From 8283237f878c796ac6a05a901cf9814ea0145862 Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Date: Sun, 24 May 2026 16:16:20 -0300 Subject: [PATCH] W5.4 followup: hero illustration on /c/[slug] + sitemap fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hero illustration: - Painterly 16:9 editorial illustration generated via Nano Banana Pro for the featured case (green-fireballs-narrative): late-1940s desert night, vivid emerald fireball over silhouetted Sandia mesas, 1948-era state-police sedan parked on US 66 shoulder with an officer in period uniform looking up, faint green glow on his face. Sandia Base 5 Miles roadsign. New Yorker-cover painterly register, NOT photorealistic, NOT sci-fi. - Stored at /data/disclosure/processing/case-art/.png, served through the existing /api/static/processing/ route. 2.7MB at 2K. - components/featured-case.tsx: prefers the illustration over the declassified-page thumbnail when present. Tags it "Editorial illustration" / "Ilustração editorial" so the reader knows it's not a photograph. - app/c/[slug]/page.tsx: full-bleed editorial hero at the top of the article when an illustration exists for the slug. Title sits on the image with gradient overlay; "Ilustração editorial" chip in the top-right corner labels the art honestly. When no illustration exists the page falls back to the plain title header. Sitemap fix: - Added export const dynamic = "force-dynamic" + revalidate = 3600 to app/sitemap.ts. Without these Next.js statically generated the sitemap at build time, when the DB and case-files volume were unreachable from the build container — which is why production was serving only the 9 static URLs instead of ~3000. Co-Authored-By: Claude Opus 4.7 (1M context) --- web/app/c/[slug]/page.tsx | 73 ++++++++++++++++++++++++++------ web/app/sitemap.ts | 6 +++ web/components/featured-case.tsx | 43 +++++++++++++++---- 3 files changed, 100 insertions(+), 22 deletions(-) diff --git a/web/app/c/[slug]/page.tsx b/web/app/c/[slug]/page.tsx index b20159a..45b3a6d 100644 --- a/web/app/c/[slug]/page.tsx +++ b/web/app/c/[slug]/page.tsx @@ -50,6 +50,17 @@ async function loadCase(slug: string): Promise<{ fm: Frontmatter; body: string } } } +async function hasIllustration(slug: string): Promise { + const fs = await import("node:fs/promises"); + const UFO_ROOT = process.env.UFO_ROOT || "/data/ufo"; + try { + await fs.stat(path.join(UFO_ROOT, "processing", "case-art", `${slug}.png`)); + return true; + } catch { + return false; + } +} + /** * Extract the first prose paragraph from the body for the meta description * + OG. We pick the locale-preferred sub-section's opener. @@ -109,6 +120,7 @@ export default async function CaseReportPage({ const dateLabel = fm.created_at ? new Date(fm.created_at).toLocaleDateString(locale === "pt-br" ? "pt-BR" : "en-US", { day: "numeric", month: "long", year: "numeric" }) : null; const canonical = `${SITE_URL}/c/${slug}`; const lead = pickLead(body, locale).slice(0, 280); + const heroArt = (await hasIllustration(slug)) ? `/api/static/processing/case-art/${slug}.png` : null; return (
@@ -118,21 +130,54 @@ export default async function CaseReportPage({ ]} /> -
-
-
- {locale === "en" ? "Declassified case file" : "Arquivo desclassificado"} - {dateLabel && <> · {dateLabel}} + {/* Full-bleed editorial hero — only when a painterly illustration + exists for this case. Other cases get the plain title header. */} + {heroArt && ( +
+
+ {/* eslint-disable-next-line @next/next/no-img-element */} + {title} +
+
+
+
+
+ {locale === "en" ? "Declassified case file" : "Arquivo desclassificado"} + {dateLabel && <> · {dateLabel}} +
+

+ {title} +

+
+
+
+ {locale === "en" ? "Editorial illustration" : "Ilustração editorial"} +
-

- {title} -

- {lead && ( -

- {lead} -

- )} -
+
+ )} + +
+ {!heroArt && ( +
+
+ {locale === "en" ? "Declassified case file" : "Arquivo desclassificado"} + {dateLabel && <> · {dateLabel}} +
+

+ {title} +

+
+ )} + {lead && ( +

+ {lead} +

+ )}
.png — + * a painterly editorial illustration generated for this case. Prefer + * this over the doc-page thumbnail. */ + hero_illustration: string | null; } const CASE_ROOT = process.env.CASE_ROOT || "/data/ufo/case"; @@ -51,6 +55,19 @@ function extractFirstDocRef(body: string): { doc_id: string; page: number } | nu return { doc_id: m[1], page: parseInt(m[2], 10) }; } +const UFO_ROOT = process.env.UFO_ROOT || "/data/ufo"; + +async function illustrationFor(slug: string): Promise { + const fs = await import("node:fs/promises"); + const p = path.join(UFO_ROOT, "processing", "case-art", `${slug}.png`); + try { + await fs.stat(p); + return `/api/static/processing/case-art/${slug}.png`; + } catch { + return null; + } +} + async function loadFeatured(locale: "pt-br" | "en"): Promise { const dir = path.join(CASE_ROOT, "reports"); try { @@ -63,13 +80,15 @@ async function loadFeatured(locale: "pt-br" | "en"): Promise @@ -116,19 +139,23 @@ export async function FeaturedCase({ locale }: { locale: "pt-br" | "en" }) {
{/* Hero image column */} -
+
{heroImg ? ( // eslint-disable-next-line @next/next/no-img-element {title} ) : (
)} -
- {c.hero_doc_id && ( +
+ {heroIsArt ? ( +
+ {locale === "en" ? "Editorial illustration" : "Ilustração editorial"} +
+ ) : c.hero_doc_id && (
{locale === "en" ? "Source · " : "Fonte · "}{c.hero_doc_id} / p{String(c.hero_page).padStart(3, "0")}