O duxt emite [um único hook de build](/reference/hooks), 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.

::callout{type="info" title="O serviço é seu, não 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](/reference/hooks).
::

## 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.

::package-managers{command="add @kirchdev/duxt-typesense"}
::

```ts [nuxt.config.ts]
export 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

::steps

1. **Instale o cliente no seu site.** É uma dependência de build: a
   sincronização roda no Node durante o `nuxt build`, nunca no navegador.

   ::package-managers{command="add -D typesense"}
   ::

2. **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:

   ```ts [server/typesense-schema.ts]
   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](/reference/hooks) — 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ó.

3. **Escreva o módulo que ouve.** Um módulo Nuxt no seu próprio site é toda a
   integração — `defineNuxtModule`, um hook, um import:

   ```ts [modules/typesense.ts]
   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.

4. **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:

   ```ts [modules/typesense.ts]
   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.

   ::callout{type="tip" title="Ou troque um alias"}
   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.
   ::

5. **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:

   ```ts
   // `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(' && ')
   });
   ```

6. **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](/reference/hooks), 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.

```ts [nuxt.config.ts]
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](/reference/hooks).

## 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`, `version` e `locale`
- [ ] 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