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.
This commit is contained in:
Ryan Salazar
2026-05-25 17:03:19 +00:00
parent 09fada759e
commit 107840c5f3
3 changed files with 97 additions and 1 deletions

View File

@@ -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) =>
({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&apos;' }[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(` <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',
},
});
}

View File

@@ -16,7 +16,10 @@ export default function robots(): MetadataRoute.Robots {
disallow: ['/api/', '/admin/', '/private/', '/dashboard/'], disallow: ['/api/', '/admin/', '/private/', '/dashboard/'],
}, },
], ],
sitemap: `${baseUrl}/sitemap.xml`, sitemap: [
`${baseUrl}/sitemap.xml`,
`${baseUrl}/news-sitemap.xml`,
],
host: baseUrl, host: baseUrl,
}; };
} }

View File

@@ -281,6 +281,9 @@ export async function getLegacyArticlesBySection(
.from("wp_imported_posts") .from("wp_imported_posts")
.select(SELECT_COLS) .select(SELECT_COLS)
.eq("status", "published") .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 }) .order("wp_published_at", { ascending: false })
.limit(limit); .limit(limit);
const tags = SECTION_TAGS[section]; const tags = SECTION_TAGS[section];