Skip to content

[Nom de l'Intégration API Pagefind]

Type de ressource : API JavaScript Pagefind (Recherche programmatique)
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 intégration API dans l’architecture de recherche du site]

  • [Objectif principal : ex. fournir une recherche custom avec contrôle total sur l’UI et le comportement]
  • [Bénéfice par rapport à Pagefind UI natif : ex. personnalisation complète, intégration design system]
  • Couverture : [Vitrine / Blog / Portfolio / Documentation / Tout]
  • Type d’API : [Pagefind JS API / Pagefind UI custom / Les deux]
  • Emplacement : [Header global / Page dédiée / Modal / Sidebar]
  • Cohérence Starlight : [Index partagé / Index séparé mais cohérent]
[Utilisateur]
│ (saisie dans la barre de recherche)
▼
[Composant SearchBar (Astro)]
│ • Import dynamique de Pagefind
│ • Initialisation de l'index
│ • Debounce sur l'input
▼
[Pagefind JS API]
│ • Chargement de l'index chunké
│ • Recherche côté client
│ • Application des filtres
▼
[Résultats]
│ • Extraction des métadonnées
│ • Distinction visuelle par type
│ • Pagination / Limitation
▼
[Interface custom]
│ • Rendu des résultats
│ • Badges (Doc, Blog, Portfolio, Page)
▼
[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 JS API Bibliothèque JS de recherche pagefind (npm)
Index Pagefind Index chunké généré au build dist/_pagefind/
src/components/ui/SearchBar.astro
<div class="search-container" data-pagefind-ignore="all">
<input
type="search"
id="search-input"
placeholder="Rechercher..."
class="search-input"
autocomplete="off"
aria-label="Rechercher dans le site"
/>
<div id="search-results" role="region" aria-label="Résultats de recherche" aria-live="polite">
<!-- Résultats injectés dynamiquement -->
</div>
</div>
<script>
// Import dynamique de Pagefind (lazy loading)
let pagefind: any = null;
async function initPagefind() {
if (pagefind) return;
// Import dynamique pour éviter de charger Pagefind au chargement initial
// @ts-ignore
pagefind = await import('/pagefind/pagefind.js');
await pagefind.init();
}
// État de la recherche
let currentQuery = '';
let activeFilters: Record<string, string[]> = {};
let searchTimeout: number;
// Recherche avec debounce
async function performSearch(query: string) {
if (!query || query.length < 2) {
clearResults();
return;
}
await initPagefind();
const options: any = {};
// Application des filtres actifs
if (Object.keys(activeFilters).length > 0) {
options.filters = activeFilters;
}
// Exécution de la recherche
const search = await pagefind.search(query, options);
// Récupération des résultats (limités à 10)
const results = await Promise.all(
search.results.slice(0, 10).map(result => result.data())
);
renderResults(results, search.filters);
}
// Effacement des résultats
function clearResults() {
const container = document.getElementById('search-results');
if (container) {
container.innerHTML = '';
}
}
// Rendu des résultats
function renderResults(results: any[], availableFilters: any) {
const container = document.getElementById('search-results');
if (!container) return;
if (results.length === 0) {
container.innerHTML = '<p class="no-results">Aucun résultat trouvé</p>';
return;
}
const html = results.map(result => {
const type = result.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="${result.url}">${result.meta?.title || result.url}</a>
</h3>
<p class="result-excerpt">${result.excerpt || ''}</p>
</article>
`;
}).join('');
container.innerHTML = html;
}
// Event listeners
const searchInput = document.getElementById('search-input') as HTMLInputElement;
searchInput?.addEventListener('input', (e) => {
const query = (e.target as HTMLInputElement).value;
currentQuery = query;
// Debounce de 300ms
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
performSearch(query);
}, 300);
});
// Écoute des changements de filtres (via CustomEvent)
window.addEventListener('search-filters-changed', (e: any) => {
activeFilters = e.detail.filters;
performSearch(currentQuery);
});
</script>
<style>
.search-container {
max-width: 600px;
margin: 0 auto;
position: relative;
}
.search-input {
width: 100%;
padding: 0.75rem 1rem;
border: 1px solid var(--color-border, #e5e7eb);
border-radius: 0.5rem;
font-size: 1rem;
}
.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.2. Utilisation des filtres programmatiques

Section titled “3.2. Utilisation des filtres programmatiques”
// Exemple de filtrage par type de contenu
const filters = {
type: ['blog', 'docs'], // Uniquement les articles de blog et la documentation
};
const search = await pagefind.search('installation', { filters });
// Exemple de filtrage multiple
const filters = {
type: ['blog'],
category: ['tech', 'design'],
author: ['john-doe'],
};
const search = await pagefind.search('tutorial', { filters });
// Structure d'un résultat Pagefind
interface PagefindResult {
id: string;
url: string;
score: number;
data: {
url: string;
meta: {
title?: string;
description?: string;
type?: string; // 'docs' | 'blog' | 'portfolio' | 'page'
category?: string;
author?: string;
image?: string;
};
content: string;
excerpt: string;
word_count: number;
filters: Record<string, string[]>;
sub_results: Array<{
title: string;
url: string;
excerpt: string;
}>;
};
}
// Extraction des métadonnées
const result = await search.results[0].data();
const title = result.meta?.title || 'Sans titre';
const type = result.meta?.type || 'page';
const excerpt = result.excerpt || result.content.substring(0, 200);
// Pagefind peut retourner des sous-résultats (sections de page)
const result = await search.results[0].data();
if (result.sub_results && result.sub_results.length > 0) {
// Afficher les sous-résultats
result.sub_results.forEach(sub => {
console.log(`Section: ${sub.title}`);
console.log(`URL: ${sub.url}`);
console.log(`Extrait: ${sub.excerpt}`);
});
}
src/components/ui/SearchFilters.astro
<div class="search-filters" role="group" aria-label="Filtres de recherche">
<fieldset>
<legend class="sr-only">Filtrer par type de contenu</legend>
<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>
</fieldset>
</div>
<script>
const filterCheckboxes = document.querySelectorAll<HTMLInputElement>('.search-filters input[type="checkbox"]');
filterCheckboxes.forEach((checkbox) => {
checkbox.addEventListener('change', () => {
const activeFilters: Record<string, string[]> = {};
const checkedTypes = Array.from(filterCheckboxes)
.filter((cb) => cb.checked)
.map((cb) => cb.value);
if (checkedTypes.length > 0 && checkedTypes.length < 4) {
activeFilters.type = checkedTypes;
}
// 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>

5. Métadonnées Pagefind dans le frontmatter

Section titled “5. Métadonnées Pagefind dans le frontmatter”
---
# Article de blog
title: "Mon article de blog"
description: "Description de l'article"
pagefind:
meta:
type: "blog"
category: "tech"
author: "john-doe"
filters:
- type: blog
- category: tech
- author: john-doe
---
---
# Page de documentation Starlight
title: "Guide d'installation"
description: "Comment installer le projet"
sidebar:
label: "Installation"
order: 1
pagefind:
meta:
type: "docs"
category: "guide"
filters:
- type: docs
- category: guide
---
---
# Projet portfolio
title: "Projet Alpha"
description: "Description du projet"
pagefind:
meta:
type: "portfolio"
category: "web"
filters:
- type: portfolio
- category: web
---
<!-- Définir des métadonnées Pagefind directement dans le HTML -->
<article data-pagefind-meta="type:blog,category:tech,author:john-doe">
<h1 data-pagefind-weight="2.0">Titre de l'article</h1>
<div data-pagefind-body>
<!-- Contenu principal -->
</div>
</article>
  • ✅ 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

6.2. Personnalisation du thème Starlight (optionnel)

Section titled “6.2. 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);
}
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>
  • ✅ 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
  • ✅ Import dynamique pour éviter de charger Pagefind au chargement initial
  • ✅ 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
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
Erreur “pagefind is not defined” Import dynamique échoué Vérifier le chemin /pagefind/pagefind.js
Styles cassés Conflit CSS Vérifier les spécificités et le scope
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
# Test de l'API dans la console du navigateur
# Ouvrir les DevTools → Console
const pagefind = await import('/pagefind/pagefind.js');
await pagefind.init();
const search = await pagefind.search('test');
console.log(search.results);
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/
  • ✅ Utiliser l’import dynamique pour charger Pagefind à la demande
  • ✅ Centraliser la logique de recherche dans un composant dédié
  • ✅ Séparer les composants (SearchBar, SearchResults, SearchFilters)
  • ✅ Utiliser des CustomEvents pour la communication entre composants
  • ✅ Typer les résultats Pagefind avec TypeScript
  • ✅ Fournir un feedback immédiat (spinner pendant la recherche)
  • ✅ Afficher un message “Aucun résultat” si la recherche est vide
  • ✅ Permettre la navigation clavier complète
  • ✅ Annoncer les résultats aux lecteurs d’écran (aria-live)
  • ✅ Utiliser le debounce pour éviter les recherches excessives
  • ✅ 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)]
  • [Évolution 5 : ex. Pagination des résultats]
  • ❌ 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 API
  • Documentation Pagefind : Metadata
  • Documentation Pagefind : Filters
  • 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'API JavaScript programmatique de Pagefind (par opposition à l'UI native)
- Détaille l'import dynamique et l'initialisation de l'index
- Inclut la gestion des filtres et des métadonnées
- Fournit l'intégration avec les composants custom (SearchBar, SearchResults, SearchFilters)
- Couvre l'accessibilité WCAG 2.1 AA (navigation clavier, ARIA)
- Sépare clairement l'implémentation technique et les bonnes pratiques UX
- Inclut un guide de débogage complet avec tests manuels dans la console
Edit page

Last updated: