From 107840c5f38261a0c4e7126370c3948234b074f7 Mon Sep 17 00:00:00 2001 From: Ryan Salazar Date: Mon, 25 May 2026 17:03:19 +0000 Subject: [PATCH] google news phase 1 - Homepage / Industry News query filters wp_imported_posts to ai_rewritten_at IS NOT NULL so raw press releases never surface on the editorial feed (NewsArticle JSON-LD was already in place). - New /news-sitemap.xml route emits a Google News sitemap covering rewritten editorial published in the last 48 hours, with news:publication + news:publication_date + news:title + keywords. - robots.ts now points crawlers at both sitemap.xml and news-sitemap.xml. After deploy, submit broadcastbeat.com to Google News Publisher Center to start serving in News. --- src/app/news-sitemap.xml/route.ts | 90 +++++++++++++++++++++++++++++++ src/app/robots.ts | 5 +- src/lib/articles/legacy-source.ts | 3 ++ 3 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 src/app/news-sitemap.xml/route.ts diff --git a/src/app/news-sitemap.xml/route.ts b/src/app/news-sitemap.xml/route.ts new file mode 100644 index 0000000..c6b36fa --- /dev/null +++ b/src/app/news-sitemap.xml/route.ts @@ -0,0 +1,90 @@ +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 = 'Broadcast Beat'; +const PUBLICATION_LANG = 'en'; +const HOST = 'https://broadcastbeat.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) + .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(` + ${HOST}/news/${esc(a.wp_slug)} + + + ${PUBLICATION_NAME} + ${PUBLICATION_LANG} + + ${pubDate} + ${esc(a.title)}${tags ? `\n ${esc(tags)}` : ''} + + `); + } + + for (const a of rewritten || []) { + if (!a.published_at) continue; + entries.push(` + ${HOST}/articles/${esc(a.slug)} + + + ${PUBLICATION_NAME} + ${PUBLICATION_LANG} + + ${a.published_at} + ${esc(a.title)}${a.category ? `\n ${esc(a.category)}` : ''} + + `); + } + + const body = ` + +${entries.join('\n')} + +`; + + return new Response(body, { + status: 200, + headers: { + 'content-type': 'application/xml; charset=utf-8', + 'cache-control': 'public, max-age=300, s-maxage=600', + }, + }); +} diff --git a/src/app/robots.ts b/src/app/robots.ts index 16c7a01..329ec35 100644 --- a/src/app/robots.ts +++ b/src/app/robots.ts @@ -16,7 +16,10 @@ export default function robots(): MetadataRoute.Robots { disallow: ['/api/', '/admin/', '/private/', '/dashboard/'], }, ], - sitemap: `${baseUrl}/sitemap.xml`, + sitemap: [ + `${baseUrl}/sitemap.xml`, + `${baseUrl}/news-sitemap.xml`, + ], host: baseUrl, }; } \ No newline at end of file diff --git a/src/lib/articles/legacy-source.ts b/src/lib/articles/legacy-source.ts index 15a8d57..9a74e58 100644 --- a/src/lib/articles/legacy-source.ts +++ b/src/lib/articles/legacy-source.ts @@ -281,6 +281,9 @@ export async function getLegacyArticlesBySection( .from("wp_imported_posts") .select(SELECT_COLS) .eq("status", "published") + // Google News-safe surface: only show articles that have been AI-rewritten + // into original editorial. Raw press-release imports are excluded. + .not("ai_rewritten_at", "is", null) .order("wp_published_at", { ascending: false }) .limit(limit); const tags = SECTION_TAGS[section];