Typesense
Consume el único hook de compilación de duxt en una colección de Typesense — cada fuente, versión e idioma, con un id estable.
duxt emite un único hook de compilación, y entrega cada página de cada fuente, versión e idioma como registros planos. Un sitio que ya opera Typesense convierte esos registros en una colección propia — sin volver a rastrear el sitio renderizado y sin reimplementar la lógica de fuentes, versiones y locales de duxt.
duxt no incluye SDK de Typesense, credenciales ni configuración del servicio. La capa complementaria opcional se encarga de la integración; el servicio y sus claves son tuyos. Sin un proveedor o listener personalizado, duxt no construye ninguna carga útil.
Usar la capa complementaria
Para la integración empaquetada, instala @kirchdev/duxt-typesense y extiéndela después de duxt. Sincroniza el conjunto completo mediante una colección temporal y un cambio de alias, elimina la colección anterior administrada y conecta el diálogo de búsqueda existente con una clave de solo búsqueda. Los pasos siguientes siguen siendo una receta para una sincronización personalizada.
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
}
})
Mantén la clave de administración exclusivamente en la configuración de compilación. searchKey debe permitir únicamente documents:search para este alias, y Typesense debe admitir solicitudes del navegador desde tu sitio.
Pasos
- Instala el cliente en tu sitio. Es una dependencia de compilación: la
sincronización corre en Node durante
nuxt build, nunca en el navegador.
pnpm add -D typesense- Declara la colección. Cada campo de un registro de duxt se asigna a un campo de Typesense, y los dos campos de procedencia son por los que filtra un lector:
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 }
]
};
Tipado como CollectionCreateSchema, porque el paso 4 entrega este mismo
objeto a Typesense para crear la colección con él.
id no está en la lista a propósito: Typesense ya trata id como la clave
primaria del documento, y el id de duxt es
un resumen hexadecimal de la URL — válido como clave,
idéntico en cualquier máquina y sin cambios mientras la página siga donde
está. Eso es lo que permite que la sincronización haga upsert en vez de
reconstruir.
version es optional, porque una fuente con una sola versión no lleva
ninguna. A source se le quita la versión deliberadamente, para que una
faceta construida sobre él sobreviva a una publicación y version a su lado
sea lo que reduce a una.
- Escribe el módulo que escucha. Un módulo de Nuxt en tu propio sitio es
toda la integración —
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;
// Sin clave es una compilación local, no un fallo: sáltala en vez de
// romperla.
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 carga útil está congelada. Asígnala a documentos propios — nunca reescribas un registro ni ordenes el array en el sitio, porque a cada otro oyente se le entregan los mismos objetos.
- Sincroniza el conjunto completo y elimina lo que ya no está. El hook entrega el conjunto completo y actual en cada compilación, así que el conjunto mismo es la instrucción — todo lo que la colección guarde y no esté en él se ha borrado, movido o renombrado:
async function sync(client: Client, records: readonly DuxtSearchRecord[]) {
const collection = client.collections(docs.name);
// Typesense no crea nada implícitamente: importar a una colección que
// no existe falla sin más, así que la primera compilación la crea.
if (!(await collection.exists())) await client.collections().create(docs);
await collection.documents().import(
records.map((record) => ({ ...record })),
{ action: 'upsert' }
);
// Todo lo que la compilación no entregó ya no está en el sitio.
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));
// Borra por lotes: cada id entra en la cadena del filtro, y un solo
// renombrado grande construiría uno que Typesense rechaza.
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 colección se crea en la primera ejecución y se deja en paz después, y los ids obsoletos se borran por lotes — Typesense toma el filtro como una sola cadena, así que un renombrado que mueva una fuente entera construiría uno que rechaza.
Importa a una colección nombrada por la compilación, apunta un alias de Typesense hacia ella y descarta la anterior. Cada compilación crea su propia colección, así que no hacen falta ni la comprobación de existencia ni el borrado por lotes, y los lectores nunca ven un índice a medio escribir — a cambio de reindexar todo en cada compilación en lugar de solo lo que se movió.
- Busca desde el navegador con una clave de solo búsqueda. La clave de administración de arriba firma escrituras y jamás debe llegar a una página. Emite una clave con alcance y solo de búsqueda para el cliente, y filtra por las facetas que declaró el esquema:
// `locale` es el idioma del CONTENIDO, no el idioma de la interfaz.
// `version` no existe en una fuente sin versiones: la cláusula solo se
// añade cuando la hay; fija, filtraría por `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(' && ')
});
- Comprueba una compilación de principio a fin. Ejecuta una compilación con la clave puesta y confirma que la colección guarda un registro de una página en cada fuente, versión e idioma que el sitio sirve — no solo el predeterminado. La primera compilación crea la colección; cada una posterior hace upsert en la misma.
Lo que los registros ya resuelven por ti
Cuatro propiedades de la carga útil son lo que hace correcta una sincronización tan corta, y las cuatro son el contrato del hook y no el consejo de esta guía:
| Propiedad | Qué le da a la sincronización |
|---|---|
id se deriva de url | Una clave primaria estable, así que upsert basta y nunca hace falta reconstruir |
| La carga útil es el conjunto completo | Eliminar es comparar contra una compilación, sin rastreo ni contabilidad |
| duxt no dispara con un análisis vacío | Una compilación que no analizó nada nunca puede decirte que vacíes el índice |
| El orden es determinista | Dos compilaciones difieren exactamente donde difirió el sitio |
Una página que escribió search: false, y una traducción en un idioma que el
sitio no enruta, no producen registros — así que ninguna llega a Typesense.
Convertirlo en un paquete
El módulo de arriba tiene ~40 líneas y pertenece a un solo sitio. En cuanto un
segundo sitio lo necesite, la forma de compartirlo es una capa de Nuxt, no
un plugin: una capa lleva el módulo, sus valores por defecto de app.config y
sus componentes en un paquete versionado, y un consumidor la añade junto a duxt.
export default defineNuxtConfig({
extends: ['@kirchdev/duxt', 'duxt-typesense']
});
Esa capa es dueña de las credenciales, la configuración del servicio, la política de sincronización y la interfaz de búsqueda en el navegador. duxt no gana ninguna de ellas — que es la razón entera de la forma del hook.
Lista de comprobación
- El cliente es una dependencia de tu sitio, no de la capa
- La clave de administración vive en el entorno de compilación y nunca llega a una página
- La colección se crea en la primera compilación y facetea
source,versionylocale - Los registros que ya no se entregan se eliminan — por comparación por lotes o por intercambio de alias
- Una compilación sin la clave se salta la sincronización en vez de fallar
- Cada fuente, versión e idioma está representada en la colección