Recherche sur le site
Fonctionnalité de recherche côté client avec interface modale, index JSON et activation par raccourci clavier.
Recherche sur le site
Fonctionnalité de recherche côté client avec une interface modale Bootstrap et une activation par raccourci clavier.

Ouvrez-la depuis l’icône de recherche dans la barre de navigation (ou appuyez sur /), saisissez une requête, et les correspondances apparaissent instantanément — l’index est construit au moment du build et la recherche s’effectue entièrement dans le navigateur, ce qui fonctionne sur GitHub Pages sans aucun backend.
Aperçu
- Côté client : Aucun serveur requis
- Index JSON : Index de recherche pré-construit
- Interface modale : Interface modale Bootstrap
- Raccourci clavier : Appuyez sur
/pour rechercher
Fonctionnement
graph LR
A[User Types] --> B[Search JSON Index]
B --> C[Filter Results]
C --> D[Display Matches]
D --> E[Navigate to Page]
- Jekyll construit
search.jsonavec tout le contenu des pages - L’utilisateur ouvre la fenêtre de recherche (clic ou touche
/) - JavaScript filtre l’index au fur et à mesure de la saisie
- Les résultats renvoient vers les pages correspondantes
Index de recherche
Fichier généré
Jekyll génère search.json :
[
{
"title": "Getting Started",
"url": "/docs/getting-started/",
"content": "Welcome to the documentation...",
"categories": ["docs"],
"tags": ["setup", "installation"]
}
]
Modèle d’index
---
layout: null
---
[
{% for page in site.pages %}
{% if page.title %}
{
"title": {{ page.title | jsonify }},
"url": {{ page.url | jsonify }},
"content": {{ page.content | strip_html | truncate: 500 | jsonify }},
"categories": {{ page.categories | jsonify }},
"tags": {{ page.tags | jsonify }}
}{% unless forloop.last %},{% endunless %}
{% endif %}
{% endfor %}
]
Interface modale
Structure HTML
<div class="modal fade" id="siteSearchModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<input type="search"
class="form-control"
data-search-input
placeholder="Search documentation..."
autofocus>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div data-search-results></div>
<div data-search-empty class="text-center text-muted py-4">
Type to search...
</div>
</div>
</div>
</div>
</div>
Bouton d’activation de la recherche
<button class="btn btn-outline-secondary" data-search-toggle>
<i class="bi bi-search"></i>
<span class="d-none d-md-inline ms-1">Search</span>
<kbd class="ms-2">/</kbd>
</button>
JavaScript
Contrôleur de recherche
function initSearchModal() {
const modalEl = document.getElementById('siteSearchModal');
const searchInput = modalEl.querySelector('[data-search-input]');
const resultsContainer = modalEl.querySelector('[data-search-results]');
let searchIndex = null;
// Load search index
async function loadIndex() {
const response = await fetch('/search.json');
searchIndex = await response.json();
}
// Perform search
function search(query) {
if (!searchIndex || !query) return [];
const terms = query.toLowerCase().split(' ');
return searchIndex.filter(item => {
const content = `${item.title} ${item.content}`.toLowerCase();
return terms.every(term => content.includes(term));
});
}
// Render results
function renderResults(results) {
if (results.length === 0) {
resultsContainer.innerHTML = '<p class="text-muted">No results found.</p>';
return;
}
resultsContainer.innerHTML = results.map(item => `
<a href="${item.url}" class="search-result d-block p-2 rounded">
<strong>${item.title}</strong>
<small class="d-block text-muted">${item.url}</small>
</a>
`).join('');
}
// Event listeners
searchInput.addEventListener('input', () => {
renderResults(search(searchInput.value));
});
// Load index when modal opens
modalEl.addEventListener('show.bs.modal', loadIndex);
}
Raccourci clavier
document.addEventListener('keydown', (e) => {
// Skip if typing in input
if (e.target.matches('input, textarea')) return;
if (e.key === '/') {
e.preventDefault();
const modal = bootstrap.Modal.getOrCreateInstance(
document.getElementById('siteSearchModal')
);
modal.show();
}
});
Configuration
Exclure des pages
# In page front matter
search: false
{% unless page.search == false %}
// Include in index
{% endunless %}
Champs de l’index
Contrôlez ce qui est indexé :
"content": {{ page.content | strip_html | truncate: 1000 | jsonify }}
Mise en forme
.search-result {
text-decoration: none;
color: inherit;
transition: background-color 0.2s;
}
.search-result:hover {
background-color: var(--bs-light);
}
.search-result.active {
background-color: var(--bs-primary);
color: white;
}
Navigation au clavier
// Arrow key navigation in results
searchInput.addEventListener('keydown', (e) => {
const results = resultsContainer.querySelectorAll('.search-result');
const active = resultsContainer.querySelector('.search-result.active');
if (e.key === 'ArrowDown') {
e.preventDefault();
const next = active ? active.nextElementSibling : results[0];
if (next) {
active?.classList.remove('active');
next.classList.add('active');
}
}
if (e.key === 'Enter' && active) {
window.location.href = active.href;
}
});
Performance
Chargement différé
Charger l’index uniquement lorsque nécessaire :
let indexPromise = null;
function getIndex() {
if (!indexPromise) {
indexPromise = fetch('/search.json').then(r => r.json());
}
return indexPromise;
}
Anti-rebond
let timeout;
searchInput.addEventListener('input', () => {
clearTimeout(timeout);
timeout = setTimeout(() => {
renderResults(search(searchInput.value));
}, 150);
});
Dépannage
L’index ne se construit pas
- Vérifiez que le modèle
search.jsonexiste - Vérifiez que le build Jekyll l’inclut
- Recherchez d’éventuelles erreurs Liquid
La recherche ne fonctionne pas
- Vérifiez que
search.jsonest accessible - Consultez la console du navigateur pour repérer les erreurs
- Testez la validité du JSON
Conflits de raccourcis clavier
- Recherchez d’autres gestionnaires
/ - Vérifiez que l’ID de la fenêtre modale correspond
- Testez dans différents navigateurs
Voir aussi
Voir aussi
- [[Features]]
- [[Keyboard Navigation]]