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.

```ts [modules/my-search.ts]
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](/add-ons/typesense).

## `duxt:search:records`

```ts
(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.

::callout{type="warning" title="La charge est gelé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`, `name` et 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`](/reference/frontmatter) 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 :

```ts [nuxt.config.ts]
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`](/reference/configuration) : `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](/guides/override-the-theme)    |
| 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](/reference/configuration) |
| Un nouveau type de section générée        | `sectionTypes` — [Sources](/reference/sources)                      |
| Ce qu’une page dessine autour d’elle      | Le frontmatter — [Frontmatter](/reference/frontmatter)              |
| Où le texte du site est indexé            | Ce hook                                                            |

::callout{type="tip" title="Demander un second 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.
::