PD-230 — Plan d'implémentation
📚 Navigation User Story
| Document | | | ---------- | -- | | 📋 [Spécification](PD-230-specification.md) | | | 🛠️ **Plan d'implémentation** | *(ce document)* | | ✅ [Critères d'acceptation](PD-230-acceptability.md) | | | 📝 [Retour d'expérience](PD-230-rex.md) | | [← Retour à site-vitrine](../PD-225-epic.md) · [↑ Index User Story](index.md)
1. Découpage en composants
Structure accessibilité
src/
├── styles/
│ └── accessibility.css # Focus visible, skip links
├── components/
│ ├── SkipLink.astro # Lien "Aller au contenu"
│ └── *.astro # Composants accessibles
├── layouts/
│ └── BaseLayout.astro # Structure sémantique
└── ...
Configuration:
├── .eslintrc.js # eslint-plugin-astro/jsx-a11y
├── pa11y.config.js # Configuration pa11y-ci
└── .gitlab-ci.yml # Job audit accessibilité
Composants clés
| Composant | Responsabilité |
SkipLink.astro | Navigation rapide clavier |
accessibility.css | :focus-visible, touch targets |
BaseLayout.astro | Landmarks ARIA, lang |
pa11y.config.js | Audit automatisé |
2. Flux techniques
Audit accessibilité CI
git push
│
▼
GitLab CI
│
├── npm run build
├── npm run lint (eslint a11y)
└── npm run test:a11y (pa11y-ci)
│
├── Lance serveur local dist/
├── Teste chaque page
└── Rapport WCAG violations
Navigation clavier
Tab
│
▼
SkipLink (visible au focus)
│
▼
Header navigation
│
▼
Main content (#main-content)
│
▼
Footer
3. Mapping invariants → mécanismes
| Invariant | Mécanisme technique |
| INV-1 : Contraste minimum | Tokens couleurs (ratio 4.5:1) |
| INV-2 : Navigation clavier | tabindex, :focus-visible |
Styles accessibilité
/* src/styles/accessibility.css */
/* Skip link */
.skip-link {
position: absolute;
top: -100%;
left: 0;
padding: var(--space-sm) var(--space-md);
background: var(--color-bleu-securite);
color: white;
z-index: 9999;
text-decoration: none;
}
.skip-link:focus {
top: 0;
}
/* Focus visible */
:focus-visible {
outline: 3px solid var(--color-bleu-securite);
outline-offset: 2px;
}
:focus:not(:focus-visible) {
outline: none;
}
/* Touch targets (WCAG 2.5.5) */
button,
a,
input,
select,
textarea,
[role="button"] {
min-width: 48px;
min-height: 48px;
}
/* Taille texte minimum */
body {
font-size: max(1rem, 13px);
}
small,
.text-sm {
font-size: max(0.875rem, 13px);
}
/* Réduire les animations si préférence utilisateur */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
Composant SkipLink
---
// src/components/SkipLink.astro
const { lang } = Astro.props;
const label = lang === 'en'
? 'Skip to main content'
: 'Aller au contenu principal';
---
<a href="#main-content" class="skip-link">
{label}
</a>
Structure sémantique BaseLayout
---
// src/layouts/BaseLayout.astro (extrait)
const { lang } = Astro.props;
---
<!DOCTYPE html>
<html lang={lang}>
<head>...</head>
<body>
<SkipLink lang={lang} />
<header role="banner">
<nav role="navigation" aria-label="Navigation principale">
...
</nav>
</header>
<main id="main-content" role="main">
<slot />
</main>
<footer role="contentinfo">
...
</footer>
</body>
</html>
Configuration ESLint a11y
// .eslintrc.js
module.exports = {
extends: [
'plugin:astro/recommended',
'plugin:astro/jsx-a11y-recommended'
],
rules: {
'astro/jsx-a11y/alt-text': 'error',
'astro/jsx-a11y/anchor-has-content': 'error',
'astro/jsx-a11y/click-events-have-key-events': 'error',
'astro/jsx-a11y/heading-has-content': 'error',
'astro/jsx-a11y/html-has-lang': 'error',
'astro/jsx-a11y/img-redundant-alt': 'error',
'astro/jsx-a11y/interactive-supports-focus': 'error',
'astro/jsx-a11y/label-has-associated-control': 'error',
'astro/jsx-a11y/no-autofocus': 'error',
'astro/jsx-a11y/no-redundant-roles': 'error',
'astro/jsx-a11y/role-has-required-aria-props': 'error',
'astro/jsx-a11y/tabindex-no-positive': 'error'
}
};
Configuration pa11y-ci
// pa11y.config.js
module.exports = {
defaults: {
standard: 'WCAG2AA',
runners: ['axe', 'htmlcs'],
timeout: 30000,
wait: 1000
},
urls: [
'http://localhost:4321/fr/',
'http://localhost:4321/en/',
'http://localhost:4321/fr/product',
'http://localhost:4321/en/product',
'http://localhost:4321/fr/pricing',
'http://localhost:4321/en/pricing',
'http://localhost:4321/fr/faq',
'http://localhost:4321/en/faq'
]
};
4. Gestion des erreurs
| Erreur | Cause | Mitigation |
| Contraste insuffisant | Mauvaise couleur | Tokens avec ratios validés |
| Image sans alt | Oubli développeur | ESLint bloque le build |
| Focus invisible | CSS override | :focus-visible obligatoire |
| Touch target < 48px | Élément trop petit | CSS min-width/height |
Job CI accessibilité
# .gitlab-ci.yml (extrait)
a11y:
stage: test
script:
- npm ci
- npm run build
- npm run preview &
- sleep 5
- npx pa11y-ci --config pa11y.config.js
artifacts:
reports:
junit: pa11y-results.xml
5. Impacts sécurité
| Aspect | Mesure |
| XSS via aria-label | Échappement Astro natif |
| Clickjacking | X-Frame-Options: DENY |
| Social engineering | Pas de liens trompeurs |
6. Hypothèses techniques
| Hypothèse | Justification |
| Navigateurs modernes | :focus-visible supporté >95% |
| pa11y-ci disponible | Package npm stable |
| axe-core comme runner | Standard industrie |
| Pas de contenu dynamique | Audit statique suffisant |
7. Points de vigilance
| Point | Risque | Action |
| Images décoratives | Alt vide vs alt manquant | alt="" explicite |
| Formulaires | Labels non associés | Vérifier for/id |
| Modales | Focus trap absent | CSS-only ou <dialog> |
| Accordéons | <details> non accessible | Tester lecteurs d'écran |
| Liens | "Cliquez ici" vague | Texte descriptif |
Fichiers à créer
| Fichier | Description |
src/styles/accessibility.css | Styles a11y |
src/components/SkipLink.astro | Skip link |
.eslintrc.js | Config ESLint a11y |
pa11y.config.js | Config pa11y-ci |
.gitlab-ci.yml | Job audit a11y |