Con la pubblicazione di un numero sempre maggiore di contenuti, anche con htmx può essere utile aggiungere una funzione di ricerca per permettere ai lettori di un blog di filtrare gli articoli in base alle proprie query.
Per aggiungere tale funzione al sito, in questa lezione creeremo un nuovo endpoint che intercetti la chiave di ricerca e restituisca i post corrispondenti. Modificheremo quindi il layout generale del sito aggiungendo un campo di ricerca nel menu di navigazione e, infine, creeremo una pagina specifica per visualizzare i risultati. Utilizzeremo htmx per rendere il processo dinamico ed evitare di dover caricare la pagina ad ogni richiesta dell'utente.
Creiamo una nuova cartella /src/pages/search e, al suo interno, aggiungiamo un file index.astro, la cui funzione sarà mostrare i risultati della ricerca.
Una pagina per i risultati della ricerca con htmx
Apriamo il file /src/pages/search/index.astro e inseriamo il seguente codice JavaScript:
---
export const prerender = false;
import { getCollection } from 'astro:content';
import Layout from '../../layouts/Layout.astro';
import ArticleCard from '../../components/ArticleCard.astro';
const query = Astro.url.searchParams.get('q') || '';
const posts = await getCollection('blog');
const filteredPosts = query
? posts.filter(
post =>
post.data.title.toLowerCase().includes(query.toLowerCase()) ||
post.data.description.toLowerCase().includes(query.toLowerCase()) ||
(post.data.tags || []).some(tag => tag.toLowerCase().includes(query.toLowerCase()))
)
: posts;
const sortedPosts = filteredPosts.sort(
(a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime()
);
// Rileva se è una richiesta htmx (parziale)
const isHtmxRequest = Astro.request.headers.get('HX-Request') === 'true';
---
Analizziamo il codice
export const prerender = false;abilita il rendering dinamico (SSR), in modo che i parametri della query string vengano letti al momento della richiesta.- Le clausole
importimportano il componenteArticleCard, il layout generale e la funzionegetCollection(vedi la lezione precedente). - L'istruzione
Astro.url.searchParams.get('q') || '';estrae il parametro di ricercaqdall'URL e lo memorizza nella costantequery. Se non è presente, viene impostata una stringa vuota(''). posts.filterfiltra i post cercando corrispondenze del valore diqintitle,descriptionetags.toLowerCaseconverte le stringhe in caratteri minuscoli, in modo da rendere la ricerca case insensitive.- Il metodo
some()verifica che almeno uno dei tag del post contenga la query di ricerca. In caso negativo, restituiscefalse. sortedPostsordina i risultati contenuti infilteredPostsper data di pubblicazione decrescente.Astro.request.headers.get('HX-Request') === 'true';verifica se la richiesta proviene da htmx (markup parziale) o da una navigazione diretta (pagina HTML completa).
Passiamo ora al markup
{isHtmxRequest ? (
<div id="posts-container" class="grid gap-10">
{sortedPosts.length > 0 ? (
sortedPosts.map(post => <ArticleCard post={post} />)
) : (
<p class="text-gray-500 italic text-sm">Nessun articolo trovato per "{query}".</p>
)}
</div>
) : (
<Layout
title={`Risultati per "${query || 'tutti gli articoli'}"`}
description={`Risultati della ricerca per "${query || 'tutti gli articoli'}" sul blog Astro.`}
>
<h1 class="text-4xl font-extrabold text-gray-900 mb-8">
Risultati per "{query || 'tutti gli articoli'}"
</h1>
<div id="posts-container" class="grid gap-10">
{sortedPosts.length > 0 ? (
sortedPosts.map(post => <ArticleCard post={post} />)
) : (
<p class="text-gray-500 italic text-sm">Nessun articolo trovato per "{query}".</p>
)}
</div>
</Layout>
)}
Ecco cosa fa questo codice:
- Questo blocco di codice inizia con la verifica della condizione
isHtmxRequest: se la richiesta proviene da htmx (markup parziale), allora genera solo il codice racchiuso tra parentesi (<div id="posts-container">), altrimenti genera l'intera pagina comprensiva di layout; - Se
sortedPostscontiene almeno un elemento, allora il metodomap()itera tra gli elementi dell'array per generare unaArticleCardper ogni elemento. Se non vengono trovati risultati utili, viene mandato a video il messaggio "Nessun articolo trovato...".
Manca ancora il campo di ricerca. Avendo deciso di inserirlo nel menu di navigazione, dovremo modificare il layout generale del sito. Prima di questo, però, abbiamo bisogno di inserire un id al container dei contenuti delle pagine blog e archivio.
Modifiche alla pagina del blog e alla pagina di archivio
Prima di creare il modulo, dobbiamo definire l'elemento target dove inserire i risultati della ricerca. È necessario, quindi, assicurarci che la div che contiene i post abbia id="posts-container" in tutte le pagine del sito che usano il Layout. Nel nostro progetto, si tratta della pagina del blog e della pagina di archivio.
Apriamo, quindi, il file /src/pages/blog/index.astro e aggiungiamo id="posts-container" alla div esterna:
<div
x-data={`{ visibleItems: ${POSTS_PER_PAGE} }`}
x-cloak
id="posts-container"
class="grid gap-10"
>
È il momento di creare il modulo di ricerca.
Un pulsante di ricerca nel menu di navigazione del sito
Apriamo ora il file /src/layouts/Layout.astro e modifichiamo il codice JavaScript del frontmatter come segue:
---
import '../styles/global.css'
interface Props {
title: string;
description?: string;
showSearch?: boolean;
}
const { title, description = "Il mio fantastico blog creato con Astro", showSearch = true } = Astro.props;
---
Ecco cosa è cambiato rispetto alla versione precedente:
- Abbiamo aggiunto la proprietà
showSearchche ci permetterà di visualizzare o nascondere condizionalmente il campo di ricerca. In questo modo potremo decidere in modo preciso in quali pagine visualizzarlo. - Di default,
showSearch = true.
Ora modifichiamo il menu di navigazione come segue:
<nav class="max-w-4xl mx-auto px-6 py-4 flex justify-between items-center">
<a href="/" class="text-xl font-bold text-purple-700">{title}</a>
<div class="flex items-center gap-x-6">
<a href="/" class="text-gray-600 hover:text-purple-700 mr-4">Home</a>
<a href="/blog" class="text-gray-600 hover:text-purple-700 mr-4">Blog</a>
<a href="/archive" class="text-gray-600 hover:text-purple-700 mr-4">Archive</a>
{showSearch && (
<form action="/search" class="relative">
<input
type="text"
name="q"
placeholder="Cerca..."
hx-get="/search"
hx-trigger="input changed delay:500ms"
hx-target="#posts-container"
hx-swap="innerHTML"
hx-push-url="/search"
class="p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 w-40 md:w-64"
/>
<div class="htmx-indicator absolute top-0 right-0 mt-2 mr-2 text-gray-500">🔍</div>
</form>
)}
</div>
</nav>
- Il
<form action="/search">gestisce l'invio del form con la query inserita dall'utente nel campo di ricerca/search?q=query; hx-get="/search"invia una richiesta GET all'endpoint specificato;hx-trigger="input changed delay:500ms"attiva la richiesta con un intervallo di 500ms dopo che l'utente ha smesso di digitare;hx-target="#posts-container"stabilisce il target della risposta;hx-swap="innerHTML"sostituisce il contenuto del target (innerHTML) con i risultati della ricerca;hx-push-url="true"aggiorna l'URL del browser in base alla richiesta dell'utente, mantenendo lo "stato" della pagina coerente con le azioni dell'utente e migliorando l'esperienza di navigazione.- Se
showSearchètrue, allora genera il modulo di ricerca.
Risoluzione dei problemi con htmx
Sembra funzionare tutto correttamente ma c'è un problema. Quando digitiamo la query, la ricerca viene eseguita correttamente ogni volta che l'utente smette di digitare per almeno mezzo secondo. Tuttavia, se proviamo a dare invio, la pagina sarà caricata in modo non corretto. Questo avviene perché il browser riceve i risultati di una richiesta htmx e carica il markup parziale quando dovrebbe caricare l'intera pagina.
Per risolvere il problema, dobbiamo fare in modo che htmx gestisca la richiesta anche quando l'utente dà invio dopo aver inserito la query ma prima dei 500ms di ritardo.
Aggiorniamo il layout generale del blog (/src/layouts/Layout.astro) modificando l'elemento form come segue:
<nav class="max-w-4xl mx-auto px-6 py-4 flex justify-between items-center">
<a href="/" class="text-xl font-bold text-purple-700">{title}</a>
<div class="flex items-center gap-x-6">
<a href="/" class="text-gray-600 hover:text-purple-700 mr-4">Home</a>
<a href="/blog" class="text-gray-600 hover:text-purple-700">Blog</a>
<a href="/archive" class="text-gray-600 hover:text-purple-700">Archive</a>
{showSearch && (
<form
action="/search"
hx-get="/search"
hx-trigger="submit"
hx-target="#posts-container"
hx-swap="innerHTML"
hx-push-url="true"
class="relative"
>
<input
type="text"
name="q"
placeholder="Cerca..."
hx-get="/search"
hx-trigger="input changed delay:500ms"
hx-target="#posts-container"
hx-swap="innerHTML"
hx-push-url="true"
class="p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 w-40 md:w-64"
/>
<div class="htmx-indicator absolute top-0 right-0 mt-2 mr-2 text-gray-500">🔍</div>
</form>
)}
</div>
</nav>
La pagina di ricerca ora è completa. Per nascondere il campo di ricerca nelle pagine in cui non desideriamo visualizzarlo, basterà impostare la proprietà showSerach a false, come ad esempio nel componente PostLayout.astro che genera le pagine dei singoli post:
<Layout title={frontmatter.title} description={frontmatter.description} showSearch={false}>

La pagina che mostra i risultati della ricerca in Astro con htmx
Se vuoi aggiornamenti su Un campo di ricerca con htmx inserisci la tua email nel box qui sotto: