From 8d548bdc9dbff372788e9c701e5671bf34724f8f Mon Sep 17 00:00:00 2001 From: Local Administrator Date: Fri, 5 Jun 2026 14:32:04 +0000 Subject: [PATCH] =?UTF-8?q?fix(av-beat):=20skip=5Frewrite=20passthrough=20?= =?UTF-8?q?=E2=80=94=20Mike=20Nann=20(Writeturn)=20now=20publishes=20verba?= =?UTF-8?q?tim=20w/=20noindex?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mike Nann's distribute.users record has skip_rewrite=true (set when task #19 added the column). distribute-rmp's submit route forwards the flag in its payload (task #20). BB's receive route handles it (task #21). AV Beat's receive route was missing the branch entirely AND was writing to .schema('bb') instead of 'av' — a copy-paste bug from when this file was cloned from BB. So every Mike Nann submission to AV Beat was either landing in BB's schema (wrong) or trying to and failing. Fixed in this commit: src/app/api/submission-receive/route.ts • .schema('bb') → .schema('av') (the copy-paste bug) • Added skip_rewrite branch: when payload has skip_rewrite=true, insert straight into av.wp_imported_posts with status=published, noindex=true, and the contributor's firm/contact_name as byline. Skips the native_articles → AI-rewrite path entirely. Mirrors BB's pattern exactly. Audit row still written to original_submissions. src/lib/articles/types.ts • Article.noindex?: boolean — surfaces the DB column to the renderer. src/lib/articles/legacy-source.ts • SELECT_COLS adds noindex • ImportedPostRow gains noindex field • rowToArticle propagates !!row.noindex src/app/news/[slug]/page.tsx + src/app/articles/[slug]/page.tsx • generateMetadata: when article.noindex, set robots = no-index + no-follow + nocache + googleBot no-image-index + other meta key "googlebot-news": "noindex". Matches BB's pattern. src/app/news-sitemap.xml/route.ts • Adds .or('noindex.is.null,noindex.eq.false') so passthrough rows don't pollute the Google News sitemap. src/app/rss/route.ts • Filters out a.noindex before emitting items. Mike's existing user record is already flagged (DB verified), so the moment this deploys + he submits, the next press release publishes verbatim, gets noindex on every Google/social surface, and shows up on the AV Beat news index without going through the AI rewrite job. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/app/api/submission-receive/route.ts | 48 +++++++++++++++++++++++-- src/app/articles/[slug]/page.tsx | 5 +++ src/app/news-sitemap.xml/route.ts | 1 + src/app/news/[slug]/page.tsx | 7 ++++ src/app/rss/route.ts | 1 + src/lib/articles/legacy-source.ts | 4 ++- src/lib/articles/types.ts | 4 +++ 7 files changed, 67 insertions(+), 3 deletions(-) diff --git a/src/app/api/submission-receive/route.ts b/src/app/api/submission-receive/route.ts index d49ff77..1ce0ba6 100644 --- a/src/app/api/submission-receive/route.ts +++ b/src/app/api/submission-receive/route.ts @@ -110,8 +110,52 @@ export async function POST(request: NextRequest) { .trim() .slice(0, 240); + // Passthrough mode (skip_rewrite=true): publish the original PR body + // VERBATIM with byline = firm + noindex=true on the wp_imported_posts + // row. No AI rewrite, no Google News indexing, no sitemap. For clients + // flagged in distribute.users.skip_rewrite (e.g. Mike Nann / Writeturn). + const skipRewrite = !!body?.skip_rewrite; + if (skipRewrite) { + const wpId = Math.floor(900_000 + Math.random() * 99_000_000); + const wpSlug = `${baseSlug}-pr-${randSuffix()}`; + const byline = contributor?.contact_name || contributor?.firm_name || 'PR Submission'; + const { error: wpErr } = await supabase + .schema('av') + .from('wp_imported_posts') + .insert({ + wp_id: wpId, + wp_slug: wpSlug, + title, + excerpt, + content, + author_name: byline, + category: 'Industry News', + status: 'published', + wp_published_at: new Date().toISOString(), + noindex: true, + }); + if (wpErr) { + return NextResponse.json({ error: wpErr.message, where: 'passthrough wp_imported_posts insert' }, { status: 500 }); + } + + // Keep the audit trail in original_submissions even for passthrough. + await supabase.schema('av').from('original_submissions').insert({ + submission_id: submissionId, + property, + title, + body: content, + featured_image_url: featuredImage, + attachments, + contributor, + contributor_email: contributor?.email || null, + raw_payload: body, + native_article_id: null, + }); + return NextResponse.json({ ok: true, slug: wpSlug, status: 'published_passthrough', noindex: true }, { status: 201 }); + } + const { data: article, error: artErr } = await supabase - .schema('bb') + .schema('av') .from('native_articles') .insert({ slug, @@ -129,7 +173,7 @@ export async function POST(request: NextRequest) { if (artErr) return NextResponse.json({ error: artErr.message, where: 'native_articles insert' }, { status: 500 }); const { error: subErr } = await supabase - .schema('bb') + .schema('av') .from('original_submissions') .insert({ submission_id: submissionId, diff --git a/src/app/articles/[slug]/page.tsx b/src/app/articles/[slug]/page.tsx index 81f613c..0a9557b 100644 --- a/src/app/articles/[slug]/page.tsx +++ b/src/app/articles/[slug]/page.tsx @@ -52,10 +52,15 @@ export async function generateMetadata({ params }: PageProps): Promise if (!article) { return { title: "Article Not Found", description: "The article you are looking for does not exist." }; } + const noindex = !!article.noindex; return { title: article.title, description: cleanExcerpt(article.excerpt), alternates: { canonical: `/articles/${article.slug}` }, + robots: noindex + ? { index: false, follow: false, nocache: true, googleBot: { index: false, follow: false, noimageindex: true } } + : undefined, + other: noindex ? { "googlebot-news": "noindex" } : undefined, openGraph: { type: "article", title: article.title, diff --git a/src/app/news-sitemap.xml/route.ts b/src/app/news-sitemap.xml/route.ts index bff4779..b78df43 100644 --- a/src/app/news-sitemap.xml/route.ts +++ b/src/app/news-sitemap.xml/route.ts @@ -28,6 +28,7 @@ export async function GET() { .select('wp_slug, title, wp_published_at, ai_rewritten_at, author_name, tags') .eq('status', 'published') .not('ai_rewritten_at', 'is', null) + .or('noindex.is.null,noindex.eq.false') // exclude noindex=true (passthrough PRs) .gte('wp_published_at', since) .order('wp_published_at', { ascending: false }) .limit(1000), diff --git a/src/app/news/[slug]/page.tsx b/src/app/news/[slug]/page.tsx index b7238ef..2f5a010 100644 --- a/src/app/news/[slug]/page.tsx +++ b/src/app/news/[slug]/page.tsx @@ -64,10 +64,17 @@ export async function generateMetadata({ params }: PageProps): Promise // contain

...

, which Next.js then HTML-escapes, surfacing as // literal `<p>` in page source. const cleanDesc = cleanExcerpt(article.excerpt); + const noindex = !!article.noindex; return { title: `${article.title} — AV Beat`, description: cleanDesc, alternates: { canonical: `/news/${article.slug}` }, + // Passthrough PR submissions (Mike Nann / Writeturn etc.) get a hard + // noindex on robots + googlebot-news so they don't pollute Google News. + robots: noindex + ? { index: false, follow: false, nocache: true, googleBot: { index: false, follow: false, noimageindex: true } } + : undefined, + other: noindex ? { "googlebot-news": "noindex" } : undefined, openGraph: { type: "article", title: article.title, diff --git a/src/app/rss/route.ts b/src/app/rss/route.ts index fccd916..6306d8f 100644 --- a/src/app/rss/route.ts +++ b/src/app/rss/route.ts @@ -19,6 +19,7 @@ export async function GET() { try { const articles = await getLegacyArticlesBySection("news", 50); items = articles + .filter((a) => !a.noindex) // exclude passthrough PR submissions .map((a) => { const url = `${SITE_URL}/news/${a.slug}`; const pubDate = a.date ? new Date(a.date).toUTCString() : new Date().toUTCString(); diff --git a/src/lib/articles/legacy-source.ts b/src/lib/articles/legacy-source.ts index bcf330b..48b61a4 100644 --- a/src/lib/articles/legacy-source.ts +++ b/src/lib/articles/legacy-source.ts @@ -26,6 +26,7 @@ interface ImportedPostRow { featured_image_alt: string | null; status: string; wp_published_at: string | null; + noindex: boolean | null; } interface RewriteRow { @@ -146,6 +147,7 @@ function rowToArticle(row: ImportedPostRow, section: Section = "news"): Article readTime: readTime(row.content), tags: row.tags || [], content, + noindex: !!row.noindex, }; } @@ -193,7 +195,7 @@ function humanBeat(beat: string): string { } 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,noindex"; const REWRITE_SELECT_COLS = "id,slug,title,excerpt,body,category,tags,status,published_at,created_at,persona:ai_personas(slug,name,beat,avatar_url)"; diff --git a/src/lib/articles/types.ts b/src/lib/articles/types.ts index c6a12b9..b6e9755 100644 --- a/src/lib/articles/types.ts +++ b/src/lib/articles/types.ts @@ -14,4 +14,8 @@ export interface Article { readTime: string; tags: string[]; content: string; + /** When true, render robots noindex + googlebot-news noindex and exclude + * from sitemap / RSS. Set on passthrough-mode imports from contributors + * flagged in distribute.users.skip_rewrite (e.g. Mike Nann / Writeturn). */ + noindex?: boolean; }