Frontend — Twig, Bootstrap 5.2, Webpack Encore, Vanilla JS first
Kohinos est une application server-rendered : Twig génère le HTML, Bootstrap 5 fournit la grille et les composants, Webpack Encore compile un bundle minimal, et le JS reste en Vanilla ES2020+. Pas de SPA, pas de React/Vue/Angular, pas de framework client lourd. L'objectif est qu'une page Comptoir ou Adhérent charge en moins de 2 s sur un Raspberry Pi servant un réseau local. Les règles ci-dessous sont volontairement contraignantes : elles empêchent le glissement vers du JS inline dans Twig et un bundle qui enfle.
Règles dures
.html.twig
- Jamais de
<script>...</script>inline contenant de la logique. Une seule exception : l'initialisation Mermaid dans la doc statique. - Jamais de
<style>...</style>inline ni d'attributstyle="..."exprimant autre chose qu'un display d'urgence — préférer une classe utilitaire Bootstrap (d-none,text-end,mt-3). - Jamais de
JSON.stringify(...)intercalé dans le HTML pour passer des données Twig au JS. Passer par des attributsdata-*. - Jamais de dépendance npm ajoutée sans audit manuel
(cf.
.claude/rules/global.md: moins de 100 stars ⇒ refus par défaut).
- Créer
assets/js/ma-feature.js(et au besoinassets/css/ma-feature.css). - Enregistrer une entry dans
webpack.config.js:.addEntry('ma-feature', './assets/js/ma-feature.js') .addStyleEntry('ma-feature-styles', './assets/css/ma-feature.css') - Inclure dans le template :
{% block javascripts %} {{ parent() }} {{ encore_entry_script_tags('ma-feature') }} {% endblock %} {% block stylesheets %} {{ parent() }} {{ encore_entry_link_tags('ma-feature-styles') }} {% endblock %} - Passer les données Twig → JS via
data-*:<div id="ssa-stepper" data-execution-date="{{ ssa.executionDate }}" data-currency="EMLC" data-min-amount="{{ ssa.minimum }}"> </div> - Lire ces attributs en JS :
const root = document.getElementById('ssa-stepper'); const day = Number(root.dataset.executionDate);
Vanilla JS first
Le projet a hérité d'une base jQuery. La règle est : à chaque édition
d'un fichier .js dans assets/js/, migrer son intégralité vers
du natif ES2020+ à comportement identique. On garde uniquement le jQuery requis par les
peer-dependencies (Select2, CKEditor).
| jQuery | Vanilla ES2020+ |
|---|---|
$('#foo').on('click', cb) | document.getElementById('foo').addEventListener('click', cb) |
$.ajax({url, data, success}) | fetch(url, {method, body}).then(r => r.json()) |
$('.x').each(fn) | document.querySelectorAll('.x').forEach(fn) |
$el.addClass('y') | el.classList.add('y') |
$el.attr('data-foo') | el.dataset.foo |
$(document).ready(fn) | document.addEventListener('DOMContentLoaded', fn) |
Exceptions tolérées (peer deps, pas de remplacement vanilla raisonnable) : Select2 (autocomplete server-side), CKEditor 4 (WYSIWYG admin), bootstrap-slider (slider montant SSA), SortableJS (drag-and-drop kanban BOffice). Tout nouveau besoin doit interroger d'abord les Web Components et l'API native.
Logging frontend
Convention : console.log('[Module] action — ', payload) partout pendant
le dev. La configuration Terser drop_console: true (cf.
webpack.config.js) supprime tous les
console.log/warn/error/info en production. Conséquence :
console.warn et console.error sont aussi strippés en prod.
Ne pas s'en servir pour du logging persistant ou pour signaler une erreur métier à
l'utilisateur — passer par une flash Symfony ou un alert contrôlé
côté UI. Le logging d'erreurs durables se fait côté serveur via Monolog.
Structure assets/
| Dossier | Rôle | Convention de nommage |
|---|---|---|
assets/js/ |
Sources JavaScript par feature, une entry = un fichier racine | kebab-case.js, préfixés par contexte : admin-*,
boffice-*, sepa-*, ssa-* |
assets/css/ |
Feuilles spécifiques par feature, déclarées via addStyleEntry |
kebab-case.css, miroir du nom de l'entry JS |
assets/controllers/ |
Stimulus controllers (Symfony UX bridge) | nom_controller.js |
assets/images/ |
Images statiques compilées par le file-loader | Fingerprintées en prod via enableVersioning |
assets/leaflet/ |
Vendor isolé Leaflet pour la cartographie prestataires | Patché en place — ne pas mélanger à assets/js/ |
assets/bootstrap.js |
Initialisation Stimulus + Symfony UX Bridge | Point d'entrée référencé par app.js |
assets/controllers.json |
Manifest des UX packages activés | Géré par flex, ne pas éditer à la main |
Cycle de développement
| Commande npm | Quand | Effet |
|---|---|---|
npm run dev | Build one-shot local | Compile non-minifié + source maps dans public/build/ |
npm run watch | Pendant la session de dev | Recompile à chaque save sur assets/ |
npm run dev-server | HMR live-reload | Démarre un serveur webpack avec rechargement |
npm run build | Publier le build (après modif d'un asset) | Minifie, hash, strip console, génère le SW Workbox — résultat à committer dans public/build/ (+ public/sw.js) |
npm run clean | Avant un reinstall | Supprime node_modules et public/build |
Les assets front sont pré-compilés et versionnés dans
public/build/ (+ public/sw.js) : le serveur de production ne
dépend plus de Node/npm/yarn/Encore. Pendant le dev on reste sur
npm run watch (build non-minifié, source maps). Après modification d'un asset,
le dev lance npm run build puis committe le contenu de
public/build/ (+ public/sw.js). Le script
scripts/build-and-stage-assets.sh et le hook de pré-commit
.claude/hooks/pre-commit-build.sh automatisent cette republication.
La CI ne build pas les assets (pas de Node en CI) : un garde-fou
assets-freshness (step des pipelines Woodpecker .woodpecker/pr.yml +
main.yml, via scripts/ci-assets-freshness.sh) échoue si
une source front change sans que public/build ait été republié. Cela garantit
que le bundle déployé correspond toujours aux sources committées.
Conventions Twig
Arborescence templates/
| Chemin | Rôle |
|---|---|
templates/base.html.twig | Layout racine — head, blocks stylesheets / javascripts / body |
templates/themes/kohinos/ | Thème actif : emails, pages publiques, surcharges Bootstrap |
templates/themes/kohinos/email/ | Templates Symfony Mailer (HTML + texte) |
templates/themes/custom/ | Surcharges admin par déploiement (whitelabel) |
templates/themes/oldcustom*/ | Templates legacy gelés — référence pour migration, ne pas étendre |
templates/bundles/ | Surcharges de bundles tiers (Sonata, FOSUser…) |
Patterns recommandés
{% extends 'base.html.twig' %}+ blocks{% block content %},{% block javascripts %},{% block stylesheets %}. Toujours appeler{{ parent() }}dans les blocs assets pour conserver la base.- Forms :
{{ form_widget(form.field) }}+{{ form_label(...) }}+{{ form_errors(...) }}plutôt qu'un<input>manuel. Le rendu vient des thèmes Symfony Bootstrap5. - Composants Bootstrap :
card,alert,btn btn-primary,table table-striped. Icônes : classesbi bi-*(Bootstrap Icons) ou Font Awesomefa fa-*selon le contexte historique. - Pas de logique métier dans Twig. Un service produit une
view-model (array ou DTO), le contrôleur passe la variable, Twig se contente
d'afficher. Pas de
{% if user.account.balance | something_calculé %}qui touche aux montants.
Données Twig → JS
Trois mécanismes acceptés, dans l'ordre de préférence :
- Attributs
data-*sur l'élément concerné — défaut systématique. - Endpoint AJAX dédié retournant du JSON. Le JS lit l'URL via
data-endpointet appellefetch(). - Balise
<script type="application/json" id="...">— autorisée uniquement pour des payloads volumineux ou structurés (sérialisés via{{ data | json_encode | raw }}). Le JS litJSON.parse(document.getElementById('id').textContent).
Translations
| Fichier | Contenu |
|---|---|
translations/messages.fr.yaml | Clés applicatives (forms, flash, contenus) |
translations/messages.en.yaml | Variante anglophone |
translations/SonataAdminBundle.fr.xliff | Surcharges du domaine SonataAdmin |
translations/validators.fr.yaml | Messages d'erreur des contraintes Symfony Validator |
En Twig : {{ 'ssa.cotisation.label' | trans }},
{{ 'flux.count' | trans({'%count%': total}) }}, balise
{% trans %}Texte avec {{ var }}{% endtrans %} pour les blocs longs.
Côté JS, pas de mécanisme de traduction : si une chaîne doit être localisée,
Twig la rend dans un data-label-* que le JS lit.
Accessibilité (WCAG AA visé)
aria-labelsystématique sur les boutons icône-only (<button class="btn btn-icon" aria-label="Supprimer le mandat">).:focus-visiblerespecté : pas deoutline:nonesans remplacement.- Contraste minimum AA (4.5:1 texte normal, 3:1 grand texte). Vérifier
les variantes de couleur thème dans
assets/css/global.scss. - Labels
<label for="">avant chaque input — pas uniquement desplaceholder. - Navigation au clavier :
tabindexuniquement quand justifié, jamais de valeurs > 0. - Tables :
<th scope="col">/scope="row",<caption>obligatoire pour les tableaux de données financières.
Anti-patterns à proscrire
<script>alert('debug');</script>dans un Twig de prod.- jQuery utilisé pour des sélecteurs basiques (
$('#id')) alors quedocument.getElementByIdsuffit. - CSS dans un attribut
style="color:red;display:flex;..."(exception : une seule property utility quand la classe Bootstrap n'existe pas). - Bundle JS d'une seule feature dépassant 200 KB minifié :
signe de duplication ou d'import incorrect. Splitter via une entry secondaire ou
un lazy
import(). - Utilisation de
innerHTMLavec une variable user-controlled — XSS direct. PréférertextContentou créer les nœuds viadocument.createElement. - Imports CSS depuis
node_modulesdans un fichier feature alors qu'ils sont déjà chargés parapp.css: gonfle le bundle final.
Webpack Encore — tips
- Tree-shaking activé d'office en prod (Webpack 5). Importer
nommément (
import {debounce} from 'lodash-es'), jamais l'intégralité d'une lib. splitEntryChunks()est actuellement désactivé pour réduire le nombre de requêtes derrière un proxy Pi. Réévaluer si une entry dépasse 300 KB.- Lazy load :
const mod = await import('./heavy-feature.js')depuis un handler d'event. Le code n'est téléchargé qu'à l'usage. - Fingerprinted assets :
enableVersioning(Encore.isProduction())⇒app.abc123.css. Toujours référencer viaencore_entry_script_tags, jamais en URL en dur. - Sass :
silenceDeprecationsest activé pourimport,global-builtin,color-functionstant que Bootstrap 5 n'a pas migré vers@use. Ne pas réactiver ces warnings. - Service Worker :
WorkboxWebpackPlugin.InjectManifestcompilesrc/src-sw.jsverspublic/sw.jsavecmaximumFileSizeToCacheInBytes: 5 MB. Toute ressource > 5 MB contourne le cache offline — éviter à tout prix. autoProvidejQuery: ne plus ajouterimport $ from 'jquery'dans les nouveaux fichiers — l'auto-provide est un filet pour le legacy uniquement.
Tests frontend
Le projet expose les scripts test:js (Vitest), test:e2e
(Playwright) dans package.json, mais la stratégie principale reste
les tests fonctionnels Symfony qui rendent le HTML serveur et
valident la structure DOM via Crawler :
$crawler = $this->client->request('GET', '/adherent/ssa');
self::assertSelectorExists('#ssa-stepper[data-execution-date="5"]');
self::assertSelectorTextContains('h1', 'Mes caisses solidaires');
Cette approche teste à la fois le contrôleur, Twig, et la présence des hooks JS sans démarrer Node/Vitest. Vitest reste disponible pour des utilitaires JS purs (parsing, formatage de montants), Playwright pour les smoke tests de bout en bout.
Pour aller plus loin
webpack.config.js— déclaration de toutes les entriesassets/js/app.js,assets/js/admin.js— entries principalestemplates/base.html.twig,templates/themes/kohinos/translations/messages.fr.yaml,translations/SonataAdminBundle.fr.xliffCLAUDE.md— section Frontend — règles dures.claude/rules/global.md— refus automatiques (SPA, deps cloud)- Conventions Kohinos (PHP & projet)
- Sonata Admin 4.x — particularités