@startuml Blog System Architecture - Complete with Categories Maintenance title Système de blog complet - Architecture, flux de données et maintenance des catégories !define LIGHTBLUE #E1F5FE !define LIGHTGREEN #E8F5E8 !define LIGHTYELLOW #FFF9C4 !define LIGHTPINK #FCE4EC !define LIGHTGRAY #F5F5F5 !define LIGHTORANGE #FFF3E0 !define LIGHTPURPLE #F3E5F5 ' === FICHIERS SOURCES === package "Fichiers sources markdown" as sources { folder "src/content/blog/" as blog_content { folder "fr/" as fr_folder LIGHTBLUE { file "tortues-marines-reunion.md" as fr_md note right of fr_md **État frontmatter avant nettoyage:** - categories_existing: ["Algues", "Biodiversité"] - categories_custom: ["Faune", "Océan"] - categories: [] (peut être vide) **État après script de maintenance:** - categories_existing: ["Algues", "Biodiversité", "Faune", "Océan"] - categories: ["Algues", "Biodiversité", "Faune", "Océan"] - categories_custom: (vide proprement) end note } folder "en/" as en_folder LIGHTBLUE { file "tortues-marines-reunion.md" as en_md note right of en_md **Souvent état incohérent:** - categories: [] (vide) → Pas de catégories en anglais → Pas de sidebar affichée → Besoin de traduction manuelle end note } } file "src/content/config.ts" as config LIGHTYELLOW note right of config Schéma Zod : - categories: z.array(z.string()).optional() - categories_existing: legacy - categories_custom: legacy end note } ' === SCRIPTS DE MAINTENANCE === package "Scripts de maintenance et génération" as maintenance { file "scripts/update-blog-md-categories.js" as categories_script LIGHTPURPLE note right of categories_script **Logique de nettoyage:** 1. Lit categories_existing + categories_custom 2. Combine et déduplique avec Set() 3. Tri alphabétique (localeCompare) 4. Met à jour categories_existing et categories 5. Vide categories_custom proprement 6. Préserve fichiers inchangés (newContent !== raw) end note file "scripts/generate-blog-json.js" as script LIGHTGREEN note right of script **Après nettoyage des catégories:** 1. Lit frontmatter harmonisé 2. Utilise champ "categories" final 3. Génère slug avec date DD-MM-YYYY 4. Crée search-data.json cohérent end note } ' === CONFIGURATION === package "Configuration et dépendances" as configuration { file "package.json" as package LIGHTGRAY note right of package **Scripts NPM en séquence:** 1. update-blog-md-categories (harmonise) 2. generate-search-data (génère JSON) 3. dev/build (astro) **Workflow rebuild:** clean → generate-search-data → toggle-lang → generate-cms-config → build → preview end note file "astro.config.mjs" as astro_config LIGHTGRAY note right of astro_config - MULTILANG_ENABLED conditionnel - i18n: locales ['fr', 'en'] - defaultLocale: 'fr' - prefixDefaultLocale: false end note } ' === TRADUCTIONS === package "Système de traductions" as i18n_system { file "src/lib/i18n.ts" as i18n_lib LIGHTORANGE file "src/i18n/fr.json" as fr_i18n LIGHTORANGE file "src/i18n/en.json" as en_i18n LIGHTORANGE note right of i18n_lib **getTranslation(lang):** - "fr" → fr.json - "en" → en.json (fallback: en) **Labels traduits:** - categories, sort_by, sort_recent - articles_found, reset_filters - blog_page_title, back_to_blog end note } ' === FICHIER GÉNÉRÉ === package "Données générées" as generated { file "public/search-data.json" as json_file LIGHTYELLOW note right of json_file **Structure finale par article:** { "id": "fr/tortues-marines-reunion", "lang": "fr", "slug": "25-03-2025-tortues-marines-reunion", "title": "Les tortues marines à La Réunion", "excerpt": "Les tortues marines font partie...", "author": "Admin", "date": "2025-03-25T00:00:00.000Z", "categories": ["Algues", "Biodiversité", "Faune", "Océan"], "image": "https://www.animaleco.com/..." } **Cohérence:** Categories harmonisées par le script end note } ' === PAGES ASTRO === package "Pages Astro (SSG)" as pages { file "src/pages/[...lang]/blog/index.astro" as blog_index LIGHTPINK note right of blog_index - getLocalizedPaths() pour routes FR/EN - Charge BlogWithFilters React component - Passe lang et base comme props - SSG: pré-rendu des pages statiques end note file "src/pages/[...lang]/blog/[slug].astro" as blog_detail LIGHTPINK note right of blog_detail **getStaticPaths() cohérent:** 1. getCollection("blog") via Astro 2. Même logique slug que generate-blog-json.js 3. Routes FR: /blog/DD-MM-YYYY-slug 4. Routes EN: /en/blog/DD-MM-YYYY-slug 5. formatDate() selon langue end note } ' === COMPOSANTS REACT === package "Composants React (Client-side)" as components { file "BlogWithFilters.tsx" as blog_component LIGHTGREEN note right of blog_component **État et logiques métier:** - articles: Article[] (filtré par langue) - selectedCategories: string[] (filtres actifs) - currentPage: number (pagination) - sortOrder: "recent"|"oldest"|"az"|"za" **Filtrage catégories intelligent:** 1. allCategories = Set(articles.flatMap(a => a.categories)) 2. hasCategories = allCategories.length > 0 3. Sidebar conditionnelle (desktop) / accordéon (mobile) 4. Logique AND: selectedCategories.every(cat => article.categories.includes(cat)) **Gestion état URL bidirectionnelle:** - URLSearchParams: ?cat=A,B&page=2 - window.history.pushState() sans reload - Synchronisation au chargement end note file "LangSwitcher.tsx" as lang_switcher LIGHTGREEN note right of lang_switcher **Logique de mapping traductions:** 1. Cache search-data.json une fois 2. Détecte contexte: /blog/slug vs autres pages 3. Parse slug actuel depuis URL 4. Trouve article via lang + slug 5. Extrait fileId depuis article.id 6. Cherche traduction: même fileId, langue différente 7. Génère URL traduite ou fallback /blog/ end note file "src/lib/utils.ts" as utils LIGHTYELLOW note right of utils **cn() function:** twMerge(clsx(...inputs)) Combine classes CSS conditionnelles avec résolution conflits Tailwind end note } ' === COMPOSANTS UI === package "Composants UI (Radix + Accessibility)" as ui_components { file "@/components/ui/pagination.tsx" as pagination_ui LIGHTGRAY file "@/components/ui/checkbox.tsx" as checkbox_ui LIGHTGRAY note right of pagination_ui **Radix UI primitives:** - PaginationContent/Item/Link - Gestion accessibility ARIA - Navigation clavier - ChevronsLeft/Right icons (lucide-react) end note } ' === FLUX PRINCIPAL DE DONNÉES === fr_md --> categories_script : **1. Harmonise catégories** en_md --> categories_script : **1. Harmonise catégories** categories_script --> fr_md : **Frontmatter nettoyé** categories_script --> en_md : **Frontmatter nettoyé** fr_md --> script : **2. Parse frontmatter harmonisé** en_md --> script : **2. Parse frontmatter harmonisé** config --> script : **Validation Zod** script --> json_file : **3. Génère JSON avec catégories cohérentes** json_file --> blog_component : **4. fetch() runtime** blog_component --> blog_index : **5. Rendu dynamique** blog_detail --> config : **getCollection("blog")** blog_detail --> blog_detail : **getStaticPaths() parallèle** ' === FLUX TRADUCTIONS === fr_i18n --> i18n_lib : **Import statique** en_i18n --> i18n_lib : **Import statique** i18n_lib --> blog_component : **getTranslation(lang)** i18n_lib --> lang_switcher : **getTranslation(lang)** ' === FLUX MAPPING LANGUES === json_file --> lang_switcher : **Cache pour mapping traductions** ' === FLUX CONFIGURATION === package --> categories_script : **npm run update-blog-md-categories** package --> script : **npm run generate-search-data** astro_config --> blog_index : **Routing i18n conditionnel** ' === FLUX UI === utils --> blog_component : **cn() classes conditionnelles** pagination_ui --> blog_component : **Composants navigation** checkbox_ui --> blog_component : **Composants filtres** ' === WORKFLOW DE MAINTENANCE === note top of categories_script **Workflow de maintenance des catégories:** **Problème résolu:** Incohérences entre categories_existing, categories_custom, et categories dans le frontmatter **Solution:** Script automatique qui: 1. Combine categories_existing + categories_custom 2. Déduplique avec Set() et tri alphabétique 3. Met à jour categories_existing ET categories 4. Vide categories_custom proprement 5. Préserve fichiers inchangés (optimisation) **Résultat:** Frontmatter harmonisé pour génération JSON cohérente end note ' === LOGIQUES AVANCÉES === note bottom of blog_component **Logiques métier avancées:** **Système de tri hybride:** - recent/oldest: Date uniquement - az/za: Titre + Date en fallback (localeCompare + ignorePunctuation) **Pagination sophistiquée:** - 6 articles/page (ARTICLES_PER_PAGE) - Premier article en layout "featured" (image + grand format) - Navigation avec icons + accessibility - Scroll automatique vers le haut (goToPage) **Responsive design intelligent:** - Desktop: Sidebar fixe (sticky top-24) si hasCategories - Mobile: Pas de sidebar, layout vertical uniquement - Largeur dynamique: w-3/4 vs max-w-5xl selon sidebar end note note top of lang_switcher **Algorithme de correspondance traductions:** **Input:** /blog/25-03-2025-tortues-marines-reunion **Processus:** 1. Parse → currentSlug = "25-03-2025-tortues-marines-reunion" 2. Trouve article FR dans JSON via lang + slug 3. Extrait fileId = "tortues-marines-reunion" depuis article.id 4. Cherche article EN avec même fileId 5. Génère URL → /en/blog/25-03-2025-tortues-marines-reunion **Fallback:** Si pas de traduction → /en/blog/ **Cache:** search-data.json chargé une seule fois end note note bottom of script **Cohérence critique du système:** **Même logique slug dans 2 endroits:** 1. generate-blog-json.js (pour JSON React) 2. [slug].astro getStaticPaths() (pour routes Astro) **Algorithme identique:** baseSlug = data.url || filename.replace('.md', '') datePrefix = DD-MM-YYYY depuis data.date finalSlug = datePrefix + "-" + baseSlug **Garantit:** Liens React ↔ Routes Astro parfaitement synchronisés end note @enduml