From 61910d6c8f772afc725b47de520a7c89fd65bd29 Mon Sep 17 00:00:00 2001 From: Ryan Salazar Date: Wed, 27 May 2026 19:43:35 +0000 Subject: [PATCH] fixes: image-url proxy + raw-publish fallback on rewrite failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Clean image URLs Article HTML was leaking the full Supabase Storage URL: https://supabase.onsethost.com/storage/v1/object/public/distribute-media// Now rewritten on render to: /img/distribute-media// Served by new /img/[...path] route handler that proxies to Supabase with 24h public cache + 7d SWR. Better SEO (first-party images) and stops leaking the storage host into article markup. 2. Stuck PR submissions When the rewrite pipeline fails (Ollama 503 / bad model output), the submission used to sit forever in ai_original_submissions without becoming a public wp_imported_posts row. PR firms got "delivered" status with no visible article anywhere. /api/ai/submit now calls publishRawFallback() if rewriteSubmission returns anything other than 'succeeded'. The fallback inserts a wp_imported_posts row containing the raw HTML body with a visible "Distributed as a press release — staff editorial rewrite pending" banner at the top, so the article surfaces immediately. A future retry job can swap in the rewritten content when Ollama recovers. --- src/app/api/ai/submit/route.ts | 20 +++++++++- src/app/img/[...path]/route.ts | 44 ++++++++++++++++++++ src/lib/ai/publish-raw-fallback.ts | 64 ++++++++++++++++++++++++++++++ src/lib/legacy-image.ts | 10 ++++- 4 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 src/app/img/[...path]/route.ts create mode 100644 src/lib/ai/publish-raw-fallback.ts diff --git a/src/app/api/ai/submit/route.ts b/src/app/api/ai/submit/route.ts index e59ee37..772dda4 100644 --- a/src/app/api/ai/submit/route.ts +++ b/src/app/api/ai/submit/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; import { ingestRawSubmission, rewriteSubmission } from "@/lib/ai/rewrite-pipeline"; import { findViolations } from "@/lib/disallowed-phrases-guard"; +import { publishRawFallback } from "@/lib/ai/publish-raw-fallback"; export const runtime = "nodejs"; export const maxDuration = 300; @@ -117,7 +118,24 @@ export async function POST(req: NextRequest) { const submissionId = await ingestRawSubmission(payload); const outcome = await rewriteSubmission({ submissionId, personaSlug: payload.personaSlug }); - return NextResponse.json({ ok: true, submissionId, outcome }); + // If the rewrite failed (Ollama 503 / model output bad), publish the raw + // press release with a "rewrite pending" notice so the submission is at + // least visible. Without this, PR-firm submissions disappear when Ollama + // is saturated. + let fallback: { wp_id: number; wp_slug: string } | null = null; + if (outcome.status !== "succeeded") { + const meta: any = payload.metadata || {}; + fallback = await publishRawFallback({ + submissionId, + rawTitle: payload.rawTitle, + rawHtml: payload.rawHtml || null, + rawContent: payload.rawContent, + contributorName: payload.contributorName || null, + featuredImageUrl: typeof meta.featured_image_url === "string" ? meta.featured_image_url : null, + }); + } + + return NextResponse.json({ ok: true, submissionId, outcome, raw_fallback: fallback }); } catch (err: any) { return NextResponse.json({ error: err.message || String(err) }, { status: 500 }); } diff --git a/src/app/img/[...path]/route.ts b/src/app/img/[...path]/route.ts new file mode 100644 index 0000000..0e88a9c --- /dev/null +++ b/src/app/img/[...path]/route.ts @@ -0,0 +1,44 @@ +import { NextRequest } from "next/server"; + +export const runtime = "nodejs"; +export const dynamic = "force-static"; + +/** + * Image proxy: serves files from Supabase Storage under a clean broadcastbeat.com + * URL so article HTML doesn't ship the long `https://supabase.onsethost.com/ + * storage/v1/object/public/...` path. Better for SEO + makes images look + * first-party. + * + * Usage: /img/distribute-media//.jpg + * /img/campaign-creatives//.jpg + * /img/bb-forum-avatars/.png + * + * Path segments are forwarded directly to the matching public bucket. + */ +const SUPABASE_BASE = (process.env.NEXT_PUBLIC_SUPABASE_URL || "https://supabase.onsethost.com") + .replace(/\/+$/, "") + "/storage/v1/object/public"; + +export async function GET(_req: NextRequest, { params }: { params: Promise<{ path: string[] }> }) { + const { path } = await params; + if (!path || path.length < 2) return new Response("bad path", { status: 400 }); + + const target = `${SUPABASE_BASE}/${path.map(encodeURIComponent).join("/")}`; + let upstream: Response; + try { + upstream = await fetch(target, { cache: "force-cache" }); + } catch (e: any) { + return new Response("upstream error: " + (e?.message || String(e)), { status: 502 }); + } + if (!upstream.ok) { + return new Response("not found", { status: upstream.status === 404 ? 404 : 502 }); + } + + return new Response(upstream.body, { + status: 200, + headers: { + "content-type": upstream.headers.get("content-type") || "image/jpeg", + "cache-control": "public, max-age=86400, s-maxage=86400, stale-while-revalidate=604800", + "x-bb-img-source": "supabase-storage", + }, + }); +} diff --git a/src/lib/ai/publish-raw-fallback.ts b/src/lib/ai/publish-raw-fallback.ts new file mode 100644 index 0000000..7c6b566 --- /dev/null +++ b/src/lib/ai/publish-raw-fallback.ts @@ -0,0 +1,64 @@ +/** + * Fallback: when the Ollama rewrite pipeline fails, publish the raw press + * release directly into wp_imported_posts so the article still surfaces on + * the public site. Includes a visible "rewrite pending" banner so editors + * (and savvy readers) can tell it hasn't been editorialized yet. + * + * Used by /api/ai/submit after rewriteSubmission returns a failed outcome. + */ + +import { createAdminClient } from "../supabase/admin"; + +export async function publishRawFallback(opts: { + submissionId: string; + rawTitle: string; + rawHtml?: string | null; + rawContent: string; + contributorName?: string | null; + featuredImageUrl?: string | null; +}): Promise<{ wp_id: number; wp_slug: string } | null> { + try { + const sb = createAdminClient(); + + // Slug from title (lower-cased, dashes only, capped) — plus the submission + // id tail so concurrent same-title submissions don't collide. + const baseSlug = (opts.rawTitle || "submission") + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, "") + .slice(0, 80); + const tail = opts.submissionId.replace(/-/g, "").slice(0, 6); + const wp_slug = `${baseSlug}-pr-${tail}`; + + const NOTICE = ``; + const content = NOTICE + (opts.rawHtml || `

${opts.rawContent.replace(/\n+/g, "

")}

`); + + const excerpt = opts.rawContent.replace(/\s+/g, " ").slice(0, 240).trim(); + const wp_id = -Math.floor(Date.now() / 1000); + + const { error } = await sb.from("wp_imported_posts").insert({ + wp_id, + wp_slug, + title: opts.rawTitle, + excerpt, + content, + author_name: opts.contributorName || "PR Submission", + category: "industry", + tags: [] as string[], + featured_image: opts.featuredImageUrl || null, + status: "published", + wp_published_at: new Date().toISOString(), + imported_at: new Date().toISOString(), + ai_rewritten_at: null, + site_id: 1, + }); + if (error) { + console.warn(`[publish-raw-fallback] insert failed: ${error.message}`); + return null; + } + return { wp_id, wp_slug }; + } catch (e: any) { + console.warn(`[publish-raw-fallback] unhandled: ${e?.message || e}`); + return null; + } +} diff --git a/src/lib/legacy-image.ts b/src/lib/legacy-image.ts index 7fb59db..1bc3fd6 100644 --- a/src/lib/legacy-image.ts +++ b/src/lib/legacy-image.ts @@ -27,10 +27,18 @@ export function rewriteLegacyImageUrl(url: string | null | undefined): string { } export function rewriteLegacyImageUrlsInHtml(html: string): string { - return html.replace( + let out = html.replace( /(https?:\/\/(?:www\.)?broadcastbeat\.com\/wp-content\/uploads\/[^"'\s)]+)/g, (m) => rewriteLegacyImageUrl(m), ); + // Rewrite raw Supabase Storage URLs to clean first-party /img// + // so article HTML never leaks the long supabase.onsethost.com URL. Better for + // SEO and keeps images visually first-party. Served by /img/[...path]/route.ts. + out = out.replace( + /https?:\/\/(?:[\w-]+\.)*supabase\.onsethost\.com\/storage\/v1\/object\/public\/([^\s"')]+)/g, + (_m, p) => `/img/${p}`, + ); + return out; } /**