documentation
Kohinos v3 Doc générée le commit 8d60b1904

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

⚠ Interdits absolus dans tout .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'attribut style="..." 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 attributs data-*.
  • Jamais de dépendance npm ajoutée sans audit manuel (cf. .claude/rules/global.md : moins de 100 stars ⇒ refus par défaut).
✅ Procédure pour ajouter une feature JS/CSS
  1. Créer assets/js/ma-feature.js (et au besoin assets/css/ma-feature.css).
  2. Enregistrer une entry dans webpack.config.js :
    .addEntry('ma-feature', './assets/js/ma-feature.js')
    .addStyleEntry('ma-feature-styles', './assets/css/ma-feature.css')
  3. 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 %}
  4. 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>
  5. 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).

jQueryVanilla 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 :

⚠ Conséquence directe

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/

DossierRôleConvention 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 npmQuandEffet
npm run devBuild one-shot localCompile non-minifié + source maps dans public/build/
npm run watchPendant la session de devRecompile à chaque save sur assets/
npm run dev-serverHMR live-reloadDémarre un serveur webpack avec rechargement
npm run buildPublier 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 cleanAvant un reinstallSupprime node_modules et public/build
Politique « publier le 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/

CheminRôle
templates/base.html.twigLayout 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

Données Twig → JS

Trois mécanismes acceptés, dans l'ordre de préférence :

  1. Attributs data-* sur l'élément concerné — défaut systématique.
  2. Endpoint AJAX dédié retournant du JSON. Le JS lit l'URL via data-endpoint et appelle fetch().
  3. 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 lit JSON.parse(document.getElementById('id').textContent).

Translations

FichierContenu
translations/messages.fr.yamlClés applicatives (forms, flash, contenus)
translations/messages.en.yamlVariante anglophone
translations/SonataAdminBundle.fr.xliffSurcharges du domaine SonataAdmin
translations/validators.fr.yamlMessages 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é)

Anti-patterns à proscrire

⚠ Code à refactorer dès qu'il est croisé
  • <script>alert('debug');</script> dans un Twig de prod.
  • jQuery utilisé pour des sélecteurs basiques ($('#id')) alors que document.getElementById suffit.
  • 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 innerHTML avec une variable user-controlled — XSS direct. Préférer textContent ou créer les nœuds via document.createElement.
  • Imports CSS depuis node_modules dans un fichier feature alors qu'ils sont déjà chargés par app.css : gonfle le bundle final.

Webpack Encore — tips

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

Sources & liens
  • webpack.config.js — déclaration de toutes les entries
  • assets/js/app.js, assets/js/admin.js — entries principales
  • templates/base.html.twig, templates/themes/kohinos/
  • translations/messages.fr.yaml, translations/SonataAdminBundle.fr.xliff
  • CLAUDE.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