From e4f68713e7e561166756a6d460fb931e25e497f4 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Mon, 23 Feb 2026 13:10:16 +0100 Subject: [PATCH] fix(ci): restore full localized sitemap coverage and strict 404 validation --- app/sitemap.ts | 64 ++++++++++++++++++++++---------- lib/slugs.ts | 85 +++++++++++++++++++++++-------------------- scripts/check-html.ts | 3 +- 3 files changed, 92 insertions(+), 60 deletions(-) diff --git a/app/sitemap.ts b/app/sitemap.ts index 683d7fae..7ddb4107 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -3,6 +3,7 @@ import { MetadataRoute } from 'next'; import { getAllProductsMetadata } from '@/lib/mdx'; import { getAllPostsMetadata } from '@/lib/blog'; import { getAllPagesMetadata } from '@/lib/pages'; +import { mapFileSlugToTranslated } from '@/lib/slugs'; export const revalidate = 3600; // Revalidate every hour @@ -12,28 +13,45 @@ export default async function sitemap(): Promise { : config.baseUrl || 'https://klz-cables.com'; const locales = ['de', 'en']; - const routes = [ - '', - '/blog', - '/contact', - '/team', - '/products', - '/products/low-voltage-cables', - '/products/medium-voltage-cables', - '/products/high-voltage-cables', - '/products/solar-cables', - ]; - const sitemapEntries: MetadataRoute.Sitemap = []; for (const locale of locales) { + // Helper to generate localized URL Segment + const getLocalizedRoute = async (pageKey: string) => { + if (pageKey === '') return ''; + const translated = await mapFileSlugToTranslated(pageKey, locale); + return `/${translated}`; + }; + // Static routes - for (const route of routes) { + const staticPages = ['', 'blog', 'contact', 'team', 'products']; + for (const page of staticPages) { + const localizedRoute = await getLocalizedRoute(page); sitemapEntries.push({ - url: `${baseUrl}/${locale}${route}`, + url: `${baseUrl}/${locale}${localizedRoute}`, lastModified: new Date(), - changeFrequency: route === '' ? 'daily' : 'weekly', - priority: route === '' ? 1 : 0.8, + changeFrequency: page === '' ? 'daily' : 'weekly', + priority: page === '' ? 1 : 0.8, + }); + } + + // Categories routes + const productCategories = [ + 'low-voltage-cables', + 'medium-voltage-cables', + 'high-voltage-cables', + 'solar-cables', + ]; + + const translatedProducts = await mapFileSlugToTranslated('products', locale); + + for (const category of productCategories) { + const translatedCategory = await mapFileSlugToTranslated(category, locale); + sitemapEntries.push({ + url: `${baseUrl}/${locale}/${translatedProducts}/${translatedCategory}`, + lastModified: new Date(), + changeFrequency: 'weekly', + priority: 0.8, }); } @@ -44,8 +62,11 @@ export default async function sitemap(): Promise { const category = product.frontmatter.categories[0]?.toLowerCase().replace(/\s+/g, '-') || 'other'; + const translatedCategory = await mapFileSlugToTranslated(category, locale); + const translatedSlug = await mapFileSlugToTranslated(product.slug, locale); + sitemapEntries.push({ - url: `${baseUrl}/${locale}/products/${category}/${product.slug}`, + url: `${baseUrl}/${locale}/${translatedProducts}/${translatedCategory}/${translatedSlug}`, lastModified: new Date(), changeFrequency: 'monthly', priority: 0.7, @@ -53,12 +74,15 @@ export default async function sitemap(): Promise { } // Blog posts + const translatedBlog = await mapFileSlugToTranslated('blog', locale); const postsMetadata = await getAllPostsMetadata(locale); for (const post of postsMetadata) { if (!post.frontmatter || !post.slug) continue; + const translatedSlug = await mapFileSlugToTranslated(post.slug, locale); + sitemapEntries.push({ - url: `${baseUrl}/${locale}/blog/${post.slug}`, + url: `${baseUrl}/${locale}/${translatedBlog}/${translatedSlug}`, lastModified: new Date(post.frontmatter.date), changeFrequency: 'monthly', priority: 0.6, @@ -70,8 +94,10 @@ export default async function sitemap(): Promise { for (const page of pagesMetadata) { if (!page.slug) continue; + const translatedSlug = await mapFileSlugToTranslated(page.slug, locale); + sitemapEntries.push({ - url: `${baseUrl}/${locale}/${page.slug}`, + url: `${baseUrl}/${locale}/${translatedSlug}`, lastModified: new Date(), changeFrequency: 'monthly', priority: 0.5, diff --git a/lib/slugs.ts b/lib/slugs.ts index f3683c07..c4008674 100644 --- a/lib/slugs.ts +++ b/lib/slugs.ts @@ -1,29 +1,36 @@ -import { getTranslations } from 'next-intl/server'; +import enMessages from '../messages/en.json'; +import deMessages from '../messages/de.json'; + +type Messages = typeof enMessages; +const getMessages = (locale: string): Messages => { + return locale === 'de' ? (deMessages as any) : enMessages; +}; /** * Maps a translated slug to original file slug * @param translatedSlug - The slug from URL (translated) - * @param _locale - The current locale (unused, kept for API consistency) + * @param locale - The current locale * @returns The original file slug, or input slug if no mapping exists */ -export async function mapSlugToFileSlug(translatedSlug: string, _locale: string): Promise { - const t = await getTranslations('Slugs'); - +export async function mapSlugToFileSlug(translatedSlug: string, locale: string): Promise { + const messages = getMessages(locale); + const slugs = messages.Slugs; + // Check pages - if (t.has(`pages.${translatedSlug}`)) { - return t(`pages.${translatedSlug}`); + if (slugs.pages && translatedSlug in slugs.pages) { + return slugs.pages[translatedSlug as keyof typeof slugs.pages]; } - + // Check products - if (t.has(`products.${translatedSlug}`)) { - return t(`products.${translatedSlug}`); + if (slugs.products && translatedSlug in slugs.products) { + return slugs.products[translatedSlug as keyof typeof slugs.products]; } - + // Check categories - if (t.has(`categories.${translatedSlug}`)) { - return t(`categories.${translatedSlug}`); + if (slugs.categories && translatedSlug in slugs.categories) { + return slugs.categories[translatedSlug as keyof typeof slugs.categories]; } - + // Return original slug if no mapping found return translatedSlug; } @@ -31,28 +38,25 @@ export async function mapSlugToFileSlug(translatedSlug: string, _locale: string) /** * Maps an original file slug to translated slug for a locale * @param fileSlug - The original file slug - * @param _locale - The target locale (unused, kept for API consistency) + * @param locale - The target locale * @returns The translated slug, or input slug if no mapping exists */ -export async function mapFileSlugToTranslated(fileSlug: string, _locale: string): Promise { - const t = await getTranslations('Slugs'); - - // Find the key that maps to this file slug - const sections = ['pages', 'products', 'categories']; - - for (const section of sections) { - if (t.has(section)) { - const sectionData = t.raw(section); - if (sectionData && typeof sectionData === 'object') { - for (const [translatedSlug, mappedSlug] of Object.entries(sectionData)) { - if (mappedSlug === fileSlug) { - return translatedSlug; - } +export async function mapFileSlugToTranslated(fileSlug: string, locale: string): Promise { + const messages = getMessages(locale); + const slugs = messages.Slugs; + + const sections = [slugs.pages, slugs.products, slugs.categories]; + + for (const sectionData of sections) { + if (sectionData && typeof sectionData === 'object') { + for (const [translatedSlug, mappedSlug] of Object.entries(sectionData)) { + if (mappedSlug === fileSlug) { + return translatedSlug; } } } } - + // Return original slug if no mapping found return fileSlug; } @@ -60,18 +64,19 @@ export async function mapFileSlugToTranslated(fileSlug: string, _locale: string) /** * Gets all translated slugs for a section * @param section - The section name (pages, products, categories) - * @param _locale - The current locale (unused, kept for API consistency) + * @param locale - The current locale * @returns Object mapping translated slugs to file slugs */ -export async function getSlugMappings(section: 'pages' | 'products' | 'categories', _locale: string): Promise> { - const t = await getTranslations('Slugs'); - - if (t.has(section)) { - const sectionData = t.raw(section); - if (sectionData && typeof (sectionData as any) === 'object') { - return sectionData as Record; - } +export async function getSlugMappings( + section: 'pages' | 'products' | 'categories', + locale: string, +): Promise> { + const messages = getMessages(locale); + const sectionData = messages.Slugs[section]; + + if (sectionData && typeof sectionData === 'object') { + return sectionData as Record; } - + return {}; } diff --git a/scripts/check-html.ts b/scripts/check-html.ts index f8a6d958..b0dcb043 100644 --- a/scripts/check-html.ts +++ b/scripts/check-html.ts @@ -48,12 +48,13 @@ async function main() { try { const res = await axios.get(u, { headers: { Cookie: `klz_gatekeeper_session=${gatekeeperPassword}` }, + validateStatus: (status) => status < 400, }); const filename = `page-${i}.html`; fs.writeFileSync(path.join(outputDir, filename), res.data); } catch (err: any) { console.error(`❌ HTTP Error fetching ${u}: ${err.message}`); - throw err; + throw new Error(`Failed to fetch page: ${u} - ${err.message}`); } }