fixes: image-url proxy + raw-publish fallback on rewrite failure

1. Clean image URLs
   Article HTML was leaking the full Supabase Storage URL:
     https://supabase.onsethost.com/storage/v1/object/public/distribute-media/<uuid>/<file>
   Now rewritten on render to:
     /img/distribute-media/<uuid>/<file>
   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.
This commit is contained in:
Ryan Salazar
2026-05-27 19:43:35 +00:00
parent 6e3a93f90d
commit 61910d6c8f
4 changed files with 136 additions and 2 deletions

View File

@@ -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 = `<aside style="background:#0d1726;border:1px solid #1e3552;padding:10px 14px;margin-bottom:16px;font-size:13px;color:#7aa7d4;border-radius:4px">Distributed as a press release — staff editorial rewrite pending.</aside>`;
const content = NOTICE + (opts.rawHtml || `<p>${opts.rawContent.replace(/\n+/g, "</p><p>")}</p>`);
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;
}
}

View File

@@ -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/<bucket>/<path>
// 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;
}
/**