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.
Lifecycle: welche Stufe wann läuft
Abschnitt betitelt „Lifecycle: welche Stufe wann läuft“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 abgeschauthooks: 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.jsDie ssr-Sammlung nutzt eigene Stufen (api/collections/ssr.yml): get.read → hooks/ssr/get_read.js, post.bind → hooks/ssr/post_bind.js.
Hook-Arten im Starter
Abschnitt betitelt „Hook-Arten im Starter“| 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() |
Rezept: je ein get/post/put/delete-Hook mit Log
Abschnitt betitelt „Rezept: je ein get/post/put/delete-Hook mit Log“-
Lies den Filter-Hook:
filter_public.jshängt denpublishedFilteran — nur wenncontext.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
returnläuft der Hook ins Leere — anonyme Nutzer sehen dann alles. -
Verstehe
clear_cache.js: Er liest Sammlung, Methode und Entry-ID auscontextund ruftutils.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 : nullconst req = context.request()const method = req.methodconst data = Array.isArray(context.data) ? {} : context.data || {}let entryId = data.id || req.param("id") || nullif (!entryId) {// DELETE-Fallback: letzte Pfad-Segmente ≥ 24 Zeichen gelten als IDconst segments = req.path.split("/")const last = segments[segments.length - 1]if (last && last.length >= 24) entryId = last}utils.clearSSRCache(collectionName, entryId, method)})() -
Merke dir die Invalidierungs-Regel aus
lib/utils.js:POSTlöscht nur Listen-Abhängigkeiten (col:*),PUT/DELETElöschen Detail plus Liste (col:idodercol:*). Ohne Argumente folgt ein Komplett-Wipe. -
Logge mit
utils.log()(console.logmitJSON.stringify) — der übliche Weg, um aus Hooks zu protokollieren.
Wichtigste Kontexte
Abschnitt betitelt „Wichtigste Kontexte“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) |
Kontext in get.read: filter, aber kein data
Abschnitt betitelt „Kontext in get.read: filter, aber kein data“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) |
Fenster-Datum: jetzt als BSON-Datum
Abschnitt betitelt „Fenster-Datum: jetzt als BSON-Datum“Vergleiche gegen „jetzt” exakt wie der Starter (api/hooks/config.js:4):
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 }.
Rezept: Filter + 60-s-Cache + Fallback-Header
Abschnitt betitelt „Rezept: Filter + 60-s-Cache + Fallback-Header“cache.* ist projektweit (nicht pro Request) — kombiniere ihn mit filter-Return und Header so:
;(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-Hooks: Rendern und Cachen
Abschnitt betitelt „SSR-Hooks: Rendern und Cachen“GET ssr?url=/de/… → get_read.js → ssrValidatePath(url) → Cache-Treffer? → app.server rendern → in ssr-Sammlung speichern → HTML werfenssrValidatePath()ausconfig.js:/und/de|/enrendern, Sprachpräfix vor dercontent-Abfrage abstreifen, kanonische URL oder-1(404) /0(kein SSR) zurückgeben.get_read.jsspeichertpath,content,validUntil,dependencies(content:<id>,content:*). Parallele Schreiber mit gleichempathverlieren per Unique-Index — der Fehler wird geschluckt.context.is404 = true(gesetzt vonNotFound.svelte) liefert HTTP404ohne zu cachen.post_bind.js: nur?clear=1ruftclearSSRCache()ohne Argumente (Komplett-Wipe) und wirft{ status: 200, removed }. Jeder andere POST wirft500—ssrist eine Dummy-Sammlung.
Fehlerbehandlung
Abschnitt betitelt „Fehlerbehandlung“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.
return hookResponse // { filter: { $and: [...] } }Nur get.read-Hooks wie filter_public.js geben ein Objekt mit filter zurück.
| 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)