Catégorie : Centralisation & Services

  • Static HTML Hell (and How I Replaced It with a CSV‑Driven Gallery)

    Last month, while working on an artist’s portfolio (paintings/illustrations), I fell into the classic “100% static” website trap that quickly becomes unmanageable: every time a new piece was added, I had to open index.html, copy/paste a block, double-check image paths, alt text, tags… and then hope I didn’t break anything.

    The real problem wasn’t the “tech”—it was the workflow. The artist needed to be able to add a new artwork without touching HTML.

    So I separated the data (the list of artworks) from the rendering (the page) using a simple images.csv file loaded via JavaScript.

    Reference repo (full code):
    https://github.com/vladimir-monari/monartistique/blob/main/


    1) The idea: data in images.csv, automatic rendering in the DOM

    CSV structure (the “single source of truth”)

    I standardized an images.csv file with stable headers (important, because the code references them exactly as written):

    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

    What each column is used for:

    • Nom de l’image: becomes the displayed title
    • Chemin de l’image: relative URL (e.g., /images/... or /img/...)
    • Description de l’image: descriptive text (also used as the image alt)
    • Tags: a list of tags separated by - (e.g., abstrait-portrait)

    That hyphen separator is intentional: it’s easy to type in Google Sheets, and trivial to parse in JS.


    2) index.html: a “shell” page (and that’s exactly what we want)

    In index.html, I kept:

    • everything structural (header, “about” section, footer)
    • the empty containers to be filled dynamically (#image-container, #tag-cloud)
    • dependencies (CSS + PapaParse) and the application script scripts.js
    • the fullscreen modal for large image display

    Key parts in your index.html:

    a) The critical containers

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

    These are the “ports” where JavaScript injects the tag cloud and the gallery.

    b) The modal is already in place

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

    This is a solid pattern: the structure is declarative in HTML, and JS only manipulates display, src, and text content.

    c) Dependencies: PapaParse is used, D3 is probably dead weight

    You currently load:

    <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 is indeed used. But D3 isn’t used in the current scripts.js (your tag cloud is built with vanilla DOM). So you can remove D3 to lighten the page—unless you plan to build a real visualization with it later.

    d) Google Tag Manager / Analytics

    You have both GTM and gtag. It works, but keep in mind:

    • it’s extra JavaScript weight
    • ideally document what’s being tracked in a README (avoids “black box” maintenance)

    3) scripts.js: the full pipeline (CSV → objects → DOM → tags → filters)

    a) Loading the 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));
    }

    Implications:

    • images.csv must be in the same folder as index.html (or you need to adjust the path)
    • fetch() won’t work properly if you open the page via file:// (more on that in pitfalls)

    b) Parsing + generating image cards

    The core part:

    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");
    ...

    Good call: the early return prevents most empty rows / malformed CSV entries from causing issues.

    Then:

    • dataset.tags powers filtering
    • alt comes from the description (good accessibility/SEO reflex)
    • the modal opens on click

    c) Tag cloud + filtering

    You count tags with:

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

    …and build the tag cloud with font sizes proportional to frequency.


    4) The automation flow (the part that truly changes everything)

    The workflow I implemented (and that holds up over time):

    1. The artist updates a sheet (Google Sheets / Excel)
    2. Export to CSV
    3. Commit the CSV + images into the repo
    4. git push
    5. Deploy (GitHub Pages or similar) → the gallery updates automatically

    The big win: no more manual HTML editing for each new artwork.


    5) Pitfalls I hit (and the concrete fixes)

    1) Empty rows / incomplete objects (PapaParse)

    You already handle this with:

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

    I’d keep that logic and also defensively handle missing tags:

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

    Otherwise, split('-') on undefined can crash.


    2) Local CORS issues (fetch + file://)

    Classic: double-clicking index.html isn’t enough.

    Fix: run a local server (VS Code Live Server, python -m http.server, etc.).
    In practice, Live Server solves it instantly during development.


    3) XSS / injection via CSV (the real sensitive one)

    In your current code, this line is risky:

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

    Even if “it’s just a CSV”, it’s still input data. If one day a value contains HTML, it will be interpreted.

    Simple and robust fix: never use innerHTML for plain text:

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

    This makes a custom sanitize() function far less critical (and you can reserve it for the rare cases where you truly need to inject HTML).


    4) Subtle bug: duplicate #tag-cloud container

    In index.html, you already have:

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

    But in displayTagCloud(), you create a new div with the same id and insert it after the <h3>:

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

    Result: you can end up with two elements with the same id (invalid HTML, weird behavior).

    Fix: reuse the existing container:

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

    6) styles.css: what works (and what I’d tweak)

    What’s good:

    • simple, readable, consistent layout
    • .image-wrapper as inline-block: great for a “mosaic” gallery
    • fullscreen modal with dark overlay: perfect for artwork viewing

    Two practical notes:

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

    You currently have:

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

    Using vh for width is unusual: on wide screens it can limit the image width in a counter-intuitive way. I generally prefer:

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

    b) Visual performance

    As the gallery grows, consider:

    • loading="lazy" on images
    • dedicated thumbnails (avoid loading a 4000px image just to display a small preview)

    7) Future improvements (SEO, UX, performance) — a realistic roadmap

    UX / Accessibility

    • close the modal by clicking outside the image + pressing Escape
    • focus management (when the modal opens, focus the close button)
    • add aria-label to the close button

    Performance

    • img.loading = "lazy";
    • pagination / batch loading (12 at a time)
    • separate thumbnails + full-res images (e.g., thumb_path and full_path columns in the CSV)

    SEO

    • alt is already good, but you can go further:
      • artwork titles as <h4> (or at least better semantic structure)
      • JSON-LD (ImageObject or CreativeWork) if you want stronger indexing

    Security

    • avoid innerHTML for anything coming from the CSV (caption, descriptions, titles)
    • validate image paths (optional, but useful if multiple people edit the CSV)

    Conclusion

    The real benefit of a CSV + JS rendering approach is that it turns a fragile static site into a lightweight “DIY CMS”—no server, no database, no back office. For an artist portfolio, it’s the right compromise: easy to maintain, fast to load, and scalable.

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