Le blog charge ses articles depuis un posts.json via fetch()
et les rend en vanilla JS. Pas de framework, pas de build. Avec 7 articles ça marche
parfaitement. À 50 articles, on veut une pagination — pour la lisibilité, pas les
performances (le JSON reste léger de toute façon). La contrainte principale : pas
d'éléments cachés dans le DOM.
La mauvaise façon de faire : tout charger puis display: none ce qu'on
ne montre pas. La bonne façon : ne rendre que les posts de la page courante. La
différence paraît cosmétique sur un blog personnel. Elle reflète une habitude de
travail — ne pas mettre dans le DOM ce qu'on n'affiche pas.
L'architecture existante
Le setup de départ : fetch('posts.json') charge le manifeste, les articles
sont triés par date, puis render(allPosts) écrit dans
#posts-container via innerHTML. La fonction
filterPosts() filtre le tableau complet par catégorie et recherche texte.
applyFilters() est appelée à chaque interaction — clic sur un filtre,
frappe dans le champ de recherche.
Le point d'entrée de la pagination est la fonction render() — c'est
elle qui décide quoi écrire dans le DOM. Modifier filterPosts() n'est
pas nécessaire : elle continue à retourner le tableau complet filtré, sans
pagination. C'est render() qui en extrait la tranche.
Le mauvais pattern à éviter
// ❌ Tout charger, tout cacher — inutile et trompeur
function render(posts) {
container.innerHTML = posts.map(function(post, index) {
return '<article style="display:' + (index < 10 ? 'block' : 'none') + '">'
+ buildCardHTML(post)
+ '</article>';
}).join('');
}
Le DOM contient 50 éléments, l'utilisateur en voit 10. Le CSS de la page fait du travail inutile, les screen readers lisent le contenu caché, et le "chargement léger" est une illusion. Ce pattern est courant parce qu'il est rapide à écrire. Il est aussi faux dans ses fondamentaux.
L'implémentation
Deux variables d'état ajoutées au niveau module :
var currentPage = 1;
var PAGE_SIZE = 10;
La fonction render() modifiée — elle reçoit le tableau filtré complet,
en extrait la tranche de la page courante, et rend uniquement ces posts :
function render(filteredPosts) {
var container = document.getElementById('posts-container');
var noResults = document.getElementById('no-results');
var pagination = document.getElementById('blog-pagination');
if (filteredPosts.length === 0) {
container.innerHTML = '';
noResults.style.display = '';
pagination.innerHTML = '';
return;
}
noResults.style.display = 'none';
var totalPages = Math.ceil(filteredPosts.length / PAGE_SIZE);
if (currentPage > totalPages) currentPage = totalPages;
var start = (currentPage - 1) * PAGE_SIZE;
container.innerHTML = filteredPosts
.slice(start, start + PAGE_SIZE)
.map(buildCardHTML)
.join('');
if (totalPages <= 1) { pagination.innerHTML = ''; return; }
var prevDisabled = currentPage === 1 ? ' disabled' : '';
var nextDisabled = currentPage === totalPages ? ' disabled' : '';
pagination.innerHTML =
'<button class="btn-page"' + prevDisabled + ' data-page="' + (currentPage - 1) + '">← Précédent</button>'
+ '<span class="page-info">Page ' + currentPage + ' sur ' + totalPages + '</span>'
+ '<button class="btn-page"' + nextDisabled + ' data-page="' + (currentPage + 1) + '">Suivant →</button>';
}
Le if (currentPage > totalPages) currentPage = totalPages en
milieu de fonction n'est pas défensif pour rien : si l'utilisateur est sur la page 5
et fait une recherche qui ne retourne que 3 articles (donc 1 page), il faut ramener
currentPage à 1 avant de calculer le start. Sinon
slice(40, 50) sur un tableau de 3 éléments retourne un tableau vide —
et le container s'affiche vide alors qu'il y a des résultats.
Reset de page sur filtre
Quand l'utilisateur change de catégorie ou fait une recherche, on revient à la
page 1. C'est le seul endroit où currentPage est réinitialisé :
function applyFilters() {
currentPage = 1; // ← toujours page 1 sur nouveau filtre
render(filterPosts());
}
Le handler de pagination, lui, ne remet pas à 1 — il met juste à jour
currentPage et re-render :
document.getElementById('blog-pagination').addEventListener('click', function (e) {
var btn = e.target.closest('[data-page]');
if (!btn || btn.disabled) return;
currentPage = parseInt(btn.getAttribute('data-page'), 10);
render(filterPosts());
window.scrollTo({ top: 0, behavior: 'smooth' });
});
Délégation d'événement sur le container #blog-pagination — pas de
listeners attachés aux boutons individuels. Les boutons sont recréés à chaque render
via innerHTML, donc des listeners attachés directement seraient perdus
à chaque mise à jour. La délégation évite ça proprement : un seul listener sur un
élément stable, qui survit aux re-renders.
Ce qu'on n'a pas fait
Quelques choix délibérés de ne PAS sur-ingénier :
- Pas de numérotation de pages (1, 2, 3, …, N) — avec 10 articles par page, le "Page X sur Y" suffit jusqu'à plusieurs centaines d'articles. La numérotation complète ajoute de la complexité pour tronquer les séries longues (les fameux "1 2 … 5 6 7 … 12 13"). Ce n'est pas justifié ici.
-
Pas d'URL avec
?page=2— le blog est un portfolio, pas un moteur de recherche. L'état de pagination n'a pas besoin d'être partageable ou indexable par Google. Ajouterhistory.pushState()représente 3x plus de code pour un bénéfice nul dans ce contexte. -
Pas de debounce sur la recherche —
posts.jsonest un fichier local déjà chargé en mémoire, le filtrage est synchrone et instantané. Le debounce est une optimisation pour les requêtes réseau, pas pour unArray.filter()sur 50 éléments. - Pas de transition CSS entre pages — une pagination de contenu blog n'est pas un carousel. Le remplacement direct est plus lisible et plus prévisible. Une animation de fade-in retarderait la lecture sans apporter d'information.
Le CSS
Cohérent avec le reste du blog (Montserrat, vert #3aaa64, bordures grises).
Les boutons désactivés ont opacity: 0.5 et cursor: default
— pas de JS pour ça, juste l'attribut HTML disabled qui déclenche les
pseudo-classes CSS :disabled. C'est l'usage prévu par la plateforme ; pas
besoin de réimplémenter manuellement ce que le navigateur fait déjà.
Conclusion
30 lignes de JS. Zéro dépendance. DOM léger à chaque page. La pagination s'intègre
proprement dans le flux existant : filterPosts() retourne toujours le
tableau complet filtré, render() en extrait la tranche — les deux
fonctions restent découplées. Ajouter la pagination n'a pas nécessité de restructurer
le code existant : juste deux variables d'état et une modification de
render().
C'est le bon niveau d'ingénierie pour un blog personnel.