- 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.
10 KiB
name, description
| name | description |
|---|---|
| tibi-actions-and-forms | Build endpoint-style website features with tibi-server actions. Covers when to use actions instead of collections, action hook flow, validation, permissions, CORS, mail/webhook patterns, and frontend integration for forms. |
tibi-actions-and-forms
When to use this skill
Use this skill when:
- Building contact forms, newsletter forms, quote requests, callbacks, or booking requests
- Adding endpoint-like backend logic without CRUD storage
- Replacing old collection hacks that only existed to accept POST requests
- Designing frontend form submissions against tibi-server actions
- Deciding whether a feature should be an action, a collection, or both
Goal
The goal is to teach an LLM how to build website workflows that behave like real endpoints.
A form feature is complete only when all of these are coherent:
- action config
- hook flow
- validation
- permissions and CORS
- optional persistence or side effects
- frontend submission and error handling
Source of truth
Use these sources when implementing or reviewing action-based features:
tibi-server/docs/19-actions.mdtibi-server/docs/06-hooks.mdapi/config.ymlapi/hooks/frontend/src/lib/api.tsfrontend/src/App.svelteor the relevant frontend form surface
Core decision: action or collection?
Use an action when the feature is primarily an endpoint or workflow.
Typical action cases:
- contact form
- newsletter signup
- quote request
- callback request
- webhook receiver
- utility endpoint
- AI-assisted helper endpoint
Use a collection when the feature is primarily stored content or stored records with CRUD semantics.
Typical collection cases:
- products
- team members
- events
- testimonials
- persisted inquiries that editors must browse/edit in admin
Use action + collection when you need a public workflow plus internal persistence.
Example:
- a contact form submits to an action
- the action validates, sends mail, and optionally creates an
inquiriesentry for staff follow-up
Do not fake endpoint logic with empty collections unless there is a very specific reason.
Routing model
Actions are exposed under:
POST /api/v1/_/:project/_actions/:action
GET /api/v1/_/:project/_actions/:action
The _actions prefix is part of the contract. Frontend form code should treat actions as explicit API endpoints, not as collection writes.
Where actions are configured
Actions are declared in api/config.yml under actions: and typically point to files under:
api/actions/for YAML configsapi/hooks/<action-name>/for hook files
Typical config shape:
actions:
- !include actions/contact-form.yml
name: contact-form
meta:
label: { de: "Kontaktformular", en: "Contact Form" }
permissions:
public:
methods:
post: true
hooks:
post:
bind:
type: javascript
file: hooks/contact-form/post_bind.js
validate:
type: javascript
file: hooks/contact-form/post_validate.js
handle:
type: javascript
file: hooks/contact-form/post_handle.js
return:
type: javascript
file: hooks/contact-form/post_return.js
Action lifecycle
POST flow
bind → validate → handle → return
Use the steps deliberately:
bind: normalize the request body, derive helper data, prepare contextvalidate: enforce required fields, anti-spam checks, shape checks, consent checkshandle: execute the business logicreturn: normalize the response payload for the frontend
GET flow
handle → return
GET actions are useful for utility endpoints, signed links, status endpoints, or controlled data retrieval that is not a collection read.
Recommended website patterns
Contact form
Recommended behavior:
- public
POSTaction - validate required fields, email format, consent, and anti-spam signal
- send mail or queue message handling
- optionally persist a normalized inquiry record
- return a small stable payload for the frontend
Newsletter signup
Recommended behavior:
- public
POSTaction - validate email and consent
- call external provider or create local opt-in entry
- keep provider-specific logic in the action, not in the frontend
Quote or booking request
Recommended behavior:
- public
POSTaction - transform raw form data into a normalized structure in
bind - validate business rules in
validate - persist or forward the request in
handle
Webhook receiver
Recommended behavior:
- restricted
POSTaction - verify secret/signature before any state change
- keep webhook-specific logic isolated from public website forms
Validation rules
Validation belongs server-side even when the frontend already validates.
Always validate:
- required fields
- string lengths
- email/phone formats when applicable
- consent flags
- expected enums or modes
- anti-spam or rate-limit conditions
Do not trust the frontend form shape.
Permissions and CORS
Actions use the same permission model as collections.
For public website forms:
- keep only the needed methods public
- avoid opening
GETunless the use case needs it - use action-level CORS only when the frontend origin truly differs from the project default
Public form access should be narrow, explicit, and auditable.
Persistence strategy
Not every form submission belongs in a collection.
Choose persistence deliberately:
- no persistence: mail, webhook, or third-party API only
- minimal persistence: store the normalized request for internal staff
- full persistence: store and manage lifecycle in a dedicated collection
If editors must browse, triage, export, or annotate the data in Nova, add a dedicated collection instead of overloading the action itself.
Frontend integration
Frontend forms should submit to actions through the normal API layer.
Keep the frontend responsible for:
- collecting input
- disabled/loading state
- optimistic or conservative UX
- success and error messages
Keep the backend responsible for:
- real validation
- side effects
- persistence
- normalization of response shape
Hook step order: bind → validate → handle → return
Action hooks run in a fixed step order in tibi-server:
- bind — runs first.
context.datais NOT yet set (body not parsed). - Body parsing — happens AFTER bind. JSON body is set to
context.data. - validate —
context.datais available here for validation. - handle — main business logic.
context.datais available. - return — final response shaping.
Critical: The bind hook runs BEFORE the HTTP body is parsed. Do NOT access context.data in bind — it will be undefined. Use handle or validate for data access.
# Correct: use handle step for data access
hooks:
post:
handle:
type: javascript
file: hooks/actions/contact/handle.js
Action URL pattern (through BrowserSync proxy): /api/_actions/{name} — NOT /api/{name}. The tibi-server registers actions under /_actions/.
curl -X POST "https://project.code.testversion.online/api/_actions/contact"
Permissions for public actions
Actions need explicit public write permission for unauthenticated access:
- name: contact
path: contact
permissions:
public:
methods:
post: true
hooks:
post:
handle:
type: javascript
file: hooks/actions/contact/handle.js
Inline form validation (frontend)
Use $state variables for inline errors instead of alert():
<script lang="ts">
let startDate = $state("")
let endDate = $state("")
let dateError = $state("")
function handleSubmit() {
if (!startDate) { dateError = "Bitte Mietbeginn wählen"; return }
if (!endDate) { dateError = "Bitte Mietende wählen"; return }
if (startDate > endDate) { dateError = "Mietende muss nach Mietbeginn liegen"; return }
dateError = ""
// submit logic
}
</script>
<form onsubmit={handleSubmit}>
<input type="date" bind:value={startDate} />
<input type="date" bind:value={endDate} />
{#if dateError}
<div class="text-red-600 bg-red-50 px-3 py-2 rounded-lg">{dateError}</div>
{/if}
<button type="submit">Absenden</button>
</form>
Errors should appear directly below the relevant field group, not as a browser alert.
Frontend form submission
Submit to the action endpoint using the correct path:
fetch("/api/_actions/contact", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name, email, message, consent: true }),
})
Response design
Return a small stable payload that the frontend can rely on.
Typical response examples:
{ "ok": true, "message": "Danke für Ihre Nachricht." }
{ "ok": true, "nextStep": "confirm-email" }
Avoid leaking internal implementation details or raw provider responses to the frontend.
Anti-patterns
- Creating empty fake collections just to receive POST requests
- Moving validation only into the browser
- Sending third-party API credentials from the frontend
- Returning unstable error shapes
- Mixing public forms and internal admin workflows into one hook without boundaries
- Persisting everything by default without a real editorial or operational need
Verification checklist
After adding an action-based workflow, verify all of these:
- The action is declared in
api/config.yml. - The hook chain exists and has the intended steps.
- Invalid submissions fail with useful status and message.
- Valid submissions trigger the intended side effects.
- Public permissions are no broader than necessary.
- The frontend handles success and failure predictably.
yarn validatestays clean.
What an LLM should inspect first
When asked to add a website form or endpoint on this starter, inspect in this order:
tibi-server/docs/19-actions.mdapi/config.yml- related hooks in
api/hooks/ - the frontend form/API surface
- whether persistence belongs in a collection too
This prevents the common mistake of starting with a fake collection when the feature is really an action.