Skip to content

[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.astro ou 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

src/components/ui/SearchBar.astro
<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

src/components/ui/SearchBar.astro
---
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

src/components/ui/SearchFilters.astro
<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

astro.config.mjs
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)

src/styles/starlight-overrides.css
/* 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 visuelle
pagefind:
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 recherche
const 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

Terminal window
# Génération de l'index
npm run build
# Vérification de l'index
ls -lh dist/_pagefind/
# Test de la recherche en local
npm run preview
# Ouvrir http://localhost:4321 et tester la recherche
# Inspection de l'index (debug)
npx pagefind --source dist --verbose

8.3. Logs et monitoring

Terminal window
# Logs de développement
npm run dev
# Vérification de la taille de l'index
du -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.astro
    • src/components/ui/SearchResults.astro
    • src/components/ui/SearchFilters.astro
    • dist/_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
Switch to normal mode

Contents