Skip to content

[Nom de l'Intégration API Pagefind]

[Nom de l'Intégration API Pagefind]

Documentation technique

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]

1. Vue d’ensemble

1.1. Description

[Description concise du rôle de cette intégration API dans l’architecture de recherche du site]

1.2. Objectif

  • [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]

1.3. Périmètre concerné

  • 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]

2. Architecture et flux

2.1. Diagramme de flux

[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]

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 JS API Bibliothèque JS de recherche pagefind (npm)
Index Pagefind Index chunké généré au build dist/_pagefind/

3. Implémentation de l’API Pagefind

3.1. Import et initialisation1/2

3.1. Import et initialisation2/2

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

// 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 });

3.3. Extraction des métadonnées

// 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);

3.4. Gestion des sous-résultats

// 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}`);
});
}

4. Composant de filtres custom1/2

4. Composant de filtres custom2/2

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

5.1. Configuration par type de contenu1/3

---
# 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
---

5.1. Configuration par type de contenu2/3

---
# 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
---

5.1. Configuration par type de contenu3/3

---
# Projet portfolio
title: "Projet Alpha"
description: "Description du projet"
pagefind:
meta:
type: "portfolio"
category: "web"
filters:
- type: portfolio
- category: web
---

5.2. Métadonnées via attributs HTML

<!-- 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>

6. Intégration avec Starlight

6.1. Cohérence de l’index

  • ✅ 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)

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);
}

7. Accessibilité (a11y)

7.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) ✅

7.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>

8. Performance et optimisation

8.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
  • ✅ Import dynamique pour éviter de charger Pagefind au chargement initial

8.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)

8.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

8.4. Taille des assets

Asset Taille Rationale
pagefind.js ~10 KB Script principal
Fragments d’index ~1-5 MB total Chunké, chargé à la demande

9. Débogage et troubleshooting

9.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
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

9.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
# 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);

9.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/

10. Bonnes pratiques

10.1. Architecture

  • ✅ 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

10.2. UX

  • ✅ 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

10.3. 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

10.4. 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

10.5. 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)

11. Évolutions et roadmap

11.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)]
  • [Évolution 5 : ex. Pagination des résultats]

11.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

12. Ressources liées1/2

  • 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

12. 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)

13. Historique des modifications1/2

Date Version Description Auteur
YYYY-MM-DD 1.0 Création initiale [Nom]
YYYY-MM-DD 1.1 [Modification] [Nom]

13. Historique des modifications2/2

---
**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
Switch to normal mode

Contents