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];