Typesense
Consuma o único hook de build do duxt em uma coleção do Typesense — cada fonte, versão e idioma, atualizados por um id estável.
O duxt emite um único hook de build, e ele entrega cada página de cada fonte, versão e idioma como registros planos. Um site que já opera o Typesense transforma esses registros em uma coleção própria — sem rastrear de novo o site renderizado e sem reimplementar a lógica de fontes, versões e locales do duxt.
O duxt não traz SDK do Typesense, credenciais nem configuração de serviço. A camada complementar opcional trata da integração; o serviço e as suas chaves são seus. Sem um fornecedor ou listener personalizado, o duxt não constrói carga alguma.
Usar a camada complementar
Para a integração pronta a usar, instala @kirchdev/duxt-typesense e estende-a depois do duxt. Sincroniza o conjunto completo através de uma coleção temporária e da troca de alias, remove a coleção anterior gerida e liga o diálogo de pesquisa existente com uma chave apenas de pesquisa. Os passos abaixo continuam a ser uma receita para sincronização 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ém a chave de administração exclusivamente na configuração de compilação. searchKey deve permitir apenas documents:search para este alias, e o Typesense deve aceitar pedidos do navegador a partir do teu site.
Passos
- Instale o cliente no seu site. É uma dependência de build: a
sincronização roda no Node durante o
nuxt build, nunca no navegador.
pnpm add -D typesense- Declare a coleção. Cada campo de um registro do duxt corresponde a um campo do Typesense, e os dois campos de procedência são aqueles por que um leitor filtra:
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 o passo 4 entrega este mesmo
objeto ao Typesense para criar a coleção com ele.
O id não está na lista de propósito: o Typesense já trata id como a
chave primária do documento, e o id do duxt é
um resumo hexadecimal da URL — válido como chave, igual
em qualquer máquina e inalterado enquanto a página permanecer onde está. É
isso que permite à sincronização fazer upsert em vez de reconstruir.
version é optional, porque uma fonte com uma única versão não carrega
nenhuma. A versão é retirada de source deliberadamente, para que uma faceta
construída sobre ele sobreviva a um lançamento e version ao lado seja o que
estreita para uma só.
- Escreva o módulo que ouve. Um módulo Nuxt no seu próprio site é toda a
integração —
defineNuxtModule, um hook, um 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;
// Sem chave é um build local, não uma falha: pule em vez de quebrá-lo.
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);
});
}
});
A carga está congelada. Mapeie-a para documentos seus — nunca reescreva um registro nem ordene o array no lugar, porque todos os outros ouvintes recebem os mesmos objetos.
- Sincronize o conjunto inteiro e remova o que se foi. O hook entrega o conjunto completo e atual a cada build, então o próprio conjunto é a instrução — tudo o que a coleção guarda e não está nele foi apagado, movido ou renomeado:
async function sync(client: Client, records: readonly DuxtSearchRecord[]) {
const collection = client.collections(docs.name);
// O Typesense não cria nada implicitamente: importar para uma coleção
// que não existe falha de imediato, então o primeiro build a cria.
if (!(await collection.exists())) await client.collections().create(docs);
await collection.documents().import(
records.map((record) => ({ ...record })),
{ action: 'upsert' }
);
// Tudo o que o build não entregou já não está no 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));
// Apague em lotes: cada id entra na string do filtro, e uma única
// renomeação grande construiria um que o Typesense rejeita.
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}]` });
}
}
A coleção é criada na primeira execução e deixada em paz depois, e os ids obsoletos são apagados em lotes — o Typesense toma o filtro como uma única string, e uma renomeação que mova uma fonte inteira construiria um que ele rejeita.
Importe para uma coleção nomeada pelo build, aponte um alias do Typesense para ela e descarte a anterior. Cada build cria a própria coleção, então nem a checagem de existência nem a exclusão em lotes são necessárias, e os leitores nunca veem um índice escrito pela metade — ao custo de reindexar tudo a cada build em vez de apenas o que mudou.
- Busque do navegador com uma chave só de busca. A chave de administração acima assina escritas e jamais pode chegar a uma página. Emita uma chave com escopo e apenas de busca para o cliente, e filtre pelas facetas que o esquema declarou:
// `locale` é o idioma do CONTEÚDO, não o idioma da interface.
// `version` não existe numa fonte sem versões: a cláusula só entra quando
// há uma; fixa, filtraria 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(' && ')
});
- Confira um build de ponta a ponta. Rode um build com a chave definida e confirme que a coleção guarda um registro de uma página em cada fonte, versão e idioma que o site serve — não apenas no padrão. O primeiro build cria a coleção; cada um depois faz upsert na mesma.
O que os registros já resolvem por você
Quatro propriedades da carga são o que torna correta uma sincronização tão curta, e todas as quatro são o contrato do hook, não o conselho deste guia:
| Propriedade | O que ela dá à sincronização |
|---|---|
id deriva de url | Uma chave primária estável, então upsert basta e reconstruir nunca é preciso |
| A carga é o conjunto completo | Remover é comparar com um build, sem rastreio e sem contabilidade |
| O duxt não dispara com uma análise vazia | Um build que nada analisou nunca pode mandar esvaziar o índice |
| A ordem é determinística | Dois builds diferem exatamente onde o site diferiu |
Uma página que escreveu search: false, e uma tradução em um idioma que o site
não roteia, não produzem registros — então nenhuma delas chega ao Typesense.
Transformar isso em um pacote
O módulo acima tem ~40 linhas e pertence a um site. Assim que um segundo site
precisar dele, a forma de compartilhá-lo é uma camada Nuxt, não um plugin:
uma camada carrega o módulo, seus padrões de app.config e seus componentes em
um pacote versionado, e um consumidor a adiciona ao lado do duxt.
export default defineNuxtConfig({
extends: ['@kirchdev/duxt', 'duxt-typesense']
});
Essa camada é dona das credenciais, da configuração do serviço, da política de sincronização e da interface de busca no navegador. O duxt não ganha nenhuma delas — que é toda a razão do formato do hook.
Checklist
- O cliente é dependência do seu site, não da camada
- A chave de administração vive no ambiente de build e nunca chega a uma página
- A coleção é criada no primeiro build e faceta
source,versionelocale - Registros não mais entregues são removidos — por comparação em lotes ou troca de alias
- Um build sem a chave pula a sincronização em vez de falhar
- Cada fonte, versão e idioma está representada na coleção