Blog: cambio categoria e pagina senza ricaricare tutto

Ogni clic su una categoria ricaricava l'intera pagina, header e hero
compresi, con lo sfarfallio che ne consegue. Ora si scarica la stessa
pagina e si sostituiscono soltanto la griglia e i filtri, con una breve
dissolvenza; l'indirizzo resta corretto e avanti/indietro funzionano.

I link restano link veri: senza JavaScript, o se la risposta non ha la
forma attesa, la navigazione avviene come prima.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 16:51:37 +02:00
parent 6a97a98325
commit 770073c45c
+59 -1
View File
@@ -38,8 +38,9 @@ const linkFor = (cat?: string, p = 1) => {
<a class:list={['bfilters__item', { 'is-active': category === c }]} href={linkFor(c)}>{c}</a>
))}
</nav>
<div id="blog-results">
{items.length === 0 ? (
<p style="text-align:center">Nessun articolo pubblicato{category ? ' in questa categoria' : ''}, torna a trovarci presto.</p>
<p class="bempty">Nessun articolo pubblicato{category ? ' in questa categoria' : ''}, torna a trovarci presto.</p>
) : (
<div class="bgrid">
{items.map((post) => (
@@ -60,14 +61,71 @@ const linkFor = (cat?: string, p = 1) => {
))}
</nav>
)}
</div>
</div>
</section>
</Base>
<script>
// Cambio categoria e pagina senza ricaricare: si scarica la stessa pagina e si sostituiscono
// solo la griglia e i filtri. I link restano normali, quindi senza JS tutto continua a
// funzionare come prima.
const risultati = document.getElementById('blog-results');
const filtri = document.querySelector('.bfilters');
if (risultati && filtri) {
let inCorso: AbortController | null = null;
async function mostra(href: string, scorri: boolean): Promise<void> {
inCorso?.abort();
const controller = new AbortController();
inCorso = controller;
risultati!.classList.add('is-loading');
try {
const res = await fetch(href, { signal: controller.signal });
const doc = new DOMParser().parseFromString(await res.text(), 'text/html');
const nuoviRisultati = doc.getElementById('blog-results');
const nuoviFiltri = doc.querySelector('.bfilters');
// Se la pagina non ha la forma attesa (errore, redirect) si lascia fare al browser.
if (!nuoviRisultati || !nuoviFiltri) { location.href = href; return; }
risultati!.innerHTML = nuoviRisultati.innerHTML;
filtri!.innerHTML = nuoviFiltri.innerHTML;
if (scorri) filtri!.scrollIntoView({ behavior: 'smooth', block: 'start' });
} catch (e) {
if ((e as Error).name === 'AbortError') return;
location.href = href;
return;
} finally {
if (inCorso === controller) {
risultati!.classList.remove('is-loading');
inCorso = null;
}
}
}
// Delega sul documento: i link vengono rigenerati a ogni cambio, così non serve riagganciarli.
document.addEventListener('click', (e) => {
const link = (e.target as Element).closest<HTMLAnchorElement>('.bfilters__item, .bpages__n');
if (!link || e.metaKey || e.ctrlKey || e.shiftKey || (e as MouseEvent).button !== 0) return;
e.preventDefault();
if (link.classList.contains('is-active')) return;
history.pushState({}, '', link.href);
mostra(link.href, link.classList.contains('bpages__n'));
});
// Avanti/indietro del browser: si ricarica il contenuto senza toccare la cronologia.
addEventListener('popstate', () => mostra(location.href, false));
}
</script>
<style>
.bfilters { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; margin-bottom: 50px; }
.bfilters__item { font-family: var(--font-heading); font-size: .7rem; letter-spacing: .16em; text-transform: uppercase; text-decoration: none; color: var(--c-text); padding: 10px 16px; }
.bfilters__item.is-active { background: var(--c-accent); color: #fff; }
.bfilters__item { transition: background .2s ease, color .2s ease; }
#blog-results { transition: opacity .25s ease; }
#blog-results.is-loading { opacity: .35; }
.bempty { text-align: center; }
.bgrid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 50px 40px; }
.bcard img { aspect-ratio: 4/3; object-fit: cover; width: 100%; margin-bottom: 14px; }
.bcard__date { font-size: .78rem; color: var(--c-text-light); margin-bottom: 4px; }