<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Projets | Julien Maxant</title><link>https://www.julien-maxant.com/projets/</link><description>Projets personnels et professionnels : contexte, choix techniques et résultats.</description><generator>Hugo -- gohugo.io</generator><language>fr-FR</language><copyright>2026 - Julien Maxant</copyright><lastBuildDate>Sun, 12 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://www.julien-maxant.com/projets/index.xml" rel="self" type="application/rss+xml"/><item><title>Portfolio Hugo</title><link>https://www.julien-maxant.com/projets/portfolio-hugo/</link><guid>https://www.julien-maxant.com/projets/portfolio-hugo/</guid><pubDate>Sun, 12 Jul 2026 00:00:00 +0000</pubDate><description>Étude de cas : le site que vous consultez — pipeline CSS et qualité outillés pour tenir une baseline navigateurs et un contraste AAA sans y repasser à l'œil à chaque changement.</description><content:encoded><![CDATA[<p><strong>En bref :</strong> un site que je peux faire évoluer sans craindre de casser l&rsquo;existant.
Accessibilité, cohérence visuelle et qualité du build sont vérifiées automatiquement à
chaque modification, plutôt que relues à la main.</p>
<p>Techniquement : Hugo sans thème (layouts et partials écrits à la main), CSS sur mesure
sans framework, pipeline qualité (lint, tokens, contraste, build) qui tourne
à l&rsquo;identique en local et en CI.</p>
<h2 id="pourquoi-hugo">Pourquoi Hugo</h2>
<p>Projet perso avec un double objectif : une vitrine professionnelle pour recruteurs et
clients freelance, et un terrain d&rsquo;entraînement pour monter en compétence sur du
templating Go, du CSS sans dépendance, et une chaîne CI/CD tenue de bout en bout — sans
sacrifier la lisibilité du contenu à la démonstration technique.</p>
<p>Un générateur de site statique répond aux deux à la fois. Côté vitrine, la cible
(recruteurs, agents de recherche) n&rsquo;a pas de rendu JS à franchir pour indexer le
contenu, contrairement à une SPA. Côté apprentissage, le HTML/CSS produit reste la
sortie principale — pas de couche framework JS entre l&rsquo;auteur et le résultat, donc
chaque décision (tokens, contraste, breakpoints) reste visible et vérifiable dans la
feuille de style elle-même plutôt que dissoute dans un système de composants.</p>
<h2 id="contexte">Contexte</h2>
<ul>
<li>Site personnel, parti d&rsquo;un squelette : thème installé, contenu en Lorem Ipsum, une
seule page <code>content/_index.md</code></li>
<li>Aucune contrainte de delivery externe — le calendrier est le seul arbitre du scope</li>
<li>Exigence posée dès la Phase 0 (avant tout contenu) : les quality gates et la CI
existent avant que le contenu s&rsquo;accumule, pas après</li>
<li>Cible recruteurs/clients : le site doit rester une vitrine lisible, pas seulement un
prétexte à empiler des scripts de vérification</li>
</ul>
<h2 id="les-contraintes">Les contraintes</h2>
<ul>
<li>Pas de framework CSS, pas de PostCSS, pas d&rsquo;autoprefixer — seul <code>css.Build</code> (esbuild,
natif Hugo) prend en charge la transpilation et les préfixes vendeur</li>
<li>Une baseline navigateurs explicite (Chrome 105+, Firefox 121+, Safari 16+, Edge 105+)
à tenir, alors que deux mécanismes différents la couvrent : transpilation de syntaxe
d&rsquo;un côté, blocage de features runtime non transpilables de l&rsquo;autre</li>
<li>Contraste ciblé à AAA (7:1) plutôt que le AA/RGAA (4.5:1), sans dérive silencieuse
tolérée à mesure que la palette ou les composants évoluent</li>
<li>Aucune valeur de couleur, d&rsquo;espacement ou de breakpoint écrite en dur dans un
composant — tout doit venir d&rsquo;un token déclaré une seule fois</li>
<li>Le hook pre-commit local et la CI doivent exécuter la même définition, pour qu&rsquo;aucun
contrôle qualité ne puisse diverger entre les deux</li>
</ul>
<h2 id="les-choix-écartés">Les choix écartés</h2>
<ul>
<li>Une revue manuelle du contraste à chaque changement de palette : tolérable une fois,
pas répétable sans dérive — d&rsquo;autant que la palette est passée de AA à AAA en cours
de route, avec des marges initiales aussi fines qu&rsquo;entre 7,00 et 7,06</li>
<li>Une exemption CSS ajoutée « au cas où » dans la config Stylelint plutôt que prouvée :
une feature ignorée sans vérification empirique dans <code>public/</code> reste ignorée même
quand la baseline évolue et que le support natif la couvre déjà</li>
<li>Dupliquer la logique de vérification entre pre-commit et CI (deux configs qui
divergent tôt ou tard) plutôt qu&rsquo;une définition unique appelée par les deux</li>
</ul>
<h2 id="décisions-techniques">Décisions techniques</h2>
<ul>
<li><strong>Tokens CSS à deux niveaux d&rsquo;indirection</strong> : palettes brutes (<code>--light-*</code>,
<code>--dark-*</code>) jamais consommées directement par un composant, tokens sémantiques
(<code>--color-surface</code>, <code>--color-text-soft</code>…) seuls exposés. Le dark mode change en
réassignant les tokens sémantiques dans un seul fichier, sans toucher aux
composants.</li>
<li><strong>Règle « zéro valeur en dur » appliquée par script</strong>, pas seulement documentée :
<code>check-tokens.mjs</code> échoue sur toute couleur, taille ou durée littérale hors
<code>base/tokens.css</code> ; l&rsquo;échappatoire est un commentaire <code>token-exception</code> justifié
inline, jamais un ajout silencieux à une liste d&rsquo;ignore.</li>
<li><strong>Contraste vérifié automatiquement</strong> : <code>check-contrast.mjs</code> lit les valeurs hex
directement dans <code>tokens.css</code> (aucune valeur dupliquée dans le script) et calcule
chaque paire de couleurs, texte à 7:1, composants à 3:1. Un token de couleur non
couvert par une paire est aussi un échec — sinon un token ajouté plus tard n&rsquo;est
simplement jamais mesuré.</li>
<li><strong>Breakpoints déclaratifs mais vérifiés à l&rsquo;exécution</strong> : les media queries ne
peuvent pas lire une custom property, donc les valeurs (<code>768px</code>, <code>576px</code>) restent en
dur — mais <code>check-breakpoints.mjs</code> échoue sur une largeur qui ne correspond à aucun
token <code>--bp-*</code>, et sur un token que plus aucune query n&rsquo;utilise.</li>
<li><strong>Deux rôles distincts pour tenir la baseline navigateurs</strong> : <code>css.Build</code> transpile
la syntaxe (nesting, media query range syntax) à la compilation ; Stylelint
(<code>stylelint-no-unsupported-browser-features</code>) bloque au lint les features runtime
qu&rsquo;aucun transpileur ne peut simuler (container queries, <code>subgrid</code>). Chaque entrée de
la liste d&rsquo;ignore Stylelint est justifiée par une vérification dans <code>public/</code> après
build, pas supposée — deux entrées obsolètes (<code>:has()</code>, <code>scroll-behavior</code>) ont été
retirées une fois la baseline remontée et le support natif confirmé.</li>
<li><strong><code>lefthook.yml</code> comme unique source de vérité qualité</strong> : le hook pre-commit local
tourne sur les fichiers stagés, la CI appelle la même commande sur l&rsquo;ensemble des
fichiers trackés — aucune règle qualité ne peut exister dans l&rsquo;un sans exister dans
l&rsquo;autre.</li>
<li><strong>Contenu piloté par cascade Hugo plutôt que par template dédié</strong> : la section
<code>/veille/</code> (teaser-only) utilise <code>build.render = 'link'</code> en cascade pour rester dans
les collections (donc alimenter les pages <code>/tags/*</code>) sans générer de page de détail —
contre <code>render = 'never'</code>, qui exclurait l&rsquo;entrée de toute collection. Comportement
vérifié après build (<code>--cleanDestinationDir</code>) plutôt que supposé : aucune page
<code>public/veille/&lt;entrée&gt;/</code> générée, sitemap propre, RSS global exempt.</li>
</ul>
<h2 id="résultat">Résultat</h2>
<p>Pas de métrique de production comparable au cas Drupal — c&rsquo;est un site personnel, pas
un site à trafic. L&rsquo;angle est différent : une CI qui tolère zéro <code>WARN</code> Hugo, un
contraste et des tokens vérifiés par script plutôt que revus à l&rsquo;œil, et un pre-commit
qui ne peut pas diverger de la CI par construction. Le pipeline qualité tient à jour
avec le contenu, pas après coup.</p>
]]></content:encoded></item><item><title>Mise en place d'une navigation multi-audience pour un site institutionnel</title><link>https://www.julien-maxant.com/projets/navigation-multi-audience/</link><guid>https://www.julien-maxant.com/projets/navigation-multi-audience/</guid><pubDate>Fri, 15 May 2026 00:00:00 +0000</pubDate><description>Étude de cas : cadrer un besoin éditorial multi-audience en un système de navigation et classification unique, sans multiplier menus ni taxonomies.</description><content:encoded><![CDATA[<h2 id="contexte">Contexte</h2>
<ul>
<li>Refonte de site institutionnel</li>
<li>Drupal 10+ (10.3.x à sa mise en ligne)</li>
<li>Site multilingue</li>
<li>Trafic international important (notamment saisonnier), allant jusqu&rsquo;à 15 000 visites uniques quotidiennes</li>
</ul>
<p>Les contenus du site sont structurés en trois espaces distincts par public cible :</p>
<ul>
<li>grand public (B2C)</li>
<li>professionnels (B2B)</li>
<li>relations publiques (PR)</li>
</ul>
<p>Dans chacune de ces sections, les gabarits et comportements des types contenus sont partagés, il n&rsquo;est donc ni pertinent ni requis de créer des types de contenus dédiés.</p>
<p>Les éléments de navigation du site (menu principal et breadcrumb) doivent refléter cette organisation, c&rsquo;est-à-dire que l&rsquo;élément &ldquo;Accueil&rdquo; du breadcrumb doit renvoyer vers la page d&rsquo;accueil de la section.</p>
<p>Le besoin éditorial, formulé simplement : permettre au contributeur de rattacher chaque contenu à l&rsquo;une des trois sections, pour que :</p>
<ul>
<li>le fil d&rsquo;Ariane affiche la bonne page d&rsquo;accueil de rattachement ;</li>
<li>le menu du header reflète l&rsquo;espace courant.</li>
</ul>
<h2 id="les-contraintes">Les contraintes</h2>
<ul>
<li>Un menu de header en deux temps : un premier bloc pour naviguer entre les trois espaces, un second pour les rubriques de l&rsquo;espace courant. Entre ces deux menus, des éléments statiques s&rsquo;intercalent, cassant le flux du rendu et compromettant l&rsquo;intégration du menu. Le rendu du menu ne peut donc pas se reposer sur le rendu natif de Drupal et impliquerait une préparation en amont de l&rsquo;affichage (<code>hook_preprocess</code>, duplication de blocks ou manipulation en javascript)</li>
<li>Une classification de contenu à gérer sur chaque page pour piloter ces deux menus et le fil d&rsquo;Ariane</li>
<li>Un besoin de simplification de la contribution</li>
<li>Côté technique, un choix délibéré de limiter autant que possible le recours aux hooks</li>
<li>Cette notion doit aussi s&rsquo;appliquer sur des pages techniques (Views, page de terme de taxonomie ou <code>Controller</code>)</li>
</ul>
<p>L&rsquo;approche la plus directe — une taxonomie dédiée en parallèle du menu, ou un menu par espace — revenait à maintenir deux structures pour une seule idée (l&rsquo;appartenance à un espace), avec le risque classique de désynchronisation entre les deux au fil des évolutions.</p>
<h2 id="les-choix-écartés">Les choix écartés</h2>
<ul>
<li>Un menu par section : multiplication du coût en maintenance et impact possible de performance (×3), requiert des hooks preprocess pour gérer l&rsquo;élément actif du menu des landing pages de sections.</li>
<li>Catégorisation par vocabulaire de taxonomie : deux sources de vérité alimentées manuellement (menu et termes de taxonomie), avec le risque de désynchronisation</li>
</ul>
<h2 id="décisions-techniques">Décisions techniques</h2>
<p>Chaque contenu porte un champ optionnel qui référence l&rsquo;un des trois éléments racine du menu principal (basé sur le plugin_id du <code>MenuLinkTreeElement</code>).
Laissé vide, il prend pour valeur par défaut, par ordre de priorité :</p>
<ol>
<li>Son élément parent, s&rsquo;il est présent dans le menu</li>
<li>La page d&rsquo;accueil générale</li>
</ol>
<p>Cette valeur est calculée dans un <code>ContextProvider</code>, qui l&rsquo;expose dans les blocks de menu. Deux blocks sont déclarés, tous deux basés sur le menu principal :</p>
<ul>
<li>Le menu des sections (B2C, B2B, PR)</li>
<li>La navigation dans les enfants (niveau 1 et plus)</li>
</ul>
<p>Le <code>ContextProvider</code> devient la source de vérité unique de la section active et permet de piloter l&rsquo;affichage et le rendu des différents éléments via une couche d&rsquo;abstraction rendant cette valeur accessible à l&rsquo;ensemble du site, pas seulement les nodes.</p>
<p>À partir de cette valeur :</p>
<ul>
<li>le fil d&rsquo;Ariane utilise la landing page correspondante comme racine, plutôt que systématiquement <code>/</code> ;</li>
<li>le premier bloc du menu header affiche les liens vers les deux autres espaces (ex. dans l&rsquo;espace B2B, il pointe vers B2C et PR) ;</li>
<li>le second bloc affiche les enfants de l&rsquo;élément racine de l&rsquo;espace courant.</li>
</ul>
<p>Le rendu dépend de cette valeur, pas de la route ni de l&rsquo;utilisateur connecté — les contextes de cache natifs de Drupal (route, utilisateur, langue&hellip;) n&rsquo;en savent rien. Deux conséquences concrètes sans contexte dédié :</p>
<ul>
<li>le filtrage du menu (accès autorisé ou refusé par élément selon l&rsquo;espace, via <code>AccessResult</code>) n&rsquo;est associé à aucun contexte de cache existant : Drupal peut resservir un menu déjà filtré pour le mauvais espace ;</li>
<li>changer l&rsquo;espace d&rsquo;un contenu produit deux jeux de contextes de cache, avant et après, sans rien en commun aux yeux de Drupal — qui lève une erreur de redirection de cache.</li>
</ul>
<p>Le cache context personnalisé rend l&rsquo;espace explicite dans la clé de cache : le menu filtré n&rsquo;est plus resservi au
mauvais espace, et le changement d&rsquo;espace d&rsquo;un contenu ne provoque plus d&rsquo;erreur de redirection de cache.</p>
<p>Le <code>ContextProvider</code> devient le point central de toute la mécanique, sans taxonomie parallèle à synchroniser ou de menu multiple à contribuer.</p>
<h2 id="résultat">Résultat</h2>
<p>En production, sans incident depuis la mise en ligne (septembre 2025), sur un site à fort trafic. La classification est prise en main par les équipes éditoriales sans formation particulière — un seul champ à renseigner, dont le sens (quel espace) leur est déjà familier puisqu&rsquo;il reprend directement la structure du menu qu&rsquo;elles connaissent.</p>
]]></content:encoded></item></channel></rss>