Système de design du site web
Gère la couche visuelle et de rendu : graphismes mobiles, mode sombre et jetons de thème. Produit et maintient BRAND.md (modèle : le fichier templates/brand.md du skill new-website) et le bloc de jetons dans templates/astro/src/styles/global.css de ce même skill. Les règles pour les images sont ci‑dessous (WebP/AVIF via astro:assets d’Astro) ; le test images.spec.ts les applique. Pour générer les ressources images, utilisez le skill image.
Mode sombre (citoyen de première classe)
- Des jetons, pas des couleurs en dur. Toute couleur passe par des propriétés CSS personnalisées ; le mode clair est la valeur par défaut de
:root, les surcharges sombres sous:root[data-theme="dark"], ainsi qu’un bloc@media (prefers-color-scheme: dark)pour les visiteurs sans JavaScript. Le starter fournit exactement cette structure. - Script anti‑FOUC dans
<head>définitdata-themedepuislocalStorage.themeou la préférence système avant le premier affichage (déjà câblé dansBase.astro). Le bouton de bascule écritlocalStorage.theme(le starter n’en a pas ; si vous en ajoutez un, mentionnez‑le dans la section 3 de la page de confidentialité). - Contraste dans les deux thèmes : chaque paire texte/fond passe WCAG AA (4,5:1 pour le corps, 3:1 pour les grands textes/éléments d’interface). Une couleur qui passe en clair échoue souvent en sombre — le test d’accessibilité parcourt la matrice complète clair+sombre, donc vérifiez les deux.
Graphismes mobiles et adaptatifs
- Mobile first : conception à partir de 360 px ; cibles tactiles ≥ 44 px ; pas de défilement horizontal. Vérifier à 360 / 768 / 1280 (
preview_resize). - Formats modernes : WebP (ou AVIF) plutôt que JPEG/PNG pour les photos.
- Dimension par rapport à l’affichage, pas à la source. Ne livrez jamais une image de 2000 px dans un emplacement de 80 px ; générez des variantes de largeur et utilisez
srcset/sizes(ou<picture>). - Pas de décalage de mise en page : définissez les attributs explicites
width/heightpour que le cadre soit réservé avant le chargement de l’image (protège CLS / LCP). Le composant<Image>d’Astro les définit automatiquement ;images.spec.tsexige ces attributs (un simpleaspect-ratioen CSS ne satisfait pas le test). - Paresseux ou immédiat :
loading="lazy"en dessous de la ligne de flottaison ; l’image LCP/héro est chargée immédiatement avecfetchpriority="high"(ne PAS la charger en différé). - Texte alternatif sur chaque image (
alt=""uniquement si purement décorative). - Polices : auto‑hébergées en woff2 avec
font-display: swap; précharger uniquement les 1 à 2 utilisées au‑dessus de la ligne de flottaison. Évitez les décalages de mise en page dus aux échanges tardifs de polices.
Réactivité de la longueur du texte (contenu traduit)
- Le texte traduit est plus long. L’allemand (et de nombreuses autres langues) est généralement 20 à 35 % plus long que l’anglais équivalent. Un bouton, un lien de navigation ou une carte dimensionné pour un texte de substitution anglais débordera silencieusement, s’enroulera de manière disgracieuse ou sera tronqué dès que le contenu traduit réel le remplacera.
- Préférez les largeurs flexibles. Utilisez
min-width, flex ou les taillesauto/frde la grille pour tout élément contenant du texte traduit, pas unewidthfixe calquée sur la chaîne anglaise. - Débordement ≠ largeur : la flexibilité n’empêche pas un seul long mot composé (
Datenschutzerklärung) de sortir d’une colonne étroite — il fauthyphens: auto(correct par langue grâce à l’attributlangqueBase.astrodéfinit déjà) plusoverflow-wrap: break-worden solution de repli ; leglobal.cssdu starter fournit les deux surbody. Les cellules de tableau désactivent la césure (voir Tableaux). - Ne vous fiez pas uniquement à
white-space: nowrap+text-overflow: ellipsispour les liens de navigation ou les étiquettes de boutons — tronquer un lien de navigation est généralement inacceptable en UX (le visiteur ne peut plus dire où il mène). Acceptable pour du contenu où la troncature est un compromis admis (par exemple une cellule de tableau), pas pour la navigation. - Vérifiez avec les étiquettes traduites réelles, pas les substituts anglais. Quand un site utilise
astro-i18n-setupavecLanguageSwitcher.astroou toute navigation bilingue/multilingue, testez la mise en page à 360/768/1280 (preview_resize) avec le contenu réel livré dans chaque langue — pas seulement le substitut anglais.
Tableaux
Un seul style pour chaque tableau ; une page n’a pas de CSS de mise en page propre à un tableau. Il réside dans src/styles/tables.css du starter (importé après global.css par Base.astro) et tests/tables.spec.ts le vérifie. Un site créé avant cette section copie tables.css, ajoute la ligne d’import dans sa mise en page et supprime d’abord le CSS de tableau propre à chaque page.
Deux types, distingués par ce que contiennent les cellules :
stack(phrases) : un tableau au‑dessus de 40 rem ; en dessous, chaque ligne s’empile, l’en‑tête de ligne comme titre et chaque cellule sous le nom de sa colonne. Quatre colonnes de prose ne peuvent pas partager 320 px.figures(courts nombres,class="num"sur une cellule numérique) : reste un tableau sur un téléphone et doit tenir dans 320 px. S’il ne tient plus, passez‑le enstack; une barre de défilement n’est pas la solution.stack figuresensemble donne l’aspect des chiffres avec la mise en page empilée.
Balises, pour chaque tableau :
<div class="table-wrap stack" role="region" aria-label="Ce que montre le tableau" tabindex="0">
<table role="table">
<thead role="rowgroup"><tr role="row">
<th scope="col" role="columnheader">{cols[0]}</th><th scope="col" role="columnheader">{cols[1]}</th>
</tr></thead>
<tbody role="rowgroup">{rows.map((r) => (
<tr role="row"><th scope="row" role="rowheader">{r.item}</th><td role="cell" data-label={cols[1]}>{r.text}</td></tr>
))}</tbody>
</table>
</div>
- Écrivez les noms des colonnes une fois, dans une liste, et utilisez‑les pour l’en‑tête et pour chaque
data-label: le libellé d’une cellule empilée doit être égal à celui de son en‑tête de colonne, et le test compare les deux. Un tableaufiguresn’a besoin ni de rôles par cellule ni dedata-label. - Un tableau empilé n’est plus
display: table. Les rôles sont une solution de repli pour un navigateur qui abandonne le tableau lorsque la disposition change ; on ne vérifie pas qu’ils fonctionnent là. Dans Chromium, mesuré une fois sur un vrai tableau, l’arborescence d’accessibilité a conservé le tableau sans eux. class="tap"sur un lien seul dans une cellule en fait une cible tactile de 44 px sur un téléphone.nowrapmaintient une courte valeur (1, 3, 6) sur une ligne.long-head(avecstack) élargit la première colonne lorsque les en‑têtes de ligne sont des phrases.- Les mots ne sont pas coupés dans les cellules. Le
hyphens: autodu corps comprime une première colonne étroite en fragments ("An-thropic"), donc le bloc y définithyphens: manual. - Une page dont les tableaux proviennent de l’extérieur et ne peuvent pas porter ce balisage va dans
TABLE_EXEMPTdans la spécification, avec la raison.PAGESreste intact : le plan du site et les vérificationsllms.txtont besoin de chaque route active qui s’y trouve.
Ce que vérifie tables.spec.ts : chaque tableau est dans une .table-wrap d’un des deux types avec sa région, son libellé et tabindex, scope="col" sur chaque cellule d’en‑tête et un <th scope="row"> comme première cellule de chaque ligne, avec <td> pour le reste ; un tableau empilé a ses rôles et un libellé égal à l’en‑tête sur chaque cellule ; à 360 et 320 px, aucune page ni aucun tableau ne défile latéralement ; à 641 px et plus, un tableau empilé redevient un tableau avec sa ligne d’en‑tête visible et aucun mot coupé (cela attrape des règles comme overflow-wrap: anywhere qui laissent une colonne comprimer un mot) ; aucune cellule n’a hyphens: auto. Il teste aussi le CSS sur un élément fixe ajouté à la page d’accueil, avec axe dans les deux thèmes, donc un site sans tableau prouve quand même le bloc. Chromium sans tête ne coupe pas les mots, donc la vérification des césures lit le style calculé, pas la mise en page. Safari et VoiceOver ne sont pas vérifiés.
Obfuscation des e-mails (cacher les adresses aux robots)
Ne mettez jamais un e-mail en clair ou mailto:votre@adresse dans le code HTML source — les robots de collecte les récupèrent pour le spam en quelques heures. Affichez chaque adresse via le composant EmailLink.astro (src/components/EmailLink.astro dans templates/astro/ du skill new-website, + son assistant lib/obfuscate.ts) : l’adresse est inversée + encodée en base64 à la construction et réassemblée dans le navigateur, de sorte que le HTML servi ne contient aucune adresse lisible. Les visiteurs sans JavaScript voient une indication utilisable "nom [at] domaine [dot] com".
<EmailLink address="[email protected]" subject="Demande concernant le projet" text="Envoyez‑nous un e-mail" />
Règle : zéro lien mailto: brut et zéro adresse littérale avec @ dans le HTML produit — website-qa le vérifie. Même principe pour tout numéro de téléphone que vous ne souhaitez pas voir collecté.
Jetons de marque
Refléter la palette de BRAND.md dans le bloc de jetons et les maintenir synchronisés. Définissez : marque (fixe), et par thème en‑tête/texte/atténué/accent/lien/trait/fond/fond‑doux/surface. La couleur d’accent et la couleur de lien doivent passer AA sur le fond dans les deux thèmes, c’est pourquoi chaque thème a les siennes. Le website-brand-kit optionnel effectue le miroir avec une vérification : son analyse lit les tableaux de BRAND.md, vérifie que chaque couleur de texte a un ratio de 4,5:1 sur chaque fond dans les deux thèmes, et écrit les valeurs dans les trois blocs de couleurs de global.css (clair, sombre, et le bloc sombre pour les visiteurs sans JavaScript). À la main, modifiez les trois blocs ensemble.
Fait signifie
BRAND.md rempli et cohérent avec le bloc de jetons ; les images sont en WebP, dimensionnées, chargées paresseusement/immédiatement correctement, avec texte alternatif ; le test d’accessibilité (clair+sombre) est vert ; la mise en page tient à 360/768/1280 sans décalage ; chaque tableau est stack ou figures et tables.spec.ts est vert. Les chiffres de performance (FCP/LCP) sont vérifiés dans website-qa. La mise en page tient aussi à 360/768/1280 avec le contenu réel (pas un substitut) dans chaque langue livrée, pas seulement la locale par défaut.