Zum Inhalt springen

Hooks schreiben

Hooks sind JavaScript-Dateien, die der tibi-server vor oder nach einem Zugriff ausführt. Du filterst damit öffentliche Lesezugriffe, räumst den SSR-Cache weg oder renderst Seiten. Suchbegriffe: Hook schreiben, Lifecycle, Cache invalidieren.

Die Stufen stehen in tibi-types/schemas/config/hooks.schema.json. Jeder Hook ist { type: "javascript", file: "hooks/…js", timeout?: number } — nur Datei-Hooks, kein Inline-Code.

Methode Stufen (in dieser Reihenfolge)
get handle, read, return, file
post bind, validate, handle, create, return, bulkCreate, bulkReturn
put bind, validate, update, return, bulkUpdate, bulkReturn
delete delete, return, bulkDelete, bulkReturn
audit return

So bindet der Starter sie ein (api/collections/content.yml):

content.yml — Hooks aus dem Starter
# content.yml — Hooks aus dem Starter abgeschaut
hooks:
get:
read:
type: javascript
file: hooks/filter_public.js
post:
return:
type: javascript
file: hooks/clear_cache.js
put:
return:
type: javascript
file: hooks/clear_cache.js
delete:
return:
type: javascript
file: hooks/clear_cache.js

Die ssr-Sammlung nutzt eigene Stufen (api/collections/ssr.yml): get.read → hooks/ssr/get_read.js, post.bind → hooks/ssr/post_bind.js.

Datei Stufe Aufgabe
hooks/filter_public.js get.read Anonyme sehen nur active:true plus Veröffentlichungsfenster
hooks/clear_cache.js post/put/delete.return SSR-Cache nach jeder Änderung invalidieren
hooks/ssr/get_read.js get.read der ssr-Sammlung Seite rendern oder Cache ausliefern
hooks/ssr/post_bind.js post.bind der ssr-Sammlung ?clear=1 räumt den Cache, sonst Fehler 500 (Dummy-Sammlung)
hooks/config.js kein Hook, require-Modul publishedFilter, ssrValidatePath(), apiSsrBaseURL
hooks/lib/utils.js kein Hook, require-Modul log(), clearSSRCache()
  1. Lies den Filter-Hook: filter_public.js hängt den publishedFilter an — nur wenn context.user.auth() leer ist (anonym).

    hooks/filter_public.js (gekürzt)
    // hooks/filter_public.js (gekürzt)
    const { publishedFilter } = require("./config")
    ;(function () {
    if (!context.user.auth()) {
    /** @type {HookResponse} */
    const hookResponse = {}
    if (context.filter && Object.keys(context.filter).length > 0) {
    hookResponse.filter = { $and: [context.filter, publishedFilter] }
    } else {
    hookResponse.filter = publishedFilter
    }
    return hookResponse
    }
    })()

    Ohne return läuft der Hook ins Leere — anonyme Nutzer sehen dann alles.

  2. Verstehe clear_cache.js: Er liest Sammlung, Methode und Entry-ID aus context und ruft utils.clearSSRCache() auf.

    hooks/clear_cache.js (gekürzt)
    // hooks/clear_cache.js (gekürzt)
    var utils = require("./lib/utils")
    ;(function () {
    const col = context.collection()
    const collectionName = col && col.name ? col.name : null
    const req = context.request()
    const method = req.method
    const data = Array.isArray(context.data) ? {} : context.data || {}
    let entryId = data.id || req.param("id") || null
    if (!entryId) {
    // DELETE-Fallback: letzte Pfad-Segmente ≥ 24 Zeichen gelten als ID
    const segments = req.path.split("/")
    const last = segments[segments.length - 1]
    if (last && last.length >= 24) entryId = last
    }
    utils.clearSSRCache(collectionName, entryId, method)
    })()
  3. Merke dir die Invalidierungs-Regel aus lib/utils.js: POST löscht nur Listen-Abhängigkeiten (col:*), PUT/DELETE löschen Detail plus Liste (col:id oder col:*). Ohne Argumente folgt ein Komplett-Wipe.

  4. Logge mit utils.log() (console.log mit JSON.stringify) — der übliche Weg, um aus Hooks zu protokollieren.

Vollständige Liste: Hook-Kontexte. Hier die fünf, die du täglich brauchst:

Kontext Zugriff im Starter Wofür
context.user.auth() filter_public.js Anmeldung prüfen (null = anonym)
context.request() clear_cache.js, get_read.js Methode, Pfad, Query, Header lesen
context.response.header() get_read.js, utils.js X-SSR-Cache, X-SSR-Cleared, Cache-Control setzen
context.db config.js, get_read.js, utils.js find, create, delete, deleteMany
context.tpl / context.http per Hook-Kontexte Templates rendern, externe Aufrufe

Weitere im Starter genutzte Zugriffe:

Zugriff Wo
context.filter, context.data Anfrage-Filter bzw. Dokument
context.collection() Sammlung des aktuellen Zugriffs
context.config.server().api.port Server-Port aus der Config
context.api().namespace Aktueller Namespace
context.fs.readFile() Datei lesen
context.debug.sentryTraceId() Trace-ID für Fehlerberichte
context.ssrCache / context.ssrDeps SSR-Zwischenspeicher (nur SSR)

In get.read siehst du die Anfrage, nicht die Dokumente:

Zugriff Inhalt in get.read Beleg
context.filter Eingehender Mongo-Filter (Objekt oder leer) — genau das Muster aus filter_public.js (context.filter && Object.keys(...)) Starter filter_public.js, Controller collection_get.go (hookData: filter, selector, offset, limit, sort, q, qName)
context.selector, context.offset, context.limit, context.sort, context.q Paging, Sortierung, Suche der Anfrage Controller collection_get.go
context.data Gibt es in get.read nicht — erst ab post/put/delete (dort das Dokument) Controller (kein data-Key im read-hookData)
response.header() Erreicht den Client auch bei normalem Listen-GET (Server schreibt per Header, kein Body-Ersatz nötig) context_response.go (c.Header)

Vergleiche gegen „jetzt” exakt wie der Starter (api/hooks/config.js:4):

Jetzt als Datum, Fenster inklusiv
const now = { $date: new Date().toISOString() }
const publishedFilter = {
active: true,
$or: [
{ publication: { $exists: false } },
{ publication: null },
{
$and: [
{ $or: [{ "publication.from": { $exists: false } }, { "publication.from": null }, { "publication.from": { $lte: now } }] },
{ $or: [{ "publication.to": { $exists: false } }, { "publication.to": null }, { "publication.to": { $gte: now } }] },
],
},
],
}

Regeln: new Date() gibt es in Hooks (goja), $lte/$gte sind inklusiv, fehlende Felder brauchen die $exists:false/null-Zweige — sonst verschwinden undatierte Dokumente. Serialisierung: { $date: ISO-String }.

cache.* ist projektweit (nicht pro Request) — kombiniere ihn mit filter-Return und Header so:

Fensterfilter mit Cache und Fallback-Header
;(function () {
if (context.user.auth()) return // nur anonym filtern
const key = "content:published" // Key-Konvention: Sammlung:Zweck
const hit = context.cache.get(key)
if (hit) {
if (context.filter && Object.keys(context.filter).length > 0) {
return { filter: { $and: [context.filter, hit] } }
}
return { filter: hit }
}
context.response.header("X-Publication-Fallback", "uncached")
const filter = { active: true } // Kurzform; Fenster siehe oben
context.cache.set(key, filter, 60000) // 60 s = 60000 ms
if (context.filter && Object.keys(context.filter).length > 0) {
return { filter: { $and: [context.filter, filter] } }
}
return { filter: filter }
})()

Key-Regel: Namen mit Sammlung:Zweck vergeben — der Cache teilt sich alle Hooks des Projekts.

SSR-Ablauf im Überblick
GET ssr?url=/de/… → get_read.js → ssrValidatePath(url) → Cache-Treffer? → app.server rendern → in ssr-Sammlung speichern → HTML werfen
  • ssrValidatePath() aus config.js: / und /de|/en rendern, Sprachpräfix vor der content-Abfrage abstreifen, kanonische URL oder -1 (404) / 0 (kein SSR) zurückgeben.
  • get_read.js speichert path, content, validUntil, dependencies (content:<id>, content:*). Parallele Schreiber mit gleichem path verlieren per Unique-Index — der Fehler wird geschluckt.
  • context.is404 = true (gesetzt von NotFound.svelte) liefert HTTP 404 ohne zu cachen.
  • post_bind.js: nur ?clear=1 ruft clearSSRCache() ohne Argumente (Komplett-Wipe) und wirft { status: 200, removed }. Jeder andere POST wirft 500 — ssr ist eine Dummy-Sammlung.
Antwort direkt werfen
throw { status: 403, msg: "invalid auth" }
throw { status: 200, log: false, html: tpl }

So antwortet get_read.js: HTML oder Fehler direkt werfen, kein return.

Der Throw-Umschlag (internal/models/hook.go): status (Zahl, Vorgabe 500), msg (Text, wird an den JS-Fehler angehängt), json (Antwortkörper — ohne json-Feld antwortet der Server das geworfene Objekt selbst), html/string/bytes/file (direkte Antwort statt JSON). Achtung: Für den Fehlertext zählt nur msg — ein Feld message landet zwar im Body (wenn du kein json setzt), ändert aber nicht den Fehlertext. Schreibe deshalb msg.

Fehlerbild Ursache Lösung
Öffentliche Liste zeigt Entwürfe filter_public.js fehlt oder gibt nichts zurück get.read-Hook in der YAML prüfen
SSR zeigt alte Seite nach PUT clear_cache.js fehlt an put.return Hook-Zeile aus content.yml übernehmen
ssr-POST liefert 500 Normal — Dummy-Sammlung ohne ?clear=1 ?clear=1 anhängen oder lassen
DELETE räumt nichts weg ID weder in data.id noch im Pfad Pfad-Segment ≥ 24 Zeichen prüfen

Weiter: Lesen und suchen, Rechte und Tokens, Hook-Kontexte.

Verifiziert am e3f92eb 2026-10-17 (tibi-svelte-starter), am 68b4278 2026-10-17 (tibi-server), am 4c55f82 2026-10-17 (tibi-types)