From a7362730baa34bab50cc4bee89c9e7f17ba9c163 Mon Sep 17 00:00:00 2001 From: Jalil Arfaoui Date: Mon, 23 Feb 2026 01:04:02 +0100 Subject: [PATCH] =?UTF-8?q?Sitemap=20hreflang=20corrig=C3=A9=20pour=20URLs?= =?UTF-8?q?=20traduites=20et=20routes=20dynamiques?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remplace l'option i18n du plugin sitemap (qui supposait des chemins identiques entre langues) par un serialize custom basé sur getAlternateUrls(). Ajoute le support des routes dynamiques (albums photo, blog posts) via des patterns regex, et l'entrée statique /photo/blog. Toutes les pages traduites ont désormais des hreflang fr/en/ar + x-default corrects dans le sitemap et le HTML. --- astro.config.mjs | 23 +++++++++++----- src/utils/page-translations.ts | 50 +++++++++++++++++++++++++++++++++- 2 files changed, 65 insertions(+), 8 deletions(-) diff --git a/astro.config.mjs b/astro.config.mjs index fa979ac..b53bbef 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -2,6 +2,7 @@ import { defineConfig } from "astro/config"; import sitemap from "@astrojs/sitemap"; import tailwind from "@astrojs/tailwind"; +import { getAlternateUrls } from "./src/utils/page-translations.ts"; // https://astro.build/config export default defineConfig({ @@ -10,13 +11,21 @@ export default defineConfig({ integrations: [ tailwind(), sitemap({ - i18n: { - defaultLocale: "fr", - locales: { - fr: "fr", - en: "en", - ar: "ar", - }, + serialize(item) { + const url = new URL(item.url); + const pathname = decodeURIComponent(url.pathname).replace(/\/$/, "") || "/"; + const alternates = getAlternateUrls(pathname); + if (alternates) { + const fullUrl = (path) => + path === "/" ? `${url.origin}/` : `${url.origin}${path}/`; + item.links = [ + { lang: "fr", url: fullUrl(alternates.fr) }, + { lang: "en", url: fullUrl(alternates.en) }, + { lang: "ar", url: fullUrl(alternates.ar) }, + { lang: "x-default", url: fullUrl(alternates.fr) }, + ]; + } + return item; }, }), ], diff --git a/src/utils/page-translations.ts b/src/utils/page-translations.ts index df469dd..0763a97 100644 --- a/src/utils/page-translations.ts +++ b/src/utils/page-translations.ts @@ -1,4 +1,5 @@ import type { Locale } from "./i18n"; +import { getPhotoAlbumsPath, getPhotoBlogPath } from "./i18n"; /** * Groupes de pages traduites. @@ -14,6 +15,7 @@ const translationGroups: Record[] = [ { fr: "/code/competences", en: "/en/code/skills", ar: "/ar/برمجة/مهارات" }, { fr: "/theatre", en: "/en/acting", ar: "/ar/مسرح" }, { fr: "/photo", en: "/en/photo", ar: "/ar/تصوير" }, + { fr: "/photo/blog", en: "/en/photo/blog", ar: "/ar/تصوير/مدونة" }, ]; /** Index inversé : pathname → groupe de traduction */ @@ -24,13 +26,59 @@ for (const group of translationGroups) { } } +/** Pattern dynamique pour les routes paramétrées */ +interface DynamicPattern { + regex: RegExp; + buildUrls: (match: RegExpMatchArray) => Record; +} + +const dynamicPatterns: DynamicPattern[] = [ + { + // Photo albums: /photo/albums/{cat}, /en/photo/albums/{cat}, /ar/تصوير/ألبومات/{cat} + regex: /^(?:\/photo\/albums|\/en\/photo\/albums|\/ar\/تصوير\/ألبومات)\/([^/]+)$/, + buildUrls: (match) => { + const category = match[1]; + return { + fr: `${getPhotoAlbumsPath("fr")}/${category}`, + en: `${getPhotoAlbumsPath("en")}/${category}`, + ar: `${getPhotoAlbumsPath("ar")}/${category}`, + }; + }, + }, + { + // Photo blog posts: /photo/blog/{year}/{slug}, /en/photo/blog/{year}/{slug}, /ar/تصوير/مدونة/{year}/{slug} + regex: /^(?:\/photo\/blog|\/en\/photo\/blog|\/ar\/تصوير\/مدونة)\/(\d{4})\/([^/]+)$/, + buildUrls: (match) => { + const [, year, slug] = match; + return { + fr: `${getPhotoBlogPath("fr")}/${year}/${slug}`, + en: `${getPhotoBlogPath("en")}/${year}/${slug}`, + ar: `${getPhotoBlogPath("ar")}/${year}/${slug}`, + }; + }, + }, +]; + +function matchDynamicPattern( + pathname: string, +): Record | undefined { + for (const pattern of dynamicPatterns) { + const match = pathname.match(pattern.regex); + if (match) { + return pattern.buildUrls(match); + } + } + return undefined; +} + /** * Retourne les URLs alternatives pour un pathname donné. * Les trailing slashes sont normalisés. + * Essaie d'abord le lookup statique, puis le matching dynamique. */ export function getAlternateUrls( pathname: string, ): Record | undefined { const normalized = pathname.replace(/\/$/, "") || "/"; - return pathIndex.get(normalized); + return pathIndex.get(normalized) ?? matchDynamicPattern(normalized); }