Catégorie : Vie Numérique

  • 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/

  • Sécuriser le LAN familial : Double filtrage DNS et isolation par VLANs avec OPNsense et AdGuard Home

    Le défi de la gouvernance numérique au sein du foyer

    En tant qu’architectes de nos propres infrastructures domestiques, nous appliquons souvent des règles de sécurité strictes au bureau, mais laissons le réseau familial vulnérable. Entre les objets connectés (IoT) bavards, les consoles des enfants et les ordinateurs portables professionnels des parents, la surface d’attaque est immense. Pour sécuriser la vie numérique de la famille sans impacter l’expérience utilisateur, une simple clé WPA2 globale ne suffit plus. La solution ? L’isolation par VLAN combinée à un filtrage DNS granulaire et dynamique.

    Étape 1 : Segmentation réseau (VLAN) sous OPNsense

    La première règle d’or de la sécurité est le cloisonnement. Nous allons segmenter notre réseau en trois zones distinctes à l’aide de VLANs configurés sur notre routeur/pare-feu OPNsense :

    • VLAN 10 (Parents/Admin) : Accès total à l’interface d’administration et à internet.
    • VLAN 20 (Kids) : Accès internet filtré, aucune communication possible avec le VLAN 10 ou l’IoT.
    • VLAN 30 (IoT) : Accès internet restreint au strict minimum, isolation complète du reste du réseau.

    Pour cela, rendez-vous dans Interfaces > Other Types > VLAN sur OPNsense. Créez vos interfaces, puis définissez des règles de pare-feu strictes dans Firewall > Rules pour interdire le trafic inter-VLAN (notamment du VLAN 20/30 vers le VLAN 10).

    Étape 2 : Configuration d’AdGuard Home pour un filtrage par profil

    Plutôt que de déployer plusieurs serveurs de filtrage, nous allons utiliser les fonctionnalités avancées d’AdGuard Home pour appliquer des politiques de sécurité et de contrôle parental différenciées selon les clients.

    Dans l’interface d’AdGuard Home, accédez à la section Paramètres du client. Vous pouvez y ajouter des appareils spécifiques ou des sous-réseaux entiers (par exemple, la plage IP du VLAN 20 des enfants). Pour ce profil « Kids », activez :

    • Le blocage des services spécifiques (TikTok, YouTube restreint, Discord selon l’âge).
    • La recherche sécurisée (SafeSearch) obligatoire sur Google, Bing et YouTube.
    • L’utilisation des serveurs DNS amont de type « Family Protection » (comme Cloudflare 1.1.1.3).

    Étape 3 : Verrouiller les échappatoires (Contrecarrer le contournement du DNS)

    Les adolescents sont astucieux. Si un enfant configure manuellement le DNS 8.8.8.8 sur sa console ou son smartphone, il contournera AdGuard Home. Pour empêcher cela, nous devons mettre en place une règle de NAT Port Forward sur OPNsense.

    Créez une règle de redirection de port (DNAT) pour intercepter toutes les requêtes sortantes sur le port 53 (UDP/TCP) qui ne sont pas destinées à l’IP de votre serveur AdGuard Home, et redirigez-les de force vers ce dernier. Faites de même pour le port 853 (DNS over TLS) pour bloquer ou rediriger le DNS sécurisé tiers.

    Conclusion

    En combinant la puissance de routage d’OPNsense et la flexibilité d’AdGuard Home, vous obtenez une infrastructure domestique digne d’une entreprise. Vos données personnelles sont isolées des objets connectés potentiellement vulnérables, et la navigation de vos enfants est protégée de manière transparente et centralisée.

  • Securing the Family LAN: Dual DNS Filtering and VLAN Isolation with OPNsense and AdGuard Home

    The Challenge of Digital Governance in the Smart Home

    As architects of our own home labs, we often apply strict security rules at work but leave our family network vulnerable. Between chatty IoT devices, children’s consoles, and parents’ work laptops, the attack surface is massive. To secure the family’s digital life without degrading the user experience, a simple global WPA2 key is no longer enough. The solution? VLAN isolation combined with granular, dynamic DNS filtering.

    Step 1: Network Segmentation (VLAN) on OPNsense

    The first golden rule of security is compartmentalization. We will segment our network into three distinct zones using VLANs configured on our OPNsense router/firewall:

    • VLAN 10 (Parents/Admin): Full access to the admin interface and the internet.
    • VLAN 20 (Kids): Filtered internet access, zero communication allowed with VLAN 10 or the IoT network.
    • VLAN 30 (IoT): Internet access restricted to the bare minimum, completely isolated from the rest of the network.

    To do this, navigate to Interfaces > Other Types > VLAN on OPNsense. Create your interfaces, then define strict firewall rules in Firewall > Rules to block inter-VLAN traffic (especially from VLAN 20/30 to VLAN 10).

    Step 2: Configuring AdGuard Home for Profile-Based Filtering

    Instead of deploying multiple filtering servers, we will use the advanced features of AdGuard Home to apply customized security and parental control policies based on the client.

    In the AdGuard Home UI, go to the Client Settings section. Here, you can add specific devices or entire subnets (such as the IP range of the Kids’ VLAN 20). For this « Kids » profile, enable:

    • Block specific services (TikTok, YouTube restricted, Discord depending on age).
    • Enforced SafeSearch on Google, Bing, and YouTube.
    • Upstream DNS servers set to « Family Protection » (like Cloudflare 1.1.1.3).

    Step 3: Blocking the Bypasses (Thwarting DNS Circumvention)

    Kids are tech-savvy. If a child manually sets their DNS to 8.8.8.8 on their console or smartphone, they will bypass AdGuard Home. To prevent this, we must set up a NAT Port Forward rule on OPNsense.

    Create a port redirection rule (DNAT) to intercept all outgoing queries on port 53 (UDP/TCP) that are not destined for your AdGuard Home IP, and forcibly redirect them to it. Do the same for port 853 (DNS over TLS) to block or redirect third-party secure DNS.

    Conclusion

    By combining the routing power of OPNsense and the flexibility of AdGuard Home, you achieve an enterprise-grade home network. Your personal data is isolated from potentially vulnerable smart devices, and your children’s browsing is protected seamlessly and centrally.