Files
tibi-svelte-starter/.agents/skills/tibi-actions-and-forms/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

377 lines
10 KiB
Markdown

---
name: tibi-actions-and-forms
description: 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.md`
- `tibi-server/docs/06-hooks.md`
- `api/config.yml`
- `api/hooks/`
- `frontend/src/lib/api.ts`
- `frontend/src/App.svelte` or 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 `inquiries` entry 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:
```text
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 configs
- `api/hooks/<action-name>/` for hook files
Typical config shape:
```yaml
actions:
- !include actions/contact-form.yml
```
```yaml
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 context
- `validate`: enforce required fields, anti-spam checks, shape checks, consent checks
- `handle`: execute the business logic
- `return`: 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 `POST` action
- 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 `POST` action
- 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 `POST` action
- 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 `POST` action
- 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 `GET` unless 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:
1. **bind** — runs first. `context.data` is NOT yet set (body not parsed).
2. Body parsing — happens AFTER bind. JSON body is set to `context.data`.
3. **validate**`context.data` is available here for validation.
4. **handle** — main business logic. `context.data` is available.
5. **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.
```yaml
# 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/`.
```sh
curl -X POST "https://project.code.testversion.online/api/_actions/contact"
```
## Permissions for public actions
Actions need explicit public write permission for unauthenticated access:
```yaml
- 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()`:
```svelte
<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:
```ts
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:
```json
{ "ok": true, "message": "Danke für Ihre Nachricht." }
```
```json
{ "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:
1. The action is declared in `api/config.yml`.
2. The hook chain exists and has the intended steps.
3. Invalid submissions fail with useful status and message.
4. Valid submissions trigger the intended side effects.
5. Public permissions are no broader than necessary.
6. The frontend handles success and failure predictably.
7. `yarn validate` stays clean.
## What an LLM should inspect first
When asked to add a website form or endpoint on this starter, inspect in this order:
1. `tibi-server/docs/19-actions.md`
2. `api/config.yml`
3. related hooks in `api/hooks/`
4. the frontend form/API surface
5. 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.