Phase B E2E: UNION rewrites into /news, accept distribute-rmp payload shape
src/lib/articles/legacy-source.ts
- getLegacyArticleBySlug, getLegacyArticlesBySection, searchLegacyArticles,
getLegacyRecentSlugs, getLegacyRecentSitemapEntries: each now UNIONs
bb.ai_rewritten_articles (status=published) with bb.wp_imported_posts.
Rewrites carry persona attribution (author/avatar/title) via a
persona:ai_personas(slug,name,beat,avatar_url) embed.
- Section routing for rewrites is by category, not tags (rewrites do not
use the WP tag taxonomy — they have category strings written by Claude).
src/app/api/ai/submit/route.ts
- Bearer auth via BB_INGEST_BEARER (preferred) or x-internal-key (legacy)
- Polymorphic payload: accepts native {rawTitle, rawContent, ...} OR the
distribute-rmp shape {title, body, contributor:{...}, attachments,
submission_id, ...}. Distribute attachments + featured image survive in
bb.ai_original_submissions.metadata.
src/app/api/ai/rewrite/route.ts
- Same bearer auth pattern as submit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,14 +4,25 @@ import { rewriteSubmission } from "@/lib/ai/rewrite-pipeline";
|
|||||||
export const runtime = "nodejs";
|
export const runtime = "nodejs";
|
||||||
export const maxDuration = 300;
|
export const maxDuration = 300;
|
||||||
|
|
||||||
|
function bearerToken(req: NextRequest): string | null {
|
||||||
|
const h = req.headers.get("authorization") || "";
|
||||||
|
const m = h.match(/^Bearer\s+(.+)$/i);
|
||||||
|
return m ? m[1].trim() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkAuth(req: NextRequest): boolean {
|
||||||
|
const wantBearer = process.env.BB_INGEST_BEARER || "";
|
||||||
|
const wantInternalKey = process.env.BB_INTERNAL_INGEST_KEY || "";
|
||||||
|
if (!wantBearer && !wantInternalKey) return false;
|
||||||
|
if (wantBearer && bearerToken(req) === wantBearer) return true;
|
||||||
|
if (wantInternalKey && (req.headers.get("x-internal-key") || "") === wantInternalKey) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const internalKey = process.env.BB_INTERNAL_INGEST_KEY;
|
if (!checkAuth(req)) {
|
||||||
if (internalKey) {
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
const got = req.headers.get("x-internal-key");
|
|
||||||
if (got !== internalKey) {
|
|
||||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { submissionId, personaSlug } = (await req.json()) as {
|
const { submissionId, personaSlug } = (await req.json()) as {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { ingestRawSubmission, rewriteSubmission } from "@/lib/ai/rewrite-pipelin
|
|||||||
export const runtime = "nodejs";
|
export const runtime = "nodejs";
|
||||||
export const maxDuration = 300;
|
export const maxDuration = 300;
|
||||||
|
|
||||||
interface SubmitBody {
|
interface NativePayload {
|
||||||
rawTitle: string;
|
rawTitle: string;
|
||||||
rawContent: string;
|
rawContent: string;
|
||||||
rawHtml?: string;
|
rawHtml?: string;
|
||||||
@@ -12,25 +12,92 @@ interface SubmitBody {
|
|||||||
contributorName?: string;
|
contributorName?: string;
|
||||||
personaSlug?: string;
|
personaSlug?: string;
|
||||||
source?: string;
|
source?: string;
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DistributePayload {
|
||||||
|
submission_id?: string;
|
||||||
|
property?: string;
|
||||||
|
title: string;
|
||||||
|
body: string;
|
||||||
|
featured_image_url?: string | null;
|
||||||
|
attachments?: unknown[];
|
||||||
|
contributor?: {
|
||||||
|
id?: string;
|
||||||
|
email?: string;
|
||||||
|
firm_name?: string;
|
||||||
|
contact_name?: string;
|
||||||
|
phone?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function bearerToken(req: NextRequest): string | null {
|
||||||
|
const h = req.headers.get("authorization") || "";
|
||||||
|
const m = h.match(/^Bearer\s+(.+)$/i);
|
||||||
|
return m ? m[1].trim() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkAuth(req: NextRequest): { ok: boolean; reason?: string } {
|
||||||
|
const wantBearer = process.env.BB_INGEST_BEARER || "";
|
||||||
|
const wantInternalKey = process.env.BB_INTERNAL_INGEST_KEY || "";
|
||||||
|
if (!wantBearer && !wantInternalKey) {
|
||||||
|
return { ok: false, reason: "ingest auth not configured (set BB_INGEST_BEARER)" };
|
||||||
|
}
|
||||||
|
if (wantBearer && bearerToken(req) === wantBearer) return { ok: true };
|
||||||
|
if (wantInternalKey && (req.headers.get("x-internal-key") || "") === wantInternalKey) return { ok: true };
|
||||||
|
return { ok: false, reason: "Unauthorized" };
|
||||||
|
}
|
||||||
|
|
||||||
|
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") {
|
||||||
|
return raw as NativePayload;
|
||||||
|
}
|
||||||
|
// Distribute-rmp shape: { title, body, contributor: {...} }
|
||||||
|
if (typeof raw.title === "string" && typeof raw.body === "string") {
|
||||||
|
const d = raw as DistributePayload;
|
||||||
|
const contributorName = d.contributor?.contact_name || d.contributor?.firm_name || undefined;
|
||||||
|
const contributorEmail = d.contributor?.email || undefined;
|
||||||
|
return {
|
||||||
|
rawTitle: d.title,
|
||||||
|
rawContent: d.body.replace(/<[^>]*>/g, "").replace(/\s+/g, " ").trim(),
|
||||||
|
rawHtml: d.body,
|
||||||
|
contributorName,
|
||||||
|
contributorEmail,
|
||||||
|
source: "distribute-rmp",
|
||||||
|
metadata: {
|
||||||
|
distribute_submission_id: d.submission_id || null,
|
||||||
|
property: d.property || null,
|
||||||
|
featured_image_url: d.featured_image_url || null,
|
||||||
|
attachments: d.attachments || [],
|
||||||
|
contributor_firm: d.contributor?.firm_name || null,
|
||||||
|
contributor_phone: d.contributor?.phone || null,
|
||||||
|
contributor_id: d.contributor?.id || null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const internalKey = process.env.BB_INTERNAL_INGEST_KEY;
|
const auth = checkAuth(req);
|
||||||
if (internalKey) {
|
if (!auth.ok) {
|
||||||
const got = req.headers.get("x-internal-key");
|
return NextResponse.json({ error: auth.reason || "Unauthorized" }, { status: 401 });
|
||||||
if (got !== internalKey) {
|
|
||||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = (await req.json()) as SubmitBody;
|
const raw = await req.json();
|
||||||
if (!body.rawTitle || !body.rawContent) {
|
const payload = normalize(raw);
|
||||||
return NextResponse.json({ error: "rawTitle and rawContent required" }, { status: 400 });
|
if (!payload) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "expected { rawTitle, rawContent } or { title, body }" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const submissionId = await ingestRawSubmission(body);
|
const submissionId = await ingestRawSubmission(payload);
|
||||||
const outcome = await rewriteSubmission({ submissionId, personaSlug: body.personaSlug });
|
const outcome = await rewriteSubmission({ submissionId, personaSlug: payload.personaSlug });
|
||||||
|
|
||||||
return NextResponse.json({ ok: true, submissionId, outcome });
|
return NextResponse.json({ ok: true, submissionId, outcome });
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|||||||
@@ -28,10 +28,30 @@ interface ImportedPostRow {
|
|||||||
wp_published_at: string | null;
|
wp_published_at: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FALLBACK_IMAGE = "/assets/images/article-placeholder.svg";
|
interface RewriteRow {
|
||||||
|
id: string;
|
||||||
|
slug: string;
|
||||||
|
title: string;
|
||||||
|
excerpt: string | null;
|
||||||
|
body: string;
|
||||||
|
category: string | null;
|
||||||
|
tags: string[] | null;
|
||||||
|
status: string;
|
||||||
|
published_at: string | null;
|
||||||
|
created_at: string;
|
||||||
|
persona: PersonaJoin | PersonaJoin[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PersonaJoin {
|
||||||
|
slug: string;
|
||||||
|
name: string;
|
||||||
|
beat: string | null;
|
||||||
|
avatar_url: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FALLBACK_IMAGE = "/legacy/site/no_image.png";
|
||||||
|
const REWRITE_FALLBACK_IMAGE = "/assets/images/article-placeholder.svg";
|
||||||
|
|
||||||
// Tag → section mapping. Source: tag distribution observed in
|
|
||||||
// bb.wp_imported_posts. Matches against any overlap (lowercased).
|
|
||||||
type Section = Article["section"];
|
type Section = Article["section"];
|
||||||
const SECTION_TAGS: Record<Section, string[]> = {
|
const SECTION_TAGS: Record<Section, string[]> = {
|
||||||
gear: [
|
gear: [
|
||||||
@@ -51,7 +71,14 @@ const SECTION_TAGS: Record<Section, string[]> = {
|
|||||||
"BroadcastAsia", "BroadcastAsia 2026", "Broadcast Asia 2026",
|
"BroadcastAsia", "BroadcastAsia 2026", "Broadcast Asia 2026",
|
||||||
"InfoComm", "MPTS",
|
"InfoComm", "MPTS",
|
||||||
],
|
],
|
||||||
news: [], // catch-all — no tag filter
|
news: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const SECTION_CATEGORIES: Record<Section, string[]> = {
|
||||||
|
gear: ["Gear", "Reviews"],
|
||||||
|
technology: ["Technology", "Streaming", "Live Production"],
|
||||||
|
"show-coverage": ["Show Coverage"],
|
||||||
|
news: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
function authorSlug(name: string | null): string {
|
function authorSlug(name: string | null): string {
|
||||||
@@ -80,16 +107,9 @@ function readTime(content: string | null): string {
|
|||||||
return `${minutes} min read`;
|
return `${minutes} min read`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Featured-image URLs that we treat as "no image" — historical placeholders
|
|
||||||
// that should resolve to the clean dark placeholder instead of the grey box.
|
|
||||||
const BROKEN_IMAGE_PATTERNS = /no_image|BB_Gray|placeholder/i;
|
|
||||||
|
|
||||||
function rowToArticle(row: ImportedPostRow, section: Section = "news"): Article {
|
function rowToArticle(row: ImportedPostRow, section: Section = "news"): Article {
|
||||||
const rawImage = row.featured_image && !BROKEN_IMAGE_PATTERNS.test(row.featured_image)
|
const image = row.featured_image
|
||||||
? row.featured_image
|
? rewriteLegacyImageUrl(row.featured_image)
|
||||||
: null;
|
|
||||||
const image = rawImage
|
|
||||||
? rewriteLegacyImageUrl(rawImage)
|
|
||||||
: FALLBACK_IMAGE;
|
: FALLBACK_IMAGE;
|
||||||
const content = rewriteLegacyImageUrlsInHtml(row.content || "");
|
const content = rewriteLegacyImageUrlsInHtml(row.content || "");
|
||||||
const author = row.author_name || "Staff Reporter";
|
const author = row.author_name || "Staff Reporter";
|
||||||
@@ -112,9 +132,77 @@ function rowToArticle(row: ImportedPostRow, section: Section = "news"): Article
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function inferSectionFromCategory(cat: string | null): Section {
|
||||||
|
if (!cat) return "news";
|
||||||
|
const c = cat.toLowerCase();
|
||||||
|
if (c === "post production" || c === "post-production") return "technology";
|
||||||
|
if (c === "streaming" || c === "technology" || c === "live production") return "technology";
|
||||||
|
if (c === "sports") return "news";
|
||||||
|
if (c === "business") return "news";
|
||||||
|
return "news";
|
||||||
|
}
|
||||||
|
|
||||||
|
function rewriteRowToArticle(row: RewriteRow, sectionOverride?: Section): Article {
|
||||||
|
const sortedPublishedAt = row.published_at || row.created_at;
|
||||||
|
const persona = Array.isArray(row.persona) ? row.persona[0] || null : row.persona || null;
|
||||||
|
const author = persona?.name || "Staff Reporter";
|
||||||
|
const personaSlug = persona?.slug || "staff-reporter";
|
||||||
|
const avatar = persona?.avatar_url || FALLBACK_IMAGE;
|
||||||
|
return {
|
||||||
|
slug: row.slug,
|
||||||
|
section: sectionOverride || inferSectionFromCategory(row.category),
|
||||||
|
title: row.title,
|
||||||
|
excerpt: row.excerpt || "",
|
||||||
|
category: (row.category || "NEWS").toUpperCase(),
|
||||||
|
date: formatDate(sortedPublishedAt),
|
||||||
|
author,
|
||||||
|
authorSlug: personaSlug,
|
||||||
|
authorTitle: persona?.beat ? humanBeat(persona.beat) : "Staff Reporter",
|
||||||
|
authorAvatar: avatar,
|
||||||
|
image: REWRITE_FALLBACK_IMAGE,
|
||||||
|
alt: row.title,
|
||||||
|
readTime: readTime(row.body),
|
||||||
|
tags: row.tags || [],
|
||||||
|
content: row.body,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function humanBeat(beat: string): string {
|
||||||
|
return beat
|
||||||
|
.split(/[-_]/)
|
||||||
|
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
||||||
|
.join(" ")
|
||||||
|
.replace("M And A", "M&A");
|
||||||
|
}
|
||||||
|
|
||||||
const SELECT_COLS =
|
const SELECT_COLS =
|
||||||
"wp_id,wp_slug,title,excerpt,content,author_name,category,tags,featured_image,featured_image_alt,status,wp_published_at";
|
"wp_id,wp_slug,title,excerpt,content,author_name,category,tags,featured_image,featured_image_alt,status,wp_published_at";
|
||||||
|
|
||||||
|
const REWRITE_SELECT_COLS =
|
||||||
|
"id,slug,title,excerpt,body,category,tags,status,published_at,created_at,persona:ai_personas(slug,name,beat,avatar_url)";
|
||||||
|
|
||||||
|
function rewriteOrderKey(a: RewriteRow): number {
|
||||||
|
return new Date(a.published_at || a.created_at || 0).getTime();
|
||||||
|
}
|
||||||
|
function legacyOrderKey(a: ImportedPostRow): number {
|
||||||
|
return new Date(a.wp_published_at || 0).getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchPublishedRewrites(limit: number): Promise<RewriteRow[]> {
|
||||||
|
try {
|
||||||
|
const { data, error } = await client()
|
||||||
|
.from("ai_rewritten_articles")
|
||||||
|
.select(REWRITE_SELECT_COLS)
|
||||||
|
.eq("status", "published")
|
||||||
|
.order("published_at", { ascending: false, nullsFirst: false })
|
||||||
|
.limit(limit);
|
||||||
|
if (error || !data) return [];
|
||||||
|
return data as unknown as RewriteRow[];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function getLegacyArticleBySlug(slug: string): Promise<Article | null> {
|
export async function getLegacyArticleBySlug(slug: string): Promise<Article | null> {
|
||||||
try {
|
try {
|
||||||
const { data, error } = await client()
|
const { data, error } = await client()
|
||||||
@@ -123,11 +211,23 @@ export async function getLegacyArticleBySlug(slug: string): Promise<Article | nu
|
|||||||
.eq("wp_slug", slug)
|
.eq("wp_slug", slug)
|
||||||
.eq("status", "published")
|
.eq("status", "published")
|
||||||
.maybeSingle();
|
.maybeSingle();
|
||||||
if (error || !data) return null;
|
if (!error && data) return rowToArticle(data as ImportedPostRow);
|
||||||
return rowToArticle(data as ImportedPostRow);
|
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
// fall through
|
||||||
}
|
}
|
||||||
|
// Try AI rewrites
|
||||||
|
try {
|
||||||
|
const { data, error } = await client()
|
||||||
|
.from("ai_rewritten_articles")
|
||||||
|
.select(REWRITE_SELECT_COLS)
|
||||||
|
.eq("slug", slug)
|
||||||
|
.eq("status", "published")
|
||||||
|
.maybeSingle();
|
||||||
|
if (!error && data) return rewriteRowToArticle(data as unknown as RewriteRow);
|
||||||
|
} catch {
|
||||||
|
// fall through
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getLegacyRelatedArticles(
|
export async function getLegacyRelatedArticles(
|
||||||
@@ -152,12 +252,13 @@ export async function getLegacyRelatedArticles(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Section-filtered listing. `news` returns all; gear/technology/show-coverage
|
|
||||||
* filter by tag overlap against SECTION_TAGS. */
|
|
||||||
export async function getLegacyArticlesBySection(
|
export async function getLegacyArticlesBySection(
|
||||||
section: Section,
|
section: Section,
|
||||||
limit = 200,
|
limit = 200,
|
||||||
): Promise<Article[]> {
|
): Promise<Article[]> {
|
||||||
|
let legacy: ImportedPostRow[] = [];
|
||||||
|
let rewrites: RewriteRow[] = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let q = client()
|
let q = client()
|
||||||
.from("wp_imported_posts")
|
.from("wp_imported_posts")
|
||||||
@@ -166,15 +267,34 @@ export async function getLegacyArticlesBySection(
|
|||||||
.order("wp_published_at", { ascending: false })
|
.order("wp_published_at", { ascending: false })
|
||||||
.limit(limit);
|
.limit(limit);
|
||||||
const tags = SECTION_TAGS[section];
|
const tags = SECTION_TAGS[section];
|
||||||
if (tags.length > 0) {
|
if (tags.length > 0) q = q.overlaps("tags", tags);
|
||||||
q = q.overlaps("tags", tags);
|
|
||||||
}
|
|
||||||
const { data, error } = await q;
|
const { data, error } = await q;
|
||||||
if (error || !data) return [];
|
if (!error && data) legacy = data as ImportedPostRow[];
|
||||||
return (data as ImportedPostRow[]).map((r) => rowToArticle(r, section));
|
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
// ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let q = client()
|
||||||
|
.from("ai_rewritten_articles")
|
||||||
|
.select(REWRITE_SELECT_COLS)
|
||||||
|
.eq("status", "published")
|
||||||
|
.order("published_at", { ascending: false, nullsFirst: false })
|
||||||
|
.limit(limit);
|
||||||
|
const cats = SECTION_CATEGORIES[section];
|
||||||
|
if (cats.length > 0) q = q.in("category", cats);
|
||||||
|
const { data, error } = await q;
|
||||||
|
if (!error && data) rewrites = data as unknown as RewriteRow[];
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
const merged: Array<{ ts: number; article: Article }> = [
|
||||||
|
...legacy.map((r) => ({ ts: legacyOrderKey(r), article: rowToArticle(r, section) })),
|
||||||
|
...rewrites.map((r) => ({ ts: rewriteOrderKey(r), article: rewriteRowToArticle(r, section) })),
|
||||||
|
];
|
||||||
|
merged.sort((a, b) => b.ts - a.ts);
|
||||||
|
return merged.slice(0, limit).map((m) => m.article);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function searchLegacyArticles(
|
export async function searchLegacyArticles(
|
||||||
@@ -183,8 +303,12 @@ export async function searchLegacyArticles(
|
|||||||
): Promise<Article[]> {
|
): Promise<Article[]> {
|
||||||
const q = (query || "").trim();
|
const q = (query || "").trim();
|
||||||
if (!q) return [];
|
if (!q) return [];
|
||||||
|
const like = `%${q.replace(/[%_]/g, " ")}%`;
|
||||||
|
|
||||||
|
let legacy: ImportedPostRow[] = [];
|
||||||
|
let rewrites: RewriteRow[] = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const like = `%${q.replace(/[%_]/g, " ")}%`;
|
|
||||||
const { data, error } = await client()
|
const { data, error } = await client()
|
||||||
.from("wp_imported_posts")
|
.from("wp_imported_posts")
|
||||||
.select(SELECT_COLS)
|
.select(SELECT_COLS)
|
||||||
@@ -192,14 +316,35 @@ export async function searchLegacyArticles(
|
|||||||
.or(`title.ilike.${like},excerpt.ilike.${like},author_name.ilike.${like}`)
|
.or(`title.ilike.${like},excerpt.ilike.${like},author_name.ilike.${like}`)
|
||||||
.order("wp_published_at", { ascending: false })
|
.order("wp_published_at", { ascending: false })
|
||||||
.limit(limit);
|
.limit(limit);
|
||||||
if (error || !data) return [];
|
if (!error && data) legacy = data as ImportedPostRow[];
|
||||||
return (data as ImportedPostRow[]).map((r) => rowToArticle(r, "news"));
|
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
// ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data, error } = await client()
|
||||||
|
.from("ai_rewritten_articles")
|
||||||
|
.select(REWRITE_SELECT_COLS)
|
||||||
|
.eq("status", "published")
|
||||||
|
.or(`title.ilike.${like},excerpt.ilike.${like}`)
|
||||||
|
.order("published_at", { ascending: false, nullsFirst: false })
|
||||||
|
.limit(limit);
|
||||||
|
if (!error && data) rewrites = data as unknown as RewriteRow[];
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
const merged: Array<{ ts: number; article: Article }> = [
|
||||||
|
...legacy.map((r) => ({ ts: legacyOrderKey(r), article: rowToArticle(r, "news") })),
|
||||||
|
...rewrites.map((r) => ({ ts: rewriteOrderKey(r), article: rewriteRowToArticle(r) })),
|
||||||
|
];
|
||||||
|
merged.sort((a, b) => b.ts - a.ts);
|
||||||
|
return merged.slice(0, limit).map((m) => m.article);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getLegacyRecentSlugs(limit = 200): Promise<string[]> {
|
export async function getLegacyRecentSlugs(limit = 200): Promise<string[]> {
|
||||||
|
let legacy: string[] = [];
|
||||||
|
let rewrites: string[] = [];
|
||||||
try {
|
try {
|
||||||
const { data } = await client()
|
const { data } = await client()
|
||||||
.from("wp_imported_posts")
|
.from("wp_imported_posts")
|
||||||
@@ -207,15 +352,28 @@ export async function getLegacyRecentSlugs(limit = 200): Promise<string[]> {
|
|||||||
.eq("status", "published")
|
.eq("status", "published")
|
||||||
.order("wp_published_at", { ascending: false })
|
.order("wp_published_at", { ascending: false })
|
||||||
.limit(limit);
|
.limit(limit);
|
||||||
return (data || []).map((r: { wp_slug: string }) => r.wp_slug);
|
legacy = (data || []).map((r: { wp_slug: string }) => r.wp_slug);
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
// ignore
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
const { data } = await client()
|
||||||
|
.from("ai_rewritten_articles")
|
||||||
|
.select("slug,published_at")
|
||||||
|
.eq("status", "published")
|
||||||
|
.order("published_at", { ascending: false, nullsFirst: false })
|
||||||
|
.limit(limit);
|
||||||
|
rewrites = (data || []).map((r: { slug: string }) => r.slug);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
return [...rewrites, ...legacy].slice(0, limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getLegacyRecentSitemapEntries(
|
export async function getLegacyRecentSitemapEntries(
|
||||||
limit = 5000,
|
limit = 5000,
|
||||||
): Promise<{ slug: string; date: string }[]> {
|
): Promise<{ slug: string; date: string }[]> {
|
||||||
|
const out: { slug: string; date: string }[] = [];
|
||||||
try {
|
try {
|
||||||
const { data } = await client()
|
const { data } = await client()
|
||||||
.from("wp_imported_posts")
|
.from("wp_imported_posts")
|
||||||
@@ -223,11 +381,36 @@ export async function getLegacyRecentSitemapEntries(
|
|||||||
.eq("status", "published")
|
.eq("status", "published")
|
||||||
.order("wp_published_at", { ascending: false })
|
.order("wp_published_at", { ascending: false })
|
||||||
.limit(limit);
|
.limit(limit);
|
||||||
return (data || []).map((r: { wp_slug: string; wp_published_at: string | null }) => ({
|
for (const r of data || []) {
|
||||||
slug: r.wp_slug,
|
out.push({
|
||||||
date: r.wp_published_at || new Date().toISOString(),
|
slug: (r as any).wp_slug,
|
||||||
}));
|
date: (r as any).wp_published_at || new Date().toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
// ignore
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
const { data } = await client()
|
||||||
|
.from("ai_rewritten_articles")
|
||||||
|
.select("slug,published_at,created_at")
|
||||||
|
.eq("status", "published")
|
||||||
|
.order("published_at", { ascending: false, nullsFirst: false })
|
||||||
|
.limit(limit);
|
||||||
|
for (const r of data || []) {
|
||||||
|
out.push({
|
||||||
|
slug: (r as any).slug,
|
||||||
|
date: (r as any).published_at || (r as any).created_at || new Date().toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
out.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
||||||
|
return out.slice(0, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getRecentAiRewrites(limit = 20): Promise<Article[]> {
|
||||||
|
const rows = await fetchPublishedRewrites(limit);
|
||||||
|
return rows.map((r) => rewriteRowToArticle(r));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user