fix(av-beat): skip_rewrite passthrough — Mike Nann (Writeturn) now publishes verbatim w/ noindex

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) <noreply@anthropic.com>
This commit is contained in:
2026-06-05 14:32:04 +00:00
parent 45cf96a203
commit 8d548bdc9d
7 changed files with 67 additions and 3 deletions

View File

@@ -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,

View File

@@ -52,10 +52,15 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
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,

View File

@@ -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),

View File

@@ -64,10 +64,17 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
// contain <p>...</p>, which Next.js then HTML-escapes, surfacing as
// literal `&lt;p&gt;` 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,

View File

@@ -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();

View File

@@ -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)";

View File

@@ -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;
}