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.
This commit is contained in:
2026-05-17 00:52:41 +00:00
parent 958b45272d
commit 4020ad62c5
44 changed files with 4276 additions and 867 deletions
+191
View File
@@ -7,6 +7,75 @@ description: Write and debug server-side hooks for tibi-server (goja Go JS runti
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:
@@ -33,6 +102,21 @@ For many hooks, throwing is the normal control flow, especially in SSR hooks whe
- 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**.
@@ -124,9 +208,116 @@ Typical website use cases:
- 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.