[Nom de l'Interface de Recherche]
[Nom de l'Interface de Recherche]
Documentation technique
Type de ressource : Interface Utilisateur Pagefind (Barre de recherche / Résultats / Filtres)
Fichier source :src/components/ui/SearchBar.astroou composant associé
Statut : [Actif / Déprécié / En développement]
1. Vue d’ensemble
1.1. Description
[Description concise du rôle de cette interface de recherche dans l’expérience utilisateur]
1.2. Objectif
- [Objectif principal : ex. fournir une recherche unifiée sur l’ensemble du site]
- [Bénéfice pour l’utilisateur : ex. trouver rapidement du contenu dans le blog, le portfolio ou la documentation]
1.3. Périmètre concerné
- Couverture : [Vitrine / Blog / Portfolio / Documentation / Tout]
- Emplacement : [Header global / Sidebar / Page dédiée / Modal]
- Type d’interface : [Pagefind UI natif / Composant custom / Les deux]
- Cohérence Starlight : [Index partagé / Index séparé mais cohérent]
2. Architecture et flux
2.1. Diagramme de flux
[Utilisateur] │ (saisie dans la barre de recherche) ▼[Composant SearchBar] │ • Initialisation Pagefind │ • Chargement de l'index chunké │ • Recherche côté client ▼[Pagefind (client-side)] │ • Matching des termes │ • Application des filtres │ • Ranking des résultats ▼[Affichage des résultats] │ • Distinction visuelle par type │ • Badges (Doc, Blog, Portfolio, Page) │ • Pagination ▼[Utilisateur] │ (clic sur un résultat) ▼[Navigation vers la page]2.2. Composants impliqués
| Composant | Rôle | Fichier |
|---|---|---|
SearchBar.astro |
Interface de recherche globale | src/components/ui/SearchBar.astro |
SearchResults.astro |
Affichage des résultats | src/components/ui/SearchResults.astro |
SearchFilters.astro |
Filtres par type de contenu | src/components/ui/SearchFilters.astro |
| Pagefind UI | Bibliothèque JS de recherche | pagefind-ui (npm) |
| Index Pagefind | Index chunké généré au build | dist/_pagefind/ |
3. Implémentation
3.1. Variante : Pagefind UI natif1/2
3.1. Variante : Pagefind UI natif2/2
<div id="search"></div>
<script> import { Search } from 'pagefind-ui';
window.addEventListener('DOMContentLoaded', () => { new Search({ element: '#search', pageSize: 10, showSubResults: true, showImages: false, processTerm: (term) => { // Normalisation des termes de recherche return term.toLowerCase().trim(); }, processResult: (result) => { // Personnalisation des résultats const type = result.meta?.type || 'page'; const badge = { docs: 'Doc', blog: 'Blog', portfolio: 'Portfolio', page: 'Page', }[type] || 'Page';
return { ...result, badge, }; }, }); });</script>
<style> /* Personnalisation des styles Pagefind UI */ :global(.pagefind-ui) { --pagefind-ui-primary: var(--color-primary, #3b82f6); --pagefind-ui-text: var(--color-text-primary, #1f2937); --pagefind-ui-background: var(--color-bg-primary, #ffffff); --pagefind-ui-border: var(--color-border, #e5e7eb); --pagefind-ui-tag: var(--color-gray-2, #e5e7eb); }
:global(.pagefind-ui__search-input) { width: 100%; padding: 0.75rem 1rem; border: 1px solid var(--pagefind-ui-border); border-radius: 0.5rem; font-size: 1rem; }
:global(.pagefind-ui__result) { padding: 1rem; border-bottom: 1px solid var(--pagefind-ui-border); }
:global(.pagefind-ui__result-title) { font-size: 1.125rem; font-weight: 600; color: var(--pagefind-ui-primary); }</style>3.2. Variante : Composant custom (contrôle total)1/2
3.2. Variante : Composant custom (contrôle total)2/2
---import SearchResults from './SearchResults.astro';import SearchFilters from './SearchFilters.astro';
interface Props { placeholder?: string; showFilters?: boolean;}
const { placeholder = 'Rechercher...', showFilters = true } = Astro.props;---
<div class="search-container" data-pagefind-ignore="all"> <div class="search-input-wrapper"> <input type="search" id="search-input" placeholder={placeholder} class="search-input" autocomplete="off" /> <svg class="search-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> </svg> </div>
{showFilters && <SearchFilters />}
<div id="search-results"> <SearchResults /> </div></div>
<script> import type * as Pagefind from 'pagefind-ui';
let pagefind: typeof Pagefind | null = null; let currentQuery = ''; let activeFilters: string[] = [];
const searchInput = document.getElementById('search-input') as HTMLInputElement; const resultsContainer = document.getElementById('search-results')!;
// Initialisation de Pagefind async function initPagefind() { if (pagefind) return; // @ts-ignore pagefind = await import('/pagefind/pagefind.js'); await pagefind.init(); }
// Recherche async function performSearch(query: string) { if (!query || query.length < 2) { resultsContainer.innerHTML = ''; return; }
await initPagefind();
const options: any = { filters: activeFilters.length > 0 ? { type: activeFilters } : undefined, };
const search = await pagefind!.search(query, options); const results = await search.results.slice(0, 10); // Limite à 10 résultats
renderResults(results); }
// Rendu des résultats async function renderResults(results: any[]) { if (results.length === 0) { resultsContainer.innerHTML = '<p class="no-results">Aucun résultat trouvé</p>'; return; }
const html = await Promise.all( results.map(async (result) => { const data = await result.data(); const type = data.meta?.type || 'page'; const badge = { docs: 'Doc', blog: 'Blog', portfolio: 'Portfolio', page: 'Page', }[type] || 'Page';
return ` <article class="search-result"> <span class="badge badge-${type}">${badge}</span> <h3 class="result-title"> <a href="${data.url}">${data.meta?.title || data.url}</a> </h3> <p class="result-excerpt">${data.excerpt || ''}</p> </article> `; }) );
resultsContainer.innerHTML = html.join(''); }
// Event listeners searchInput?.addEventListener('input', (e) => { const query = (e.target as HTMLInputElement).value; currentQuery = query; performSearch(query); });
// Debounce let searchTimeout: number; searchInput?.addEventListener('input', (e) => { clearTimeout(searchTimeout); searchTimeout = setTimeout(() => { performSearch((e.target as HTMLInputElement).value); }, 300); });</script>
<style> .search-container { max-width: 600px; margin: 0 auto; }
.search-input-wrapper { position: relative; }
.search-input { width: 100%; padding: 0.75rem 1rem 0.75rem 2.5rem; border: 1px solid var(--color-border, #e5e7eb); border-radius: 0.5rem; font-size: 1rem; }
.search-icon { position: absolute; left: 0.75rem; top: 50%; transform: translateY(-50%); width: 1.25rem; height: 1.25rem; color: var(--color-gray-5, #6b7280); }
.search-result { padding: 1rem; border-bottom: 1px solid var(--color-border, #e5e7eb); }
.badge { display: inline-block; padding: 0.25rem 0.5rem; border-radius: 0.25rem; font-size: 0.75rem; font-weight: 600; margin-bottom: 0.5rem; }
.badge-docs { background-color: #dbeafe; color: #1e40af; }
.badge-blog { background-color: #d1fae5; color: #065f46; }
.badge-portfolio { background-color: #fed7aa; color: #9a3412; }
.badge-page { background-color: #e5e7eb; color: #374151; }
.result-title { font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; }
.result-title a { color: var(--color-primary, #3b82f6); text-decoration: none; }
.result-title a:hover { text-decoration: underline; }
.result-excerpt { color: var(--color-gray-6, #4b5563); font-size: 0.875rem; line-height: 1.5; }
.no-results { text-align: center; padding: 2rem; color: var(--color-gray-5, #6b7280); }</style>3.3. Composant de filtres1/2
3.3. Composant de filtres2/2
<div class="search-filters"> <label class="filter-label"> <input type="checkbox" value="docs" checked /> <span class="filter-badge badge-docs">Doc</span> Documentation </label>
<label class="filter-label"> <input type="checkbox" value="blog" checked /> <span class="filter-badge badge-blog">Blog</span> Blog </label>
<label class="filter-label"> <input type="checkbox" value="portfolio" checked /> <span class="filter-badge badge-portfolio">Portfolio</span> Portfolio </label>
<label class="filter-label"> <input type="checkbox" value="page" checked /> <span class="filter-badge badge-page">Page</span> Pages </label></div>
<script> const filterCheckboxes = document.querySelectorAll<HTMLInputElement>('.search-filters input[type="checkbox"]');
filterCheckboxes.forEach((checkbox) => { checkbox.addEventListener('change', () => { const activeFilters = Array.from(filterCheckboxes) .filter((cb) => cb.checked) .map((cb) => cb.value);
// Déclencher une nouvelle recherche avec les filtres actifs window.dispatchEvent(new CustomEvent('search-filters-changed', { detail: { filters: activeFilters }, })); }); });</script>
<style> .search-filters { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 1rem; }
.filter-label { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 0.75rem; border: 1px solid var(--color-border, #e5e7eb); border-radius: 0.5rem; cursor: pointer; transition: all 0.2s; }
.filter-label:hover { background-color: var(--color-gray-1, #f3f4f6); }
.filter-label input[type="checkbox"] { width: 1rem; height: 1rem; }
.filter-badge { font-size: 0.75rem; padding: 0.125rem 0.375rem; }</style>4. Intégration avec Starlight
4.1. Cohérence de l’expérience
- ✅ Starlight utilise nativement Pagefind pour sa propre barre de recherche
- ✅ L’objectif est de partager le même index (ou des index très proches)
- ✅ Les résultats de documentation sont distingués par le badge “Doc”
- ✅ L’expérience de recherche est homogène sur tout le site
4.2. Configuration Starlight
import starlight from '@astrojs/starlight';
export default defineConfig({ integrations: [ starlight({ title: 'Documentation Technique', // Starlight utilise Pagefind nativement // Pas de configuration supplémentaire nécessaire }), ],});4.3. Personnalisation du thème Starlight (optionnel)
/* Aligner les styles de recherche Starlight avec le reste du site */:root { --sl-color-accent: var(--color-primary, #3b82f6);}
starlight-search { --sl-search-border: var(--color-border, #e5e7eb); --sl-search-background: var(--color-bg-primary, #ffffff);}5. Distinction visuelle des résultats
5.1. Matrice des badges
| Type de contenu | Badge | Couleur | Rationale |
|---|---|---|---|
| Documentation | “Doc” | Bleu (#dbeafe / #1e40af) | Contenu technique |
| Article de blog | “Blog” | Vert (#d1fae5 / #065f46) | Contenu éditorial |
| Projet portfolio | “Portfolio” | Orange (#fed7aa / #9a3412) | Contenu showcase |
| Page vitrine | “Page” | Gris (#e5e7eb / #374151) | Contenu institutionnel |
5.2. Implémentation via métadonnées Pagefind1/3
---title: "Guide d'installation"# Métadonnées Pagefind pour distinction visuellepagefind: meta: type: "docs"---5.2. Implémentation via métadonnées Pagefind2/3
---title: "Mon article de blog"pagefind: meta: type: "blog"---5.2. Implémentation via métadonnées Pagefind3/3
---title: "Projet Alpha"pagefind: meta: type: "portfolio"---5.3. Extraction du type dans le composant
// Dans le composant de rechercheconst type = result.meta?.type || 'page';const badge = { docs: 'Doc', blog: 'Blog', portfolio: 'Portfolio', page: 'Page',}[type] || 'Page';6. Accessibilité (a11y)
6.1. Conformité WCAG 2.1 AA
| Critère | Implémentation | Statut |
|---|---|---|
| 1.3.1 Info and Relationships | Label explicite pour le champ de recherche | ✅ |
| 2.1.1 Keyboard | Navigation clavier complète (Tab, Enter, Escape) | ✅ |
| 2.4.3 Focus Order | Ordre de focus logique (input → filtres → résultats) | ✅ |
| 2.4.7 Focus Visible | Focus visible sur tous les éléments interactifs | ✅ |
| 3.3.2 Labels or Instructions | Placeholder descriptif + label visible | ✅ |
| 4.1.2 Name, Role, Value | Attributs ARIA appropriés (aria-label, aria-live) |
✅ |
6.2. Attributs ARIA utilisés
<!-- Champ de recherche --><input type="search" id="search-input" aria-label="Rechercher dans le site" placeholder="Rechercher..." autocomplete="off"/>
<!-- Conteneur de résultats (annoncé aux lecteurs d'écran) --><div id="search-results" role="region" aria-label="Résultats de recherche" aria-live="polite"> <!-- Résultats --></div>
<!-- Filtres --><fieldset aria-label="Filtres de recherche"> <legend class="sr-only">Filtrer par type de contenu</legend> <!-- Checkboxes --></fieldset>6.3. Navigation clavier
| Touche | Action |
|---|---|
Tab |
Navigation vers le champ de recherche, puis les filtres, puis les résultats |
Enter |
Validation de la recherche ou navigation vers un résultat |
Escape |
Fermeture de la recherche (si modal) |
Flèches |
Navigation dans les résultats (si implémenté) |
7. Performance et optimisation
7.1. Chargement de l’index
- ✅ L’index Pagefind est chunké (chargé à la demande)
- ✅ Seul le script principal (
pagefind.js) est chargé initialement (~10 KB) - ✅ Les fragments d’index sont chargés au fur et à mesure des recherches
- ✅ Pas de chargement de l’index complet au démarrage
7.2. Optimisations appliquées
- ✅ Debounce sur l’input (300ms) pour éviter les recherches excessives
- ✅ Limite de 10 résultats affichés (pagination si nécessaire)
- ✅ Pas d’images dans les résultats (réduit la bande passante)
- ✅ Index chunké (faible bande passante)
- ✅ Recherche côté client (pas d’appel API)
7.3. Core Web Vitals
| Métrique | Objectif | Stratégie |
|---|---|---|
| LCP | < 2.5s | Pas d’impact (recherche post-chargement) |
| INP | < 200ms | Debounce + recherche rapide |
| CLS | < 0.1 | Dimensions fixes pour le conteneur de résultats |
7.4. Taille des assets
| Asset | Taille | Rationale |
|---|---|---|
pagefind.js |
~10 KB | Script principal |
pagefind-ui.css |
~5 KB | Styles UI (optionnel) |
| Fragments d’index | ~1-5 MB total | Chunké, chargé à la demande |
8. Débogage et troubleshooting
8.1. Problèmes courants
| Symptôme | Cause probable | Solution |
|---|---|---|
| Aucun résultat | Index non généré | Vérifier que npm run build a été exécuté |
| Résultats non pertinents | Ranking mal configuré | Ajuster les poids dans pagefind.yml |
| Filtres non fonctionnels | Métadonnées manquantes | Ajouter pagefind.meta.type dans le frontmatter |
| Recherche lente | Index trop volumineux | Exclure les éléments non pertinents |
| Styles cassés | Conflit CSS | Vérifier les spécificités et le scope |
| Starlight non indexé | Configuration incorrecte | Vérifier l’intégration Starlight |
8.2. Tests manuels
# Génération de l'indexnpm run build
# Vérification de l'indexls -lh dist/_pagefind/
# Test de la recherche en localnpm run preview# Ouvrir http://localhost:4321 et tester la recherche
# Inspection de l'index (debug)npx pagefind --source dist --verbose8.3. Logs et monitoring
# Logs de développementnpm run dev
# Vérification de la taille de l'indexdu -sh dist/_pagefind/
# Test de l'accessibiliténpx axe http://localhost:4321/9. Bonnes pratiques
9.1. UX
- ✅ Fournir un feedback immédiat (spinner pendant la recherche)
- ✅ Afficher un message “Aucun résultat” si la recherche est vide
- ✅ Suggérer des termes de recherche populaires (optionnel)
- ✅ Permettre la navigation clavier complète
- ✅ Annoncer les résultats aux lecteurs d’écran (
aria-live)
9.2. Accessibilité
- ✅ Toujours fournir un label explicite pour le champ de recherche
- ✅ Utiliser des attributs ARIA appropriés
- ✅ Assurer une navigation clavier complète
- ✅ Vérifier le contraste (WCAG AA minimum)
- ✅ Tester avec un lecteur d’écran
9.3. Performance
- ✅ Utiliser le debounce pour éviter les recherches excessives
- ✅ Limiter le nombre de résultats affichés
- ✅ Charger l’index de manière chunkée
- ✅ Éviter les images dans les résultats
- ✅ Mesurer les Core Web Vitals avec la recherche active
9.4. Maintenance
- ✅ Documenter les changements de configuration dans ce fichier
- ✅ Tester la recherche après chaque modification majeure
- ✅ Vérifier les filtres et les métadonnées régulièrement
- ✅ Surveiller la taille de l’index (objectif < 5 MB)
10. Évolutions et roadmap
10.1. Améliorations planifiées
- [Évolution 1 : ex. Suggestions de recherche (autocomplete)]
- [Évolution 2 : ex. Historique des recherches (localStorage)]
- [Évolution 3 : ex. Recherche facette avancée (par auteur, catégorie, date)]
- [Évolution 4 : ex. Support du multi-langue (i18n)]
10.2. Limitations connues
- ❌ Pagefind ne supporte pas nativement la recherche dynamique (SSR)
- ❌ L’index doit être régénéré à chaque build (pas de mise à jour incrémentale)
- ❌ Les filtres sont limités aux métadonnées définies au build
- ❌ La recherche full-text ne supporte pas nativement les langues non-latines
11. Ressources liées1/2
- Documentation Pagefind : Search UI
- Documentation Pagefind : Search API
- Documentation astro-pagefind : npm package
- DAT L2 : §8 - Stratégie de Recherche Unifiée (Pagefind)
- DAT L2 : §8.4 - Expérience utilisateur & distinction visuelle des résultats
11. Ressources liées2/2
- DAT L2 : §8.5 - Cohérence avec Starlight
- Fichiers concernés :
src/components/ui/SearchBar.astrosrc/components/ui/SearchResults.astrosrc/components/ui/SearchFilters.astrodist/_pagefind/(généré au build)
12. Historique des modifications1/2
| Date | Version | Description | Auteur |
|---|---|---|---|
| YYYY-MM-DD | 1.0 | Création initiale | [Nom] |
| YYYY-MM-DD | 1.1 | [Modification] | [Nom] |
12. Historique des modifications2/2
---
**Points clés de ce template :**- Documente l'interface de recherche unifiée (Pagefind UI natif ou custom)- Détaille la distinction visuelle des résultats par type de contenu (badges)- Inclut l'intégration avec Starlight (index partagé)- Couvre l'accessibilité WCAG 2.1 AA (navigation clavier, ARIA)- Fournit les bonnes pratiques de performance (debounce, index chunké)- Sépare clairement l'implémentation, les filtres, et la personnalisation- Inclut un guide de débogage complet