Zum Inhalt springen
duxt

Hooks

Der eine Build-Hook, den duxt aussendet, was er übergibt und warum es nur einen gibt.

duxt sendet einen Hook aus. Er ist ein Nuxt-Build-Hook, feuert einmal pro Build und übergibt jede Seite der Website als flache Suchdatensätze.

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);
    });
  }
});

Ein durchgearbeitetes Beispiel dafür — das Collection-Schema, das Upsert, das Entfernen dessen, was fort ist, und der Schlüssel, der niemals eine Seite erreichen darf — steht in Deine Seiten an Typesense senden.

duxt:search:records

(records: readonly DuxtSearchRecord[]) => void | Promise<void>

Wann er feuert. Nachdem Content jede Quelle, jede Version, jede Sprache und jede generierte Sektion geparst hat — die heruntergeladenen entfernten Repositories eingeschlossen — und bevor Nitro irgendetwas prerendert oder bündelt. Ein Listener darf also noch eine Datei nach public/ schreiben, und sie landet in der Ausgabe.

Was ein Datensatz ist. Eine Seite, plus einer je Überschrift darauf.

FeldTypWas es enthält
idstringDie Identität des Datensatzes, und ein gültiger Primärschlüssel überall
urlstringWo ein Leser landet — inklusive Sprachsegment und Anker
titlestringDie Überschrift dieses Datensatzes, beim ersten der Seitentitel
contentstringDie Wörter darunter, ohne Markup und mit normalisierten Leerzeichen
sourcestringAus welcher Quelle er stammt, als URL-Segmente ohne die Version
versionstringDas Versionslabel, wo die Quelle Versionen hat — sonst nicht vorhanden
localestringDie Sprache, in der der Inhalt verfasst ist

id ist ein Hex-Digest statt der URL, weil Meilisearch als Dokument-ID nur A-Za-z0-9_- akzeptiert. Sie wird aus url abgeleitet, ist also auf jeder Maschine dieselbe und bleibt gleich, solange die Seite bleibt, wo sie ist — genau das erlaubt einem Provider ein Upsert statt eines Neuaufbaus.

source ist eine Identität, keine Adresse: / für die Dokumentation an der Wurzel, /harbour für ein zweites Repository, /harbour/api für eine generierte Sektion darin. Die Version gehört bewusst nicht dazu, damit eine Facette auf source ein Release überlebt und version daneben das ist, was auf eine Version einschränkt.

locale ist die Sprache des Inhalts, nicht die Oberflächensprache, in der gerade gelesen wird. Eine Website mit unübersetzten Quellen bekommt einen Satz Datensätze, auch wenn sie sieben Sprachen ausliefert — derselbe Absatz siebenmal indiziert ist eine wiederholte Antwort.

Was er bewusst nicht trägt

  • Keinen Textausschnitt. Ein Snippet ist eine Entscheidung über eine Trefferliste, und die Trefferliste gehört dir.
  • Keinen Collection-Namen. Das ist duxts eigene Index-Buchführung, und ein Dokument mit diesem Schlüssel bricht an dem Tag, an dem eine Quelle eine Sprache bekommt.
  • Kein Anzeigelabel. label, name und die Versionsbeschriftung sind Prosa für Leser, je Sprache übersetzt. Einen Datensatz liest kein Leser.

Was aus dem Index herausfällt

Eine Seite mit search: false erzeugt überhaupt keine Datensätze — dieselbe Abmeldung, die der Suchdialog, das MCP-Werkzeug search_docs und beide llms-Indizes bereits beachten. Ebenso eine übersetzte Quelle in einer Sprache, die duxt.locales nicht ausliefert, denn sie zu indizieren hieße, einem Leser eine URL anzubieten, die 404 liefert.

Wo eine Website zwei Regionen einer Sprache über einen Baum ausliefert — pt-PT und pt-BR lesen beide denselben pt/-Ordner —, adressieren die Datensätze die erste. Beide URLs tragen dieselben Wörter; beide zu indizieren würde den Index verdoppeln, um zweimal dasselbe zu sagen. Ein Provider, der einen Leser in dessen Region schicken will, tauscht das Segment aus — duxt kann das nicht, weil es nicht weiß, wer gerade sucht.

Und wenn niemand zuhört, baut duxt gar keine Nutzlast — eine Website ohne Provider zahlt also nichts dafür, dass es den Hook gibt.

Warum es nur einen gibt

Eine verteilbare duxt-Erweiterung ist ein Nuxt-Layer, kein Plugin-Objekt:

export default defineNuxtConfig({
  extends: ['@kirchdev/duxt', 'duxt-typesense']
});

Das trägt bereits Komponenten, app.config-Vorgaben, content.config.ts- Collections, Server-Routen, MCP-Werkzeuge und ein eigenes Modul — in einem Paket mit einer eigenen Version. Eine duxt.plugins-API würde die Layer-Auflösung innerhalb eines Layers nachbauen — und eine Registry, in die ein Plugin beim Import schreibt, läuft in genau die Wand, die sectionTypes geformt hat: content.config.ts und duxts Modul werden von verschiedenen Loadern geladen, eine veränderliche Registry wäre also zwei Registries. Ein Nuxt-Build-Hook hat dieses Problem nicht, weil ihn nur die Modulhälfte je sieht.

Die Oberfläche bleibt also mit Absicht klein, und jeder andere Weg hinein ist weiterhin die richtige Antwort auf seine eigene Frage:

Du willst ändernNimm
Eine Farbe, eine Schrift, einen AbstandEin CSS-Token — Theme überschreiben
Was eine Komponente zeichnetSie überschatten, oder einen Slot, wo es einen gibt
Was die Website über sich selbst sagtDen duxt-Schlüssel in app.config — Konfiguration
Eine neue Art generierter SektionsectionTypes — Quellen
Was eine Seite um sich herum zeichnetFrontmatter — Frontmatter
Wo der Text der Website indiziert wirdDiesen Hook
War diese Seite hilfreich?