Files
my-notes-viewer/.agents/skills/tibi-hook-authoring/SKILL.md
T
apairon 4020ad62c5 feat: enhance medialib image handling and add asset URL resolution
- Implemented `resolveApiAssetUrl` function to normalize asset URLs based on API base.
- Updated `MedialibImage` component to utilize new asset URL resolution and added support for alt text and class properties.
- Enhanced image loading behavior with improved width measurement and focal point handling.
- Added placeholder image handling and improved accessibility with alt text.
- Introduced new test script for auditing broken links in skill documentation.
- Expanded seeded test content to include medialib entries and updated related tests for pagebuilder previews.
- Improved global setup and teardown logging for clarity on seeded content management.
2026-05-17 00:52:41 +00:00

324 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: tibi-hook-authoring
description: Write and debug server-side hooks for tibi-server (goja Go JS runtime). Covers IIFE structure, HookResponse/HookException types, context.filter Go-object quirk, single-item vs list retrieval, and MongoDB filter patterns. Use when creating or modifying files in api/hooks/.
---
# tibi-hook-authoring
Use this skill for **current tibi-server hook architecture**, not just simple CRUD filters. A real website project on this starter typically needs hooks for public filtering, SSR invalidation, action endpoints, validation, and editor safety.
## Source of truth
Use these sources when implementing or reviewing hooks:
- `tibi-server/docs/06-hooks.md`
- `tibi-server/docs/19-actions.md`
- `tibi-server/internal/models/eval_context.go`
- `tibi-server/internal/hook/context_*.go`
- `api/hooks/config.js`
- `api/hooks/filter_public.js`
- `api/hooks/clear_cache.js`
- `.agents/skills/tibi-ssr-caching/SKILL.md`
When hook examples and prose ever disagree about how helpers are exposed, trust the current implementation in `eval_context.go` plus the `context_*.go` registrations.
## First routing decision: collection hook or action
Before writing hook code, decide whether the workflow belongs to CRUD data or to an endpoint.
Use collection hooks when:
- the workflow is about reads or writes on a real collection
- publication filtering belongs to collection reads
- cache invalidation belongs to collection mutations
Use actions when:
- the workflow is endpoint-style business logic
- there is no durable CRUD collection behind it
- validation and side effects matter more than storage
Typical action use cases:
- contact forms
- newsletter signups
- quote or order requests
- webhook receivers
- helper endpoints
Do not implement fake empty collections just to gain a hook surface.
## Action hook context.data quirk
In **action hooks**, the body is NOT parsed before the `bind` step runs. `context.data` is only available starting from the `validate` step. The order is:
1. `bind` — runs, but `context.data` is undefined (body not yet parsed)
2. Body parsing — server parses JSON body into `context.data`
3. `validate``context.data` is now available
4. `handle``context.data` available, this is where main logic goes
5. `return` — final response
**Never access `context.data` in a bind hook** — it will be empty. Use `handle` for data access.
For action config, always use the `handle` step:
```yaml
hooks:
post:
handle:
type: javascript
file: hooks/actions/contact/handle.js
```
Also note: `context.data` can be an array or object depending on the request. Always guard:
```js
const data = (Array.isArray(context.data) ? {} : context.data) || {}
```
## Hook file structure
Wrap every hook in an IIFE:
```js
;(function () {
/** @type {HookResponse} */
const response = { status: 200 }
// ... hook logic ...
return response
})()
```
Always return a `HookResponse` or throw a `HookException`.
For many hooks, throwing is the normal control flow, especially in SSR hooks where HTML/status are returned via a thrown object.
## Type safety
- Use inline JSDoc type casting: `/** @type {TypeName} */ (value)`.
- Reference typed collection entries from `types/global.d.ts`.
- Avoid `@ts-ignore`; use proper casting instead.
- Use `const` and `let` instead of `var` — the goja runtime supports them.
## Hook API exposure model
In hook JavaScript, the server injects one top-level object: `context`.
That means runtime helpers and registered packages are accessed through `context`, for example:
- `context.request()`
- `context.db.find()`
- `context.http.fetch()`
- `context.smtp.sendMail()`
- `context.debug.dump()`
- `context.exec.command()`
Do not silently rewrite these to bare `request()`, `db.find()`, or `http.fetch()` when editing docs or examples for hook code.
## context.filter — Go object quirk
`context.filter` is a Go object, not a regular JS object. Even when empty, it is **truthy**.
Always check with `Object.keys()`:
```js
const requestedFilter =
context.filter &&
typeof context.filter === "object" &&
!Array.isArray(context.filter) &&
Object.keys(context.filter).length > 0
? context.filter
: null
```
**Never** use `context.filter || null` — it is always truthy and produces an empty filter inside `$and`, which crashes the Go server.
## Single-item vs. list retrieval
For `GET /:collection/:id`, the Go server sets `_id` automatically from the URL parameter.
GET read hooks should **not** set their own `_id` filter for `req.param("id")`. Only add authorization filters (e.g. `{ userId: userId }`).
## Current hook surfaces that matter for website projects
- Collection CRUD hooks under `get`, `post`, `put`, `delete`
- Bulk hooks for optimized bulk operations
- `audit.return` hooks for stripping sensitive data from audit output
- `actions:` hook chains for endpoint-like behavior without a backing CRUD collection
For website builds on this starter, do not force everything into collections. Contact forms, newsletter signups, webhook receivers, import jobs, calculators, or other endpoint-style logic often belong into `actions:` instead.
## HookResponse fields (GET hooks)
| Field | Purpose |
| ------------- | ------------------------------------------------------------------- |
| `filter` | MongoDB filter (list retrieval, or restrict single-item) |
| `selector` | MongoDB projection (`{ field: 0 }` exclude, `{ field: 1 }` include) |
| `offset` | Pagination offset |
| `limit` | Pagination limit |
| `sort` | Sort specification |
| `pipelineMod` | Function to manipulate the aggregation pipeline |
## context.data for write hooks
- `context.data` can be an array for bulk operations — always guard with `!Array.isArray(context.data)`.
- For POST hooks, `context.data.id` may contain the new entry ID.
- For PUT, `req.param("id")` gives the entry ID.
## Bulk and optimized paths
- tibi-server supports optimized bulk paths.
- In bulk scenarios, `bind` still runs once at the start.
- Per-document validation/update/delete hooks may be skipped depending on the chosen bulk path.
If a website feature depends on per-entry logic, do not assume a bulk update behaves exactly like N single updates. Check whether a dedicated bulk hook exists or whether the optimized path changes the behavior you rely on.
## Action hooks
Actions are first-class endpoints and should be part of the skill set for complete website builds.
Typical action steps:
- `post.bind`
- `post.validate`
- `post.handle`
- `post.return`
- `get.handle`
- `get.return`
Use actions when the website needs business logic without a CRUD collection.
Typical website use cases:
- contact forms
- newsletter signups
- quote/order requests
- webhook receivers
- utility endpoints
- AI-assisted helper endpoints
## Practical hook patterns for this starter family
- public read filtering for `active`/publication state
- SSR cache invalidation after writes
- route-level SSR validation
- mutation safeguards for readonly/system-managed fields
- custom form/action validation
- audit-output sanitizing for sensitive fields
## Public filter and publication contract
For website projects, `filter_public.js` and `publishedFilter` are not optional examples. They are part of the public-delivery contract.
Later agents should validate all of these deliberately:
- anonymous public reads see only the intended active/published records
- token-backed or admin-backed reads can still reach records needed for cleanup or operator workflows
- collections that feed navigation or pages do not silently disappear because `active: true` was forgotten
If the public site depends on a collection, a broken public filter is a delivery bug, not only a hook bug.
## Mutation-side SSR invalidation
If a mutation can change rendered HTML, the invalidation belongs in hooks.
Typical SSR-critical mutation domains:
- content
- navigation
- medialib or page-critical referenced media
- publication-relevant fields
For these collections, later agents should verify:
- which mutation steps call cache-clearing behavior
- whether post/put/delete are all covered when needed
- whether a representative mutation actually changes the next SSR response
## Public filter: token bypass for testdata cleanup
`filter_public.js` applies `publishedFilter` (active=true + publication window) to all unauthenticated GET requests. This works well for public traffic but causes a problem: **Playwright test cleanup can't see inactive `_testdata` entries** because `context.user.auth()` returns false even when a static `Token:` header is present. The filter runs, inactive entries are hidden from the API response, and the cleanup never deletes them. Over multiple test runs, stale entries accumulate in MongoDB.
**Fix:** check for any auth header in `filter_public.js` and skip the filter when present:
```js
const req = context.request()
const hasToken =
req.header &&
(req.header("Token") || req.header("X-Admin-Token") || req.header("X-Auth-Token") || req.header("Authorization"))
if (!context.user.auth() && !hasToken) {
// apply publishedFilter
}
```
This way:
- **Anonymous requests** → public filter applies (only active entries visible)
- **Requests with Token header** → no filter → all entries visible → cleanup works
The same fix applies to any collection that uses a public filter hook and also receives testdata writes from Playwright.
## Common pitfalls
- Do not assume browser/Node APIs in hooks. The runtime is goja-based server-side JS.
- Do not treat actions as fake collections unless there is a good reason.
- Do not assume bulk hooks run per document.
- Do not build SSR/cache logic into frontend code when the invalidation belongs in hooks.
## Verification checklist
After hook-related changes, verify all of these:
1. the hook is attached to the right lifecycle step
2. actions are used for endpoint workflows instead of fake collections
3. anonymous vs token-backed reads behave correctly where public filtering exists
4. representative valid and invalid action submissions behave as designed
5. representative SSR-critical mutations invalidate or preserve cache as intended
6. bulk behavior is understood when the workflow depends on per-document logic
## Sending emails from hooks (`context.smtp.sendMail()`)
The `sendMail()` function is registered on `context.smtp` (NOT as a global). Always call via:
```js
context.smtp.sendMail({
to: "recipient@example.com", // string or string[]
cc: "cc@example.com", // optional
bcc: "bcc@example.com", // optional
from: "sender@example.com", // required
fromName: "Sender Name", // optional
replyTo: "reply@example.com", // optional
subject: "Subject line",
plain: "Plain text version",
html: "<h1>HTML version</h1>", // optional
})
```
The SMTP host is configured via:
- Server-level `config.yml`: `mail.host`
- Environment variable: `MAIL_HOST` (e.g. `maildev:1025`)
MailDev (dev SMTP server) runs in the Docker stack at `maildev:25` (SMTP) with a web UI at `:1080`.
## publishedFilter: `active: true` erforderlich
Der `publishedFilter` in `api/hooks/config.js` filtert nach `active: true`:
```js
const publishedFilter = {
active: true,
$or: [ ... ]
}
```
Einträge OHNE `active`-Feld werden bei öffentlichen API-Calls UNSICHTBAR. Das betrifft besonders:
- **Navigationseinträge** werden via `getCachedEntries("navigation", ...)` geladen. Fehlt `active: true`, bleibt `navItems` leer.
- **Manuell via API/MongoDB angelegte Einträge** das `active`-Feld muss explizit gesetzt werden.
Der `filter_public.js`-Hook überspringt den Filter nur wenn ein Token-Header gesetzt ist. Bei öffentlichen API-Calls (z.B. aus dem SPA ohne Token) greift der Filter immer. Daher: alle Einträge in allen Collections müssen `active: true` haben, sonst sind sie auf der Website nicht sichtbar.