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-natureWhat 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">×</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.csvmust be in the same folder asindex.html(or you need to adjust the path)fetch()won’t work properly if you open the page viafile://(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.tagspowers filteringaltcomes 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):
- The artist updates a sheet (Google Sheets / Excel)
- Export to CSV
- Commit the CSV + images into the repo
git push- 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-wrapperasinline-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-labelto the close button
Performance
img.loading = "lazy";- pagination / batch loading (12 at a time)
- separate thumbnails + full-res images (e.g.,
thumb_pathandfull_pathcolumns in the CSV)
SEO
altis already good, but you can go further:- artwork titles as
<h4>(or at least better semantic structure) - JSON-LD (
ImageObjectorCreativeWork) if you want stronger indexing
- artwork titles as
Security
- avoid
innerHTMLfor 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/
Laisser un commentaire