From 8fd7165a6a2b0c7a74fa17a5bfecf3a7dc15e182 Mon Sep 17 00:00:00 2001 From: "Claude (Phase B)" Date: Thu, 14 May 2026 19:38:20 +0000 Subject: [PATCH] Phase B polish: revalidatePath on approve, reject empty submissions src/app/api/admin/review-queue/[id]/route.ts - Call revalidatePath on /news, /news/, /articles/, /home-page, section pages, and /sitemap.xml on approve/reject. The /news listing is ISR-cached (revalidate=1800) so approval would otherwise be invisible for up to 30 minutes; this flushes the cache immediately. src/app/api/ai/submit/route.ts - Reject submissions where rawTitle/title is whitespace or rawContent/body is empty after stripping HTML. Previously empty bodies fell through into the LLM and Claude dutifully wrote a 400-word piece about the empty submission. Hard-cap at the gate to avoid the spend. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/app/api/admin/review-queue/[id]/route.ts | 20 ++++++++++++++++++-- src/app/api/ai/submit/route.ts | 2 ++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/app/api/admin/review-queue/[id]/route.ts b/src/app/api/admin/review-queue/[id]/route.ts index b7c997d..7db06f4 100644 --- a/src/app/api/admin/review-queue/[id]/route.ts +++ b/src/app/api/admin/review-queue/[id]/route.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; +import { revalidatePath, revalidateTag } from "next/cache"; import { createClient } from "@/lib/supabase/server"; import { createAdminClient } from "@/lib/supabase/admin"; @@ -55,7 +56,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: ...extras, }) .eq("id", id) - .select("id, original_submission_id") + .select("id, slug, original_submission_id") .single(); if (error || !data) return NextResponse.json({ error: error?.message || "Not found" }, { status: 500 }); @@ -67,5 +68,20 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: .eq("id", (data as any).original_submission_id); } - return NextResponse.json({ ok: true, id: (data as any).id, status: nextStatus }); + const slug = (data as any).slug as string; + for (const path of [ + "/news", + `/news/${slug}`, + `/articles/${slug}`, + "/home-page", + "/technology", + "/gear", + "/show-coverage", + "/sitemap.xml", + ]) { + try { revalidatePath(path); } catch {} + } + try { revalidateTag("articles"); } catch {} + + return NextResponse.json({ ok: true, id: (data as any).id, status: nextStatus, slug }); } diff --git a/src/app/api/ai/submit/route.ts b/src/app/api/ai/submit/route.ts index b81de51..368abe1 100644 --- a/src/app/api/ai/submit/route.ts +++ b/src/app/api/ai/submit/route.ts @@ -52,10 +52,12 @@ function normalize(raw: any): NativePayload | null { if (!raw || typeof raw !== "object") return null; // Native shape: { rawTitle, rawContent, ... } if (typeof raw.rawTitle === "string" && typeof raw.rawContent === "string") { + if (!raw.rawTitle.trim() || !raw.rawContent.trim()) return null; return raw as NativePayload; } // Distribute-rmp shape: { title, body, contributor: {...} } if (typeof raw.title === "string" && typeof raw.body === "string") { + if (!raw.title.trim() || !raw.body.replace(/<[^>]*>/g, "").trim()) return null; const d = raw as DistributePayload; const contributorName = d.contributor?.contact_name || d.contributor?.firm_name || undefined; const contributorEmail = d.contributor?.email || undefined;