✨ 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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user