Hooks
L’unique hook de build que duxt émet, ce qu’il vous remet et pourquoi il n’y en a qu’un.
duxt émet un hook. C’est un hook de build Nuxt, il se déclenche une fois par build et vous remet chaque page du site sous forme d’enregistrements de recherche plats.
import { defineNuxtModule } from '@nuxt/kit';
import type { DuxtSearchRecord } from '@kirchdev/duxt/search-records';
export default defineNuxtModule({
setup(_options, nuxt) {
nuxt.hook('duxt:search:records', async (records) => {
await upload(records);
});
}
});
Un exemple complet — le schéma de la collection, l’upsert, la suppression de ce qui a disparu et la clé qui ne doit jamais atteindre une page — se trouve dans Envoyer vos pages vers Typesense.
duxt:search:records
(records: readonly DuxtSearchRecord[]) => void | Promise<void>
Quand il se déclenche. Après que Content a analysé chaque source, chaque
version, chaque langue et chaque section générée — y compris les dépôts
distants qu’il a téléchargés — et avant que Nitro ne prérende ou n’empaquette
quoi que ce soit. Un listener peut donc encore écrire un fichier dans public/
et le voir arriver dans la sortie.
Ce qu’est un enregistrement. Une page, plus un par titre qu’elle porte.
| Champ | Type | Ce qu’il contient |
|---|---|---|
id | string | L’identité de l’enregistrement, et une clé primaire valide partout |
url | string | Où atterrit le lecteur — segment de langue et ancre compris |
title | string | Le titre que couvre l’enregistrement, ou le titre de la page pour le premier |
content | string | Les mots sous ce titre, sans balisage et avec les espaces normalisés |
source | string | De quelle source il vient, en segments d’URL sans la version |
version | string | L’étiquette de version, là où la source en a — absente sinon |
locale | string | La langue dans laquelle le contenu est écrit |
id est une empreinte hexadécimale plutôt que l’URL, parce que Meilisearch
n’accepte que A-Za-z0-9_- comme identifiant de document. Elle est dérivée de
url, donc identique sur toutes les machines et stable tant que la page reste
où elle est — ce qui permet à un fournisseur de faire un upsert plutôt qu’une
reconstruction.
source est une identité, pas une adresse : / pour la documentation
servie à la racine, /harbour pour un second dépôt, /harbour/api pour une
section générée à l’intérieur. La version en est délibérément absente, afin
qu’une facette bâtie sur source survive à une publication et que version, à
côté, soit ce qui restreint à une seule.
locale est la langue du contenu, pas la langue d’interface dans laquelle on
lit. Un site dont les sources ne sont pas traduites obtient un jeu
d’enregistrements même s’il sert sept langues : le même paragraphe indexé sept
fois, c’est une réponse répétée.
Construisez ou téléversez votre propre index à partir d’elle. Ne réécrivez pas un enregistrement et ne triez pas le tableau sur place : tous les autres listeners reçoivent le même objet, et une modification au passage serait héritée par le suivant sans que rien ne le signale.
Ce qu’il ne porte délibérément pas
- Aucun extrait. Un extrait est une décision sur une liste de résultats, et la liste de résultats est la vôtre.
- Aucun nom de collection. C’est la comptabilité interne de l’index de duxt, et un document indexé là-dessus casse le jour où une source gagne une langue.
- Aucun libellé d’affichage.
label,nameet la mention de version sont de la prose pour un lecteur, traduite par langue. Un enregistrement n’est lu par aucun lecteur.
Ce qui reste hors de l’index
Une page qui a écrit search: false ne produit aucun
enregistrement — le même retrait que respectent déjà la boîte de recherche,
l’outil MCP search_docs et les deux index llms. Il en va de même d’une source
traduite dans une langue que duxt.locales ne sert pas, car l’indexer
reviendrait à proposer au lecteur une URL qui renvoie 404.
Là où un site sert deux régions d’une même langue sur un seul arbre —
pt-PT et pt-BR lisant le même dossier pt/ —, les enregistrements visent la
première. Les deux URL portent les mêmes mots : les indexer toutes les deux
doublerait l’index pour dire deux fois la même chose. Un fournisseur qui veut
envoyer un lecteur dans sa propre région remplace le segment, ce que duxt ne
peut pas faire puisqu’il ignore qui cherche.
Et quand personne n’écoute, duxt ne construit aucune charge : un site sans fournisseur ne paie rien pour l’existence du hook.
Pourquoi il n’y en a qu’un
Une extension duxt distribuable est une couche Nuxt, pas un objet plugin :
export default defineNuxtConfig({
extends: ['@kirchdev/duxt', 'duxt-typesense']
});
Cela porte déjà des composants, des valeurs par défaut d’app.config, des
collections content.config.ts, des routes serveur, des outils MCP et un module
à soi, dans un paquet avec sa propre version. Une API duxt.plugins
réimplémenterait la résolution de couches à l’intérieur d’une couche — et un
registre dans lequel un plugin écrit à l’import se heurte au mur qui a façonné
sectionTypes : content.config.ts et le module de
duxt sont chargés par des chargeurs différents, donc un registre mutable
qu’ils rempliraient tous les deux ferait deux registres. Un hook de build Nuxt
n’a pas ce problème, car seule la moitié module le voit.
La surface reste donc petite à dessein, et chacune des autres entrées demeure la bonne réponse à sa propre question :
| Vous voulez changer | Utilisez |
|---|---|
| Une couleur, une police, un espacement | Un token CSS — Surcharger le thème |
| Ce qu’un composant dessine | Le masquer, ou un slot là où il y en a un |
| Ce que le site dit de lui-même | La clé duxt d’app.config — Configuration |
| Un nouveau type de section générée | sectionTypes — Sources |
| Ce qu’une page dessine autour d’elle | Le frontmatter — Frontmatter |
| Où le texte du site est indexé | Ce hook |
Un nom de hook est une surface publique : le renommer est un changement cassant, et un hook inutilisé ne peut plus jamais être retiré. Un nouveau arrive donc quand il existe un consommateur impossible à écrire sans lui — pas à l’avance, et pas parce qu’une couture a l’air d’être un jour souhaitable.