Skip to content

[Nom de l'Interface de Recherche]

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]

[Description concise du rĂŽle de cette interface de recherche dans l’expĂ©rience utilisateur]

  • [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]
  • 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]
[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]
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/
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>
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>
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>
  • ✅ 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
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)

Section titled “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);
}
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
---
title: "Guide d'installation"
# Métadonnées Pagefind pour distinction visuelle
pagefind:
meta:
type: "docs"
---
---
title: "Mon article de blog"
pagefind:
meta:
type: "blog"
---
---
title: "Projet Alpha"
pagefind:
meta:
type: "portfolio"
---
// Dans le composant de recherche
const type = result.meta?.type || 'page';
const badge = {
docs: 'Doc',
blog: 'Blog',
portfolio: 'Portfolio',
page: 'Page',
}[type] || 'Page';
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) ✅
<!-- 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>
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é)
  • ✅ 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
  • ✅ 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)
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
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
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
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
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/
  • ✅ 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)
  • ✅ 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
  • ✅ 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
  • ✅ 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)
  • [É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)]
  • ❌ 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
  • 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
  • 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)
Date Version Description Auteur
YYYY-MM-DD 1.0 Création initiale [Nom]
YYYY-MM-DD 1.1 [Modification] [Nom]
---
**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
Edit page

Last updated: