Sphinx Doc et Pagefind
»On ne nous a pas dit qu’intégrer Pagefind dans une documentation Sphinx était impossible, donc on l’a fait. Bon, on ne nous a pas non plus dit que ça n’était pas impossible.
Sphinx (site) est un générateur de documentation en Python. À Gandi, on l’utilise pour la plupart de nos documentations internes. Comme Hugo, c’est un générateur de site statique. En gros, ça veut dire qu’on écrit notre documentationdans dans des fichiers au format Markdown ou ReST, et Sphinx nous donne un site en HTML, avec les fichiers CSS, JS, etc. Sphinx peut aussi générer la doc en PDF ou en ePub, mais ça n’est pas le propos du jour.
Pagefind (site), qu’on utilise sur ce blog, est une bibliothèque front-end de recherche de contenus pour sites statiques. Ça fonctionne en générant un index du contenu juste après la génération du site en HTML, puis en mettant cet index à disposition du navigateur, via des fichiers JavaScript servis avec le site.
Sphinx est super mais ses fonctionnalités de recherche n’ont pas convaincu tout le monde dans l’équipe. Voyons ce que donne Pagefind dans Sphinx.
Voilà comment ça marche
Tout d’abord, on aura besoin d’indexer les contenus du site web. Pour ce faire, j’ai ajouté un fichier de config de Pagefind (pagefind.yml
) à la racine du projet, où j’ai spécifié le répertoire source
à parcourir, le sélecteur root_selector
dans les fichiers HTML pour indiquer où trouver le contenu pertinent, et un sélecteur exclude_selectors
qui exclut de l’index les permaliens ajoutés dans les titres par Sphinx.
# pagefind.yml
source: public
root_selector: article[role=main]
exclude_selectors:
- a.headerlink
L’indexation est ensuite lancée avec la commande suivante :
npx pagefind
Maintenant, il faut afficher l’interface de Pagefind dans la page de recherche de Sphinx.
Un nouveau gabarit est nécessaire. Il contient la structure de la page (une extension de la page de base du thème courant, furo), et le code spécifique à Pagefind. Je dois placer ce gabarit dans un des répertoires définis dans la propriété templates_path
du fichier de configuration de Sphinx (docs/conf.py
). Dans mon cas, c’est dans docs/_templates
.
<!-- docs/_templates/search.html -->
{% extends "page.html" %}
{%- block htmltitle -%}
<title>{{ _("Search") }} - {{ docstitle }}</title>
{%- endblock htmltitle -%}
{% block content %}
<h1>{{ _("Search") }}</h1>
<div id="search"></div>
{% endblock %}
{% block scripts -%}
{{ super() }}
<script src="/_pagefind/pagefind-ui.js" type="text/javascript"></script>
<script>
window.addEventListener('DOMContentLoaded', () => {
// Chargement de l’interface de Pagefind
new PagefindUI({
element: "#search",
showImages: false,
});
});
</script>
{%- endblock scripts %}
Pour remplacer la page de recherche par défaut de Sphinx, je dois aussi ajouter ce code dans le fichier de config de Sphinx :
# docs/conf.py
html_additional_pages = {
'search': 'search.html', # chemin relatif au répertoire docs/_templates
}
On peut compiler la documentation, et avec la commande npx pagefind --serve
on peut voir le résultat à l’adresse http://localhost:1414/search.html
.
Cool, ça marche ! Bravo Joachim, t’as assuré, je paye mon coup au bar après le boulot.
Mais attends ! Quand j’utilise le champ de recherche dans la barre de menu de Sphinx, ça serait pas super bien si ça renseignait automatiquement le champ de recherche de l’interface de Pagefind ? Ouais, ça serait vraiment super bien. Pour que ça fonctionne, il faut d’abord qu’on attende le chargement de l’interface de Pagefind, puis on récupère la valeur du paramètre q
transmis par le champ de recherche de Sphinx via l’URL, on l’injecte dans le champ de recherche, et on déclenche l’événement que Pagefind attend pour charger les résultats. Voilà comment ça marche en JS :
window.addEventListener('DOMContentLoaded', () => {
// Chargement de l’interface de Pagefind
new PagefindUI({
element: "#search",
showImages: false,
});
// On récupère les paramètres
const urlParams = new URLSearchParams(window.location.search);
// On récupère la valeur de `q`
const query = urlParams.get('q');
// On identifie le champ dans l’interface de Pagefind
const searchInputElement = document.querySelector("#search form[role=search] input");
// On crée un nouvel événement `input` pour le champ
const inputChangeEvent = new Event('input');
// On change la valeur du champ
searchInputElement.value = query;
// On déclenche l’événement `input` pour Pagefind
searchInputElement?.dispatchEvent(inputChangeEvent);
// Et on déplace le focus de la page vers le champ de recherche
searchInputElement?.focus();
});
Super ! L’interactivité fonctionne bien. Maintenant on peut ajouter quelques styles basiques sur le champ :
#search form input[type="text"] {
width: 100%;
box-sizing: border-box;
line-height: 2em;
padding-inline: 0.6em;
font-size: 1.2rem;
border-radius: 0.25rem;
/* Les CSS Custom Properties proviennent du thème Sphinx */
border: 2px solid var(--color-foreground-border);
border-bottom-color: 2px solid var(--color-foreground-secondary);
transition: border-color 20ms ease;
}
#search form input[type="text"]:focus {
border-color: var(--color-foreground-primary);
}
Résultat
Le code final de la page de recherche ressemble maintenant à ça :
<!-- docs/_templates/search.html -->
{% extends "page.html" %}
{%- block htmltitle -%}
<title>{{ _("Search") }} - {{ docstitle }}</title>
{%- endblock htmltitle -%}
{% block content %}
<h1>{{ _("Search") }}</h1>
<div id="search"></div>
{% endblock %}
{% block scripts -%}
{{ super() }}
<script src="/_pagefind/pagefind-ui.js" type="text/javascript"></script>
<script>
window.addEventListener('DOMContentLoaded', () => {
new PagefindUI({
element: "#search",
showImages: false,
});
const urlParams = new URLSearchParams(window.location.search);
const query = urlParams.get('q');
const searchInputElement = document.querySelector("#search form[role=search] input");
const inputChangeEvent = new Event('input');
searchInputElement.value = query;
searchInputElement?.dispatchEvent(inputChangeEvent);
searchInputElement?.focus();
});
</script>
{%- endblock scripts %}
{% block extra_styles -%}
{{ super() }}
<style type="text/css">
#search form input[type="text"] {
box-sizing: border-box;
width: 100%;
line-height: 2em;
padding-inline: 0.6em;
font-size: 1.2rem;
border-radius: 0.25rem;
border: 2px solid var(--color-foreground-border);
border-bottom-color: 2px solid var(--color-foreground-secondary);
transition: border-color 20ms ease;
}
#search form input[type="text"]:focus {
border-color: var(--color-foreground-primary);
}
</style>
{%- endblock extra_styles %}
On peut recompiler la doc, recharger http://localhost:1414/search.html
, and voilà !