L’enfer du HTML statique (et comment je l’ai remplacé par une galerie pilotée par CSV)

Le mois dernier, sur un portfolio d’artiste (peintures/illustrations), je suis retombé dans le piège classique du site “100% statique” qui devient vite ingérable : à chaque nouvelle toile, il fallait ouvrir index.html, copier/coller un bloc, vérifier les chemins d’images, l’alt, les tags… puis prier pour ne rien casser.

Le problème n’était pas la “tech”, c’était le workflow : l’artiste devait pouvoir ajouter une œuvre sans toucher au HTML.

Résultat : j’ai séparé la donnée (liste des œuvres) du rendu (la page) via un simple images.csv chargé en JavaScript.

Repo de référence (code complet) :
https://github.com/vladimir-monari/monartistique/blob/main/


1) L’idée : des données dans images.csv, un rendu automatique dans le DOM

Structure CSV (la “source de vérité”)

J’ai standardisé un fichier images.csv avec des en-têtes stables (importants, car le code les référence tels quels) :

Nom de l'image,Chemin de l'image,Description de l'image,Tags
Portrait Bleu,/img/bleu.jpg,Huile sur toile,abstrait-portrait
Coucher de soleil,/img/soleil.jpg,Aquarelle,paysage-nature
  • Nom de l’image : sert de titre affiché
  • Chemin de l’image : URL relative (ex: /images/... ou /img/...)
  • Description de l’image : sert de texte descriptif (et alt)
  • Tags : une liste séparée par - (ex: abstrait-portrait)

Ce point (séparateur -) est volontaire : c’est simple à saisir dans Google Sheets, et trivial à parser côté JS.


2) index.html : une page “coquille” (et c’est exactement ce qu’on veut)

Ce que j’ai gardé dans index.html, c’est :

  • tout ce qui est structurel (header, section “about”, footer)
  • les conteneurs vides destinés à être remplis (#image-container, #tag-cloud)
  • les dépendances (CSS + PapaParse) et le script applicatif scripts.js
  • la modal d’affichage grand format

Points importants dans ton index.html :

a) Les conteneurs clés

<div id="tag-cloud"></div>
<div id="image-container"></div>

Ils définissent les “ports d’entrée” où le JS injecte la galerie.

b) La modal déjà prête

<div id="modal" class="modal">
<span class="close">&times;</span>
<img class="modal-content" id="modal-image">
<div id="caption"></div>
</div>

C’est un bon pattern : la structure est déclarative dans le HTML, et le JS ne fait que manipuler display, src et le texte.

c) Dépendances : PapaParse OK, D3 probablement inutile

Tu charges :

<script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.3.0/papaparse.min.js"></script>
<script src="https://d3js.org/d3.v5.min.js"></script>

PapaParse est effectivement utilisé. En revanche, D3 n’est pas utilisé dans scripts.js tel qu’il est actuellement (le nuage de tags est construit au DOM “vanilla”). Donc tu peux supprimer D3 pour alléger la page, sauf si tu prévois une vraie visualisation.

d) Google Tag Manager / Analytics

Tu as GTM + gtag. Ça fonctionne, mais garde en tête :

  • c’est du poids JS supplémentaire
  • idéalement, documente dans un README ce qui est mesuré (évite la “boîte noire” côté maintenance)

3) scripts.js : le pipeline complet (CSV → objets → DOM → tags → filtres)

a) Chargement du CSV

function loadCSV(callback) {
fetch('images.csv')
.then(response => response.text())
.then(data => callback(data))
.catch(error => console.error('Erreur lors du chargement du CSV:', error));
}

Ce que ça implique :

  • images.csv doit être dans le même dossier que index.html (ou il faut ajuster le chemin)
  • fetch() ne marchera pas correctement en ouvrant le fichier en file:// (cf. pièges plus bas)

b) Parsing + génération des cartes image

Le cœur est ici :

Papa.parse(data, {
header: true,
complete: function (results) {
results.data.forEach(function (d) {
if (!d["Nom de l'image"] || !d["Chemin de l'image"]) return;
var container = document.createElement(‘div’);
container.classList.add(‘image-wrapper’);
container.dataset.tags = d[‘Tags’];

var img = new Image();
img.src = d[« Chemin de l’image »];
img.alt = d[« Description de l’image »];
img.classList.add(« image-responsive »);

Bon choix : tu fais un early return si les champs essentiels manquent (ça neutralise déjà pas mal de lignes vides / erreurs CSV).

Ensuite :

  • dataset.tags sert à filtrer
  • alt est alimenté par la description (bon réflexe accessibilité/SEO)
  • l’ouverture de modal se fait au clic

c) Nuage de tags + filtrage

Tu comptes les tags :

d['Tags'].split('-').forEach(function (tag) {
tags[tag] = (tags[tag] || 0) + 1;
});

et tu construis le nuage avec une taille proportionnelle au nombre d’occurrences.


4) Le flux d’automatisation (celui qui change vraiment la vie)

Le flux que j’ai mis en place (et qui tient bien dans le temps) :

  1. L’artiste met à jour une feuille (Google Sheets / Excel)
  2. Export en CSV
  3. Dépôt du CSV + des images dans le repo
  4. git push
  5. Déploiement (GitHub Pages ou autre) → la galerie se met à jour automatiquement

Le gros gain : plus jamais d’édition manuelle du HTML pour une nouvelle œuvre.


5) Les pièges rencontrés (et les corrections concrètes)

1) Lignes vides / objets incomplets (PapaParse)

Tu l’as déjà traité en partie avec :

if (!d["Nom de l'image"] || !d["Chemin de l'image"]) return;

Je garde exactement cette logique, et j’ajoute souvent une défense sur Tags :

const rawTags = (d['Tags'] || '').trim();
container.dataset.tags = rawTags;

Sinon, split('-') sur undefined peut casser.


2) CORS en local (fetch + file://)

Classique : ouvrir index.html en double-cliquant ne suffit pas.

Solution : serveur local (Live Server, python -m http.server, etc.).
Tu l’as bien noté : Live Server sous VS Code règle ça immédiatement.


3) XSS / injection via CSV (le vrai point sensible)

Dans ton code actuel, il y a un endroit franchement risqué :

document.getElementById("caption").innerHTML = this.alt;

Même si “c’est juste un CSV”, c’est une entrée de données. Si un jour une valeur contient des balises, tu les interprètes.

Correction simple et robuste : ne jamais utiliser innerHTML pour du texte :

document.getElementById("caption").textContent = this.alt;

Ça te rend la fonction sanitize() beaucoup moins critique (et tu peux la réserver aux cas où tu dois vraiment injecter du HTML).


4) Bug discret : double conteneur #tag-cloud

Dans index.html, tu as déjà :

<div id="tag-cloud"></div>

Mais dans displayTagCloud, tu recrées un nouveau div avec le même id et tu l’insères après le <h3> :

var tagCloud = document.createElement('div');
tagCloud.id = 'tag-cloud';
document.querySelector('#container h3').insertAdjacentElement('afterend', tagCloud);

Conséquence : tu peux te retrouver avec deux éléments portant le même id (invalid HTML, comportements bizarres).

Correction : réutiliser le conteneur existant :

function displayTagCloud(tags) {
var tagCloud = document.getElementById('tag-cloud');
tagCloud.innerHTML = ''; // reset
...
}

6) styles.css : ce qui marche bien (et ce que j’ajusterais)

Points positifs :

  • layout simple, lisible, cohérent
  • .image-wrapper en inline-block : pratique pour une galerie “mosaïque”
  • modal plein écran avec fond noir : efficace pour de l’art

Deux remarques terrain :

a) max-width: 100vh sur .modal-content

Tu as :

.modal-content {
max-width: 100vh;
max-height: 90vh;
}

max-width en vh (hauteur viewport) est inhabituel : sur écran large, ça peut limiter la largeur de façon contre-intuitive. En général, je préfère :

.modal-content {
max-width: 95vw;
max-height: 90vh;
}

b) Performance visuelle

Si la galerie grandit, pense à :

  • loading="lazy" côté image
  • éventuellement des miniatures dédiées (évite de charger un 4000px pour une vignette)

7) Améliorations futures (SEO, UX, perf) — plan d’évolution réaliste

UX / Accessibilité

  • fermer la modal au clic hors image + touche Escape
  • focus management (quand la modal s’ouvre, focus sur le bouton close)
  • ajouter aria-label au bouton close

Performance

  • img.loading = "lazy";
  • pagination / chargement par lots (12 par 12)
  • miniatures + images HD séparées (ex: thumb_path et full_path dans le CSV)

SEO

  • alt déjà OK, mais tu peux enrichir :
    • titres d’œuvres en <h4> (ou au moins structure sémantique)
    • JSON-LD (type ImageObject ou CreativeWork) si tu veux pousser le référencement

Sécurité

  • bannir innerHTML pour tout texte issu du CSV (caption, descriptions, titres)
  • valider les chemins d’images (optionnel, mais utile si le CSV est édité par plusieurs personnes)

Conclusion

Le vrai bénéfice de cette approche CSV + rendu JS, c’est qu’elle transforme un site statique fragile en mini “CMS maison”, sans serveur, sans base de données, sans back-office. Pour un portfolio d’artiste, c’est exactement le bon compromis : simple à maintenir, rapide à charger, et évolutif.

Code complet (structure, index.html, images.csv, scripts.js, styles.css) :
https://github.com/vladimir-monari/monartistique/blob/main/

Commentaires

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *