TRUvector Site V3
Architecture complète, cycle de vie des pages, détail de chaque module JavaScript, référence CSS, systèmes i18n et recherche. Pour comprendre comment ça marche vraiment.
Vue d'ensemble de l'architecture
TRUvector Site V3 est un site 100% statique — pas de serveur applicatif, pas de base de données, pas de build step obligatoire. Chaque page est un fichier HTML autonome chargé directement par le navigateur depuis le CDN Cloudflare.
La "dynamique" du site (thèmes, langue, recherche, navbar/footer) est entièrement gérée par du JavaScript Vanilla côté client. Aucun framework (React, Vue, Angular) n'est utilisé.
La navbar et le footer sont définis une seule fois dans components.js et injectés dynamiquement dans chaque page via <div id="nav-placeholder">. Modifier la navbar = modifier 1 fichier JS, pas 14 fichiers HTML.
Chaque page est à une profondeur différente dans l'arborescence. components.js calcule automatiquement le chemin relatif vers la racine (./, ../, ../../) pour que tous les liens et assets soient corrects quel que soit l'emplacement de la page.
Le thème est appliqué via un script inline dans le <head>, avant que le CSS ne soit rendu. Cela évite le flash de contenu non stylisé (FOUC) quand l'utilisateur préfère le thème light.
Structure des fichiers
Règle de profondeur (depth)
| pathname | segments | dirs | root calculé | exemple asset |
|---|---|---|---|---|
| / | [] | 0 | ./ | ./assets/css/truvector.css |
| /index.html | [index.html] | 0 | ./ | ./assets/css/truvector.css |
| /about.html | [about.html] | 0 | ./ | ./assets/css/truvector.css |
| /legal/page.html | [legal, page.html] | 1 | ../ | ../assets/css/truvector.css |
| /blog/article.html | [blog, article.html] | 1 | ../ | ../assets/css/truvector.css |
| /products/ironlock/x.html | [products, ironlock, x.html] | 2 | ../../ | ../../assets/css/truvector.css |
Cycle de vie d'une page
Séquence exacte d'exécution quand le navigateur charge une page TRUvector :
localStorage.getItem('trv-theme') ou détecte prefers-color-scheme. Applique immédiatement data-theme="dark|light|contrast" sur <html>. S'exécute AVANT le rendu CSS → zéro flash.data-theme. Le design system complet s'applique avec les bonnes couleurs dès le premier rendu.<div id="nav-placeholder"> et <div id="footer-placeholder"> sont présents mais vides.root path. Détecte la langue et le thème. Injecte la navbar HTML dans nav-placeholder.outerHTML. Injecte le footer. Injecte le modal search dans document.body. Applique le thème sur les boutons. Déclenche loadTranslations() en async.assets/i18n/fr.json ou en.json. Remplace tous les data-i18n du DOM. Met à jour le lang de <html>. Page entièrement interactive.Root path dynamique
Code actuel (version finale corrigée)
Historique des 3 bugs corrigés
| Version | Code | Problème | Cas qui plante |
|---|---|---|---|
| Bug #1 (V2) | slashes - 1 / repeat(depth-1) | -1 sur profondeur 1, doublon sur profondeur 2 | /legal/ et /products/ironlock/ |
| Bug #2 (V3.0) | segments.length - 1 | Pas de Math.max → -1 sur '/' | pathname='/' → RangeError |
| Fix final (V3.1) | Math.max(0, segments.length-1) | — | Tous les cas couverts ✅ |
components.js — Anatomie complète
components.js est le cœur du site. C'est une IIFE (Immediately Invoked Function Expression) — tout son code s'exécute immédiatement au chargement, en mode strict. Voici ses 8 responsabilités :
Calcule root depuis window.location.pathname. Utilisé dans tous les href et src générés dynamiquement.
detectLang() : lit localStorage('trv-lang'), sinon détecte navigator.language, sinon fallback 'fr'. Langues supportées : ['fr', 'en'].
loadTranslations(lang) : fetch de assets/i18n/{lang}.json. En cas d'échec, fallback sur fr.json. Stocke dans translations = {}.
applyTranslations() : itère sur tous les [data-i18n], [data-i18n-title]. Gère les inputs (placeholder), options et textes. Remplace {year}.
detectTheme() → applyTheme(theme). Applique data-theme sur documentElement. Met à jour les boutons .theme-btn. Persiste en localStorage.
Template literals avec ${root} pour tous les liens. navTarget.outerHTML = navHtml remplace le placeholder. Le modal search est inséré avec insertAdjacentHTML('beforeend').
Array de 11 objets {cat, title, excerpt, url}. Tous les strings utilisent des Unicode escapes (\u2019 etc.) pour éviter les erreurs de syntaxe JS.
openSearch(), closeSearch(), renderResults(query), highlight(text, q). Navigation clavier ↑↓, Ctrl+K, Escape. Résultats filtrés en temps réel.
Attributs HTML reconnus par components.js
| Attribut | Effet | Exemple |
|---|---|---|
| data-i18n="clé" | Remplace textContent par la traduction | <span data-i18n="nav.home"> |
| data-i18n sur INPUT | Remplace placeholder | <input data-i18n="search.placeholder"> |
| data-i18n sur OPTION | Remplace textContent | <option data-i18n="pricing.tier.indie"> |
| data-i18n-title="clé" | Remplace l'attribut title | <button data-i18n-title="theme.dark"> |
| data-lang="fr|en" | Bouton de changement de langue | <button class="lang-btn" data-lang="en"> |
| data-theme="dark|light|contrast" | Bouton de changement de thème | <button class="theme-btn" data-theme="light"> |
main.js — Modules d'interaction
main.js est organisé en 8 IIFEs indépendantes. Chaque IIFE teste la présence des éléments DOM qu'elle cible avant d'agir — elle n'a aucun effet sur les pages qui ne contiennent pas les éléments concernés.
| Module | Élément ciblé | Comportement |
|---|---|---|
| Mobile nav toggle | .nav-toggle + .nav-links | Toggle classe .open, aria-expanded, fermeture au clic sur lien |
| Navbar scroll shadow | #navbar | boxShadow si scrollY > 20px, passive listener |
| Scroll reveal | .reveal | IntersectionObserver threshold=0.1, setTimeout stagger 80ms/élément |
| Active nav link | .nav-links a | Compare new URL(a.href).pathname vs window.location.pathname, normalise /index.html → / |
| Fix opacity:0 | [style*="opacity:0"] | Retire le inline opacity:0 après 1200ms si l'animation CSS ne s'est pas déclenchée |
| Code copy buttons | pre, .code-wrap | Ajoute .copy-btn sur chaque bloc code, navigator.clipboard.writeText(), feedback visuel 2s |
| Newsletter forms | form[id*="newsletter"] | Event delegation sur submit, trouve le msg div par convention d'ID |
| Contact form | #contact-form | Fetch vers Formspree, gestion erreur, dégrade gracieusement si action="#" |
| SHA-256 verifier | #verify-dropzone | Drag & drop + click, FileReader → ArrayBuffer → crypto.subtle.digest('SHA-256') |
| Pricing toggle | .pricing-toggle-btn | Bascule data-price-onetime / data-price-annual, met à jour data-price-label |
Système de thèmes CSS
Variables par thème
| Variable | Dark | Light | Contrast |
|---|---|---|---|
| --bg0 | #06080d | #f4f6f9 | #000000 |
| --bg1 | #0a0d14 | #eef1f5 | #0a0a0a |
| --text | #c8d8ea | #2c3e55 | #eeeeee |
| --textd | #4a6080 | #3a5068 | #aaaaaa |
| --textb | #e8f4ff | #0f1e2e | #ffffff |
| --border | #1a2535 | #c0ccd8 | #444444 |
| --nav-bg | rgba(6,8,13,.94) | rgba(255,255,255,.98) | rgba(0,0,0,.98) |
| --acc | #ff6b35 | #d94f1e | #ffaa00 |
| --acc2 | #00ffcc | #007a6e | #00ffff |
Séquence d'application
var(--xxx). Les overrides [data-theme="light"] .navbar { color: var(--textb); } ne fonctionnent que si le composant utilisait déjà une CSS variable.Référence CSS — Design System
Grilles
Boutons
Badges
Cards
Alertes
Animations
Responsive breakpoints
| Breakpoint | Cible | Changements |
|---|---|---|
| max-width: 1100px | Tablettes larges | .g4 → 2 colonnes, padding sections réduit, pricing en colonne unique, blog 2 colonnes |
| max-width: 768px | Tablettes / Mobile | Nav links masqués → hamburger, footer en colonne unique, stats-bar 2 colonnes, hero-terminal masqué |
| max-width: 480px | Petits mobiles | Toutes les grilles en 1 colonne, padding minimal |
Moteur i18n
Flux de données complet
'trv-lang'Structure d'une clé
<span data-i18n="nav.home">Accueil</span>) sert de fallback visible pendant le chargement async du JSON. Il doit toujours être le texte FR.Moteur de recherche
La recherche est un index JavaScript statique défini directement dans components.js. Aucune requête serveur — filtrage en temps réel sur l'array searchIndex.
Structure d'une entrée
searchIndex. Utiliser des Unicode escapes : \u2019 pour ', \u2014 pour —, \u2013 pour –, \u00e9 pour é. Une apostrophe directe casse le parser JS et toute la navbar disparaît.Algorithme de filtrage
Accessibilité WCAG 2.1 AA
| Critère | Implémentation | Fichier |
|---|---|---|
| Skip-to-content | <a class="skip-link" href="#main-content"> visible au focus clavier | Chaque .html + truvector.css |
| ARIA navigation | role="navigation" aria-label sur #navbar, role="menubar" sur .nav-links | components.js navHtml |
| ARIA dialog | role="dialog" aria-modal="true" aria-hidden sur #search-modal | components.js searchModalHtml |
| ARIA live regions | aria-live="polite" role="status" sur les messages de formulaire | contact.html, pricing.html |
| Focus visible | a:focus-visible { outline: 2px solid var(--acc2); } — jamais outline:none | truvector.css |
| Contraste texte | --textb #0f1e2e sur --nav-bg white → ratio 15:1 (WCAG AAA) | truvector.css thème light |
| Alt text images | alt="TRUvector" sur logo, aria-hidden="true" sur décoratifs | components.js + pages HTML |
| Étiquettes formulaires | <label for="..."> sur chaque champ, honeypot aria-hidden | contact.html, pricing.html |
| Boutons aria-pressed | Mis à jour dynamiquement sur .theme-btn et .lang-btn | components.js applyTheme() |
| Hamburger aria-expanded | Mis à jour au toggle | main.js mobile nav |
Infrastructure Cloudflare
Headers HTTP appliqués par Cloudflare sur toutes les requêtes. Sécurité (X-Frame-Options SAMEORIGIN, nosniff), cache (assets 1 an, HTML 1h).
Redirections HTTP 301 gérées par Cloudflare Pages. URLs propres sans extension pour les sections principales.
Configuration Cloudflare Pages/Workers. Mode assets (site statique pur), compatibilité Node.js activée, observabilité activée.
Stratégie de cache
| Ressource | Cache-Control | Raison |
|---|---|---|
| assets/* (JS, CSS, img) | max-age=31536000, immutable | Jamais modifiés (versionnés dans le nom si besoin) |
| *.html | max-age=3600, must-revalidate | Contenu peut changer, revalidation après 1h |
| sitemap.xml | max-age=86400 | Mis à jour au plus 1 fois/jour |
| robots.txt | Non configuré (défaut Cloudflare) | Très rarement modifié |