Documentation · Architecture & Code

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.

HTML5 Statique JS ES6+ Vanilla CSS Custom Props 0 Framework Cloudflare Pages
01

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é.

Navigateur Requête HTTP/3
Cloudflare Edge Cache CDN mondial
HTML + CSS + JS Rendu navigateur
localStorage Thème + Langue persistés
fetch() JSON Traductions async
DOM inject Navbar + Footer + Modal
Principe "Zéro duplication"

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.

Principe "Root path dynamique"

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.

Principe "Anti-FOUC"

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.

02

Structure des fichiers

truvector-v3/ │ ├── index.html ← depth=0 → root='./' ├── products.html ← depth=0 → root='./' ├── about.html ← depth=0 → root='./' ├── contact.html ← depth=0 → root='./' │ ├── products/ironlock/ │ ├── index.html ← depth=2 → root='../../' │ ├── pricing.html ← depth=2 → root='../../' │ ├── changelog.html ← depth=2 → root='../../' │ ├── manuel.html ← depth=2 → root='../../' │ └── exemples.html ← depth=2 → root='../../' │ ├── blog/ │ ├── index.html ← depth=1 → root='../' │ └── proteger-python-*.html ← depth=1 → root='../' │ ├── downloads/ │ └── index.html ← depth=1 → root='../' │ ├── legal/ │ ├── mentions-legales.html ← depth=1 → root='../' │ └── confidentialite.html ← depth=1 → root='../' │ ├── assets/ │ ├── css/truvector.css ← Design System (689 lignes) │ ├── js/components.js ← Navbar+Footer+i18n+Search (340 lignes) │ ├── js/main.js ← Interactions (222 lignes) │ ├── i18n/fr.json ← 80+ clés FR │ ├── i18n/en.json ← 80+ clés EN │ └── img/logo.webp|svg|png|favicon.ico │ ├── _headers ← HTTP headers Cloudflare ├── _redirects ← Redirections Cloudflare ├── wrangler.jsonc ← Config Cloudflare Pages ├── sitemap.xml ← 14 URLs SEO └── robots.txt

Règle de profondeur (depth)

pathnamesegmentsdirsroot 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
03

Cycle de vie d'une page

Séquence exacte d'exécution quand le navigateur charge une page TRUvector :

1
Script Anti-FOUC (inline <head>)
Lit 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.
2
Chargement CSS (truvector.css)
Le CSS lit les custom properties selon data-theme. Le design system complet s'applique avec les bonnes couleurs dès le premier rendu.
3
Rendu DOM HTML
Le contenu de la page est rendu. Les <div id="nav-placeholder"> et <div id="footer-placeholder"> sont présents mais vides.
4
components.js — IIFE synchrone
Calcule le 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.
5
main.js — IIFEs séquentielles
Active le hamburger mobile, l'ombre scroll navbar, le scroll reveal (IntersectionObserver), l'active nav link, le fix opacity, les boutons copy-code, les formulaires newsletter/contact, le SHA-256 verifier, le pricing toggle.
6
fetch() i18n JSON (async)
Récupère assets/i18n/fr.json ou en.json. Remplace tous les data-i18n du DOM. Met à jour le lang de <html>. Page entièrement interactive.
04

Root path dynamique

C'est le mécanisme le plus critique du site. Un calcul incorrect brise tous les assets (logo, CSS, JS) sur les pages en sous-dossier. Trois bugs ont été introduits et corrigés autour de ce seul mécanisme.

Code actuel (version finale corrigée)

// Dans components.js, lignes 19-22 const segments = window.location.pathname .split('/') .filter(Boolean); // ['products','ironlock','index.html'] ou [] // Math.max évite dirs=-1 quand pathname='/' (segments=[]) const dirs = Math.max(0, segments.length - 1); // dirs=0 → './' | dirs=1 → '../' | dirs=2 → '../../' const root = dirs === 0 ? './' : '../'.repeat(dirs);

Historique des 3 bugs corrigés

VersionCodeProblèmeCas 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 - 1Pas de Math.max → -1 sur '/'pathname='/' → RangeError
Fix final (V3.1)Math.max(0, segments.length-1)Tous les cas couverts ✅
05

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 :

1 — Root path

Calcule root depuis window.location.pathname. Utilisé dans tous les href et src générés dynamiquement.

2 — Détection langue

detectLang() : lit localStorage('trv-lang'), sinon détecte navigator.language, sinon fallback 'fr'. Langues supportées : ['fr', 'en'].

3 — Chargement i18n async

loadTranslations(lang) : fetch de assets/i18n/{lang}.json. En cas d'échec, fallback sur fr.json. Stocke dans translations = {}.

4 — Application traductions

applyTranslations() : itère sur tous les [data-i18n], [data-i18n-title]. Gère les inputs (placeholder), options et textes. Remplace {year}.

5 — Système de thèmes

detectTheme()applyTheme(theme). Applique data-theme sur documentElement. Met à jour les boutons .theme-btn. Persiste en localStorage.

6 — Injection Navbar + Footer

Template literals avec ${root} pour tous les liens. navTarget.outerHTML = navHtml remplace le placeholder. Le modal search est inséré avec insertAdjacentHTML('beforeend').

7 — Index de recherche

Array de 11 objets {cat, title, excerpt, url}. Tous les strings utilisent des Unicode escapes (\u2019 etc.) pour éviter les erreurs de syntaxe JS.

8 — Moteur de recherche

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

AttributEffetExemple
data-i18n="clé"Remplace textContent par la traduction<span data-i18n="nav.home">
data-i18n sur INPUTRemplace placeholder<input data-i18n="search.placeholder">
data-i18n sur OPTIONRemplace 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">
06

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-linksToggle classe .open, aria-expanded, fermeture au clic sur lien
Navbar scroll shadow#navbarboxShadow si scrollY > 20px, passive listener
Scroll reveal.revealIntersectionObserver threshold=0.1, setTimeout stagger 80ms/élément
Active nav link.nav-links aCompare 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 buttonspre, .code-wrapAjoute .copy-btn sur chaque bloc code, navigator.clipboard.writeText(), feedback visuel 2s
Newsletter formsform[id*="newsletter"]Event delegation sur submit, trouve le msg div par convention d'ID
Contact form#contact-formFetch vers Formspree, gestion erreur, dégrade gracieusement si action="#"
SHA-256 verifier#verify-dropzoneDrag & drop + click, FileReader → ArrayBuffer → crypto.subtle.digest('SHA-256')
Pricing toggle.pricing-toggle-btnBascule data-price-onetime / data-price-annual, met à jour data-price-label
07

Système de thèmes CSS

Variables par thème

VariableDarkLightContrast
--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-bgrgba(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

/* 1. Script inline <head> (AVANT CSS) */ (function(){ var t = localStorage.getItem('trv-theme') || (matchMedia('(prefers-color-scheme: light)') .matches ? 'light' : 'dark'); document.documentElement .setAttribute('data-theme', t); })(); /* 2. CSS lit data-theme */ [data-theme="dark"] { --bg0: #06080d; ... } [data-theme="light"] { --bg0: #f4f6f9; ... } /* 3. components.js → applyTheme() */ function applyTheme(theme) { document.documentElement .setAttribute('data-theme', theme); localStorage.setItem('trv-theme', theme); }
Règle critique thème light : Ne jamais hardcoder des couleurs hexadécimales dans le CSS. Toujours utiliser var(--xxx). Les overrides [data-theme="light"] .navbar { color: var(--textb); } ne fonctionnent que si le composant utilisait déjà une CSS variable.
08

Référence CSS — Design System

Grilles

.g2 ← 2 colonnes, gap 16px .g3 ← 3 colonnes, gap 16px .g4 ← 4 col (→2 à 1100px)

Boutons

.btn-primary ← Orange filled .btn-outline ← Cyan border .btn-ghost ← Discret/gris

Badges

.badge .b-o ← Orange .badge .b-c ← Cyan .badge .b-p ← Violet .badge .b-g ← Vert .badge .b-r ← Rouge .badge .b-y ← Jaune .badge .b-b ← Bleu

Cards

.card ← Base .card.ac-c ← Accent cyan .card.ac-p ← Accent violet .card.ac-g ← Accent vert .card.ac-b ← Accent bleu .card.ac-y ← Accent jaune

Alertes

.alert .a-info ← Bleu info .alert .a-warn ← Jaune warning .alert .a-ok ← Vert success .alert .a-crit ← Rouge danger .alert .a-tip ← Violet tip

Animations

.reveal ← Scroll reveal .reveal.visible ← État final .cursor ← Clignotement .anim-fadeup ← fadeUp .6s

Responsive breakpoints

BreakpointCibleChangements
max-width: 1100pxTablettes larges.g4 → 2 colonnes, padding sections réduit, pricing en colonne unique, blog 2 colonnes
max-width: 768pxTablettes / MobileNav links masqués → hamburger, footer en colonne unique, stats-bar 2 colonnes, hero-terminal masqué
max-width: 480pxPetits mobilesToutes les grilles en 1 colonne, padding minimal
09

Moteur i18n

Flux de données complet

1. detectLang() → lit localStorage 'trv-lang'
2. Fallback → navigator.language (2 chars)
3. Fallback → 'fr' (défaut)
4. loadTranslations(lang) → fetch async
5. translations = await resp.json()
6. applyTranslations() → DOM update
7. document.documentElement.lang = lang

Structure d'une clé

// fr.json { "nav.home": "Accueil", "hero.sub": "Des logiciels...", "footer.copy": "© {year} TRUvector" } // Utilisation HTML <a data-i18n="nav.home">Accueil</a> // {year} remplacé automatiquement let val = t(key) .replace('{year}', new Date().getFullYear());
Le texte initial dans le HTML (ex: <span data-i18n="nav.home">Accueil</span>) sert de fallback visible pendant le chargement async du JSON. Il doit toujours être le texte FR.
11

Accessibilité WCAG 2.1 AA

CritèreImplémentationFichier
Skip-to-content<a class="skip-link" href="#main-content"> visible au focus clavierChaque .html + truvector.css
ARIA navigationrole="navigation" aria-label sur #navbar, role="menubar" sur .nav-linkscomponents.js navHtml
ARIA dialogrole="dialog" aria-modal="true" aria-hidden sur #search-modalcomponents.js searchModalHtml
ARIA live regionsaria-live="polite" role="status" sur les messages de formulairecontact.html, pricing.html
Focus visiblea:focus-visible { outline: 2px solid var(--acc2); } — jamais outline:nonetruvector.css
Contraste texte--textb #0f1e2e sur --nav-bg white → ratio 15:1 (WCAG AAA)truvector.css thème light
Alt text imagesalt="TRUvector" sur logo, aria-hidden="true" sur décoratifscomponents.js + pages HTML
Étiquettes formulaires<label for="..."> sur chaque champ, honeypot aria-hiddencontact.html, pricing.html
Boutons aria-pressedMis à jour dynamiquement sur .theme-btn et .lang-btncomponents.js applyTheme()
Hamburger aria-expandedMis à jour au togglemain.js mobile nav
12

Infrastructure Cloudflare

_headers

Headers HTTP appliqués par Cloudflare sur toutes les requêtes. Sécurité (X-Frame-Options SAMEORIGIN, nosniff), cache (assets 1 an, HTML 1h).

/* X-Frame-Options: SAMEORIGIN X-Content-Type-Options: nosniff /assets/* Cache-Control: max-age=31536000, immutable /*.html Cache-Control: max-age=3600
_redirects

Redirections HTTP 301 gérées par Cloudflare Pages. URLs propres sans extension pour les sections principales.

/products/ironlock → /products/ironlock/index.html /blog → /blog/index.html /downloads → /downloads/index.html /index.html → /
wrangler.jsonc

Configuration Cloudflare Pages/Workers. Mode assets (site statique pur), compatibilité Node.js activée, observabilité activée.

{ "name": "truvector", "assets": { "directory": "." }, "compatibility_flags": ["nodejs_compat"] }

Stratégie de cache

RessourceCache-ControlRaison
assets/* (JS, CSS, img)max-age=31536000, immutableJamais modifiés (versionnés dans le nom si besoin)
*.htmlmax-age=3600, must-revalidateContenu peut changer, revalidation après 1h
sitemap.xmlmax-age=86400Mis à jour au plus 1 fois/jour
robots.txtNon configuré (défaut Cloudflare)Très rarement modifié