From 306e7bb12f7232ec93c6856f7312c4d58815797d Mon Sep 17 00:00:00 2001 From: kayjaydee Date: Wed, 22 Apr 2026 11:20:09 +0200 Subject: [PATCH] feat(07-04): add dynamic sitemap URL feed for bilingual blog articles - Nitro route server/api/__sitemap__/urls.ts via defineSitemapEventHandler - Queries blog_fr + blog_en with literal strings and event first-arg (Pitfalls 1 & 2) - Filters draft=false (D-10, T-07-06 mitigation) - lastmod = updated ?? date (D-09) - Emits hreflang alternates fr/en/x-default for bilingual pairs, none for single-language (D-11) - Feeds @nuxtjs/sitemap via sitemap.sources declared in 07-01 --- server/api/__sitemap__/urls.ts | 76 ++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 server/api/__sitemap__/urls.ts diff --git a/server/api/__sitemap__/urls.ts b/server/api/__sitemap__/urls.ts new file mode 100644 index 0000000..42c7db2 --- /dev/null +++ b/server/api/__sitemap__/urls.ts @@ -0,0 +1,76 @@ +/** + * Dynamic sitemap URL feed for @nuxtjs/sitemap. + * Referenced via nuxt.config.ts > sitemap.sources: ['/api/__sitemap__/urls']. + * Emits /fr/blog/{slug} + /en/blog/{slug} with hreflang alternates for bilingual pairs. + * Excludes drafts (D-10). lastmod = updated ?? date (D-09). See Pitfalls 1, 2, 5, 6 in RESEARCH. + */ +import { queryCollection } from '@nuxt/content/server' +import type { SitemapUrl } from '#sitemap/types' + +const SITE_URL = 'https://killiandalcin.fr' + +type BlogRow = { + path: string + date: string + updated?: string +} + +export default defineSitemapEventHandler(async (event) => { + // Literal collection strings (Pitfall 2). Pass event first (Pitfall 1). + const [frArticles, enArticles] = await Promise.all([ + queryCollection(event, 'blog_fr') + .where('draft', '=', false) + .order('date', 'DESC') + .select('path', 'date', 'updated') + .all() as unknown as Promise, + queryCollection(event, 'blog_en') + .where('draft', '=', false) + .order('date', 'DESC') + .select('path', 'date', 'updated') + .all() as unknown as Promise, + ]) + + // Build slug → { fr?, en? } index for pair detection (D-11) + const extractSlug = (p: string) => p.split('/').filter(Boolean).pop()! + const index = new Map() + for (const a of frArticles) { + const s = extractSlug(a.path) + const e = index.get(s) ?? {} + e.fr = a + index.set(s, e) + } + for (const a of enArticles) { + const s = extractSlug(a.path) + const e = index.get(s) ?? {} + e.en = a + index.set(s, e) + } + + const urls: SitemapUrl[] = [] + for (const [slug, pair] of index) { + const bilingual = !!(pair.fr && pair.en) + const alternatives = bilingual + ? [ + { hreflang: 'fr', href: `${SITE_URL}/fr/blog/${slug}` }, + { hreflang: 'en', href: `${SITE_URL}/en/blog/${slug}` }, + { hreflang: 'x-default', href: `${SITE_URL}/fr/blog/${slug}` }, + ] + : [] + + if (pair.fr) { + urls.push({ + loc: `/fr/blog/${slug}`, + lastmod: pair.fr.updated ?? pair.fr.date, + alternatives, + }) + } + if (pair.en) { + urls.push({ + loc: `/en/blog/${slug}`, + lastmod: pair.en.updated ?? pair.en.date, + alternatives, + }) + } + } + return urls +})