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:
90
src/app/news-sitemap.xml/route.ts
Normal file
90
src/app/news-sitemap.xml/route.ts
Normal 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) =>
|
||||||
|
({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -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];
|
||||||
|
|||||||
Reference in New Issue
Block a user