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>
92 lines
3.4 KiB
TypeScript
92 lines
3.4 KiB
TypeScript
import { createClient } from '@supabase/supabase-js';
|
|
|
|
export const dynamic = 'force-dynamic';
|
|
export const revalidate = 600; // 10 min — Google rechecks frequently
|
|
|
|
// Google News sitemap. Only original-editorial articles (ai_rewritten_at IS
|
|
// NOT NULL or rewritten-articles surface) from the last 48 hours so we
|
|
// stay inside the GN window. Standard sitemap.xml covers older pages.
|
|
const PUBLICATION_NAME = 'AV Beat';
|
|
const PUBLICATION_LANG = 'en';
|
|
const HOST = 'https://avbeat.com';
|
|
|
|
function esc(s: string): string {
|
|
return (s || '').replace(/[&<>"']/g, (c) =>
|
|
({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c] as string));
|
|
}
|
|
|
|
export async function GET() {
|
|
const url = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
|
const key = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
|
const schema = process.env.NEXT_PUBLIC_SUPABASE_SCHEMA || 'bb';
|
|
const sb = createClient(url, key, { db: { schema: schema as 'public' }, auth: { persistSession: false } });
|
|
|
|
const since = new Date(Date.now() - 48 * 3600 * 1000).toISOString();
|
|
|
|
const [{ data: imported }, { data: rewritten }] = await Promise.all([
|
|
sb.from('wp_imported_posts')
|
|
.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),
|
|
sb.from('ai_rewritten_articles')
|
|
.select('slug, title, published_at, category, source_author')
|
|
.eq('status', 'published')
|
|
.gte('published_at', since)
|
|
.order('published_at', { ascending: false })
|
|
.limit(1000),
|
|
]);
|
|
|
|
const entries: string[] = [];
|
|
|
|
for (const a of imported || []) {
|
|
const pubDate = a.wp_published_at || a.ai_rewritten_at;
|
|
if (!pubDate) continue;
|
|
const tags = Array.isArray(a.tags) ? (a.tags as string[]).slice(0, 5).join(', ') : '';
|
|
entries.push(` <url>
|
|
<loc>${HOST}/news/${esc(a.wp_slug)}</loc>
|
|
<news:news>
|
|
<news:publication>
|
|
<news:name>${PUBLICATION_NAME}</news:name>
|
|
<news:language>${PUBLICATION_LANG}</news:language>
|
|
</news:publication>
|
|
<news:publication_date>${pubDate}</news:publication_date>
|
|
<news:title>${esc(a.title)}</news:title>${tags ? `\n <news:keywords>${esc(tags)}</news:keywords>` : ''}
|
|
</news:news>
|
|
</url>`);
|
|
}
|
|
|
|
for (const a of rewritten || []) {
|
|
if (!a.published_at) continue;
|
|
entries.push(` <url>
|
|
<loc>${HOST}/articles/${esc(a.slug)}</loc>
|
|
<news:news>
|
|
<news:publication>
|
|
<news:name>${PUBLICATION_NAME}</news:name>
|
|
<news:language>${PUBLICATION_LANG}</news:language>
|
|
</news:publication>
|
|
<news:publication_date>${a.published_at}</news:publication_date>
|
|
<news:title>${esc(a.title)}</news:title>${a.category ? `\n <news:keywords>${esc(a.category)}</news:keywords>` : ''}
|
|
</news:news>
|
|
</url>`);
|
|
}
|
|
|
|
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
|
|
xmlns:news="http://www.google.com/schemas/sitemap-news/0.9">
|
|
${entries.join('\n')}
|
|
</urlset>
|
|
`;
|
|
|
|
return new Response(body, {
|
|
status: 200,
|
|
headers: {
|
|
'content-type': 'application/xml; charset=utf-8',
|
|
'cache-control': 'public, max-age=300, s-maxage=600',
|
|
},
|
|
});
|
|
}
|