Typesense
Consommez l’unique hook de build de duxt dans une collection Typesense — chaque source, version et langue, mise à jour par un id stable.
duxt émet un seul hook de build, et il remet chaque page de chaque source, version et langue sous forme d’enregistrements plats. Un site qui exploite déjà Typesense en fait une collection à lui — sans re-explorer le site rendu et sans réimplémenter la logique de sources, de versions et de locales de duxt.
duxt n’embarque aucun SDK Typesense, aucune information d’identification ni configuration de service. La couche complémentaire facultative gère l’intégration ; le service et ses clés restent les vôtres. Sans fournisseur ni écouteur personnalisé, duxt ne construit aucune charge utile.
Utiliser la couche complémentaire
Pour l’intégration prête à l’emploi, installe @kirchdev/duxt-typesense et étends-la après duxt. Elle synchronise l’ensemble complet via une collection temporaire et un changement d’alias, supprime la collection précédente gérée et relie le dialogue de recherche existant avec une clé de recherche seule. Les étapes suivantes restent une recette pour une synchronisation personnalisée.
pnpm add @kirchdev/duxt-typesenseexport default defineNuxtConfig({
extends: ['@kirchdev/duxt', '@kirchdev/duxt-typesense'],
duxtTypesense: {
endpoint: process.env.TYPESENSE_ENDPOINT,
collection: 'docs',
adminKey: process.env.TYPESENSE_ADMIN_KEY,
searchKey: process.env.TYPESENSE_SEARCH_KEY
}
})
Garde la clé d’administration exclusivement dans la configuration de build. searchKey doit autoriser uniquement documents:search pour cet alias, et Typesense doit accepter les requêtes du navigateur depuis ton site.
Étapes
- Installez le client dans votre site. C’est une dépendance de build : la
synchronisation tourne dans Node pendant
nuxt build, jamais dans le navigateur.
pnpm add -D typesense- Déclarez la collection. Chaque champ d’un enregistrement duxt correspond à un champ Typesense, et les deux champs de provenance sont ceux sur lesquels un lecteur filtre :
import type { CollectionCreateSchema } from 'typesense';
export const docs: CollectionCreateSchema = {
name: 'docs',
fields: [
{ name: 'url', type: 'string' },
{ name: 'title', type: 'string' },
{ name: 'content', type: 'string' },
{ name: 'source', type: 'string', facet: true },
{ name: 'version', type: 'string', facet: true, optional: true },
{ name: 'locale', type: 'string', facet: true }
]
};
Typé en CollectionCreateSchema, car c’est cet objet même que l’étape 4
remet à Typesense pour créer la collection.
id n’est pas dans la liste, à dessein : Typesense traite déjà id comme la
clé primaire du document, et l’id de duxt est
une empreinte hexadécimale de l’URL — valide comme clé,
identique sur toutes les machines et inchangée tant que la page reste où elle
est. C’est ce qui permet à la synchronisation de faire un upsert plutôt
qu’une reconstruction.
version est optional, car une source à une seule version n’en porte
aucune. La version est délibérément retirée de source, pour qu’une facette
bâtie dessus survive à une publication et que version à côté soit ce qui
restreint à une seule.
- Écrivez le module qui écoute. Un module Nuxt dans votre propre site est
toute l’intégration —
defineNuxtModule, un hook, un import :
import { defineNuxtModule } from '@nuxt/kit';
import { Client } from 'typesense';
import type { DuxtSearchRecord } from '@kirchdev/duxt/search-records';
import { docs } from '../server/typesense-schema';
export default defineNuxtModule({
meta: { name: 'typesense-sync' },
setup(_options, nuxt) {
const key = process.env.TYPESENSE_ADMIN_KEY;
// Pas de clé, c’est un build local, pas un échec : on saute plutôt que
// de le casser.
if (!key) return;
nuxt.hook('duxt:search:records', async (records) => {
const client = new Client({
nodes: [{ url: process.env.TYPESENSE_URL! }],
apiKey: key
});
await sync(client, records);
});
}
});
La charge utile est gelée. Transformez-la en documents à vous — ne réécrivez jamais un enregistrement et ne triez pas le tableau sur place, car tous les autres auditeurs reçoivent les mêmes objets.
- Synchronisez l’ensemble complet, et retirez ce qui a disparu. Le hook remet l’ensemble complet et courant à chaque build : l’ensemble est donc lui-même l’instruction — tout ce que la collection contient et qui n’y figure pas a été supprimé, déplacé ou renommé :
async function sync(client: Client, records: readonly DuxtSearchRecord[]) {
const collection = client.collections(docs.name);
// Typesense ne crée rien implicitement : importer dans une collection
// qui n’existe pas échoue net, c’est donc le premier build qui la crée.
if (!(await collection.exists())) await client.collections().create(docs);
await collection.documents().import(
records.map((record) => ({ ...record })),
{ action: 'upsert' }
);
// Tout ce que le build n’a pas remis n’est plus sur le site.
const kept = new Set(records.map((record) => record.id));
const live = await collection.documents().export();
const stale = live
.split('\n')
.filter(Boolean)
.map((line) => JSON.parse(line).id as string)
.filter((id) => !kept.has(id));
// Supprimez par lots : chaque id entre dans la chaîne du filtre, et un
// seul gros renommage en bâtirait un que Typesense refuse.
for (let index = 0; index < stale.length; index += 100) {
const batch = stale.slice(index, index + 100).join(',');
await collection.documents().delete({ filter_by: `id:[${batch}]` });
}
}
La collection est créée au premier passage puis laissée telle quelle, et les ids périmés sont supprimés par lots — Typesense prend le filtre comme une seule chaîne, et un renommage déplaçant une source entière en bâtirait autrement un qu’il refuse.
Importez dans une collection nommée d’après le build, pointez un alias Typesense dessus, puis jetez la précédente. Chaque build crée sa propre collection : ni la vérification d’existence ni la suppression par lots ne sont nécessaires, et les lecteurs ne voient jamais un index à moitié écrit — au prix d’une réindexation complète à chaque build plutôt que du seul delta.
- Cherchez depuis le navigateur avec une clé de recherche seule. La clé d’administration ci-dessus signe les écritures et ne doit jamais atteindre une page. Émettez une clé restreinte, en lecture de recherche seule pour le client, et filtrez sur les facettes que le schéma a déclarées :
// `locale` est la langue du CONTENU, pas la langue de l’interface.
// `version` est absente d’une source sans versions : la clause ne s’ajoute
// que s’il y en a une ; figée, elle filtrerait sur `undefined`.
const filters = [`locale:${locale}`];
if (version) filters.push(`version:${version}`);
const results = await client.collections('docs').documents().search({
q: query,
query_by: 'title,content',
filter_by: filters.join(' && ')
});
- Vérifiez un build de bout en bout. Lancez un build avec la clé, et confirmez que la collection contient un enregistrement pour une page dans chaque source, version et langue que le site sert — pas seulement la langue par défaut. Le premier build crée la collection ; chacun d’après fait un upsert dans la même.
Ce que les enregistrements règlent déjà pour vous
Quatre propriétés de la charge utile rendent une synchronisation aussi courte correcte, et toutes les quatre relèvent du contrat du hook, non du conseil de ce guide :
| Propriété | Ce qu’elle apporte à la synchronisation |
|---|---|
id dérive de url | Une clé primaire stable : upsert suffit, une reconstruction n’est jamais nécessaire |
| La charge utile est l’ensemble complet | Supprimer, c’est comparer à un build, sans exploration ni comptabilité |
| duxt ne se déclenche pas sur une analyse vide | Un build qui n’a rien analysé ne peut jamais vous dire de vider l’index |
| L’ordre est déterministe | Deux builds diffèrent exactement là où le site a différé |
Une page qui a écrit search: false, et une traduction dans une langue que le
site ne route pas, ne produisent aucun enregistrement — donc ni l’une ni l’autre
n’atteint Typesense.
En faire un paquet
Le module ci-dessus fait ~40 lignes et appartient à un site. Dès qu’un deuxième
site en a besoin, la façon de le partager est une couche Nuxt, pas un
plugin : une couche porte le module, ses valeurs app.config par défaut et ses
composants dans un paquet versionné, et un consommateur l’ajoute à côté de duxt.
export default defineNuxtConfig({
extends: ['@kirchdev/duxt', 'duxt-typesense']
});
Cette couche possède les informations d’identification, la configuration du service, la politique de synchronisation et l’interface de recherche côté navigateur. duxt n’en gagne aucune — c’est toute la raison de la forme du hook.
Liste de contrôle
- Le client est une dépendance de votre site, pas de la couche
- La clé d’administration vit dans l’environnement de build et n’atteint jamais une page
- La collection est créée au premier build et facette
source,versionetlocale - Les enregistrements qui ne sont plus remis sont retirés — par comparaison par lots ou par échange d’alias
- Un build sans la clé saute la synchronisation au lieu d’échouer
- Chaque source, version et langue est représentée dans la collection