From ee2b8a4ac77beef0bb8c2109a6932c46dd7723a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9drix?= Date: Fri, 15 May 2026 18:26:16 +0200 Subject: [PATCH 1/9] =?UTF-8?q?docs=20:=20documenter=20la=20configuration?= =?UTF-8?q?=20sudoers=20pour=20le=20bouton=20Mettre=20=C3=A0=20jour?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- README.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/README.md b/README.md index 4154806..85c5eda 100644 --- a/README.md +++ b/README.md @@ -133,12 +133,46 @@ Ou créer directement `$DATA_PATH/site_settings.json` : ## Mise à jour +### Manuelle + ```bash git pull composer install --no-dev php database/migrate.php ``` +### Via le bouton admin ("Mettre à jour") + +L'interface d'administration propose un bouton **Mettre à jour** qui déclenche un déploiement complet (clone fresh, permissions, Composer, migrations SQL, répertoire de sessions). Ce bouton appelle `sudo /usr/local/bin/folio-upgrade.sh` depuis PHP (`www-data`). + +**Configuration requise une fois sur chaque serveur :** + +```bash +# 1. Installer le script (adapté à votre serveur) +sudo install -o root -m 750 /var/www/mon-site/scripts/server/folio-upgrade.sh \ + /usr/local/bin/folio-upgrade.sh + +# 2. Éditer APP_DIR et REPO_URL dans le script installé +sudo nano /usr/local/bin/folio-upgrade.sh + +# 3. Autoriser www-data à l'exécuter sans mot de passe +echo "www-data ALL=(root) NOPASSWD: /usr/local/bin/folio-upgrade.sh" \ + | sudo tee /etc/sudoers.d/folio-upgrade +sudo chmod 440 /etc/sudoers.d/folio-upgrade + +# 4. Vérifier la syntaxe +sudo visudo -c +``` + +Le script est dans `scripts/server/folio-upgrade.sh`. Les deux variables à adapter en tête de fichier : + +| Variable | Description | +|---|---| +| `APP_DIR` | Chemin absolu du document root (ex. `/var/www/mon-site`) | +| `REPO_URL` | URL du dépôt Git Folio | + +Sans cette configuration, le bouton retourne une erreur `sudo: a password is required`. + ## Structure du projet ``` From 1d051383298415700c58d19f6737524c7ecc6b0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9drix?= Date: Fri, 15 May 2026 18:27:28 +0200 Subject: [PATCH 2/9] =?UTF-8?q?docs=20:=20deployment.md=20=E2=80=94=20bout?= =?UTF-8?q?on=20Mettre=20=C3=A0=20jour=20(sudoers)=20+=20flux=20trending?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- README.md | 30 +----------- docs/deployment.md | 115 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 28 deletions(-) create mode 100644 docs/deployment.md diff --git a/README.md b/README.md index 85c5eda..9519714 100644 --- a/README.md +++ b/README.md @@ -143,35 +143,9 @@ php database/migrate.php ### Via le bouton admin ("Mettre à jour") -L'interface d'administration propose un bouton **Mettre à jour** qui déclenche un déploiement complet (clone fresh, permissions, Composer, migrations SQL, répertoire de sessions). Ce bouton appelle `sudo /usr/local/bin/folio-upgrade.sh` depuis PHP (`www-data`). +L'interface d'administration propose un bouton **Mettre à jour** qui déclenche un déploiement complet via `sudo /usr/local/bin/folio-upgrade.sh`. Une configuration sudoers est requise une fois par serveur. -**Configuration requise une fois sur chaque serveur :** - -```bash -# 1. Installer le script (adapté à votre serveur) -sudo install -o root -m 750 /var/www/mon-site/scripts/server/folio-upgrade.sh \ - /usr/local/bin/folio-upgrade.sh - -# 2. Éditer APP_DIR et REPO_URL dans le script installé -sudo nano /usr/local/bin/folio-upgrade.sh - -# 3. Autoriser www-data à l'exécuter sans mot de passe -echo "www-data ALL=(root) NOPASSWD: /usr/local/bin/folio-upgrade.sh" \ - | sudo tee /etc/sudoers.d/folio-upgrade -sudo chmod 440 /etc/sudoers.d/folio-upgrade - -# 4. Vérifier la syntaxe -sudo visudo -c -``` - -Le script est dans `scripts/server/folio-upgrade.sh`. Les deux variables à adapter en tête de fichier : - -| Variable | Description | -|---|---| -| `APP_DIR` | Chemin absolu du document root (ex. `/var/www/mon-site`) | -| `REPO_URL` | URL du dépôt Git Folio | - -Sans cette configuration, le bouton retourne une erreur `sudo: a password is required`. +→ Voir **[docs/deployment.md](docs/deployment.md)** pour la procédure complète. ## Structure du projet diff --git a/docs/deployment.md b/docs/deployment.md new file mode 100644 index 0000000..2a5a333 --- /dev/null +++ b/docs/deployment.md @@ -0,0 +1,115 @@ +# Déploiement et mise à jour + +## Mise à jour via le bouton admin + +L'interface d'administration propose un bouton **Mettre à jour** (onglet Dashboard). Il appelle `sudo /usr/local/bin/folio-upgrade.sh` depuis PHP (`www-data`) et exécute la séquence complète : + +1. Sauvegarde du `.env` +2. `git clone --depth=1` dans un répertoire temporaire +3. Remplacement atomique du répertoire applicatif +4. `chown -R www-data:www-data` + `chmod g+rwX,o=` +5. Restauration du `.env` +6. `composer install --no-dev --optimize-autoloader` +7. `php database/migrate.php` (migrations SQL) +8. Création de `.sessions/` avec les bons droits +9. `git config --system --add safe.directory` + +### Pré-requis serveur (à faire une fois) + +```bash +# 1. Installer le script (copié depuis le dépôt) +sudo install -o root -m 750 /var/www/mon-site/scripts/server/folio-upgrade.sh \ + /usr/local/bin/folio-upgrade.sh + +# 2. Adapter APP_DIR et REPO_URL en tête du script +sudo nano /usr/local/bin/folio-upgrade.sh + +# 3. Créer la règle sudoers (www-data sans mot de passe) +echo "www-data ALL=(root) NOPASSWD: /usr/local/bin/folio-upgrade.sh" \ + | sudo tee /etc/sudoers.d/folio-upgrade +sudo chmod 440 /etc/sudoers.d/folio-upgrade + +# 4. Vérifier la syntaxe sudoers +sudo visudo -c +``` + +Variables à configurer dans le script : + +| Variable | Exemple | +|---|---| +| `APP_DIR` | `/var/www/lan.acegrp.abonnel-www` | +| `REPO_URL` | `https://git.abonnel.fr/cedricAbonnel/folio.git` | + +> **Sans cette configuration**, le bouton retourne : +> `sudo: a terminal is required to read the password` + +### Fonctionnement du cache de mise à jour + +Le résultat de la dernière mise à jour est conservé dans `DATA_PATH/.upgrade-log` et affiché en `
` dans l'admin. + +--- + +## Mise à jour manuelle + +Si le bouton admin n'est pas configuré ou si une mise à jour d'urgence est nécessaire : + +```bash +# Sauvegarde du .env +cp /var/www/mon-site/.env /tmp/.env.bak + +# Clone fresh +sudo rm -rf /var/www/mon-site +sudo git clone --depth=1 https://git.abonnel.fr/cedricAbonnel/folio.git /var/www/mon-site + +# Permissions +sudo chown -R www-data:www-data /var/www/mon-site +sudo chmod -R g+rwX,o= /var/www/mon-site + +# Restaurer .env +cp /tmp/.env.bak /var/www/mon-site/.env + +# Dépendances et migrations +cd /var/www/mon-site +composer install --no-dev --optimize-autoloader +php database/migrate.php + +# Répertoire de sessions +sudo mkdir -p /var/www/mon-site/.sessions +sudo chown www-data:www-data /var/www/mon-site/.sessions +sudo chmod 700 /var/www/mon-site/.sessions + +# Autoriser git (accès multi-utilisateurs) +sudo git config --system --add safe.directory /var/www/mon-site +``` + +--- + +## Flux RSS des tendances (`/trending`) + +Le flux RSS des articles les plus consultés est alimenté par `TrendingParser` qui lit les logs Apache. + +- **Source** : `GET /trending?period=` — parse les logs et écrit `DATA_PATH/_cache/trending_.json` +- **Consommateurs** (lecture seule du cache) : page d'accueil (rubrique "Meilleures audiences") et `/tendances` + +### Périodes disponibles + +| Paramètre | Fenêtre | Cache TTL | +|---|---|---| +| `10m` | 10 min | 2 min | +| `20m` | 20 min | 4 min | +| `30m` | 30 min | 6 min | +| `1h` | 1 heure | 12 min | +| `8h` | 8 heures | 96 min | +| `1d` | 24 heures | 5 h | +| `7d` | 7 jours | 8 h | +| `14d` | 14 jours | 8 h | +| `30d` | 30 jours | 8 h | +| `1y` | 1 an | 8 h | + +### Prérequis + +`www-data` doit appartenir au groupe `adm` pour lire `/var/log/apache2/` : + +```bash +sudo usermod -aG adm www-data +``` From 5cea473d1708266a49f5116f5bcc5ca9f146f5ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9drix?= Date: Fri, 15 May 2026 20:08:24 +0200 Subject: [PATCH 3/9] feat : "Meilleures audiences" + admin/stats pages via flux RSS XML (v1.6.6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - post_list.php : section AJAX qui lit /trending?period=1h en XML (DOMParser) — plus de rendu PHP - admin_stats.php : colonne "Pages les plus visitées" chargée en AJAX depuis /trending?period=14d XML - index.php/stats : suppression de topGrouped pour /post/ ; seuls /book/ et ASN restent côté serveur Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 9 ++++ public/index.php | 13 ++---- public/version.txt | 2 +- templates/admin_stats.php | 89 ++++++++++++++++++++++----------------- templates/post_list.php | 56 ++++++++++++++++++++---- 5 files changed, 112 insertions(+), 57 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8ba47f..8f0d92d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,15 @@ Format : [Keep a Changelog](https://keepachangelog.com/fr/1.0.0/) — versionnag --- +## [1.6.6] - 2026-05-15 + +### Modifié +- Page d'accueil "Meilleures audiences" : chargement AJAX depuis le flux RSS XML `/trending?period=1h` (DOMParser côté client, plus de rendu PHP) +- `/admin/stats` section "Pages les plus visitées" : chargement AJAX depuis le flux RSS XML `/trending?period=14d` — plus de parsing de logs direct pour cette colonne +- `/admin/stats` : suppression de `topGrouped` pour les pages ; seuls les livres (`/book/`) et l'ASN conservent le parsing log côté serveur + +--- + ## [1.6.5] - 2026-05-15 ### Modifié diff --git a/public/index.php b/public/index.php index 41e71df..183f740 100644 --- a/public/index.php +++ b/public/index.php @@ -2559,25 +2559,20 @@ switch ($action) { $statsRaw = json_decode((string) file_get_contents($statsCacheFile), true) ?: null; } if ($statsRaw === null) { - $cutoff14 = strtotime('-14 days midnight') ?: (time() - 14 * 86400); - $tParser = new TrendingParser('/var/log/apache2', apacheAccessLog()); - $grouped = $tParser->topGrouped($cutoff14, ['/post/' => 30, '/book/' => 20]); - - // IPs pour le lookup ASN (AccessLogParser conserve le comptage brut par IP) + $cutoff14 = strtotime('-14 days midnight') ?: (time() - 14 * 86400); + $tParser = new TrendingParser('/var/log/apache2', apacheAccessLog()); $accessParser = new AccessLogParser('/var/log/apache2', apacheAccessLog()); $topIps = array_slice($accessParser->stats()['ips'], 0, 200, true); $asnMap = (new AsnLookup())->batchLookup(array_keys($topIps)); $statsRaw = [ - 'readable' => $tParser->isReadable(), - 'pages' => $grouped['/post/'], - 'books' => $grouped['/book/'], + 'readable' => $accessParser->isReadable(), + 'books' => $tParser->top($cutoff14, 20, ['/book/']), 'as' => AsnLookup::aggregateByAs($topIps, $asnMap), ]; @file_put_contents($statsCacheFile, json_encode($statsRaw)); } $adminData['stats_readable'] = $statsRaw['readable']; - $adminData['stats_pages'] = $statsRaw['pages']; $adminData['stats_books'] = $statsRaw['books']; $adminData['stats_as'] = $statsRaw['as']; $adminData['stats_as_groups'] = AsnLookup::applyGroups($statsRaw['as'], asGroups()); diff --git a/public/version.txt b/public/version.txt index 9f05f9f..ec70f75 100644 --- a/public/version.txt +++ b/public/version.txt @@ -1 +1 @@ -1.6.5 +1.6.6 diff --git a/templates/admin_stats.php b/templates/admin_stats.php index cfaf90b..8dca9e0 100644 --- a/templates/admin_stats.php +++ b/templates/admin_stats.php @@ -2,7 +2,6 @@ $_statsSaved = isset($_GET['saved']); $_statsError = ($_GET['error'] ?? '') === 'write'; $_readable = $adminData['stats_readable'] ?? false; -$_pages = $adminData['stats_pages'] ?? []; $_books = $adminData['stats_books'] ?? []; $_asList = $adminData['stats_as'] ?? []; $_asGroups = $adminData['stats_as_groups'] ?? []; @@ -23,51 +22,19 @@ $_activeGroup = trim($_GET['group'] ?? ''); -

14 derniers jours · visiteurs uniques · cache 60 s

+

14 derniers jours · visiteurs uniques · flux RSS XML

- +
Pages les plus visitées - URLs +
-
- -

Aucune donnée.

- -
- - - $hits): - $rankP++; - $slug = rawurldecode(substr($url, 6)); - $pct = round($hits / $maxP * 100); - ?> - - - - - - - -
- - - -
-
-
-
vis.
-
- +
+

Chargement…

@@ -238,4 +205,50 @@ document.getElementById('as-groups-list').addEventListener('click', e => { e.target.closest('.as-group-row').remove(); } }); + +// ── Chargement des pages via le flux RSS XML ────────────────────────────────── +(function(){ + var container = document.getElementById('stats-pages-container'); + var badge = document.getElementById('stats-pages-count'); + if (!container) return; + function esc(s){ return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } + fetch('/trending?period=14d') + .then(function(r){ return r.ok ? r.text() : Promise.reject(); }) + .then(function(xml){ + var doc = new DOMParser().parseFromString(xml, 'application/xml'); + var items = Array.from(doc.querySelectorAll('item')); + if (!items.length) { + container.innerHTML = '

Aucune donnée.

'; + return; + } + var rows = items.map(function(item){ + var raw = (item.querySelector('title') || {textContent:''}).textContent; + var link = ((item.querySelector('link') || {}).textContent || '').trim(); + var m = raw.match(/\((\d+)\s+visiteurs?\)$/); + var vis = m ? parseInt(m[1], 10) : 0; + var title = raw.replace(/\s*\(\d+\s+visiteurs?\)$/, ''); + var slug = decodeURIComponent(link.replace(/.*\/post\//, '')); + return {title: title, link: link, slug: slug, vis: vis}; + }); + var maxV = Math.max.apply(null, rows.map(function(r){ return r.vis; })) || 1; + var html = '
'; + rows.forEach(function(row, i){ + var pct = Math.round(row.vis / maxV * 100); + var vis = row.vis.toLocaleString('fr-FR'); + html += '' + + '' + + '' + + '' + + ''; + }); + html += '
' + (i+1) + '' + + esc(row.title || row.slug) + '' + + '
' + vis + ' vis.
'; + if (badge) badge.textContent = rows.length + ' URLs'; + container.innerHTML = html; + }) + .catch(function(){ + container.innerHTML = '

Impossible de charger le flux.

'; + }); +})(); diff --git a/templates/post_list.php b/templates/post_list.php index 10be95d..1401cba 100644 --- a/templates/post_list.php +++ b/templates/post_list.php @@ -155,19 +155,57 @@ function _renderCard(array $post, array $privateCats, array $allCats, \Parsedown - - -
+ + - + From a55e22f1f4892cf539bbc9f32054f59daf4f92d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9drix?= Date: Fri, 15 May 2026 20:24:28 +0200 Subject: [PATCH 4/9] =?UTF-8?q?feat=20:=20s=C3=A9lecteur=20de=20densit?= =?UTF-8?q?=C3=A9=20L/M/S=20sur=20la=20page=20liste=20(v1.6.7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Boutons [L][M][S] dans la barre de recherche hero : pleine largeur (défaut), 980 px centré, 660 px compact. Préférence localStorage. Anti-FOUC inline dans layout.php. Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 7 +++++ public/assets/css/style.css | 30 ++++++++++++++++++- public/version.txt | 2 +- templates/layout.php | 1 + templates/post_list.php | 60 ++++++++++++++++++++++++++++++------- 5 files changed, 88 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f0d92d..4f65f29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,13 @@ Format : [Keep a Changelog](https://keepachangelog.com/fr/1.0.0/) — versionnag --- +## [1.6.7] - 2026-05-15 + +### Ajouté +- Sélecteur de densité L / M / S sur la page liste : pleine largeur (défaut), normal (980 px), compact (660 px) — préférence persistée dans `localStorage` + +--- + ## [1.6.6] - 2026-05-15 ### Modifié diff --git a/public/assets/css/style.css b/public/assets/css/style.css index cbcd4dc..9e49b76 100644 --- a/public/assets/css/style.css +++ b/public/assets/css/style.css @@ -1259,13 +1259,41 @@ footer.mt-5 { margin-top: 0 !important; } transition: background .2s; } .hero-search-btn:hover { background: var(--vl-accent-dark); } -.hero-search-stats { +.hero-search-footer { + display: flex; + align-items: center; + justify-content: center; + gap: .75rem; margin-top: .8rem; +} +.hero-search-stats { font-size: .83rem; color: var(--vl-muted); letter-spacing: .01em; + margin: 0; } +/* ─── Densité d'affichage L / M / S ──────── */ +body[data-density="m"] main { max-width: 980px; margin-left: auto; margin-right: auto; } +body[data-density="s"] main { max-width: 660px; margin-left: auto; margin-right: auto; } + +.density-toggle { display: flex; gap: 2px; } +.density-btn { + background: none; + border: 1px solid var(--vl-border); + border-radius: 4px; + color: var(--vl-muted); + cursor: pointer; + font-size: .68rem; + font-weight: 700; + letter-spacing: .06em; + line-height: 1; + padding: 3px 7px; + transition: background .15s, color .15s, border-color .15s; +} +.density-btn:hover { background: rgba(0,0,0,.05); color: var(--vl-text); border-color: var(--vl-muted); } +.density-btn.active { background: var(--vl-text); border-color: var(--vl-text); color: var(--vl-bg); } + /* ─── Page de recherche ───────────────────── */ .search-page { max-width: 780px; margin: 0 auto; } diff --git a/public/version.txt b/public/version.txt index ec70f75..400084b 100644 --- a/public/version.txt +++ b/public/version.txt @@ -1 +1 @@ -1.6.6 +1.6.7 diff --git a/templates/layout.php b/templates/layout.php index bc71e05..6987c5f 100644 --- a/templates/layout.php +++ b/templates/layout.php @@ -50,6 +50,7 @@ class=""> +
@@ -266,10 +271,17 @@ function _renderCard(array $post, array $privateCats, array $allCats, \Parsedown autofocus> -

- article 1 ? 's' : '' ?> - 0): ?>· à venir -

+
@@ -340,6 +352,34 @@ if (!empty($_tagCats)): + + + Date: Fri, 15 May 2026 20:35:59 +0200 Subject: [PATCH 5/9] =?UTF-8?q?fix=20:=20densit=C3=A9=20L/M/S=20=E2=80=94?= =?UTF-8?q?=20widget=20fixe=20haut-droite,=20CSS=20!important?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Widget retiré du hero-search, replacé en position:fixed top-right (sous navbar) - max-width !important pour garantir l'override de Bootstrap sur main.container-fluid - transition douce sur main, caché en < 576px Co-Authored-By: Claude Sonnet 4.6 --- public/assets/css/style.css | 38 ++++++++++++++++++++++--------------- templates/post_list.php | 36 ++++++++++++++--------------------- 2 files changed, 37 insertions(+), 37 deletions(-) diff --git a/public/assets/css/style.css b/public/assets/css/style.css index 9e49b76..82dc90f 100644 --- a/public/assets/css/style.css +++ b/public/assets/css/style.css @@ -1259,28 +1259,35 @@ footer.mt-5 { margin-top: 0 !important; } transition: background .2s; } .hero-search-btn:hover { background: var(--vl-accent-dark); } -.hero-search-footer { - display: flex; - align-items: center; - justify-content: center; - gap: .75rem; - margin-top: .8rem; -} .hero-search-stats { + margin-top: .8rem; font-size: .83rem; color: var(--vl-muted); letter-spacing: .01em; - margin: 0; } /* ─── Densité d'affichage L / M / S ──────── */ -body[data-density="m"] main { max-width: 980px; margin-left: auto; margin-right: auto; } -body[data-density="s"] main { max-width: 660px; margin-left: auto; margin-right: auto; } +main { transition: max-width .22s ease, padding-left .22s ease, padding-right .22s ease; } +body[data-density="m"] main { max-width: 980px !important; margin-left: auto !important; margin-right: auto !important; } +body[data-density="s"] main { max-width: 660px !important; margin-left: auto !important; margin-right: auto !important; } -.density-toggle { display: flex; gap: 2px; } +/* Widget fixe haut-droite */ +.density-widget { + position: fixed; + top: 3.6rem; + right: 1rem; + z-index: 1010; + display: flex; + gap: 2px; + background: var(--vl-surface); + border: 1px solid var(--vl-border); + border-radius: 6px; + padding: 3px; + box-shadow: var(--vl-shadow-sm); +} .density-btn { background: none; - border: 1px solid var(--vl-border); + border: 1px solid transparent; border-radius: 4px; color: var(--vl-muted); cursor: pointer; @@ -1288,11 +1295,12 @@ body[data-density="s"] main { max-width: 660px; margin-left: auto; margin-right: font-weight: 700; letter-spacing: .06em; line-height: 1; - padding: 3px 7px; + padding: 4px 8px; transition: background .15s, color .15s, border-color .15s; } -.density-btn:hover { background: rgba(0,0,0,.05); color: var(--vl-text); border-color: var(--vl-muted); } -.density-btn.active { background: var(--vl-text); border-color: var(--vl-text); color: var(--vl-bg); } +.density-btn:hover { background: rgba(0,0,0,.06); color: var(--vl-text); } +.density-btn.active { background: var(--vl-text); color: var(--vl-bg); } +@media (max-width: 576px) { .density-widget { display: none; } } /* ─── Page de recherche ───────────────────── */ .search-page { max-width: 780px; margin: 0 auto; } diff --git a/templates/post_list.php b/templates/post_list.php index 4135dae..86b3169 100644 --- a/templates/post_list.php +++ b/templates/post_list.php @@ -86,17 +86,10 @@ function _renderCard(array $post, array $privateCats, array $allCats, \Parsedown autofocus> - +

+ article 1 ? 's' : '' ?> + 0): ?>· à venir +

@@ -271,17 +264,10 @@ function _renderCard(array $post, array $privateCats, array $allCats, \Parsedown autofocus> - +

+ article 1 ? 's' : '' ?> + 0): ?>· à venir +

@@ -352,6 +338,12 @@ if (!empty($_tagCats)): + +
+ + + +
+ +