✨ 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:
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: admin-ui-config
|
||||
description: Configure the admin UI for collections — meta labels, preview/viewHint, field widgets, inputProps, sidebar layout, choices, foreign references, and image handling. Use when setting up or customizing collection admin views.
|
||||
description: Configure the admin UI for collections and project-level Nova behavior — meta labels, preview/viewHint, sidebar layout, collectionGroups, i18n, field widgets, foreign references, and image handling. Use when setting up or customizing admin views.
|
||||
---
|
||||
|
||||
# admin-ui-config
|
||||
@@ -24,6 +24,46 @@ Treat this skill as Nova-first. Use current Nova concepts such as `preview`, `si
|
||||
|
||||
---
|
||||
|
||||
## Project-level admin contracts
|
||||
|
||||
Not every important Nova contract lives in a collection YAML file. Some of the most important admin behaviors are configured at project level in `api/config.yml` under `meta:`.
|
||||
|
||||
Current starter example:
|
||||
|
||||
```yaml
|
||||
meta:
|
||||
imageUrl:
|
||||
eval: "$projectBase + '_/assets/img/admin-pic.svg'"
|
||||
i18n:
|
||||
defaultLanguage: de
|
||||
languages:
|
||||
- code: de
|
||||
label: Deutsch
|
||||
- code: en
|
||||
label: English
|
||||
collectionGroups:
|
||||
- name: content
|
||||
label: { de: "Inhalte", en: "Content" }
|
||||
icon: article
|
||||
- name: media
|
||||
label: { de: "Medien", en: "Media" }
|
||||
icon: image_multiple
|
||||
```
|
||||
|
||||
Treat these as part of the admin design, not as optional polish:
|
||||
|
||||
- `meta.imageUrl` — project card/preview imagery in the admin
|
||||
- `meta.i18n` — project-wide language model for field-level and entry-level translation workflows
|
||||
- `meta.collectionGroups` — ordered collection groups for the sidebar
|
||||
|
||||
Important rule:
|
||||
|
||||
- collection-level `meta.group` must reference one of the project-level `meta.collectionGroups[].name` values if the collection should appear inside an explicit group
|
||||
|
||||
If project-level `meta.i18n` is missing or inconsistent, even well-modeled collections can become confusing in Nova.
|
||||
|
||||
---
|
||||
|
||||
## Collection meta configuration
|
||||
|
||||
The `meta` key in a collection YAML controls how the collection appears in the admin sidebar and collection/list UI.
|
||||
@@ -84,6 +124,42 @@ meta:
|
||||
- `preview.select` can reduce lookup work for preview table columns.
|
||||
- `meta.subNavigation` defines filtered entry tabs in the sidebar.
|
||||
|
||||
### Sub-navigation tabs
|
||||
|
||||
Use `meta.subNavigation` when one collection needs multiple curated views in the admin without splitting into multiple collections.
|
||||
|
||||
```yaml
|
||||
meta:
|
||||
subNavigation:
|
||||
- name: pages
|
||||
label: { de: "Seiten", en: "Pages" }
|
||||
muiIcon: article
|
||||
filter:
|
||||
type: page
|
||||
defaultSort:
|
||||
field: insertTime
|
||||
order: DESC
|
||||
setDefault:
|
||||
field: type
|
||||
value: page
|
||||
- name: news
|
||||
label: { de: "News", en: "News" }
|
||||
muiIcon: feed
|
||||
filter:
|
||||
type: news
|
||||
setDefault:
|
||||
field: type
|
||||
value: news
|
||||
```
|
||||
|
||||
Use sub-navigation when:
|
||||
|
||||
- one collection has several stable editorial slices
|
||||
- the underlying schema is still shared enough to stay one collection
|
||||
- authors benefit from filtered entry views and sensible defaults
|
||||
|
||||
Do not use sub-navigation to hide a bad collection model. If the workflows truly diverge, split the collection instead.
|
||||
|
||||
---
|
||||
|
||||
## Field configuration
|
||||
@@ -247,6 +323,8 @@ Use `foreign.id: id` for the outward FK field identity. Only Mongo-style filters
|
||||
imageEditor: true # Enable crop/rotate editor
|
||||
```
|
||||
|
||||
This field config controls the editor widget, not the filesystem target. Configure file storage once at collection level via top-level `uploadPath` (for this starter typically `../media/<collection>`), not on the individual file field.
|
||||
|
||||
---
|
||||
|
||||
## Layout: position, sections, sidebar
|
||||
@@ -466,13 +544,9 @@ import BlockRenderer from "./blocks/BlockRenderer.svelte"
|
||||
// Creates a block definition that renders the same Svelte component
|
||||
// used in the public frontend. The block is mounted inside Shadow DOM
|
||||
// for style isolation.
|
||||
function createContentBlockDefinition(presentation: {
|
||||
label: string
|
||||
icon: string
|
||||
color: string
|
||||
}) {
|
||||
function createContentBlockDefinition(presentation: { label: string; icon: string; color: string }) {
|
||||
return {
|
||||
css: [previewCssUrl], // CSS files to inject into Shadow DOM
|
||||
css: [previewCssUrl], // CSS files to inject into Shadow DOM
|
||||
label: presentation.label,
|
||||
icon: presentation.icon,
|
||||
color: presentation.color,
|
||||
@@ -517,6 +591,7 @@ export { blockRegistry }
|
||||
```
|
||||
|
||||
**Key points:**
|
||||
|
||||
- Each registry entry wraps the Svelte `BlockRenderer` to render the block in the admin preview.
|
||||
- The `row` object is the block data (same shape as `ContentBlockEntry`).
|
||||
- Preview data may contain hydrated `_lookup.<fieldPath>` foreign key data and absolute file URLs — do not prepend `apiBase` or attempt re-fetching.
|
||||
@@ -665,7 +740,7 @@ indexes:
|
||||
- name: price_sort
|
||||
key: [price]
|
||||
- name: category_active
|
||||
key: [category, -active] # -prefix for descending
|
||||
key: [category, -active] # -prefix for descending
|
||||
- name: slug_unique
|
||||
key: [slug]
|
||||
unique: true
|
||||
@@ -682,9 +757,26 @@ search:
|
||||
|
||||
See `tibi-server/docs/04-collections.md` (sections on indexes and search config) for full reference.
|
||||
|
||||
## Checklist-facing verification
|
||||
|
||||
For a real project, do not stop after writing the YAML. Validate the authoring contract explicitly.
|
||||
|
||||
Minimum review points:
|
||||
|
||||
1. project-level `meta.i18n` and `meta.collectionGroups` are coherent
|
||||
2. each collection has a readable `meta.preview`
|
||||
3. list views show meaningful columns instead of raw IDs or empty rows
|
||||
4. foreign references render with readable previews
|
||||
5. sidebars and `containerProps.layout` produce usable forms
|
||||
6. pagebuilder collections expose both a working chooser and working preview path
|
||||
|
||||
Committed admin Playwright coverage is preferred for stable contracts that should not regress.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **`meta.label` supports both strings and i18n objects** — Use i18n objects only when the collection or field label must be localized.
|
||||
- **Project-level admin config is easy to forget** — `collectionGroups` and project-level `meta.i18n` live in `api/config.yml`, not in individual collection files.
|
||||
- **`meta.group` without a matching project group** — The collection still exists, but the sidebar grouping model becomes inconsistent.
|
||||
- **`choices.id` must match stored value** — The `id` in choices is what gets saved to the database.
|
||||
- **`inputProps` depends on widget** — Not all props work with all widgets. Check tibi-admin-nova source if unsure.
|
||||
- **`position: sidebar` without group** — Fields go to an ungrouped area. Use `position: "sidebar:GroupName"` for grouping.
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
---
|
||||
name: audit-and-compliance
|
||||
description: Configure and verify audit logging for tibi website projects. Covers server-level audit config, collection audit actions, audit.return filtering, TTL/retention, source semantics, and what later agents must check before relying on audit trails.
|
||||
---
|
||||
|
||||
# audit-and-compliance
|
||||
|
||||
## When to use this skill
|
||||
|
||||
Use this skill when:
|
||||
|
||||
- a project needs auditable create/update/delete activity
|
||||
- operators or stakeholders need a trace of who changed content and how
|
||||
- hooks, jobs, or actions mutate important collections and that history matters
|
||||
- sensitive data could land in audit snapshots and must be filtered safely
|
||||
|
||||
## Goal
|
||||
|
||||
Give later agents a concrete workflow for deciding, configuring, and validating audit logging on this stack.
|
||||
|
||||
Audit is not just “turn it on”. A usable audit setup must answer:
|
||||
|
||||
- what is logged
|
||||
- how long it is kept
|
||||
- who can read it
|
||||
- which sensitive fields must be stripped
|
||||
- how hook/job/action side effects appear in the audit trail
|
||||
|
||||
## Source of truth
|
||||
|
||||
Use these sources when implementing or reviewing audit behavior:
|
||||
|
||||
- `tibi-server/docs/10-audit.md`
|
||||
- `tibi-server/docs/06-hooks.md`
|
||||
- `tibi-server/docs/11-jobs.md`
|
||||
- `tibi-server/docs/19-actions.md`
|
||||
- active server/project config
|
||||
|
||||
## Core audit model
|
||||
|
||||
Audit requires an explicit server-level decision:
|
||||
|
||||
```yaml
|
||||
audit:
|
||||
enabled: true
|
||||
defaultTTL: "720h"
|
||||
defaultLimit: 50
|
||||
maxLimit: 10000
|
||||
```
|
||||
|
||||
At collection level, audit is controlled separately:
|
||||
|
||||
```yaml
|
||||
name: content
|
||||
audit:
|
||||
enabled: true
|
||||
actions:
|
||||
- create
|
||||
- update
|
||||
- delete
|
||||
```
|
||||
|
||||
Default audited collection actions are `create`, `update`, and `delete`.
|
||||
|
||||
Important:
|
||||
|
||||
- `get` can also be audited when needed
|
||||
- system actions such as `login`, `reload`, and `shutdown` are controlled by server-level audit enablement
|
||||
- bulk API mutations are still audited even though the internal action naming differs (`bulkCreate`, `bulkUpdate`, `bulkDelete`)
|
||||
|
||||
## Source semantics matter
|
||||
|
||||
Audit entries are not all equal. The `source.type` tells you where the mutation came from:
|
||||
|
||||
- `controller` — direct API CRUD request
|
||||
- `hook` — database mutation performed from a hook
|
||||
- `job` — database mutation performed from a scheduled job
|
||||
- `system` — internal system operation
|
||||
|
||||
For website projects this matters because content can change through:
|
||||
|
||||
- direct editor CRUD
|
||||
- action-triggered persistence
|
||||
- hook-side side effects
|
||||
- cleanup or synchronization jobs
|
||||
|
||||
If a project relies on audit trails for governance or debugging, later agents must understand these source types instead of assuming every change came from an editor UI save.
|
||||
|
||||
## Authentication context in audit entries
|
||||
|
||||
Audit also records how a request was authenticated.
|
||||
|
||||
Relevant fields include:
|
||||
|
||||
- `authMethod`
|
||||
- `tokenLabel`
|
||||
- `tokenPrefix`
|
||||
- `userId`
|
||||
- `username`
|
||||
- `ip`
|
||||
|
||||
Practical implication:
|
||||
|
||||
- use labeled admin tokens for operator workflows so audit output stays readable
|
||||
- do not treat all token-based writes as anonymous noise
|
||||
|
||||
## Snapshot exposure and `audit.return`
|
||||
|
||||
Audit snapshots can contain more than a normal API read would expose.
|
||||
|
||||
That means fields that are stripped by normal read hooks can still appear in audit snapshots unless you filter them explicitly.
|
||||
|
||||
Use `audit.return` hooks when a collection may contain:
|
||||
|
||||
- passwords
|
||||
- API keys
|
||||
- internal secrets
|
||||
- sensitive operator-only notes
|
||||
|
||||
Example shape:
|
||||
|
||||
```yaml
|
||||
hooks:
|
||||
audit:
|
||||
return:
|
||||
type: javascript
|
||||
file: hooks/users/audit_return.js
|
||||
```
|
||||
|
||||
Use this hook to delete or suppress sensitive `snapshot` and `changes` data before the audit response is returned.
|
||||
|
||||
## Retention and limits
|
||||
|
||||
Audit is an operator/compliance concern, not just a developer concern.
|
||||
|
||||
Decide deliberately:
|
||||
|
||||
- whether audit is enabled at all
|
||||
- how long audit logs should be retained (`defaultTTL`)
|
||||
- what read limits make sense (`defaultLimit`, `maxLimit`)
|
||||
- whether the target environment expects short-term diagnostics or longer retention
|
||||
|
||||
Do not enable audit and leave retention undefined by habit.
|
||||
|
||||
## Recommended patterns for website projects
|
||||
|
||||
### Content governance
|
||||
|
||||
Recommended shape:
|
||||
|
||||
- audit enabled for collections whose changes affect public output or editorial accountability
|
||||
- clear retention decision with operations
|
||||
- readable token labels for automation or deploy-related writes
|
||||
|
||||
### Hook-heavy workflows
|
||||
|
||||
Recommended shape:
|
||||
|
||||
- understand which hook-side writes will appear as `source.type: "hook"`
|
||||
- do not assume audit trails point only to controller actions
|
||||
- document important side effects when hooks fan out into multiple writes
|
||||
|
||||
### Jobs and automation
|
||||
|
||||
Recommended shape:
|
||||
|
||||
- know that job-side DB mutations appear as `source.type: "job"`
|
||||
- if job behavior matters operationally, make that visible in the job design and ops notes
|
||||
|
||||
### Sensitive collections
|
||||
|
||||
Recommended shape:
|
||||
|
||||
- explicit `audit.return` filtering
|
||||
- do not expose raw snapshots just because admins technically can read the endpoint
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- enabling audit without deciding retention
|
||||
- treating audit as equivalent to normal collection reads
|
||||
- forgetting that hooks and jobs create their own audit source types
|
||||
- relying on unlabeled tokens for important automated writes
|
||||
- storing sensitive data in snapshots without `audit.return` filtering
|
||||
|
||||
## Verification checklist
|
||||
|
||||
After audit-related changes, verify all of these:
|
||||
|
||||
1. server-level audit settings are deliberate
|
||||
2. collection-level audited actions match the actual governance need
|
||||
3. a representative write produces the expected audit entry
|
||||
4. hook/job/action side effects produce understandable source metadata
|
||||
5. sensitive fields are filtered from audit output where required
|
||||
6. read visibility and retention expectations are documented
|
||||
|
||||
## What an LLM should inspect first
|
||||
|
||||
When asked to design or review audit behavior on this starter, inspect in this order:
|
||||
|
||||
1. `tibi-server/docs/10-audit.md`
|
||||
2. active server audit config
|
||||
3. collection-level `audit:` sections
|
||||
4. any hook/job/action workflow that mutates important collections
|
||||
5. whether `audit.return` filtering is needed
|
||||
|
||||
This prevents “audit enabled” setups that are technically on but operationally weak.
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: content-authoring
|
||||
description: Add new pages, content blocks, and collections to a tibi project. Covers the content-based routing model, block registration in BlockRenderer, collection YAML authoring, and TypeScript type definitions. Use when creating new pages, block types, or collections.
|
||||
description: Add new pages, content blocks, and collections to a tibi project. Covers the content-based routing model, block registration in BlockRenderer and frontend/src/admin.ts, lookup-aware reference modeling, collection YAML authoring, and TypeScript type ownership. Use when creating new pages, block types, or collections.
|
||||
---
|
||||
|
||||
# content-authoring
|
||||
@@ -29,6 +29,20 @@ This project does **NOT** use file-based routing (no SvelteKit router). Instead:
|
||||
|
||||
**Important:** When adding new page types, inspect both the frontend route/i18n layer and `api/hooks/config.js` (SSR route validation). A page can exist in the DB and still fail under SSR if the public URL shape and `content.path` mapping are not aligned.
|
||||
|
||||
## Cross-surface ownership rule
|
||||
|
||||
For real project work, treat content authoring as a multi-surface contract.
|
||||
|
||||
When you add or change blocks, pages, or collections, check these surfaces together:
|
||||
|
||||
1. collection YAML in `api/collections/*.yml`
|
||||
2. type ownership in `types/global.d.ts`
|
||||
3. typed API mapping in `frontend/src/lib/api.ts` via `EntryTypeSwitch`
|
||||
4. public rendering in `frontend/src/blocks/BlockRenderer.svelte`
|
||||
5. admin pagebuilder preview in `frontend/src/admin.ts`
|
||||
|
||||
If one of these surfaces is skipped, the project often still looks half-correct until SSR, admin preview, or typed API usage exposes the mismatch.
|
||||
|
||||
---
|
||||
|
||||
## Adding a new page
|
||||
@@ -148,7 +162,30 @@ import MyNewBlock from "./MyNewBlock.svelte"
|
||||
|
||||
If block types become numerous, plan for grouping and registry discipline early. A real website built on this starter should not keep extending a demo-style renderer forever without structure.
|
||||
|
||||
### Step 3: Extend TypeScript types (if new fields are needed)
|
||||
### Step 3: Register in the admin block registry
|
||||
|
||||
If the block is authored through a pagebuilder field, also register it in `frontend/src/admin.ts`.
|
||||
|
||||
Example:
|
||||
|
||||
```ts
|
||||
const blockRegistry = {
|
||||
hero: createContentBlockDefinition({ label: "Hero", icon: "image", color: "#1d4ed8" }),
|
||||
"my-new-block": createContentBlockDefinition({
|
||||
label: "My New Block",
|
||||
icon: "view_compact",
|
||||
color: "#0f766e",
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
Important:
|
||||
|
||||
- `BlockRenderer.svelte` controls public rendering
|
||||
- `frontend/src/admin.ts` controls Nova pagebuilder preview availability
|
||||
- both should point at the same block contract instead of drifting into separate preview-only logic
|
||||
|
||||
### Step 4: Extend TypeScript types (if new fields are needed)
|
||||
|
||||
Edit `types/global.d.ts` — add fields to `ContentBlockEntry`:
|
||||
|
||||
@@ -161,7 +198,9 @@ interface ContentBlockEntry {
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Extend collection YAML (if new fields need admin editing)
|
||||
If the change also introduces a new collection or new API usage surface, update the corresponding entry interfaces in the same change instead of leaving `Record<string, unknown>` as a long-term placeholder.
|
||||
|
||||
### Step 5: Extend collection YAML (if new fields need admin editing)
|
||||
|
||||
Edit `api/collections/content.yml` — add subFields under `blocks`:
|
||||
|
||||
@@ -192,11 +231,13 @@ Use current Nova patterns when extending block schemas:
|
||||
- `dependsOn` for block-type-specific fields
|
||||
- collection- or field-level `meta.pagebuilder` for registry/default viewport settings
|
||||
|
||||
### Step 5: Update mock data (if using MOCK=1)
|
||||
When blocks contain foreign references such as medialib images, model the reference path deliberately so later loaders can request the needed `lookup` data.
|
||||
|
||||
### Step 6: Update mock data (if using MOCK=1)
|
||||
|
||||
Add a block with your new type to `frontend/mocking/content.json`.
|
||||
|
||||
### Step 6: Verify
|
||||
### Step 7: Verify
|
||||
|
||||
```sh
|
||||
yarn validate # TypeScript check — must be warning-free
|
||||
@@ -209,6 +250,12 @@ yarn build:server
|
||||
# then request the SSR endpoint directly and check that the block content appears in HTML
|
||||
```
|
||||
|
||||
For blocks that are authored in pagebuilder and use images or foreign references, also verify:
|
||||
|
||||
- the block appears in the admin chooser
|
||||
- the preview renders in Nova
|
||||
- image/reference data is present through the intended lookup path
|
||||
|
||||
### Existing block types for reference
|
||||
|
||||
| Type | Component | Purpose |
|
||||
@@ -309,17 +356,21 @@ interface MyCollectionEntry {
|
||||
If you need typed helpers, extend the `EntryTypeSwitch` in `frontend/src/lib/api.ts`:
|
||||
|
||||
```typescript
|
||||
type CollectionNameT = "medialib" | "content" | "mycollection" | string
|
||||
type CollectionNameT = "medialib" | "content" | "navigation" | "mycollection" | string
|
||||
|
||||
type EntryTypeSwitch<T extends string> = T extends "medialib"
|
||||
? MedialibEntry
|
||||
: T extends "content"
|
||||
? ContentEntry
|
||||
: T extends "mycollection"
|
||||
? MyCollectionEntry
|
||||
: Record<string, unknown>
|
||||
: T extends "navigation"
|
||||
? NavigationEntry
|
||||
: T extends "mycollection"
|
||||
? MyCollectionEntry
|
||||
: Record<string, unknown>
|
||||
```
|
||||
|
||||
Do not treat `EntryTypeSwitch` as optional cleanup. If the frontend or tests consume the collection in a typed way, update this mapping in the same change.
|
||||
|
||||
### Step 5: Add hooks (optional)
|
||||
|
||||
Common hook patterns:
|
||||
@@ -374,8 +425,28 @@ For collections intended for rich editorial usage, also verify in Nova:
|
||||
- foreign-key displays use meaningful previews
|
||||
- pagebuilder fields render previews and screenshots correctly
|
||||
|
||||
If the collection feeds public pages or admin block previews, also verify that the typed API helpers and runtime components agree on the same data shape.
|
||||
|
||||
---
|
||||
|
||||
## Seed data pattern (Playwright)
|
||||
|
||||
Test seed data uses `_testdata: true` as a hidden marker field. **Real content must NEVER use this flag** — otherwise test teardown will delete it.
|
||||
|
||||
```yaml
|
||||
# Last field in every collection schema
|
||||
- name: _testdata
|
||||
type: boolean
|
||||
meta:
|
||||
hide: true
|
||||
```
|
||||
|
||||
Test setup:
|
||||
|
||||
1. `globalSetup` removes entries with `_testdata: true`, then creates new test entries
|
||||
2. `globalTeardown` removes entries with `_testdata: true`
|
||||
3. Real editorial content has no `_testdata` field → survives all test runs
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Path format**: Content paths do NOT include the language prefix. The path `/ueber-uns` becomes `/{lang}/ueber-uns` via the i18n layer.
|
||||
@@ -385,3 +456,47 @@ For collections intended for rich editorial usage, also verify in Nova:
|
||||
- **After adding a collection**: The tibi-server auto-reloads hooks on file change, but a new collection in `config.yml` may require `make docker-restart-frontend` or a full `make docker-up`.
|
||||
- **Do not fake forms as collections** if they are really endpoint logic. Use `actions:` when no CRUD collection is needed.
|
||||
- **Do not overfit to demo blocks**. Real projects should shape block schemas and admin ergonomics around actual editor workflows.
|
||||
|
||||
## API lookup für aufgelöste Referenzen
|
||||
|
||||
Beim Laden von Collections können Fremdschlüssel via `lookup`-Parameter automatisch aufgelöst werden. Der `lookup`-Parameter wird als 8. Argument an `getCachedEntries` übergeben:
|
||||
|
||||
```ts
|
||||
const products = await getCachedEntries<"machines">(
|
||||
"machines",
|
||||
{ active: true, category: catId },
|
||||
"sortOrder",
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
"images:medialib" // lookup: "feld:collection"
|
||||
)
|
||||
```
|
||||
|
||||
Das Format ist `"feldname:zielcollection"` (z.B. `"images:medialib"`). Die aufgelösten Daten landen in `entry._lookup.feldname` als Array der Ziel-Collection-Objekte. Ohne lookup bleiben `string[]`-Felder reine ID-Arrays.
|
||||
|
||||
Wichtig: der `lookup`-Parameter muss auch in `getDBEntries` und `apiRequest` durchgereicht werden (siehe `api.ts`).
|
||||
|
||||
Für blockbasierte Inhalte ist der Lookup-Pfad oft verschachtelt, nicht flach. Beispiel:
|
||||
|
||||
```ts
|
||||
const entries = await getCachedEntries<"content">(
|
||||
"content",
|
||||
{ active: true, path: "/preview-page" },
|
||||
"sort",
|
||||
undefined,
|
||||
1,
|
||||
undefined,
|
||||
undefined,
|
||||
"blocks.heroImage.image:medialib"
|
||||
)
|
||||
```
|
||||
|
||||
Merke:
|
||||
|
||||
- flache Relationen nutzen Pfade wie `images:medialib`
|
||||
- block- oder objektverschachtelte Relationen nutzen Dot-Paths wie `blocks.heroImage.image:medialib`
|
||||
- ohne den passenden Lookup fehlen Admin-Preview, SSR oder Frontend-Rendern oft erst zur Laufzeit
|
||||
|
||||
Treat public rendering, SSR rendering, and admin preview as the same reference contract whenever possible. If a block renders a medialib image in the site, the admin preview should usually depend on the same resolved media assumption instead of inventing a separate preview-only data path.
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
---
|
||||
name: deployment
|
||||
description: Production deployment setup for tibi-projects – Basispanel subdomain, .env, CI-Pipeline, Makefile. Use when deploying a new project to production or setting up a staging environment.
|
||||
---
|
||||
|
||||
# Deployment
|
||||
|
||||
## Überblick
|
||||
|
||||
Ein tibi-Projekt wird per Gitea Actions CI gebaut und via rsync auf den Produktionsserver (dock4) deployed. Davor muss die Subdomain im Basispanel angelegt und der Kunde korrekt konfiguriert sein.
|
||||
|
||||
## 1. Basispanel – Subdomain anlegen
|
||||
|
||||
### Kunde prüfen
|
||||
|
||||
```bash
|
||||
# Domain des Kunden suchen
|
||||
mcp_call(server="basispanel", tool="bp_list_domains", args={"search": "<kunde>"})
|
||||
# → liefert Customer-ID, Domain-ID, Company, Username
|
||||
```
|
||||
|
||||
### Subdomain anlegen
|
||||
|
||||
```bash
|
||||
# 1. Config holen (verfügbare Webserver + Storages sehen)
|
||||
mcp_call(server="basispanel", tool="bp_get_config")
|
||||
|
||||
# 2. Subdomain erstellen (ohne Webserver)
|
||||
mcp_call(server="basispanel", tool="bp_create_subdomain", args={
|
||||
"domainId": <domain-id>,
|
||||
"name": "<subdomain>", # oder leer für bare domain
|
||||
})
|
||||
|
||||
# 3. Löschen + neu mit Webserver (wenn Update nicht klappt)
|
||||
mcp_call(server="basispanel", tool="bp_delete_subdomain", args={"id": <subdomain-id>})
|
||||
|
||||
mcp_call(server="basispanel", tool="bp_create_subdomain", args={
|
||||
"domainId": <domain-id>,
|
||||
"name": "<subdomain>",
|
||||
"webserverKey": "dock4_lamp2",
|
||||
"webserverStorage": "dock4_webroots2",
|
||||
"webserverSettings": {
|
||||
"redirectType": "docroot",
|
||||
"docroot": "/<subdomain>.<domain>/frontend",
|
||||
"gitbaseRepository": "<org>/<repo>",
|
||||
"deployRoot": "./..",
|
||||
"defaultAlias": "wwwAlias",
|
||||
"defaultSubdomain": "defaultSubdomain",
|
||||
"wwwRedirect": "wwwRedirect",
|
||||
"php": "phpDisabled", # tibi-SPA kein PHP
|
||||
"https": "noHttps", # erstmal aus, später aktivieren
|
||||
"certbot": "noCertbot",
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Wichtige Keys (aus `bp_get_config`):
|
||||
| Server | Key | Storage |
|
||||
|--------|-----|---------|
|
||||
| dock4 | `dock4_lamp2` | `dock4_webroots2` |
|
||||
| dock1 | `dock1_...` | `dock1_webroots...` |
|
||||
|
||||
### Status prüfen
|
||||
|
||||
```bash
|
||||
mcp_call(server="basispanel", tool="bp_get_subdomain_status", args={"id": <subdomain-id>})
|
||||
```
|
||||
|
||||
Achtung: Health-Check zeigt DNS-Warnungen (externe Nameserver) – das ist normal solange der Kunde sein DNS selbst verwaltet.
|
||||
|
||||
## 2. `.env` konfigurieren
|
||||
|
||||
```env
|
||||
# Basis
|
||||
PROJECT_NAME=<project>
|
||||
TIBI_PREFIX=tibi
|
||||
TIBI_NAMESPACE=<project>
|
||||
|
||||
# RSYNC für Deploy
|
||||
RSYNC_HOST=ftp1.webmakers.de
|
||||
RSYNC_PORT=22223
|
||||
PRODUCTION_RSYNC_UID=100<customer-id>00 # z.B. 10051300
|
||||
PRODUCTION_RSYNC_GID=33
|
||||
|
||||
# Production Server
|
||||
PRODUCTION_SERVER=dock4.basehosts.de
|
||||
PRODUCTION_TIBI_PREFIX=tibi
|
||||
PRODUCTION_PATH=/webroots2/customers/<customer-id>/htdocs
|
||||
|
||||
# Staging
|
||||
STAGING_PATH=/staging/<org>/<project>/dev
|
||||
|
||||
# URLs
|
||||
LIVE_URL=http://<subdomain>.<domain>.dock4.basispanel.de # Preview-URL
|
||||
STAGING_URL=https://dev-<project>.staging.testversion.online
|
||||
CODING_URL=https://<project>.code.testversion.online
|
||||
```
|
||||
|
||||
## 3. CI-Pipeline (`.gitea/workflows/deploy.yml`)
|
||||
|
||||
```yaml
|
||||
name: deploy to production
|
||||
on: "push"
|
||||
jobs:
|
||||
deploy:
|
||||
steps:
|
||||
- checkout + git fetch --tags
|
||||
- node 22 + yarn install
|
||||
- yarn validate
|
||||
- ./scripts/ci-modify-config.sh # injiziert LIVE_URL, release, preview
|
||||
- yarn build # frontend
|
||||
- yarn build:server # SSR
|
||||
- sourcemaps → sentry
|
||||
- if dev-branch: ./scripts/ci-staging.sh
|
||||
- if master-branch: ./scripts/ci-deploy.sh
|
||||
```
|
||||
|
||||
**Wichtig:** Das aktuelle Workflow-File führt `yarn validate`, `yarn build` und `yarn build:server` im CI aus. Wenn `validate` dort scheitert, behebe den eigentlichen Typ- oder Pfadfehler statt den Schritt stillschweigend zu entfernen.
|
||||
|
||||
## 4. Deploy-Skripte
|
||||
|
||||
### `scripts/ci-deploy.sh` (Production)
|
||||
|
||||
- Liest `.env` und `api/config.yml.env`
|
||||
- rsynct `frontend/`, `api/`, `media/` via SSH zu `RSYNC_HOST`
|
||||
- deshalb muessen Collection-Dateiuploads auf den Repo-Root `media/` zeigen, typischerweise via `uploadPath: ../media/<collection>` in `api/collections/*.yml`
|
||||
- `api/media` ist in diesem Setup nicht der persistente Deploy-Zielpfad fuer Uploads
|
||||
- Nutzt `RSYNC_USER` + `RSYNC_PASS` (aus Gitea Secrets)
|
||||
- Auf master: excludiert `src/` und `*.map`
|
||||
- Reloadt den projektlokalen Proxy-Endpunkt via `LIVE_URL/api/_/admin/reload` mit `Authorization: Bearer ${ADMIN_TOKEN}`
|
||||
- Cleared SSR cache via `LIVE_URL/api/ssr?clear=1`
|
||||
|
||||
### `scripts/ci-staging.sh` (Dev/Staging)
|
||||
|
||||
- rsynct `api/`, `frontend/dist`, und `frontend/assets` nach `/data/${{ github.repository }}/${{ github.ref_name }}`
|
||||
- Startet `docker-compose-staging.yml`
|
||||
- Reloadt den projektlokalen Proxy-Endpunkt via `STAGING_URL/api/_/admin/reload` mit `Authorization: Bearer ${ADMIN_TOKEN}`
|
||||
|
||||
### `scripts/ci-modify-config.sh`
|
||||
|
||||
- Injiziert `LIVE_URL` als `originURL` in `api/hooks/config-client.js`
|
||||
- Injiziert `LIVE_URL` als `PREVIEW_URL` in `api/config.yml.env`
|
||||
- Setzt `release` + `buildTime` für Sentry
|
||||
- Kopiert `frontend/spa.html` → `api/templates/spa.html` (SSR-Template)
|
||||
- Ersetzt `__TIMESTAMP__` in spa.html (Cache-Busting)
|
||||
|
||||
## 5. Makefile
|
||||
|
||||
Wichtige Targets:
|
||||
|
||||
```makefile
|
||||
# Media von Production syncen
|
||||
media-sync-master-to-local:
|
||||
rsync -v -e "ssh ... -l $(PRODUCTION_RSYNC_UID),$(PRODUCTION_RSYNC_GID),$(PRODUCTION_PATH)/media" \
|
||||
-az $(RSYNC_HOST):/ media/
|
||||
|
||||
# MongoDB von Production syncen (via Chisel-Tunnel)
|
||||
mongo-sync-master-to-local:
|
||||
chisel client --auth coder:$$PASSWORD http://$(PRODUCTION_SERVER):10987 27017:mongo:27017 &
|
||||
mongodump ... | mongorestore ...
|
||||
```
|
||||
|
||||
## 6. DNS
|
||||
|
||||
Der Kunde verwaltet sein DNS selbst (externe Nameserver). Für die Subdomain muss ein A-Record gesetzt werden:
|
||||
|
||||
```
|
||||
<subdomain>.<domain> IN A 45.129.180.102 (IP von dock4)
|
||||
```
|
||||
|
||||
Die Preview-URL `http://<subdomain>.<domain>.dock4.basispanel.de` funktioniert ohne DNS (wird von Basispanel intern aufgelöst).
|
||||
|
||||
## 7. HTTPS nachträglich aktivieren
|
||||
|
||||
Sobald das Projekt live geht:
|
||||
|
||||
1. Im Basispanel Subdomain updaten:
|
||||
- `https`: `"https"` (statt `"noHttps"`)
|
||||
- `certbot`: `"certbot"` (automatisches Letsencrypt)
|
||||
- `httpsRedirect`: `"httpsRedirect"` (HTTP→HTTPS)
|
||||
2. `.env`: `LIVE_URL` auf `https://www.<domain>` ändern
|
||||
3. `api/hooks/config-client.js`: `originURL` entsprechend setzen (wird von CI überschrieben)
|
||||
|
||||
## 8. Typische Fehler
|
||||
|
||||
| Problem | Ursache | Fix |
|
||||
| ------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------ |
|
||||
| `invalid webserverKey: dock4` | Falscher Key | Mit `bp_get_config` prüfen → `dock4_lamp2` |
|
||||
| `subdomain exists` | Doppelt angelegt | Mit `bp_delete_subdomain` löschen, neu anlegen |
|
||||
| `yarn validate` scheitert in CI | Typen/Submodule/Pfade nicht sauber eingecheckt | Checkout-, Submodule- und Include-Pfade korrigieren; `validate` im Workflow belassen |
|
||||
| Rsync "Permission denied" | Falscher RSYNC_USER | In Gitea Secrets prüfen |
|
||||
| 404 auf Subdomain | DNS nicht gesetzt | A-Record beim Kunden-DNS-Provider eintragen |
|
||||
@@ -343,6 +343,48 @@ decrementRequests() → LoadingBar disappears
|
||||
Return { data, count, buildTime }
|
||||
```
|
||||
|
||||
### `_count` endpoint
|
||||
|
||||
Der tibi-server stellt einen dedizierten `_count`-Endpoint bereit, der **nur** `{"count": N}` zurückgibt – kein Data-Transfer:
|
||||
|
||||
```
|
||||
GET /api/{collection}/_count?filter={"active":true,"category":"<id>"}
|
||||
→ {"count": 8}
|
||||
```
|
||||
|
||||
Der Endpoint wird durch den BrowserSync-Proxy korrekt geroutet (`/api` → `/api/v1/_/{namespace}`).
|
||||
|
||||
**Frontend-Aufruf:**
|
||||
|
||||
```ts
|
||||
const res = await api<{ count: number }>("machines/_count", {
|
||||
filter: { active: true, category: catId },
|
||||
})
|
||||
// res.data.count === 8
|
||||
```
|
||||
|
||||
Das ist effizienter als `count=1&limit=1`, weil keine Collection-Objekte serialisiert/übertragen werden.
|
||||
|
||||
### `select` für schlanke Queries
|
||||
|
||||
Der tibi-server unterstützt einen `select`-Parameter als Komma-Liste der gewünschten Felder. Nicht gelistete Felder werden nicht übertragen:
|
||||
|
||||
```ts
|
||||
const res = await api<MachineEntry[]>("machines", {
|
||||
filter: { active: true, category: catId },
|
||||
sort: "sortOrder",
|
||||
limit: 20,
|
||||
params: {
|
||||
lookup: "images:medialib",
|
||||
select: "name,slug,tagline,priceFrom,weight,sku,images",
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Nicht aufgeführte Felder (z.B. `description`, `specs`) entfallen – spart Bandbreite bei Listen/Grids. `_lookup` und `id` werden automatisch ergänzt.
|
||||
|
||||
**Wichtig:** `select` muss als String im `params`-Objekt übergeben werden (der `apiRequest` hängt es als Query-Parameter an). Es wird direkt an den tibi-server durchgereicht.
|
||||
|
||||
---
|
||||
|
||||
## i18n system
|
||||
@@ -389,3 +431,30 @@ Return { data, count, buildTime }
|
||||
- **Content cache is 1 hour** — `getCachedEntries` caches in memory for 1h. For admin previews, use `getDBEntries` (uncached).
|
||||
- **`$effect` alone is not SSR** — server-side rendering must trigger the same data path explicitly outside browser-only reactive effects.
|
||||
- **A rendered shell is not enough** — always verify that SSR HTML actually contains page-critical content and navigation.
|
||||
|
||||
## Cart persistence (localStorage)
|
||||
|
||||
For SSR-safe cart/inquiry persistence:
|
||||
|
||||
```ts
|
||||
let cartItems = $state<any[]>([])
|
||||
// Laden
|
||||
$effect(() => {
|
||||
try {
|
||||
if (typeof localStorage !== "undefined") {
|
||||
const saved = localStorage.getItem("cart_key")
|
||||
if (saved) cartItems = JSON.parse(saved)
|
||||
}
|
||||
} catch {}
|
||||
})
|
||||
// Speichern
|
||||
$effect(() => {
|
||||
try {
|
||||
if (typeof localStorage !== "undefined") {
|
||||
localStorage.setItem("cart_key", JSON.stringify(cartItems))
|
||||
}
|
||||
} catch {}
|
||||
})
|
||||
```
|
||||
|
||||
Immer mit `typeof localStorage !== "undefined"` für SSR-Sicherheit.
|
||||
|
||||
@@ -15,6 +15,239 @@ Use this skill when:
|
||||
- Defining publication windows and how they interact with runtime and SSR
|
||||
- Building authoring workflows around images, metadata, and release control
|
||||
|
||||
## Medialib file serving
|
||||
|
||||
Images uploaded to medialib (via base64 or multipart) are stored with a relative `file.src` path such as `"file/example.jpg"`. In this starter, `MedialibImage` resolves that stored path together with the medialib entry ID into the project-local URL:
|
||||
|
||||
```
|
||||
/api/medialib/{id}/file/example.jpg
|
||||
```
|
||||
|
||||
Responsive image filters are then applied by the shared widget as query params:
|
||||
|
||||
```
|
||||
/api/medialib/{id}/file/example.jpg?filter=s-webp
|
||||
/api/medialib/{id}/file/example.jpg?filter=m-webp
|
||||
/api/medialib/{id}/file/example.jpg?filter=l-webp
|
||||
```
|
||||
|
||||
Those `/api/...` URLs are starter-local proxy URLs. For frontend rendering in Tibi website projects, the preferred approach is **not** to hand-build medialib URLs in each block or route component.
|
||||
|
||||
## Collection uploadPath
|
||||
|
||||
For Tibi file fields, the storage root is configured per collection via top-level `uploadPath`.
|
||||
|
||||
Important rules:
|
||||
|
||||
- `uploadPath` belongs on the collection itself, not on the individual `type: file` field
|
||||
- in this starter, collection YAML files live in `api/collections/`, while deploy syncs the repo-root `media/` directory
|
||||
- the current starter collections can omit `uploadPath` and rely on the tibi-server default derived from project config
|
||||
- if you explicitly override `uploadPath` in this starter, it should normally point to `../media/<collection-name>` so deploy and runtime stay aligned
|
||||
- do not write uploads into `api/media`; that path is not the persistent deploy target used by the project
|
||||
|
||||
Explicit override example:
|
||||
|
||||
```yaml
|
||||
name: medialib
|
||||
uploadPath: ../media/medialib
|
||||
|
||||
fields:
|
||||
- name: file
|
||||
type: file
|
||||
```
|
||||
|
||||
For hidden thumbnails stored on the `content` collection, the same rule applies when you choose an explicit override at collection level:
|
||||
|
||||
```yaml
|
||||
name: content
|
||||
uploadPath: ../media/content
|
||||
|
||||
fields:
|
||||
- name: _pagebuilderThumbnail
|
||||
type: file
|
||||
```
|
||||
|
||||
Tibi then stores uploads below:
|
||||
|
||||
```text
|
||||
{uploadPath}/{entryId}/{fieldName}/{filename}
|
||||
```
|
||||
|
||||
So an explicitly overridden medialib upload typically lands at `../media/medialib/{entryId}/file/{filename}`, while a content thumbnail lands at `../media/content/{entryId}/_pagebuilderThumbnail/{filename}`.
|
||||
|
||||
## Preferred frontend integration
|
||||
|
||||
Use a shared media widget such as `MedialibImage` as the frontend boundary for medialib-rendered images.
|
||||
|
||||
### MedialibImage with minimal entry (ID only)
|
||||
|
||||
When only a medialib ID is available (no resolved `_lookup` entry), pass a minimal entry structure together with the `id` prop:
|
||||
|
||||
```svelte
|
||||
<MedialibImage
|
||||
id={medialibId}
|
||||
entry={{ file: { src: "file/example.jpg", type: "image/jpeg" } }}
|
||||
class="w-full h-full object-cover"
|
||||
noPlaceholder
|
||||
/>
|
||||
```
|
||||
|
||||
`resolveFileSrc()` in `MedialibImage` kombiniert `entry.file.src` (zum Beispiel `"file/example.jpg"`) mit der `id` zur korrekten URL: `${apiBase}/medialib/{id}/{src}`. Filter werden im Widget als `?filter=...` angehängt. Wenn `_lookup`-Daten mit vollständigem Entry verfügbar sind, bevorzugt diese verwenden:
|
||||
|
||||
```svelte
|
||||
<MedialibImage entry={resolvedEntry} class="w-full h-full object-cover" noPlaceholder />
|
||||
```
|
||||
|
||||
The preferred flow is:
|
||||
|
||||
1. request medialib references with `lookup`
|
||||
2. pass the resolved `MedialibEntry` to the shared widget
|
||||
3. let that widget own URL resolution, filter choice, SSR markup, and admin/pagebuilder compatibility
|
||||
|
||||
Typical usage:
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import MedialibImage from "../widgets/MedialibImage.svelte"
|
||||
|
||||
let { block }: { block: ContentBlockEntry } = $props()
|
||||
</script>
|
||||
|
||||
{#if block._lookup?.image}
|
||||
<MedialibImage
|
||||
entry={block._lookup.image}
|
||||
class="w-full h-full object-cover"
|
||||
minWidth={900}
|
||||
lazy={true}
|
||||
/>
|
||||
{/if}
|
||||
```
|
||||
|
||||
For repeated collection data such as galleries, teaser lists, or detail-page image arrays, also request the lookup instead of rendering from raw ID strings:
|
||||
|
||||
```ts
|
||||
const entries = await getCachedEntries<CollectionName>(
|
||||
"your-collection",
|
||||
{ active: true },
|
||||
"sortOrder",
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
"imageField:medialib"
|
||||
)
|
||||
```
|
||||
|
||||
`getCachedEntries()` expects `lookup` as the 8th argument and as a string, not as part of the `params` object.
|
||||
|
||||
Then consume the resolved entry from `_lookup`, for example `_lookup.imageField` or `_lookup.imageField?.[0]` depending on whether the schema stores one image or an array.
|
||||
|
||||
### URL resolution strategy (frontend)
|
||||
|
||||
At the shared widget boundary, resolve image data with this priority:
|
||||
|
||||
1. Prefer a resolved `MedialibEntry` from `_lookup`
|
||||
2. If `entry.file.src` is already absolute, use it directly
|
||||
3. Otherwise construct the file URL from `{apiBase}/medialib/{entryId}/{file.src}` inside the shared widget
|
||||
4. Only fall back to raw ID-to-URL construction in legacy code paths that cannot yet pass resolved entries
|
||||
|
||||
Do not duplicate medialib URL logic in every block. Keep it in one widget/helper layer so SSR, admin preview, and filter behavior stay consistent.
|
||||
|
||||
```ts
|
||||
function resolveFileSrc(src: string | undefined, entryId: string | undefined, apiBase: string): string | null {
|
||||
if (!src) return null
|
||||
if (/^(https?:)?\/\//.test(src) || src.startsWith("/")) return src
|
||||
if (!entryId) return null
|
||||
return `${apiBase.replace(/\/+$/, "")}/medialib/${entryId}/${src.replace(/^\/+/, "")}`
|
||||
}
|
||||
```
|
||||
|
||||
### Lookup parameter
|
||||
|
||||
Use the `lookup` API parameter to resolve medialib references automatically:
|
||||
|
||||
```
|
||||
/api/{collection}?lookup={fieldPath}:medialib
|
||||
```
|
||||
|
||||
The resolved data is available in the `_lookup` field of the returned object at the corresponding path.
|
||||
|
||||
Typical project patterns:
|
||||
|
||||
- pagebuilder blocks with nested image refs: `blocks.someImageField:medialib`
|
||||
- collection entries with a single image field: `image:medialib`
|
||||
- collection entries with image arrays: `images:medialib`
|
||||
- attachment arrays or document previews: `attachments:medialib`
|
||||
|
||||
Prefer adding the lookup at the data-loading boundary rather than rehydrating IDs later in the component tree.
|
||||
|
||||
## Filter sizing strategy
|
||||
|
||||
Do not hardcode one fixed filter per component unless the rendered width is truly fixed.
|
||||
|
||||
Preferred rules:
|
||||
|
||||
1. explicit `filter` prop wins when the caller already knows the right output size
|
||||
2. otherwise pass a meaningful `minWidth` for layout-stable contexts such as hero, card, or gallery slots
|
||||
3. let the shared widget derive the final filter from the measured width on the client
|
||||
4. keep the width-to-filter mapping centralized in the widget instead of repeating `xs/s/m/l/xl` logic in blocks
|
||||
|
||||
This keeps image payloads reasonable without forcing each block author to manually guess the correct filter.
|
||||
|
||||
Typical examples:
|
||||
|
||||
- hero/background image: `minWidth={1600}`
|
||||
- card image: `minWidth={640}`
|
||||
- product detail main gallery image: `minWidth={960}`
|
||||
- thumbnail image: `minWidth={240}`
|
||||
|
||||
The exact breakpoints can vary per project, but the sizing logic should remain centralized.
|
||||
|
||||
## SSR and no-JS behavior
|
||||
|
||||
For raster images, SSR cannot always know the final client width. The shared widget should therefore render deterministically:
|
||||
|
||||
1. render a normal `img` with a real `src` in SSR when a filter is explicit, `minWidth` is known, or admin rendering requires a fallback filter
|
||||
2. emit a `noscript` fallback for raster images so crawlers and JS-disabled clients still receive a concrete image URL
|
||||
3. avoid unstable per-block SSR hacks that guess widths differently from the client
|
||||
|
||||
This is especially important for image-bearing blocks, teasers, galleries, and detail views where HTML must remain stable between SSR, hydration, and no-JS rendering.
|
||||
|
||||
## Admin and pagebuilder compatibility
|
||||
|
||||
Medialib rendering must also work inside admin/pagebuilder preview contexts, not only on the public website.
|
||||
|
||||
Important rules:
|
||||
|
||||
- respect `apiBaseOverride` or the admin-provided project base when constructing medialib URLs
|
||||
- do not prepend frontend public paths blindly when the admin already passes an absolute file URL
|
||||
- consume hydrated `_lookup` data directly in preview/runtime code instead of trying to re-fetch references inside preview components
|
||||
- keep placeholder asset resolution admin-safe too, not just medialib file URLs
|
||||
|
||||
If a block preview loads collection data in the admin, the preview context must provide the project-specific API base and the frontend code must route requests through that base.
|
||||
|
||||
In practice, that means the media widget and any helper used by it should be aware of `apiBaseOverride` or an equivalent admin-provided API base so the same component works in:
|
||||
|
||||
- public frontend
|
||||
- SSR render
|
||||
- admin pagebuilder preview
|
||||
- collection-driven block previews
|
||||
|
||||
### Base64 file upload via API
|
||||
|
||||
Upload images to medialib via JSON API by including base64 data inline:
|
||||
|
||||
```json
|
||||
POST /api/medialib
|
||||
{
|
||||
"file": { "src": "data:image/jpeg;base64,..." },
|
||||
"title": "Image title",
|
||||
"alt": "Alt text"
|
||||
}
|
||||
```
|
||||
|
||||
The tibi-server saves the file below the collection's `uploadPath`, for example `../media/medialib/{entryId}/file/{filename}`, and creates the medialib entry.
|
||||
|
||||
## Goal
|
||||
|
||||
The goal is to make media, SEO, and publishing part of the actual solution design instead of leaving them as late add-ons.
|
||||
@@ -32,11 +265,12 @@ For real website projects, these concerns affect:
|
||||
Use these sources when implementing or reviewing these areas:
|
||||
|
||||
- `tibi-server/docs/08-file-upload-images.md`
|
||||
- `api/collections/medialib.yml`
|
||||
- `api/collections/content.yml`
|
||||
- the project's medialib collection config
|
||||
- the relevant content or domain collection configs
|
||||
- `tibi-admin-nova/types/admin.d.ts`
|
||||
- `tibi-admin-nova/docs/collection-config.md`
|
||||
- `api/hooks/config.js`
|
||||
- `api/hooks/lib/ssr-server.js`
|
||||
- the project's SSR/runtime config hooks
|
||||
- the project's frontend media widget/helper implementation
|
||||
|
||||
## Media modeling
|
||||
|
||||
@@ -175,19 +409,37 @@ Recommended shape:
|
||||
- No explicit alt field
|
||||
- Mixing captions and alt text into one field
|
||||
- Hardcoding image sizes only in frontend CSS/components
|
||||
- Building raw `/api/medialib/{id}/{src}` URLs in every block or hardcoding `file/file` instead of using one shared widget
|
||||
- Repeating filter breakpoint logic in multiple components
|
||||
- Rendering public image URLs in frontend code that break in admin pagebuilder preview
|
||||
- Treating publication as frontend-only logic
|
||||
- Forgetting that publish windows can invalidate SSR HTML
|
||||
|
||||
## Publication verification matrix
|
||||
|
||||
If the project uses publication timing, do not verify only one happy-path entry.
|
||||
|
||||
Check a small matrix deliberately:
|
||||
|
||||
1. currently published entry
|
||||
2. future-scheduled entry
|
||||
3. expired entry
|
||||
4. token/admin visibility when editorial or operator access should still exist
|
||||
|
||||
This keeps publication modeling tied to the real public-read and SSR behavior instead of to optimistic field design.
|
||||
|
||||
## Verification checklist
|
||||
|
||||
After changing media/SEO/publishing behavior, verify all of these:
|
||||
|
||||
1. Upload validation matches the intended asset type.
|
||||
2. Image filters are named and used consistently.
|
||||
3. Alt/caption/SEO fields are explicit and editor-friendly.
|
||||
4. Publication state affects public output correctly.
|
||||
5. SSR HTML still reflects the intended published state.
|
||||
6. `yarn validate` stays clean.
|
||||
3. Shared image widgets receive resolved entries instead of rebuilding URLs ad hoc.
|
||||
4. Alt/caption/SEO fields are explicit and editor-friendly.
|
||||
5. Publication state affects public output correctly.
|
||||
6. SSR HTML still reflects the intended published state.
|
||||
7. Admin/pagebuilder preview resolves medialib images correctly.
|
||||
8. `yarn validate` stays clean.
|
||||
|
||||
## What an LLM should inspect first
|
||||
|
||||
@@ -195,8 +447,9 @@ When asked to work on media, SEO, or publishing on this starter, inspect in this
|
||||
|
||||
1. `tibi-server/docs/08-file-upload-images.md`
|
||||
2. the relevant collection YAML
|
||||
3. admin layout and previews for those fields
|
||||
4. frontend components consuming the media/SEO data
|
||||
5. SSR publish-check and invalidation logic if timing matters
|
||||
3. the shared media widget/helper layer used by the frontend
|
||||
4. admin layout and previews for those fields
|
||||
5. frontend components consuming the media/SEO data
|
||||
6. SSR publish-check and invalidation logic if timing matters
|
||||
|
||||
This prevents “just add an image field” changes that break runtime, editorial UX, or caching.
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
---
|
||||
name: mongodb-and-indexes
|
||||
description: Design MongoDB prerequisites and index strategy for tibi website projects. Covers replica-set requirements, collection index modeling, text/search index implications, index auto-management on reload, and operator checks for persistence, backups, and regeneration-sensitive features.
|
||||
---
|
||||
|
||||
# mongodb-and-indexes
|
||||
|
||||
## When to use this skill
|
||||
|
||||
Use this skill when:
|
||||
|
||||
- a project moves beyond simple demo data and needs deliberate MongoDB design
|
||||
- collections need unique, text, sparse, or compound indexes
|
||||
- search, audit, or larger datasets introduce operational database requirements
|
||||
- operators need clarity about replica-set, persistence, or backup assumptions
|
||||
|
||||
## Goal
|
||||
|
||||
Give later agents one place for MongoDB prerequisites and index strategy.
|
||||
|
||||
This skill exists because tibi projects do not just “use MongoDB”. They rely on config-driven indexes, reload-time index reconciliation, and environment choices that can help or break production behavior.
|
||||
|
||||
## Source of truth
|
||||
|
||||
Use these sources when implementing or reviewing MongoDB/index decisions:
|
||||
|
||||
- `tibi-server/docs/02-configuration.md`
|
||||
- `tibi-server/docs/04-collections.md`
|
||||
- `tibi-server/docs/12-deployment.md`
|
||||
- active project/server config
|
||||
|
||||
## MongoDB prerequisites
|
||||
|
||||
Current upstream deployment requirements include:
|
||||
|
||||
- MongoDB 4.4+
|
||||
- replica set for transactions
|
||||
|
||||
That means later agents should not promise production-like behavior while ignoring the actual database topology.
|
||||
|
||||
For this starter family, check explicitly:
|
||||
|
||||
- which MongoDB version is targeted
|
||||
- whether the environment runs as a replica set
|
||||
- where persistent data lives
|
||||
- how backup and restore are expected to work
|
||||
|
||||
## Index ownership model
|
||||
|
||||
Indexes can be defined in two places.
|
||||
|
||||
### Field-level indexes
|
||||
|
||||
Use for simple per-field indexes:
|
||||
|
||||
```yaml
|
||||
fields:
|
||||
- name: email
|
||||
type: string
|
||||
index:
|
||||
- single
|
||||
- unique
|
||||
```
|
||||
|
||||
Available field-level flags include:
|
||||
|
||||
- `single`
|
||||
- `unique`
|
||||
- `text`
|
||||
- `sparse`
|
||||
|
||||
### Collection-level indexes
|
||||
|
||||
Use for compound or more explicit index definitions:
|
||||
|
||||
```yaml
|
||||
indexes:
|
||||
- name: category_publish_date
|
||||
key:
|
||||
- category
|
||||
- -publishDate
|
||||
background: true
|
||||
- name: title_text
|
||||
key:
|
||||
- $text:title
|
||||
defaultLanguage: german
|
||||
```
|
||||
|
||||
Use collection-level indexes when:
|
||||
|
||||
- sort patterns span multiple fields
|
||||
- uniqueness depends on more than one field
|
||||
- you need text-index language control
|
||||
- the index needs a stable explicit name
|
||||
|
||||
## Important auto-management behavior
|
||||
|
||||
On project setup or reload:
|
||||
|
||||
1. configured indexes are created
|
||||
2. indexes present in MongoDB but no longer in config are dropped
|
||||
3. `_id_` is never dropped
|
||||
|
||||
This is a critical workflow fact.
|
||||
|
||||
Do not treat indexes as one-off manual DBA work while also expecting config reload to stay authoritative. In this stack, the YAML config owns the intended index state.
|
||||
|
||||
## Search and index interplay
|
||||
|
||||
Some search modes depend directly on index strategy:
|
||||
|
||||
- `mode: text` requires a text index
|
||||
- explicit text search can use field-level `index: [text]` or collection-level `$text:` indexes
|
||||
- regex fallback can become expensive without deliberate field/index choices
|
||||
- enrichment-based modes such as `ngram` and `vector` add `_search` data and may require regeneration workflows rather than classic MongoDB indexes alone
|
||||
|
||||
Keep classic indexes and search config aligned. Search should not be modeled in isolation from database cost and reload behavior.
|
||||
|
||||
## Query shape matters
|
||||
|
||||
Before adding indexes, inspect the real access patterns:
|
||||
|
||||
- which fields appear in permission filters
|
||||
- which fields appear in list sorting
|
||||
- which fields appear in frequent admin or public lookups
|
||||
- whether uniqueness is a real business rule or only a nice-to-have
|
||||
|
||||
Index design should follow concrete read and write patterns, not schema aesthetics.
|
||||
|
||||
## Operational decisions
|
||||
|
||||
At project-delivery level, document these choices explicitly:
|
||||
|
||||
- MongoDB version and topology
|
||||
- replica-set availability
|
||||
- persistence location for database and uploads
|
||||
- backup/restore responsibility
|
||||
- whether audit/search features introduce extra operational expectations
|
||||
|
||||
If these decisions are absent, later agents tend to over-focus on schema YAML while missing the operator-critical data layer.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- adding indexes without checking actual query or sort behavior
|
||||
- relying on manual indexes that the config will later drop
|
||||
- enabling text search without provisioning the needed text index
|
||||
- treating replica-set requirements as optional trivia
|
||||
- shipping production projects without a backup and restore story
|
||||
|
||||
## Verification checklist
|
||||
|
||||
After MongoDB/index-related changes, verify all of these:
|
||||
|
||||
1. the target environment satisfies MongoDB version and replica-set needs
|
||||
2. configured indexes match real query, sort, and uniqueness requirements
|
||||
3. project reload succeeds after index changes
|
||||
4. text/search features have the required index support
|
||||
5. persistence and backup assumptions are documented
|
||||
|
||||
## What an LLM should inspect first
|
||||
|
||||
When asked to design or review the data layer on this starter, inspect in this order:
|
||||
|
||||
1. `tibi-server/docs/12-deployment.md`
|
||||
2. `tibi-server/docs/02-configuration.md`
|
||||
3. `tibi-server/docs/04-collections.md`
|
||||
4. current collection YAML for indexes/search
|
||||
5. actual query, sort, audit, and search requirements
|
||||
|
||||
This keeps index design tied to real runtime behavior and not just to field definitions.
|
||||
@@ -0,0 +1,156 @@
|
||||
---
|
||||
name: monitoring-and-performance
|
||||
description: Configure and verify observability for tibi website projects. Covers OpenAPI exposure, Prometheus metrics, Sentry wiring, health/reachability checks, and the operator-facing validation that should exist before a project is considered production-ready.
|
||||
---
|
||||
|
||||
# monitoring-and-performance
|
||||
|
||||
## When to use this skill
|
||||
|
||||
Use this skill when:
|
||||
|
||||
- a project needs operator-facing visibility beyond “the page loads”
|
||||
- you need OpenAPI output for integrations or documentation
|
||||
- you need Prometheus/Grafana visibility
|
||||
- you need Sentry or similar error visibility in frontend or server flows
|
||||
- you want to define the minimum health and observability checks for deploys
|
||||
|
||||
## Goal
|
||||
|
||||
Give later agents a concrete workflow for deciding what should be observable, how it is exposed, and how to verify that exposure.
|
||||
|
||||
This skill is not about arbitrary performance tuning. It is about making the running system inspectable enough that operators and developers can see whether it is healthy.
|
||||
|
||||
## Source of truth
|
||||
|
||||
Use these sources when implementing or reviewing observability:
|
||||
|
||||
- `tibi-server/docs/13-openapi-metrics.md`
|
||||
- `tibi-server/docs/02-configuration.md`
|
||||
- `.agents/skills/deployment/SKILL.md`
|
||||
- `frontend/src/config.ts`
|
||||
- relevant deploy scripts and env/config files
|
||||
|
||||
## OpenAPI exposure
|
||||
|
||||
Tibi-server generates an OpenAPI spec per project:
|
||||
|
||||
```text
|
||||
GET /api/v1/{namespace}/openapi
|
||||
```
|
||||
|
||||
Use this when:
|
||||
|
||||
- a project exposes public API surfaces that need documentation
|
||||
- integrations or client generators benefit from a machine-readable contract
|
||||
|
||||
Collection-level OpenAPI customization lives in collection metadata via `meta.openapi`.
|
||||
|
||||
Use that metadata deliberately to:
|
||||
|
||||
- hide endpoints that should not appear in the spec
|
||||
- add summaries and descriptions
|
||||
- keep the public API contract readable
|
||||
|
||||
## Metrics exposure
|
||||
|
||||
Prometheus metrics are exposed at:
|
||||
|
||||
```text
|
||||
GET /metrics
|
||||
```
|
||||
|
||||
Key upstream metric documented today:
|
||||
|
||||
- `tibi_request_duration_seconds`
|
||||
|
||||
This is useful for:
|
||||
|
||||
- request latency visibility
|
||||
- collection-level timing comparisons
|
||||
- basic traffic and error observation in Grafana/Prometheus
|
||||
|
||||
Do not enable metrics-like operator expectations in a project and then forget to verify the endpoint actually works in the target environment.
|
||||
|
||||
## Sentry and error visibility
|
||||
|
||||
This stack can surface errors through Sentry-related configuration.
|
||||
|
||||
Relevant surfaces include:
|
||||
|
||||
- server-level `sentry` config in tibi-server
|
||||
- frontend runtime wiring in `frontend/src/config.ts`
|
||||
- deploy-time release/build metadata injection where the project uses it
|
||||
|
||||
Use Sentry deliberately:
|
||||
|
||||
- define DSN, environment, and release expectations
|
||||
- know whether tracing is wanted or only error capture
|
||||
- make sure deploy scripts and build metadata agree with the runtime setup
|
||||
|
||||
Do not leave a half-configured Sentry setup that looks present but produces unusable traces.
|
||||
|
||||
## Health and reachability checks
|
||||
|
||||
At minimum, operators should be able to verify:
|
||||
|
||||
- website URL responds
|
||||
- admin URL responds
|
||||
- API responds
|
||||
- OpenAPI and metrics endpoints respond when they are intended to be used
|
||||
|
||||
In this repo family, simple reachability probes are often the first useful health signal. For project delivery, these checks belong next to deploy and sign-off work, not only in ad-hoc troubleshooting.
|
||||
|
||||
## Recommended patterns
|
||||
|
||||
### Public API projects
|
||||
|
||||
Recommended shape:
|
||||
|
||||
- expose OpenAPI intentionally
|
||||
- add `meta.openapi` summaries for meaningful endpoints
|
||||
- verify the spec against the current collection model
|
||||
|
||||
### Operated production projects
|
||||
|
||||
Recommended shape:
|
||||
|
||||
- metrics endpoint reachable in the target environment
|
||||
- at least one documented Grafana/Prometheus use case for request timing
|
||||
- explicit decision whether Sentry is used or intentionally not used
|
||||
|
||||
### Basic website deployments
|
||||
|
||||
Recommended shape:
|
||||
|
||||
- website/admin/API reachability checks are part of deploy verification
|
||||
- observability is documented enough that later operators know what exists
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- treating observability as optional once the build passes
|
||||
- exposing OpenAPI or metrics accidentally without deciding who uses them
|
||||
- half-configured Sentry with no useful environment or release handling
|
||||
- relying on manual browser clicks as the only production health check
|
||||
|
||||
## Verification checklist
|
||||
|
||||
After observability-related work, verify all of these:
|
||||
|
||||
1. intended OpenAPI exposure works and reflects the current collection config
|
||||
2. intended metrics exposure works in the target environment
|
||||
3. Sentry/error visibility is either intentionally configured or intentionally absent
|
||||
4. deploy-time reachability checks cover website, admin, and API
|
||||
5. `yarn build`, `yarn build:server`, and `yarn validate` still pass when observability wiring touched frontend/server config
|
||||
|
||||
## What an LLM should inspect first
|
||||
|
||||
When asked to set up monitoring or observability on this starter, inspect in this order:
|
||||
|
||||
1. `tibi-server/docs/13-openapi-metrics.md`
|
||||
2. `tibi-server/docs/02-configuration.md`
|
||||
3. deploy scripts and env/config files
|
||||
4. `frontend/src/config.ts`
|
||||
5. whether the project truly needs OpenAPI, metrics, Sentry, or only reachability checks
|
||||
|
||||
This prevents over-documenting features that are not actually wired and under-documenting the ones that matter operationally.
|
||||
@@ -0,0 +1,150 @@
|
||||
---
|
||||
name: multi-tenancy-and-orgs
|
||||
description: Model org/team-aware tibi projects. Covers the org-team-user hierarchy, visibility vs working rights, permission resolution, project assignment, audit visibility, and how later agents should make an explicit single-tenant vs org/team decision.
|
||||
---
|
||||
|
||||
# multi-tenancy-and-orgs
|
||||
|
||||
## When to use this skill
|
||||
|
||||
Use this skill when:
|
||||
|
||||
- a project might need org/team-aware isolation or working rights
|
||||
- multiple organizations or departments share one tibi installation
|
||||
- project visibility and editing rights must be separated cleanly
|
||||
- LLM budget ownership or audit visibility must follow org boundaries
|
||||
|
||||
## Goal
|
||||
|
||||
Give later agents a concrete implementation workflow for enterprise-aware projects and a clear off-ramp for single-tenant projects.
|
||||
|
||||
The most important decision is not how to model orgs. It is whether the project should use org/team features at all.
|
||||
|
||||
## Source of truth
|
||||
|
||||
Use these sources when implementing or reviewing org/team-aware projects:
|
||||
|
||||
- `tibi-server/docs/18-orgs-teams.md`
|
||||
- `tibi-server/docs/05-authentication.md`
|
||||
- `tibi-server/docs/17-field-level-permissions.md`
|
||||
- `.agents/skills/permissions-and-editor-workflows/SKILL.md`
|
||||
- `.agents/skills/search-and-embeddings/SKILL.md` when LLM budget or shared AI/search budgets matter
|
||||
|
||||
## First decision: single-tenant or org/team-aware
|
||||
|
||||
Make this decision explicitly.
|
||||
|
||||
### Single-tenant projects
|
||||
|
||||
Recommended shape:
|
||||
|
||||
- do not model org/team support by default
|
||||
- document that visibility and permissions are handled without enterprise isolation
|
||||
- keep the project simpler unless there is a real multi-tenant requirement
|
||||
|
||||
### Org/team-aware projects
|
||||
|
||||
Use this branch only when you actually need:
|
||||
|
||||
- org-scoped project visibility
|
||||
- team-based working rights
|
||||
- cross-user governance inside shared organizations
|
||||
- org-level budget ownership or audit visibility constraints
|
||||
|
||||
## Core model
|
||||
|
||||
The upstream hierarchy is:
|
||||
|
||||
- org = visibility boundary
|
||||
- team = working-rights unit
|
||||
- user = member of orgs and teams
|
||||
|
||||
Key fields include:
|
||||
|
||||
- `project.orgId`
|
||||
- `project.teams[]`
|
||||
- `user.orgs[]`
|
||||
- `user.teams[]`
|
||||
- `user.primaryOrgId`
|
||||
|
||||
Do not flatten these concepts into a generic “role” discussion. Org visibility and team working rights are different concerns.
|
||||
|
||||
## Visibility vs working rights
|
||||
|
||||
Important rule:
|
||||
|
||||
- org membership controls which projects a user can see
|
||||
- team assignment controls which permission sets a user gets inside those visible projects
|
||||
|
||||
This is the main conceptual boundary later agents must keep intact.
|
||||
|
||||
If a user can see a project but cannot edit it, that can be correct. Visibility is not the same as edit access.
|
||||
|
||||
## Permission resolution
|
||||
|
||||
Team permissions map onto collection permission keys.
|
||||
|
||||
Example idea:
|
||||
|
||||
- team carries `permissions: ["editor"]`
|
||||
- collection defines an `editor` permission set
|
||||
- assigned team members inherit that working-rights set on the project
|
||||
|
||||
Also important:
|
||||
|
||||
- multiple team permissions merge as a union
|
||||
- admin tokens and system admins still sit above team-based resolution
|
||||
- token/public/user/team/custom permission order matters
|
||||
|
||||
## Project assignment rules
|
||||
|
||||
For org/team-aware projects, later agents must design all of these deliberately:
|
||||
|
||||
- which org owns the project
|
||||
- which teams are assigned to the project
|
||||
- who is allowed to manage those assignments
|
||||
- how custom collection permission keys map to teams
|
||||
|
||||
Half-modeled org/team setups create the worst of both worlds: extra complexity without trustworthy isolation.
|
||||
|
||||
## Audit and LLM implications
|
||||
|
||||
Enterprise modeling affects more than CRUD visibility.
|
||||
|
||||
Relevant side effects:
|
||||
|
||||
- audit visibility follows the team → project → collection access chain
|
||||
- non-admin users do not see system audit entries
|
||||
- LLM/org budget logic uses `user.primaryOrgId`
|
||||
|
||||
That means enterprise design can affect audit reviews and AI cost ownership even if the public website itself looks simple.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- enabling org/team concepts “just in case”
|
||||
- using teams as visibility boundaries instead of orgs
|
||||
- mixing custom collection permissions and team permissions without naming discipline
|
||||
- forgetting that users can belong to multiple teams
|
||||
- introducing org-aware billing or audit expectations without modeling `primaryOrgId`
|
||||
|
||||
## Verification checklist
|
||||
|
||||
After org/team-related changes, verify all of these:
|
||||
|
||||
1. the project explicitly states single-tenant or org/team-aware
|
||||
2. org ownership and team assignment are defined for enterprise projects
|
||||
3. collection permission keys map cleanly to team permissions
|
||||
4. representative visibility and edit-rights scenarios behave as designed
|
||||
5. audit and LLM budget implications are understood when those features are in scope
|
||||
|
||||
## What an LLM should inspect first
|
||||
|
||||
When asked whether a project needs enterprise features, inspect in this order:
|
||||
|
||||
1. the project's actual tenancy requirement
|
||||
2. `tibi-server/docs/18-orgs-teams.md`
|
||||
3. current permission model in collection configs
|
||||
4. whether audit visibility or LLM budgets depend on org ownership
|
||||
5. whether the simpler single-tenant branch is the right answer
|
||||
|
||||
This prevents unnecessary enterprise complexity in projects that only need normal editorial permissions.
|
||||
@@ -25,8 +25,8 @@ Use AI where it improves editorial throughput or content quality. Do not add AI
|
||||
Use these sources when implementing or reviewing AI-backed website features:
|
||||
|
||||
- `tibi-server/docs/09-llm-integration.md`
|
||||
- `tibi-admin-nova/docs/collection-config.md`
|
||||
- `tibi-admin-nova/types/admin.d.ts`
|
||||
- `tibi-admin-nova/docs/collection-config.md`
|
||||
- `api/config.yml`
|
||||
- the project's actual Nova runtime config when such a file exists
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ On this stack, navigation influences:
|
||||
|
||||
Use these sources when implementing or reviewing navigation modeling:
|
||||
|
||||
- `tibi-admin-nova/types/admin.d.ts`
|
||||
- `tibi-admin-nova/docs/collection-config.md`
|
||||
- `api/collections/navigation.yml`
|
||||
- `frontend/src/App.svelte`
|
||||
@@ -186,7 +187,7 @@ After changing navigation modeling, verify all of these:
|
||||
When asked to work on navigation in this starter, inspect in this order:
|
||||
|
||||
1. `api/collections/navigation.yml`
|
||||
2. `tibi-admin-nova/docs/collection-config.md` section for `viewHint.navigation`
|
||||
2. `tibi-admin-nova/types/admin.d.ts` plus `tibi-admin-nova/docs/collection-config.md` for `viewHint.navigation`
|
||||
3. the frontend navigation loading/rendering path
|
||||
4. SSR assumptions around header/footer shell data
|
||||
5. the website's language and information-architecture requirements
|
||||
|
||||
@@ -198,7 +198,7 @@ The exact shape can vary, but the pattern stays the same: block type first, then
|
||||
|
||||
### Step 3a: Build and wire the block registry
|
||||
|
||||
For this starter, the pagebuilder registry is not implicit. Nova loads it from the admin bundle via `meta.pagebuilder.blockRegistry.file`.
|
||||
In current Tibi/Nova projects, the pagebuilder registry is typically not implicit. Nova loads it from the admin bundle via `meta.pagebuilder.blockRegistry.file`.
|
||||
|
||||
The concrete chain is:
|
||||
|
||||
@@ -207,7 +207,7 @@ The concrete chain is:
|
||||
3. build the admin bundle with `yarn build`
|
||||
4. point the collection field or collection meta to the built module
|
||||
|
||||
The current starter already does this in `frontend/src/admin.ts` and `api/collections/content.yml`.
|
||||
The concrete file names vary by project, but the pattern is the same: registry code lives in the admin bundle and collection config points to the built admin asset.
|
||||
|
||||
Typical starter pattern:
|
||||
|
||||
@@ -238,7 +238,7 @@ meta:
|
||||
file: /_/assets/dist/admin.mjs?v=${ADMIN_ASSET_VERSION}
|
||||
```
|
||||
|
||||
Important constraints for this starter:
|
||||
Important constraints for this setup:
|
||||
|
||||
- the registry module must be part of the admin bundle, not a random standalone file outside the build pipeline
|
||||
- the exported registry keys must match the block type values stored in the collection
|
||||
@@ -246,6 +246,26 @@ Important constraints for this starter:
|
||||
- if the registry file path in YAML and the built admin asset diverge, Nova can still render the schema but the pagebuilder preview/picker loses its real block definitions
|
||||
- in Nova pagebuilder preview, file fields are already normalized by the admin backend to absolute `http(s)://...` URLs when appropriate; preview code must not prepend `apiBase`, `projectBase`, or other frontend URL helpers when the value is already absolute
|
||||
- Nova may also pass preview rows with hydrated `_lookup` data for FK-like fields; the registry/block preview should consume that data directly instead of trying to re-fetch or manually hydrate references inside the admin preview
|
||||
- For medialib-based images, prefer the same shared frontend widget used on the public site rather than preview-only URL logic. The widget/helper must honor `apiBaseOverride` so filter URLs, placeholders, and medialib files keep working inside admin preview.
|
||||
- Nova's `render(container, row, context)` provides API path information. **`context.projectBase`** contains the full project-specific API base including namespace. `context.apiBase` may only contain the generic `/api/` root and is often not sufficient for project-scoped collection endpoints. Blocks that load collection data in preview should therefore use the project-specific base when one is available:
|
||||
|
||||
```ts
|
||||
import { apiBaseOverride } from "./lib/store"
|
||||
import { get } from "svelte/store"
|
||||
const prev = get(apiBaseOverride)
|
||||
if (context?.projectBase) apiBaseOverride.set(String(context.projectBase))
|
||||
// mount(BlockRenderer, ...)
|
||||
// in destroy(): apiBaseOverride.set(prev)
|
||||
```
|
||||
|
||||
When a pagebuilder block renders images from medialib, prefer this pattern:
|
||||
|
||||
1. request lookup-resolved medialib entries in the data load
|
||||
2. pass the resolved entry into the shared image widget
|
||||
3. let that widget decide filter sizing from explicit `filter` or `minWidth`
|
||||
4. rely on `apiBaseOverride` / `context.projectBase` for admin-safe URL resolution
|
||||
|
||||
Do not create a second, preview-only image rendering path that diverges from the public frontend. That usually causes broken placeholders, wrong filter URLs, or SSR/admin mismatches later.
|
||||
|
||||
Use collection-level `meta.pagebuilder.blockRegistry.file` when several pagebuilder fields share the same registry. Override at field level only when one field genuinely needs a different registry.
|
||||
|
||||
@@ -358,6 +378,91 @@ Avoid pushing these concerns into block components unless there is a strong reas
|
||||
- unrelated API fetching
|
||||
- page-level navigation concerns
|
||||
|
||||
### 7a. Admin pagebuilder preview: CSS custom properties in shadow DOM
|
||||
|
||||
The Nova pagebuilder renders block previews in an isolated DOM context (shadow DOM or detached subtree). Tailwind 4's `@theme` directive generates CSS custom properties on `:root`, but these do **not** cascade into shadow DOM contexts.
|
||||
|
||||
**Consequence:** Block previews in the admin can have wrong colors (light text instead of dark, missing brand colors) because `var(--color-ink)` resolves to nothing.
|
||||
|
||||
**Fix:** Add a `:host` selector in the project's CSS file that redeclares the theme variables for the shadow DOM context. Also set a hardcoded `color` fallback on `[data-admin-preview]` since the Nova preview container has this attribute.
|
||||
|
||||
```css
|
||||
:host,
|
||||
[data-admin-preview] {
|
||||
--color-ink: #2c3e45;
|
||||
/* … all theme color variables … */
|
||||
font-family: "Inter Tight", system-ui, sans-serif;
|
||||
color: #2c3e45; /* hardcoded fallback, not var() */
|
||||
}
|
||||
```
|
||||
|
||||
Verify by checking admin pagebuilder block preview after any CSS theme changes.
|
||||
|
||||
### 7b. Admin pagebuilder preview: API calls from dynamic blocks
|
||||
|
||||
Blocks that load data via API (e.g. `CategoryGridBlock` using `getCachedEntries`) need the correct API base URL in the admin preview. The Nova pagebuilder's `render()` callback receives a `context` object with optional `apiBase` and `namespace`, but not all versions provide these.
|
||||
|
||||
The `admin.mjs` is loaded by the pagebuilder via dynamic `import()` — the URL is resolved relative to the admin page, NOT relative to the API. So `import.meta.url` contains the admin's page URL (e.g. `/_/assets/dist/admin.mjs`), not the tibi-server's API URL. Regex extraction from `import.meta.url` for the pattern `/api/_/{namespace}/` does NOT work for this reason.
|
||||
|
||||
**Reliable approach:** use multiple fallbacks in `admin.ts`, with the admin hostname pattern as the most robust:
|
||||
|
||||
1. `context.apiBase` (from Nova when available)
|
||||
2. `context.namespace` (from Nova)
|
||||
3. `import.meta.url` regex (works when admin serves admin.mjs through its own API proxy)
|
||||
4. **Hostname extraction**: admin URL is `{project}-tibiadmin.{domain}` → extract project name
|
||||
5. DOM scan: find any element with `src`/`href` containing `/api/_/{namespace}/`
|
||||
|
||||
```ts
|
||||
const prevApiBase = get(apiBaseOverride)
|
||||
let ns: string | null = null
|
||||
|
||||
if (context?.apiBase) {
|
||||
apiBaseOverride.set(String(context.apiBase))
|
||||
} else {
|
||||
if (context?.namespace) ns = String(context.namespace)
|
||||
if (!ns) {
|
||||
try {
|
||||
ns = ((import.meta as any).url || "").match(/\/api\/_\/([^/]+)\//)?.[1]
|
||||
} catch {}
|
||||
}
|
||||
// Most reliable: admin hostname is always {namespace}-tibiadmin.{domain}
|
||||
if (!ns && typeof window !== "undefined") {
|
||||
const h = window.location.hostname.match(/^(.+?)-tibiadmin\./)
|
||||
if (h) ns = h[1]
|
||||
}
|
||||
// Fallback: scan DOM for API references
|
||||
if (!ns && typeof document !== "undefined") {
|
||||
const el = document.querySelector('[src*="/api/_/"], [href*="/api/_/"]')
|
||||
if (el) {
|
||||
const a = el.getAttribute("src") || el.getAttribute("href") || ""
|
||||
ns = a.match(/\/api\/_\/([^/]+)\//)?.[1] || null
|
||||
}
|
||||
}
|
||||
if (ns) apiBaseOverride.set(`/api/_/${ns}/`)
|
||||
}
|
||||
```
|
||||
|
||||
Set the `apiBaseOverride` store BEFORE mounting the block component so API calls inside `$effect` use the correct base.
|
||||
|
||||
The Nova pagebuilder renders block previews in an isolated DOM context (shadow DOM or detached subtree). Tailwind 4's `@theme` directive generates CSS custom properties on `:root`, but these do **not** cascade into shadow DOM contexts.
|
||||
|
||||
**Consequence:** Block previews in the admin can have wrong colors (light text instead of dark, missing brand colors) because `var(--color-ink)` resolves to nothing.
|
||||
|
||||
**Fix:** Add a `:host` selector in the project's CSS file that redeclares the theme variables for the shadow DOM context. Also set a hardcoded `color` fallback on `[data-admin-preview]` since the Nova preview container has this attribute.
|
||||
|
||||
```css
|
||||
:host,
|
||||
[data-admin-preview] {
|
||||
--color-ink: #2c3e45;
|
||||
--color-ink-2: #3a4d56;
|
||||
/* … all theme color variables … */
|
||||
font-family: "Inter Tight", system-ui, sans-serif;
|
||||
color: #2c3e45; /* hardcoded fallback, not var() */
|
||||
}
|
||||
```
|
||||
|
||||
Verify by checking admin pagebuilder block preview after any CSS theme changes.
|
||||
|
||||
### 8. Prepare for styling consistency across blocks
|
||||
|
||||
A block system works better when blocks share a few stable layout conventions.
|
||||
|
||||
@@ -89,6 +89,23 @@ Important behavior:
|
||||
|
||||
This means field permissions are not mere UI hints. They are enforced server-side.
|
||||
|
||||
## Config delivery matters to the admin UX
|
||||
|
||||
Field permissions also affect what the client receives from the project config.
|
||||
|
||||
Important behavior for non-admin users:
|
||||
|
||||
- effective readonly information is exposed through `yourPermissions[collection].readonlyFields`
|
||||
- statically hidden field definitions are removed from `fields[]`
|
||||
- `hiddenFields` arrays are not delivered as-is to non-admin clients
|
||||
- eval-based field rules stay relevant because they depend on document context
|
||||
|
||||
Implication:
|
||||
|
||||
- Nova and other clients should reflect the real config/permission output instead of pretending every field is always present and editable
|
||||
|
||||
If later agents debug “missing” fields in the admin, check permission-shaped config delivery before assuming the admin UI is broken.
|
||||
|
||||
## Dynamic field rules
|
||||
|
||||
Use eval-based field rules when permissions depend on document state.
|
||||
@@ -101,6 +118,12 @@ Typical examples:
|
||||
|
||||
Use these rules to model real editorial transitions, not to create confusing surprises.
|
||||
|
||||
For each eval-based rule, later agents should be able to name:
|
||||
|
||||
- one allowed write scenario
|
||||
- one denied write scenario
|
||||
- the document state that flips the rule
|
||||
|
||||
## Admin UX must reflect permission reality
|
||||
|
||||
If a field is hidden or readonly for a role, the Nova configuration and layout should support that reality.
|
||||
@@ -114,6 +137,29 @@ Recommended patterns:
|
||||
|
||||
Server permissions are authoritative, but poor admin layout can still create a bad workflow.
|
||||
|
||||
## Permission matrix before YAML
|
||||
|
||||
Before writing or changing permission sets, write down a small matrix for the real actors.
|
||||
|
||||
Typical matrix columns:
|
||||
|
||||
- actor or role
|
||||
- collections they can read
|
||||
- collections they can write
|
||||
- fields hidden from them
|
||||
- fields readonly for them
|
||||
- machine/token access they need
|
||||
|
||||
Typical actors:
|
||||
|
||||
- public
|
||||
- editor
|
||||
- reviewer or publisher
|
||||
- admin
|
||||
- machine token or integration
|
||||
|
||||
This avoids permission YAML that is locally correct but globally incoherent.
|
||||
|
||||
## Tokens and integrations
|
||||
|
||||
Remember that token-based integrations can have their own permission sets.
|
||||
@@ -178,9 +224,11 @@ After changing permissions or editor workflows, verify all of these:
|
||||
1. Collection methods match the intended role model.
|
||||
2. Hidden and readonly field behavior is correct on API reads/writes.
|
||||
3. Dynamic eval rules behave correctly for the intended document states.
|
||||
4. Nova forms remain usable for the non-admin roles that actually work there.
|
||||
5. Token/integration permissions are narrower than admin access when possible.
|
||||
6. `yarn validate` stays clean.
|
||||
4. At least one representative allowed write and one denied write were checked for each important workflow state.
|
||||
5. Non-admin config delivery still makes sense for the admin UI and field layout.
|
||||
6. Nova forms remain usable for the non-admin roles that actually work there.
|
||||
7. Token/integration permissions are narrower than admin access when possible.
|
||||
8. `yarn validate` stays clean.
|
||||
|
||||
## What an LLM should inspect first
|
||||
|
||||
|
||||
@@ -202,6 +202,19 @@ When adding new deterministic coverage, extend the seed data instead of assertin
|
||||
|
||||
## Which test type to use
|
||||
|
||||
## Checklist-facing minimum contract for derived projects
|
||||
|
||||
When this starter is used to build a real website project, the testing layer should usually cover these contracts explicitly:
|
||||
|
||||
1. deterministic seed setup for the data the suite depends on
|
||||
2. API smoke coverage for public reads and important write/action behavior
|
||||
3. desktop E2E coverage for core public journeys such as homepage, navigation, language switching, and 404 behavior
|
||||
4. admin smoke coverage for stable collection/admin contracts
|
||||
5. pagebuilder registry plus real preview rendering when the project uses block-based authoring
|
||||
6. SSR validation through direct endpoint checks, and committed tests where the SSR contract is central and stable enough
|
||||
|
||||
Do not treat the test suite as an optional polish step. It is one of the delivery contracts of the project.
|
||||
|
||||
### API tests
|
||||
|
||||
Use `tests/api/` when validating:
|
||||
@@ -249,6 +262,28 @@ Use `tests/e2e-mobile/` when validating:
|
||||
|
||||
Use `tests/e2e-visual/` only when layout/styling stability matters and a semantic DOM assertion is not enough.
|
||||
|
||||
## SSR validation placement
|
||||
|
||||
Do not try to prove every SSR property only through browser navigation.
|
||||
|
||||
Use direct SSR endpoint checks when:
|
||||
|
||||
- validating route acceptance and canonicalization
|
||||
- validating SSR HTML content
|
||||
- validating cache-hit / cache-miss behavior
|
||||
- validating publication-window effects or cache invalidation after mutations
|
||||
|
||||
Use committed API/E2E tests when:
|
||||
|
||||
- the SSR-related behavior is stable enough to be a long-lived regression contract
|
||||
- the project depends heavily on SSR for page-critical content
|
||||
- a browser-level journey would otherwise hide SSR-specific regressions
|
||||
|
||||
Preferred rule:
|
||||
|
||||
- infrastructure-like SSR checks start as direct endpoint checks
|
||||
- promote them into committed tests when the behavior is important and deterministic enough
|
||||
|
||||
## Admin config coverage strategy
|
||||
|
||||
Use a hybrid approach:
|
||||
@@ -429,3 +464,83 @@ When extending or fixing tests:
|
||||
5. Fix selectors or seed shape before widening scope.
|
||||
|
||||
Keep the test basis deterministic. Do not fall back to existing editorial demo content just because it is already present in the database.
|
||||
|
||||
## Admin E2E: Boot abwarten
|
||||
|
||||
Admin-SPA lädt Chunks asynchron. Vor Interaktionen auf sichtbares Login-Formular warten:
|
||||
|
||||
```ts
|
||||
await page.goto("/login")
|
||||
await expect(page.getByLabel(/Benutzername|Username/i)).toBeVisible({ timeout: 20000 })
|
||||
```
|
||||
|
||||
Danach erst fill/click – das Formular erscheint erst wenn die App vollständig gebootet ist.
|
||||
|
||||
## MailDev E-Mail-Testing
|
||||
|
||||
MailDev läuft im Docker-Stack (SMTP Port 25, Web-API Port 1080). Die REST-API erlaubt E-Mails zu lesen und zu löschen:
|
||||
|
||||
```ts
|
||||
const MAILDEV = "https://{project}-maildev.code.testversion.online"
|
||||
|
||||
// Alle E-Mails abrufen
|
||||
const res = await request.get(`${MAILDEV}/email`)
|
||||
const emails = await res.json()
|
||||
|
||||
// Alle löschen (vor Test)
|
||||
await request.delete(`${MAILDEV}/email/all`)
|
||||
```
|
||||
|
||||
### Polling-Pattern für asynchrone E-Mails
|
||||
|
||||
Formular → Action-Hook sendet E-Mail via `context.smtp.sendMail()`. MailDev braucht Zeit zum Verarbeiten:
|
||||
|
||||
```ts
|
||||
// Nach Form-Submit auf E-Mails warten
|
||||
for (let i = 0; i < 15; i++) {
|
||||
await new Promise((r) => setTimeout(r, 1000))
|
||||
const res = await request.get(`${MAILDEV}/email`)
|
||||
if (res.ok()) {
|
||||
const emails = await res.json()
|
||||
if (emails.length >= 2) break // Kunde + Betreiber
|
||||
}
|
||||
}
|
||||
emails.find((e) => e.to.some((t) => t.address === "kunde@test.de"))
|
||||
```
|
||||
|
||||
Wichtig: Tests mit MailDev müssen sequentiell laufen (`--workers=1`), da parallele Tests sich gegenseitig die MailDev-Inbox überschreiben.
|
||||
|
||||
## Admin pagebuilder registry coverage
|
||||
|
||||
For starter-like projects, committed admin coverage should include both sides of the pagebuilder contract:
|
||||
|
||||
1. registry/chooser coverage on a new entry form
|
||||
2. actual preview rendering on an existing seeded entry
|
||||
|
||||
The second check is important because it catches failures that the chooser alone does not see:
|
||||
|
||||
- broken `meta.pagebuilder.blockRegistry.file` wiring
|
||||
- preview components that no longer mount through the shared block renderer
|
||||
- missing `_lookup` hydration for foreign media fields
|
||||
- image widgets that work on the public site but fail in admin preview because the API base or URL resolution is wrong
|
||||
|
||||
Preferred starter pattern:
|
||||
|
||||
- seed one deterministic medialib image through the collection API
|
||||
- seed one deterministic content entry that references that image in at least one pagebuilder block
|
||||
- open that entry in `tests/e2e-admin/pagebuilder.spec.ts`
|
||||
- assert both block text and `img[data-entry-id]` preview rendering
|
||||
|
||||
Keep this test generic. Do not tie it to customer-specific block sets unless the project has already diverged from the starter pattern.
|
||||
|
||||
## Delivery-checklist alignment
|
||||
|
||||
When using this skill together with `.agents/BUILD_CHECKLIST.md`, the testing phase should leave behind explicit evidence for:
|
||||
|
||||
- which specs were run
|
||||
- which seed data was extended or reused
|
||||
- whether admin smoke coverage exists for the configured collections
|
||||
- whether pagebuilder preview rendering is covered when pagebuilder is in scope
|
||||
- whether SSR was verified by direct endpoint checks, committed tests, or both
|
||||
|
||||
If that evidence only exists in chat history and not in the repo or task notes, the testing work is too fragile for later agents.
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
---
|
||||
name: search-and-embeddings
|
||||
description: Model search and semantic retrieval for tibi website projects. Covers embedding provider configuration, collection search modes, auto-regeneration, regenerate-search admin flows, and how later agents should decide between no search, classic search, ngram search, and vector search.
|
||||
---
|
||||
|
||||
# search-and-embeddings
|
||||
|
||||
## When to use this skill
|
||||
|
||||
Use this skill when:
|
||||
|
||||
- a project needs explicit search behavior beyond generic CRUD filtering
|
||||
- search should be typo-tolerant, weighted, or semantic
|
||||
- embedding providers must be configured
|
||||
- later agents need a clear yes/no decision for search instead of vague optionality
|
||||
|
||||
## Goal
|
||||
|
||||
Give later agents a practical workflow for deciding whether search is needed and, if yes, which search mode belongs to the project.
|
||||
|
||||
This skill is separate from editor AI features. Search and embeddings affect content retrieval, operational setup, and index/regeneration behavior, not just editor assistance.
|
||||
|
||||
## Source of truth
|
||||
|
||||
Use these sources when implementing or reviewing search behavior:
|
||||
|
||||
- `tibi-server/docs/02-configuration.md`
|
||||
- `tibi-server/docs/04-collections.md`
|
||||
- `tibi-server/docs/09-llm-integration.md`
|
||||
- `.agents/skills/nova-ai-editor-features/SKILL.md`
|
||||
- `.agents/skills/mongodb-and-indexes/SKILL.md`
|
||||
|
||||
## First decision: no search vs explicit search
|
||||
|
||||
Do not leave search in an implied state.
|
||||
|
||||
Make one explicit decision:
|
||||
|
||||
- no search in this project
|
||||
- classic keyword search only
|
||||
- fuzzy substring search (`ngram`)
|
||||
- semantic/vector search
|
||||
- hybrid search with deliberate ranking behavior
|
||||
|
||||
If the answer is “not used”, document that clearly so later agents do not accidentally wire providers or regress into half-configured search.
|
||||
|
||||
## Server-level provider setup
|
||||
|
||||
Embedding providers are configured server-side:
|
||||
|
||||
```yaml
|
||||
embedding:
|
||||
providers:
|
||||
- name: bge-m3
|
||||
type: native
|
||||
modelPath: /models/bge-m3
|
||||
dimensions: 1024
|
||||
- name: openai-embed
|
||||
type: openai
|
||||
model: text-embedding-3-small
|
||||
apiKey: ${EMBEDDING_OPENAI-EMBED_APIKEY}
|
||||
baseURL: https://api.openai.com/v1
|
||||
dimensions: 1536
|
||||
```
|
||||
|
||||
Important:
|
||||
|
||||
- collection search config references the provider by name
|
||||
- embedding secrets and model paths can come from environment variables
|
||||
- vector search is not only a collection concern; the server must actually provide the embedding backend
|
||||
|
||||
## Collection search modes
|
||||
|
||||
Tibi supports multiple search modes via collection `search:` config:
|
||||
|
||||
- `text`
|
||||
- `regex`
|
||||
- `eval`
|
||||
- `filter`
|
||||
- `ngram`
|
||||
- `vector`
|
||||
|
||||
Use explicit search configs when search is a real product feature. Auto-fallback is useful, but it is not a substitute for a deliberate retrieval model.
|
||||
|
||||
## Choosing the right mode
|
||||
|
||||
### `text`
|
||||
|
||||
Use when:
|
||||
|
||||
- MongoDB text indexing is sufficient
|
||||
- exact field ownership of the text index is clear
|
||||
- keyword search is enough
|
||||
|
||||
Requires a text index.
|
||||
|
||||
### `regex`
|
||||
|
||||
Use when:
|
||||
|
||||
- the searchable fields are explicit
|
||||
- case-insensitive matching is enough
|
||||
- weighted field scoring is useful
|
||||
|
||||
Good for smaller datasets or precise keyed fields.
|
||||
|
||||
### `filter` or `eval`
|
||||
|
||||
Use when:
|
||||
|
||||
- search logic depends on auth, project context, or business-specific filtering
|
||||
- plain keyword matching is not the full contract
|
||||
|
||||
Treat these as controlled power tools. The resulting filters are still sanitized against blocked operators.
|
||||
|
||||
### `ngram`
|
||||
|
||||
Use when:
|
||||
|
||||
- typo tolerance or substring matching is needed
|
||||
- users search codes, names, transliterated terms, or partial inputs
|
||||
|
||||
This is enrichment-based search. It stores generated `_search` data and benefits from clear regeneration expectations.
|
||||
|
||||
### `vector`
|
||||
|
||||
Use when:
|
||||
|
||||
- semantic similarity matters more than literal keyword overlap
|
||||
- the project can support embedding-provider setup and operator cost expectations
|
||||
- search quality justifies added complexity
|
||||
|
||||
Vector mode can use:
|
||||
|
||||
- `fields`
|
||||
- custom `eval` transformation
|
||||
- `documentPrefix`
|
||||
- `queryPrefix`
|
||||
- `overflow: truncate|chunk`
|
||||
- `rrf` tuning for hybrid scoring
|
||||
|
||||
## Auto-regeneration and admin flows
|
||||
|
||||
For `ngram` and `vector`, `autoRegenerate: true` can refresh stale enrichment data after config changes.
|
||||
|
||||
If regeneration is needed manually, the admin flow depends on project admin tokens with:
|
||||
|
||||
- `allowRegenerateSearch: true`
|
||||
|
||||
Treat regeneration as part of the search contract, not as an implementation footnote.
|
||||
|
||||
## Search and LLM are related but not identical
|
||||
|
||||
The LLM system and the embedding system are adjacent, but they are not the same thing.
|
||||
|
||||
- `llm.providers` drive chat/completion features
|
||||
- `embedding.providers` drive vector search enrichment
|
||||
- org/user budgets affect LLM usage workflows
|
||||
- search design still needs its own retrieval and operator decisions
|
||||
|
||||
Do not assume that enabling editor AI automatically defines a sound search architecture.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- leaving search unspecified and hoping auto-fallback is “good enough”
|
||||
- enabling vector search without a real provider/runtime plan
|
||||
- forgetting text indexes for `mode: text`
|
||||
- enabling enrichment modes without a regeneration story
|
||||
- mixing editor AI decisions with search decisions until neither is clear
|
||||
|
||||
## Verification checklist
|
||||
|
||||
After search-related changes, verify all of these:
|
||||
|
||||
1. the project has an explicit yes/no search decision
|
||||
2. server-side embedding providers exist when vector search is configured
|
||||
3. required text or search indexes exist
|
||||
4. `?q=` and `?qName=` behavior matches the intended search contract
|
||||
5. regeneration behavior is defined for enrichment-based modes
|
||||
|
||||
## What an LLM should inspect first
|
||||
|
||||
When asked to add or review search on this starter, inspect in this order:
|
||||
|
||||
1. `tibi-server/docs/04-collections.md`
|
||||
2. `tibi-server/docs/02-configuration.md`
|
||||
3. existing collection `search:` config
|
||||
4. whether the project needs keyword, fuzzy, semantic, or no search
|
||||
5. operator expectations for regeneration and provider secrets
|
||||
|
||||
This prevents over-engineered vector setups and under-specified search behavior.
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: security-hardening-and-token-strategy
|
||||
description: Apply current tibi-server security practices to website projects. Covers secret handling, token strategies, bulk-permission safety, cookie settings, SSRF/exec risks in hooks, and secure operator decisions for this stack.
|
||||
description: Apply current tibi-server security practices to website projects. Covers token strategy, secret handling, rate limiting, bulk-permission safety, cookie settings, risky hook capabilities, and secure operator decisions for this stack.
|
||||
---
|
||||
|
||||
# security-hardening-and-token-strategy
|
||||
@@ -9,150 +9,199 @@ description: Apply current tibi-server security practices to website projects. C
|
||||
|
||||
Use this skill when:
|
||||
|
||||
- Setting up or reviewing authentication and token use on this stack
|
||||
- Deciding how admin tokens, JWT auth, and token permissions should be used
|
||||
- Hardening hooks, actions, and project config against obvious security mistakes
|
||||
- Reviewing bulk permissions, secrets, cookie settings, or risky server-side capabilities
|
||||
- setting up or reviewing authentication and token usage on this stack
|
||||
- deciding how admin tokens, JWT auth, and token permissions should be used
|
||||
- hardening hooks, actions, and project config against current upstream security risks
|
||||
- reviewing bulk permissions, rate limiting, cookies, secrets, or risky server-side capabilities
|
||||
|
||||
## Goal
|
||||
|
||||
The goal is to keep projects on this starter aligned with the current tibi-server security model and known risk areas.
|
||||
Keep projects on this starter aligned with the current tibi-server security model and with the security-sensitive operator decisions the stack exposes.
|
||||
|
||||
This skill is not a generic web security guide. It is about the concrete operator and implementation choices this stack exposes.
|
||||
This is not a generic web-security primer. It is the practical security workflow for this repo family.
|
||||
|
||||
## Source of truth
|
||||
|
||||
Use these sources when implementing or reviewing security-related decisions:
|
||||
Use these sources when implementing or reviewing security decisions:
|
||||
|
||||
- `tibi-server/docs/05-authentication.md`
|
||||
- `tibi-server/docs/14-security.md`
|
||||
- relevant collection/action permissions in `api/`
|
||||
- project environment/config files
|
||||
- `tibi-server/docs/17-field-level-permissions.md`
|
||||
- `tibi-server/docs/02-configuration.md`
|
||||
- project config and collection/action permission YAML files
|
||||
|
||||
## Security review order
|
||||
|
||||
When asked to harden a project, inspect in this order:
|
||||
|
||||
1. secret sourcing in config/env
|
||||
2. token type and scope
|
||||
3. collection/action permissions
|
||||
4. bulk permission exposure
|
||||
5. field-level restrictions
|
||||
6. rate limiting and cookie settings
|
||||
7. risky hook capabilities such as outbound fetch or exec
|
||||
|
||||
This prevents “secure enough” changes that leave the real attack surface untouched.
|
||||
|
||||
## Authentication surfaces
|
||||
|
||||
This stack exposes multiple auth mechanisms:
|
||||
This stack exposes multiple auth mechanisms. Do not mix them casually.
|
||||
|
||||
- JWT user auth
|
||||
- refresh token cookie flow
|
||||
- refresh-token cookie flow
|
||||
- admin tokens
|
||||
- token-based permissions for API-style access
|
||||
- token-based permission sets for narrower machine access
|
||||
|
||||
Do not mix them casually. Each one has a different operational purpose.
|
||||
Recommended default:
|
||||
|
||||
## Recommended token strategy
|
||||
- use JWT user auth for real users and editor/admin sessions
|
||||
- use refresh cookies for session continuation where appropriate
|
||||
- use admin tokens only for system/admin/ops flows that truly need them
|
||||
- use token permissions for narrow machine integrations
|
||||
|
||||
Use:
|
||||
## Token header distinction
|
||||
|
||||
- **JWT user auth** for real users in admin or authenticated workflows
|
||||
- **refresh cookies** for session continuation where appropriate
|
||||
- **admin tokens** only for server/admin/ops scenarios that truly need that level of access
|
||||
- **token permissions** for narrow integration access or machine clients
|
||||
Use the right header for the right surface:
|
||||
|
||||
Avoid using broad admin tokens where a narrow project or collection-level token permission is enough.
|
||||
- system-level API such as project CRUD, admin reload, shutdown: `X-Admin-Token`
|
||||
- collection-level CRUD via static project token: `Token`
|
||||
- JWT-authenticated user flow: `X-Auth-Token`
|
||||
|
||||
## Secrets handling
|
||||
Do not assume a working `Token` header implies system-level admin rights.
|
||||
|
||||
Do not keep production secrets as plain literals in committed config.
|
||||
## Secret handling
|
||||
|
||||
Prefer environment-variable substitution for:
|
||||
Do not keep production secrets as committed literals if the deployment can source them from env or operator-managed secrets.
|
||||
|
||||
Review at minimum:
|
||||
|
||||
- JWT secrets
|
||||
- SMTP credentials
|
||||
- LLM API keys
|
||||
- external API tokens
|
||||
- admin token values
|
||||
- admin tokens
|
||||
- external API keys
|
||||
- LLM/embedding provider keys
|
||||
|
||||
If a project ships real secrets in config, treat that as a structural problem, not a cosmetic cleanup.
|
||||
If secrets are hardcoded in committed config, treat that as a structural problem, not as cleanup trivia.
|
||||
|
||||
## Bulk permission safety
|
||||
|
||||
Bulk mutations are explicitly more dangerous than single-document mutations.
|
||||
Bulk operations are more dangerous than single-document mutations.
|
||||
|
||||
Important rule:
|
||||
|
||||
- boolean `post: true` / `put: true` / `delete: true` does not imply bulk access
|
||||
- bulk requires object-form permissions with `bulk: true`
|
||||
|
||||
Do not enable bulk operations casually in website projects. Most editor workflows do not need them.
|
||||
Example:
|
||||
|
||||
```yaml
|
||||
permissions:
|
||||
user:
|
||||
methods:
|
||||
post:
|
||||
allow: true
|
||||
bulk: true
|
||||
```
|
||||
|
||||
Do not enable bulk access casually in website projects. Most editorial workflows do not need it.
|
||||
|
||||
## Field-level security
|
||||
|
||||
Security on this stack is not only collection-method based.
|
||||
|
||||
Review all of these layers together:
|
||||
|
||||
- collection methods
|
||||
- `readonlyFields`
|
||||
- `hiddenFields`
|
||||
- field-level `readonly` / `hidden`
|
||||
- eval-based field rules
|
||||
- collection visibility in the admin UI
|
||||
|
||||
If a field should not be editable or visible, enforce that on the server. Do not rely on frontend omission.
|
||||
|
||||
## Rate limiting and login hardening
|
||||
|
||||
Current upstream tibi-server supports login rate limiting with exponential backoff.
|
||||
|
||||
Review these config points:
|
||||
|
||||
- `ratelimit.enabled`
|
||||
- `ratelimit.loginInitialDelay`
|
||||
- `ratelimit.loginMaxDelay`
|
||||
- `ratelimit.loginResetAfter`
|
||||
|
||||
Security implication:
|
||||
|
||||
- a project may look fine in normal use while still being too soft against brute-force attempts if rate limiting is not configured as expected
|
||||
|
||||
For serious deployments, do not leave this unreviewed just because login works.
|
||||
|
||||
## Cookie and session hardening
|
||||
|
||||
For refresh-token flows, ensure the deployment matches secure cookie expectations.
|
||||
Refresh-token flows should respect the target environment.
|
||||
|
||||
Important considerations:
|
||||
Review:
|
||||
|
||||
- secure cookies should stay enabled in HTTPS environments
|
||||
- local non-HTTPS development may need explicit relaxation
|
||||
- do not debug production cookie issues by weakening production defaults globally
|
||||
- `api.secureCookies`
|
||||
- HTTPS vs local HTTP expectations
|
||||
- whether debugging shortcuts are accidentally bleeding into production config
|
||||
|
||||
## Hook risk surfaces
|
||||
Do not weaken secure-cookie behavior globally just to make a dev shortcut work.
|
||||
|
||||
Current tibi-server exposes powerful server-side capabilities. Some of them require explicit restraint.
|
||||
## Query-parameter token risk
|
||||
|
||||
Token-based permissions can be passed via query parameters in some cases, but this is a documented risk surface.
|
||||
|
||||
If query tokens are unavoidable:
|
||||
|
||||
- scope them narrowly
|
||||
- avoid logging full URLs with sensitive query strings
|
||||
- understand proxy, history, and referrer exposure
|
||||
|
||||
Prefer header-based transport whenever possible.
|
||||
|
||||
## Risky hook capabilities
|
||||
|
||||
Current tibi-server exposes powerful capabilities in hooks. Treat them as explicit design decisions, not utilities.
|
||||
|
||||
Particularly important:
|
||||
|
||||
- `http.fetch` / `http.fetchStream` can create SSRF risk
|
||||
- `exec.command` can create command-execution risk
|
||||
- `context.http.fetch()` / `context.http.fetchStream()` can create SSRF risk
|
||||
- `context.exec.command()` can create command-execution risk
|
||||
- broad filesystem/network access in hooks should not be treated as harmless
|
||||
|
||||
If a feature can be implemented without shell execution or arbitrary internal fetches, prefer the safer path.
|
||||
|
||||
## Query-parameter token risk
|
||||
When such capabilities are used, document:
|
||||
|
||||
Token permissions may be passed through query parameters for specific cases, but this is a documented risk surface.
|
||||
|
||||
Prefer header-based token transport when possible.
|
||||
|
||||
If query tokens are unavoidable:
|
||||
|
||||
- avoid logging full URLs with sensitive query strings
|
||||
- understand proxy/referrer/history exposure
|
||||
- scope the token as narrowly as possible
|
||||
|
||||
## Permission boundaries
|
||||
|
||||
Security on this stack is layered.
|
||||
|
||||
Think in terms of:
|
||||
|
||||
- project visibility
|
||||
- collection method permissions
|
||||
- field-level restrictions
|
||||
- token scope
|
||||
- public vs authenticated action access
|
||||
|
||||
Do not rely on frontend hiding or convention where server-side permissions should be explicit.
|
||||
- why they are necessary
|
||||
- what the allowed target surface is
|
||||
- what the safer rejected alternatives were
|
||||
|
||||
## CORS configuration
|
||||
|
||||
CORS follows a 3-level hierarchy. Configure it in `api/config.yml` under `cors:` for project-wide settings, or in individual collection/action YAML for per-endpoint overrides:
|
||||
CORS follows a hierarchy. Configure it deliberately instead of widening it reactively.
|
||||
|
||||
| Level | Configuration location | Scope |
|
||||
|-------|----------------------|-------|
|
||||
| Server | tibi-server `config.yml` | Global default |
|
||||
| Project | `api/config.yml` → `cors:` | Per project |
|
||||
| Collection/Action | Collection or action YAML → `cors:` | Per endpoint |
|
||||
Levels:
|
||||
|
||||
Each level can `merge: true` (append to parent) or `merge: false` (replace entirely).
|
||||
- server-level `config.yml`
|
||||
- project-level `api/config.yml`
|
||||
- collection/action-level YAML overrides
|
||||
|
||||
For a project that serves a browser-based SPA to end users on its own domain and serves API/tibiadmin on separate subdomains, the default (no explicit CORS config) is usually correct since the SPA makes same-origin API calls via the BrowserSync/production reverse proxy. Add explicit CORS only when:
|
||||
- the API is called from external origins (e.g. third-party integrations)
|
||||
- the admin UI is served on a different origin than the API
|
||||
- an action endpoint needs to support cross-origin form submissions
|
||||
For typical website projects on this starter, the default proxy setup often means no aggressive cross-origin opening is required. Add explicit CORS only when the real deployment needs external origins.
|
||||
|
||||
See `tibi-server/docs/02-configuration.md` (section "CORS Configuration Hierarchy") for details.
|
||||
## Recommended implementation patterns
|
||||
|
||||
## Secure implementation patterns
|
||||
|
||||
### Public form endpoint
|
||||
### Public form workflow
|
||||
|
||||
Recommended shape:
|
||||
|
||||
- public action with narrow allowed methods
|
||||
- public action with narrow methods
|
||||
- server-side validation
|
||||
- no broad admin tokens in the browser
|
||||
- no unnecessary collection write permissions exposed publicly
|
||||
- no admin token in the browser
|
||||
- separate internal persistence only when truly required
|
||||
|
||||
### Integration token
|
||||
|
||||
@@ -162,32 +211,42 @@ Recommended shape:
|
||||
- minimal collection/action scope
|
||||
- header-based transport preferred
|
||||
|
||||
### Hook that calls external services
|
||||
### Sensitive internal fields
|
||||
|
||||
Recommended shape:
|
||||
|
||||
- fixed or validated destination URLs
|
||||
- no arbitrary user-controlled internal target fetching
|
||||
- minimal capability needed for the feature
|
||||
- use hidden/readonly restrictions explicitly
|
||||
- keep admin UI aligned with those restrictions
|
||||
- do not let previews depend on hidden-only data
|
||||
|
||||
### Hook that calls external systems
|
||||
|
||||
Recommended shape:
|
||||
|
||||
- fixed or validated targets
|
||||
- no user-controlled arbitrary internal fetches
|
||||
- no shell execution unless unavoidable
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- Hardcoded production secrets in committed config
|
||||
- Using admin tokens for routine frontend or integration traffic
|
||||
- Enabling bulk write permissions without a strong operational reason
|
||||
- Treating hook `http.fetch` and `exec.command` as risk-free utilities
|
||||
- Solving access control in the UI instead of on the server
|
||||
- hardcoded production secrets in committed config
|
||||
- broad admin tokens used for normal frontend or integration traffic
|
||||
- bulk permissions enabled without a concrete operator need
|
||||
- risky hook capabilities treated as harmless helpers
|
||||
- collection security solved in the UI instead of the server
|
||||
- production cookie or rate-limit settings weakened for convenience
|
||||
|
||||
## Verification checklist
|
||||
|
||||
After security-relevant changes, verify all of these:
|
||||
|
||||
1. Secrets are sourced appropriately.
|
||||
2. Token type matches the intended actor and scope.
|
||||
3. Bulk permissions are not broader than necessary.
|
||||
4. Public endpoints expose only the required methods.
|
||||
5. Risky hook capabilities are constrained by design.
|
||||
6. `yarn validate` stays clean.
|
||||
1. secrets are sourced appropriately
|
||||
2. token type matches the intended actor and scope
|
||||
3. bulk permissions are not broader than necessary
|
||||
4. readonly/hidden behavior is correct on the API
|
||||
5. rate limiting and cookie settings match the environment
|
||||
6. risky hook capabilities are constrained by design
|
||||
7. `yarn validate` stays clean
|
||||
|
||||
## What an LLM should inspect first
|
||||
|
||||
@@ -195,8 +254,9 @@ When asked to harden or design secure access on this starter, inspect in this or
|
||||
|
||||
1. `tibi-server/docs/05-authentication.md`
|
||||
2. `tibi-server/docs/14-security.md`
|
||||
3. the relevant collection/action permission sets
|
||||
4. secret sourcing in config/env
|
||||
5. whether hooks use risky capabilities like outbound fetch or exec
|
||||
3. `tibi-server/docs/17-field-level-permissions.md`
|
||||
4. the relevant collection/action permission sets
|
||||
5. secret sourcing in config/env
|
||||
6. whether hooks use risky capabilities like outbound fetch or exec
|
||||
|
||||
This prevents “working” implementations that quietly widen the attack surface.
|
||||
This prevents working implementations that quietly widen the attack surface.
|
||||
|
||||
@@ -238,6 +238,94 @@ Keep the backend responsible for:
|
||||
- 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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: tibi-project-setup
|
||||
description: Set up a new tibi project from the tibi-svelte-starter template. Covers cloning, placeholder replacement, environment config, Docker startup, mock mode, demo cleanup, and build verification. Use when creating a new project or onboarding into this template.
|
||||
description: Set up a new tibi project from the tibi-svelte-starter template. Covers placeholder replacement, env/config setup, Docker startup, optional shared-server registration, and build verification. Use when creating a new project or onboarding into this template.
|
||||
---
|
||||
|
||||
# tibi-project-setup
|
||||
@@ -9,257 +9,339 @@ description: Set up a new tibi project from the tibi-svelte-starter template. Co
|
||||
|
||||
Use this skill when:
|
||||
|
||||
- Creating a new project from the `tibi-svelte-starter` template
|
||||
- Onboarding into a freshly cloned starter project where placeholders haven't been replaced yet
|
||||
- The user asks to "set up", "initialize", or "bootstrap" a new tibi project
|
||||
- creating a new project from `tibi-svelte-starter`
|
||||
- onboarding into a freshly cloned project where starter placeholders are still present
|
||||
- fixing a project that was renamed but never fully registered/configured in the current tibi stack
|
||||
|
||||
Goal: a new website project should end up as a **fully working tibi-server + tibi-admin-nova project**, not just a renamed starter clone.
|
||||
Goal: end with a project that is not only renamed, but actually reachable as a working website, admin, and API project in the current Docker/tibi-server setup.
|
||||
|
||||
## Source of truth
|
||||
|
||||
Use these sources when bootstrapping or auditing setup:
|
||||
|
||||
- `.agents/BUILD_CHECKLIST.md` phase 0
|
||||
- `AGENTS.md`
|
||||
- `README.md`
|
||||
- `Makefile`
|
||||
- `docker-compose-local.yml`
|
||||
- `.env`
|
||||
- `api/config.yml`
|
||||
- `api/config.yml.env`
|
||||
- `api/hooks/config-client.js`
|
||||
- `.gitea/workflows/deploy.yml`
|
||||
- `scripts/ci-deploy.sh`
|
||||
- `scripts/ci-staging.sh`
|
||||
- tibi-server server-level config requirements from `tibi-server/docs/02-configuration.md` when the project does not run on the starter's local Docker stack
|
||||
|
||||
## Core setup rule
|
||||
|
||||
Do not stop after placeholder replacement.
|
||||
|
||||
A project is only set up when all of these are true:
|
||||
|
||||
- placeholders and visible starter identity leftovers are gone
|
||||
- env and token values are present
|
||||
- Docker stack comes up
|
||||
- the intended operator path is explicit: local starter Docker stack or shared/external tibi-server stack
|
||||
- website, admin, and API respond on the expected project URLs
|
||||
- if the current stack requires server-level config and project registration, that operator flow is completed
|
||||
- `yarn build`, `yarn build:server`, and `yarn validate` pass
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Code-Server environment at `*.code.testversion.online`
|
||||
- `git`, `yarn`, `make`, `docker compose` available
|
||||
- Traefik reverse proxy running on the host (manages `*.code.testversion.online` subdomains automatically via Docker labels)
|
||||
- `git`, `yarn`, `make`, `docker compose`, `curl`
|
||||
- current Code-Server / Docker environment for `*.code.testversion.online`
|
||||
- reverse proxy/Traefik managed by the host environment
|
||||
|
||||
## Step 1 — Clone and set up remotes
|
||||
## Step 1 — Clone and prepare remotes
|
||||
|
||||
Skip this step if already inside a cloned project.
|
||||
Skip if the project is already cloned.
|
||||
|
||||
```sh
|
||||
# In the workspace directory (e.g. /WM_Dev/src/gitbase.de/cms/)
|
||||
git clone https://gitbase.de/cms/tibi-svelte-starter.git my-project
|
||||
cd my-project
|
||||
git remote rename origin template
|
||||
# Create a new remote repo (e.g. on gitbase.de) and add as origin:
|
||||
# git remote add origin https://gitbase.de/org/my-project.git
|
||||
git remote add origin https://gitbase.de/<org>/<repo>.git
|
||||
```
|
||||
|
||||
**Verify:** `git remote -v` shows `template` pointing to the starter and optionally `origin` pointing to the new repo.
|
||||
## Step 2 — Replace starter placeholders and identity surfaces
|
||||
|
||||
## Step 2 — Replace placeholders and starter values
|
||||
Replace placeholders in all required files:
|
||||
|
||||
Replace the starter placeholders and starter-derived values in the correct files:
|
||||
- `.env`
|
||||
- `api/config.yml`
|
||||
- `frontend/.htaccess` when the deployment path uses the shipped Apache rewrite/proxy file
|
||||
- `api/hooks/config-client.js`
|
||||
- `package.json`
|
||||
- `README.md` or other visible starter naming surfaces when the repo is already project-facing
|
||||
- any other file that still contains starter markers
|
||||
|
||||
| Placeholder | Files | Format | Example |
|
||||
| -------------------- | ---------------------------------------------- | --------------------------------------------------------- | ------------ |
|
||||
| `__PROJECT_NAME__` | `.env` | kebab-case (used for URLs, Docker containers, subdomains) | `my-project` |
|
||||
| `__TIBI_NAMESPACE__` | `.env`, `api/config.yml`, `frontend/.htaccess` | kebab-case, same value as `PROJECT_NAME` | `my-project` |
|
||||
Minimum placeholders to replace:
|
||||
|
||||
- `__PROJECT_NAME__`
|
||||
- `__TIBI_NAMESPACE__`
|
||||
- `__ORG__`
|
||||
- `__PROJECT__`
|
||||
|
||||
Verify with:
|
||||
|
||||
```sh
|
||||
PROJECT=my-project # kebab-case
|
||||
NAMESPACE=my-project # same kebab-case value as PROJECT
|
||||
|
||||
sed -i "s/__PROJECT_NAME__/$PROJECT/g" .env
|
||||
sed -i "s/__TIBI_NAMESPACE__/$NAMESPACE/g" .env api/config.yml frontend/.htaccess
|
||||
rg '__[A-Z0-9_]+__' . --glob '*.{yml,js,env,htaccess,json,md,ts,svelte}'
|
||||
```
|
||||
|
||||
Also update the starter-derived values that are not placeholder tokens anymore, especially `STAGING_PATH`, `STAGING_URL`, `CODING_URL`, `CODING_TIBIADMIN_URL`, `api/hooks/config-client.js`, and starter metadata in `package.json`.
|
||||
If anything remains, the setup is not complete.
|
||||
|
||||
**Important:** The file `api/hooks/config-client.js` contains a **separate** placeholder `__PROJECT__` (not `__PROJECT_NAME__`):
|
||||
## Step 3 — Fill project env, token, and metadata files
|
||||
|
||||
Set the current project URLs in `.env`:
|
||||
|
||||
- `LIVE_URL`
|
||||
- `CODING_URL`
|
||||
- `STAGING_URL`
|
||||
- `CODING_TIBIADMIN_URL`
|
||||
- `CODING_TIBISERVER_URL` only when the current environment exposes a dedicated raw tibi-server host
|
||||
|
||||
Generate `api/config.yml.env` values:
|
||||
|
||||
```sh
|
||||
# api/hooks/config-client.js has: const originURL = "https://__PROJECT__.code.testversion.online"
|
||||
sed -i "s/__PROJECT__/$PROJECT/g" api/hooks/config-client.js
|
||||
token=$(openssl rand -hex 20)
|
||||
cat > api/config.yml.env <<EOF
|
||||
ADMIN_TOKEN=$token
|
||||
ADMIN_ASSET_VERSION=$(node -e "process.stdout.write(require('crypto').randomBytes(6).toString('hex'))")-dirty-$(date +%s)
|
||||
EOF
|
||||
```
|
||||
|
||||
**Verify all placeholders:**
|
||||
Important:
|
||||
|
||||
```sh
|
||||
grep -n '__PROJECT_NAME__\|__TIBI_NAMESPACE__\|__PROJECT__\|__ORG__' .env api/config.yml frontend/.htaccess api/hooks/config-client.js
|
||||
# Expected: no output (all placeholders replaced)
|
||||
```
|
||||
- `ADMIN_TOKEN` is used for collection-level writes through the header name declared by the collection permission key; in this starter that is typically `Token` via `token:${ADMIN_TOKEN}`
|
||||
- the current deploy scripts also use the same secret as a bearer token on the project-local reload endpoint
|
||||
- `ADMIN_ASSET_VERSION` is required so Nova picks up the current admin bundle
|
||||
- `PROJECT_NAME`, `TIBI_NAMESPACE`, `PRODUCTION_PATH`, and `STAGING_PATH` should be project-specific before the first deploy
|
||||
- `package.json` should no longer advertise the starter repository or default package name once the project is bootstrapped
|
||||
|
||||
**Result in `.env`:**
|
||||
## Step 4 — Install and start the Docker stack
|
||||
|
||||
```dotenv
|
||||
PROJECT_NAME=my-project
|
||||
TIBI_NAMESPACE=my-project
|
||||
CODING_URL=https://my-project.code.testversion.online
|
||||
STAGING_URL=https://dev-my-project.staging.testversion.online
|
||||
```
|
||||
|
||||
### Common mistakes
|
||||
|
||||
- **Using different values for `PROJECT` and `NAMESPACE`**: In this starter, `TIBI_NAMESPACE` must match `PROJECT_NAME` and use the same kebab-case value.
|
||||
- **Forgetting `frontend/.htaccess`**: Contains the namespace for API rewrite rules. If missed, API calls from the frontend will fail silently.
|
||||
- **Forgetting `api/config.yml`**: First line is `namespace: __TIBI_NAMESPACE__`. If not replaced, tibi-server won't start correctly.
|
||||
|
||||
## Step 3 — Page title
|
||||
|
||||
The page title is set dynamically via `<svelte:head>` in `frontend/src/App.svelte`. The demo app uses the constant `SITE_NAME` for this. In a new project, `App.svelte` is typically rewritten completely — just make sure `<svelte:head>` with a `<title>` is present. SSR automatically injects it via the `<!--HEAD-->` placeholder in `spa.html`.
|
||||
|
||||
Also verify that SSR still renders meaningful page content and not just the shell after the rewrite.
|
||||
|
||||
## Step 4 — Admin token and config.yml.env
|
||||
|
||||
**How config.yml.env works:** The file `api/config.yml.env` is **not** a standard `.env` file. It is an env-file that the tibi-server reads from the same directory as `config.yml`. The server resolves `${ADMIN_TOKEN}` and `${ADMIN_ASSET_VERSION}` variables in the YAML config from this file. This is separate from the project-root `.env` which serves Docker Compose and the Makefile.
|
||||
|
||||
`api/config.yml.env` ships with a default `ADMIN_TOKEN`. For production projects, generate a secure one:
|
||||
|
||||
```sh
|
||||
token=$(openssl rand -hex 20) && sed -i "s/^ADMIN_TOKEN=.*/ADMIN_TOKEN=$token/" api/config.yml.env
|
||||
```
|
||||
|
||||
This updates only `ADMIN_TOKEN` and keeps the other env keys in the file intact.
|
||||
|
||||
**Verify:** `cat api/config.yml.env` shows a 40-character hex token while preserving entries such as `ADMIN_ASSET_VERSION`.
|
||||
|
||||
**Note:** The `ADMIN_ASSET_VERSION` in the same file is used for cache-busting the admin bundle (`frontend/dist/admin.mjs`). It is auto-generated on build — but if missing, the admin bundle may not load correctly.
|
||||
|
||||
## Step 5 — Install, upgrade, and start
|
||||
Use the Docker targets from the project. Do not try to start the frontend with local dev servers.
|
||||
|
||||
```sh
|
||||
yarn install
|
||||
make docker-up # Start stack in background
|
||||
# or
|
||||
make docker-start # Start stack in foreground (CTRL-C to stop)
|
||||
make docker-up
|
||||
```
|
||||
|
||||
Do not blindly run a full dependency upgrade as part of project bootstrap unless the task explicitly includes dependency maintenance. First get the starter running as-is, then upgrade intentionally and validate.
|
||||
Notes:
|
||||
|
||||
**Important:** After changing `.env` or `api/config.yml.env`, you must run `make docker-down && make docker-up` for the changes to take effect. Docker Compose reads `.env` at startup time; tibi-server reads `config.yml.env` at reload. A simple `make docker-restart-frontend` is not sufficient for environment variable changes.
|
||||
- `make docker-up` already depends on `init`; do not duplicate bootstrap steps unless debugging Make targets directly
|
||||
- for foreground operation use `make docker-start`
|
||||
|
||||
**Verify containers are running:**
|
||||
## Step 5 — Choose the active bootstrap path
|
||||
|
||||
### Path A — Local starter Docker stack
|
||||
|
||||
This repo's default local path is the Docker stack in `docker-compose-local.yml` started via `make docker-up`.
|
||||
|
||||
Important characteristics:
|
||||
|
||||
- the project is mounted into `tibiserver` as `/data`
|
||||
- `DB_DIAL`, `DB_PREFIX`, `MAIL_HOST`, and security overrides are injected via container environment
|
||||
- the project is served from the repo's own `api/config.yml`
|
||||
- no extra root `config.yml` or `/api/v1/project` registration step is required for basic local startup
|
||||
|
||||
Use this path unless the operator environment clearly tells you otherwise.
|
||||
|
||||
### Path B — Shared or external tibi-server stack
|
||||
|
||||
Only use this path when the project is not started through the local starter compose stack and the operator environment requires explicit server-level config or project registration.
|
||||
|
||||
In that case, confirm all of these with the operator first:
|
||||
|
||||
- where the server-level `config.yml` lives
|
||||
- which admin token is valid for raw system-level APIs
|
||||
- which base URL exposes `/api/v1/project`
|
||||
- how the project path is mounted into the shared tibi-server instance
|
||||
|
||||
Do not invent Path B steps in the local starter Docker stack just because upstream tibi-server docs mention them.
|
||||
|
||||
## Step 6 — Optional server-level config and project registration for Path B
|
||||
|
||||
Shared or external tibi-server setups may require a server-level `config.yml` outside the project config. That file defines database connection, JWT secret, and admin tokens used for project CRUD and reload.
|
||||
|
||||
Create a root-level `config.yml` such as:
|
||||
|
||||
```yaml
|
||||
db:
|
||||
dial: mongodb://mongo
|
||||
prefix: tibi
|
||||
|
||||
api:
|
||||
port: 8080
|
||||
jwtSecret: <random-secret>
|
||||
adminTokens:
|
||||
- token: "<ADMIN_TOKEN>"
|
||||
label: "admin"
|
||||
permissions:
|
||||
- project
|
||||
- project.reload
|
||||
- user
|
||||
- namespace.<PROJECT_NAME>
|
||||
- server.shutdown
|
||||
|
||||
mail:
|
||||
host: localhost:25
|
||||
|
||||
security:
|
||||
allowAbsolutePaths: false
|
||||
allowUpperPaths: true
|
||||
```
|
||||
|
||||
Then copy it into the tibi-server container and restart that container if the current environment requires this manual step.
|
||||
|
||||
## Step 7 — Verify website, admin, and API reachability
|
||||
|
||||
Run the project-local checks after startup:
|
||||
|
||||
```sh
|
||||
make docker-ps # All containers should be "Up"
|
||||
make docker-logs # Check for errors
|
||||
curl -I "$CODING_URL"
|
||||
curl -I "$CODING_TIBIADMIN_URL"
|
||||
curl -I "$CODING_URL/api/content?limit=1"
|
||||
```
|
||||
|
||||
## Step 6 — Verify URLs
|
||||
|
||||
Traefik picks up Docker labels automatically — no manual config needed. After `make docker-up`, these URLs become available:
|
||||
|
||||
| Service | URL |
|
||||
| --------------------- | ------------------------------------------------------------ |
|
||||
| Website (BrowserSync) | `https://{PROJECT_NAME}.code.testversion.online/` |
|
||||
| Tibi Admin | `https://{PROJECT_NAME}-tibiadmin.code.testversion.online/` |
|
||||
| Tibi Server API | `https://{PROJECT_NAME}-tibiserver.code.testversion.online/` |
|
||||
| Maildev | `https://{PROJECT_NAME}-maildev.code.testversion.online/` |
|
||||
|
||||
The subdomains are registered via the Docker label `online.testversion.code.subdomain=${PROJECT_NAME}`. Traefik watches Docker events and creates routes dynamically.
|
||||
|
||||
**Verify:** `curl -sI https://{PROJECT_NAME}.code.testversion.online/ | head -1` returns `HTTP/2 200`.
|
||||
|
||||
## Step 7 — Mock mode (optional)
|
||||
|
||||
For frontend development without a running tibi-server backend:
|
||||
If the current environment also exposes a raw tibi-server host, add:
|
||||
|
||||
```sh
|
||||
# Set in .env:
|
||||
MOCK=1
|
||||
# Then full restart (env change requires docker-down first):
|
||||
make docker-down && make docker-up
|
||||
curl -I "$CODING_TIBISERVER_URL/api/v1/version"
|
||||
```
|
||||
|
||||
**When to use mock mode:** Early UI prototyping, frontend-only work, CI environments without a database.
|
||||
If `/api/...` returns HTML instead of JSON, the reverse-proxy/setup path is still wrong.
|
||||
|
||||
## Step 8 — Remove demo content
|
||||
## Step 8 — Optional project registration for Path B
|
||||
|
||||
For a real project, remove or replace the demo files:
|
||||
Projects are not assumed to exist just because files are present on disk. Register and reload them explicitly when the current stack requires project registration.
|
||||
|
||||
| File/Folder | Content |
|
||||
| ---------------------------------- | ------------------------------------------------------ |
|
||||
| `frontend/src/blocks/` | Demo block components (HeroBlock, RichtextBlock, etc.) |
|
||||
| `frontend/mocking/content.json` | Demo mock data for content |
|
||||
| `frontend/mocking/navigation.json` | Demo mock data for navigation |
|
||||
| `api/collections/content.yml` | Content collection config |
|
||||
| `api/collections/navigation.yml` | Navigation collection config |
|
||||
| `tests/e2e/` | Demo E2E tests |
|
||||
| `video-tours/tours/` | Demo video tour |
|
||||
```sh
|
||||
curl -s -X POST "$CODING_TIBISERVER_URL/api/v1/project" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Admin-Token: $ADMIN_TOKEN" \
|
||||
-d '{
|
||||
"name": "<PROJECT_NAME>",
|
||||
"description": "...",
|
||||
"configFile": "/data/api/config.yml",
|
||||
"enabled": true
|
||||
}'
|
||||
```
|
||||
|
||||
Then adapt `frontend/src/App.svelte` (header, footer, content loading) to your own data model.
|
||||
Reload after creation or config changes:
|
||||
|
||||
But do not delete starter structures blindly. For a serious project build-out, first decide which parts remain useful foundations:
|
||||
```sh
|
||||
curl -s -X POST "$CODING_TIBISERVER_URL/api/v1/_/<PROJECT_NAME>/_/admin/reload" \
|
||||
-H "X-Admin-Token: $ADMIN_TOKEN"
|
||||
```
|
||||
|
||||
- SSR pipeline
|
||||
- i18n route model
|
||||
- pagebuilder-based content collection
|
||||
- navigation collection
|
||||
- media library and image handling
|
||||
- tests / tours as scaffolding
|
||||
### Token header distinction
|
||||
|
||||
The goal is not "delete the demo". The goal is "reshape the starter into a project architecture that editors can use productively".
|
||||
- raw system-level API such as project CRUD or direct admin reload: `X-Admin-Token`
|
||||
- collection-level CRUD such as content/navigation writes: use the header name from the collection permission key, typically `Token` in this starter via `token:${ADMIN_TOKEN}`
|
||||
- JWT-authenticated user requests: `X-Auth-Token`
|
||||
|
||||
**Decision guide:**
|
||||
The current starter deploy scripts are a separate case: they call the reverse-proxied reload endpoint on `LIVE_URL` or `STAGING_URL` with `Authorization: Bearer ${ADMIN_TOKEN}`.
|
||||
|
||||
- **Keep demo content** if you want to use it as a reference while building your own components.
|
||||
- **Delete immediately** if you're starting with a completely custom design and the demo files would only cause confusion.
|
||||
Do not mix these headers casually. A working collection token does not imply project-admin access.
|
||||
|
||||
## Step 9 — Build and validate
|
||||
|
||||
```sh
|
||||
yarn build # Frontend bundle for modern browsers
|
||||
yarn build:server # SSR bundle (for tibi-server goja hooks)
|
||||
yarn validate # TypeScript + Svelte checks (must show 0 errors and 0 warnings)
|
||||
```
|
||||
|
||||
**These commands must succeed before the project is considered set up.**
|
||||
|
||||
## Step 10 — Shape the project for real editor workflows
|
||||
|
||||
For a complete website on this starter, setup is not done when Docker runs. It is done when the project has a coherent content/admin model.
|
||||
|
||||
Inspect and adapt at least these areas:
|
||||
|
||||
- `api/collections/content.yml`: page types, block schema, SEO, i18n, pagebuilder config
|
||||
- `api/collections/navigation.yml`: header/footer structure and editor UX
|
||||
- `frontend/src/blocks/`: real block set for the website, not just demo showcase blocks
|
||||
- `frontend/src/blocks/BlockRenderer.svelte`: final block registry
|
||||
- `types/global.d.ts`: actual project data model
|
||||
- `frontend/src/App.svelte`: final shell, content-loading, SSR-safe behavior
|
||||
|
||||
For Nova specifically, use current capabilities where they improve the website build process:
|
||||
|
||||
- `preview` for readable row, breadcrumb, and foreign-key display
|
||||
- `sidebar` groups for publication/SEO/settings
|
||||
- `containerProps.layout` for usable forms
|
||||
- `dependsOn` for block-specific fields
|
||||
- `drillDown` for complex arrays
|
||||
- `pagebuilder` for heterogeneous page content
|
||||
- `subNavigation`, `singleton`, `viewHint`, and foreign previews where appropriate
|
||||
|
||||
For tibi-server specifically, decide early whether the site also needs:
|
||||
|
||||
- `actions:` for forms, newsletter, calculators, imports, or webhooks
|
||||
- publication-aware SSR invalidation
|
||||
- field-level permissions
|
||||
- AI/LLM integration for admin/editor workflows
|
||||
|
||||
## SSR debugging: manual project reload
|
||||
|
||||
After changing collection YAML files, hook code, or `config.js` (SSR route validation), you may need to trigger a project reload for tibi-server to pick up the changes. Hook files auto-reload, but structural changes (new collections, config changes) require explicit reload:
|
||||
|
||||
```bash
|
||||
curl -X POST "$CODING_URL/api/v1/_/$TIBI_NAMESPACE/_/admin/reload" \
|
||||
-H "Token: $ADMIN_TOKEN"
|
||||
```
|
||||
|
||||
The starter's `api/config.yml` has `allowReload: true` for the admin token by default. Response `{"message": "ok"}` confirms the reload.
|
||||
|
||||
Use this when:
|
||||
- A new collection was added to `api/config.yml` but the API doesn't see it
|
||||
- Hook files changed but the server hasn't picked them up
|
||||
- SSR route validation (`api/hooks/config.js`) was updated and the old behavior persists
|
||||
|
||||
## Step 11 — Functional verification for a real website project
|
||||
|
||||
After the first project shaping pass, verify more than just TypeScript:
|
||||
|
||||
```sh
|
||||
yarn build
|
||||
yarn build:server
|
||||
yarn validate
|
||||
```
|
||||
|
||||
Then also verify:
|
||||
The project is not considered bootstrapped until all three succeed.
|
||||
|
||||
- the public site loads via the website URL
|
||||
- the Nova admin loads via the admin URL
|
||||
- pages can be created and edited in admin
|
||||
- pagebuilder blocks are usable in admin
|
||||
- SSR renders real page content, not only the shell
|
||||
- navigation and media references render correctly
|
||||
- forms/actions work if the project uses them
|
||||
## Step 10 — Optional immediate follow-up work
|
||||
|
||||
If the goal is "LLM can build a complete website automatically", the project setup skill must lead to a fully functional content/admin/runtime stack, not merely a placeholder replacement.
|
||||
Depending on the project state, continue with:
|
||||
|
||||
- seed or create initial content/navigation entries
|
||||
- remove demo content and demo assets
|
||||
- update project imagery/icons
|
||||
- run the first targeted Playwright smoke checks
|
||||
|
||||
## Recommended verification sequence
|
||||
|
||||
Use this exact order when debugging a broken setup:
|
||||
|
||||
1. placeholder scan
|
||||
2. env/token/metadata presence
|
||||
3. Docker stack or target operator stack up
|
||||
4. choose Path A or Path B explicitly
|
||||
5. if Path B: server-level config and project registration/reload succeed
|
||||
6. website/admin/API reachability
|
||||
7. build/SSR build/validate
|
||||
|
||||
This prevents wasting time in frontend code when the real issue is project registration or server-level config.
|
||||
|
||||
## Common failure modes
|
||||
|
||||
### Placeholders still present
|
||||
|
||||
Symptom:
|
||||
|
||||
- URLs or namespace stay wrong even though the project name was changed manually
|
||||
|
||||
Fix:
|
||||
|
||||
- rerun the placeholder scan and replace every remaining marker
|
||||
|
||||
### Website works but API probes return HTML
|
||||
|
||||
Symptom:
|
||||
|
||||
- `curl "$CODING_URL/api/content?limit=1"` returns HTML
|
||||
|
||||
Fix:
|
||||
|
||||
- verify reverse-proxy routing and the configured API/admin URLs
|
||||
|
||||
### Files exist but the project is invisible to tibi-server
|
||||
|
||||
Symptom:
|
||||
|
||||
- project does not show in admin or reload endpoint fails
|
||||
|
||||
Fix:
|
||||
|
||||
- this is a Path B problem; verify the shared-stack server-level config and project registration flow instead of changing the local starter stack
|
||||
|
||||
### Admin bundle changes do not appear
|
||||
|
||||
Symptom:
|
||||
|
||||
- Nova still loads stale admin assets
|
||||
|
||||
Fix:
|
||||
|
||||
- regenerate or bump `ADMIN_ASSET_VERSION`
|
||||
|
||||
### Build passes locally but operational setup is still broken
|
||||
|
||||
Symptom:
|
||||
|
||||
- files compile, but website/admin/API are not all reachable
|
||||
|
||||
Fix:
|
||||
|
||||
- return to the reachability and registration checks instead of continuing with feature work
|
||||
|
||||
## What an LLM should inspect first
|
||||
|
||||
When asked to bootstrap or audit a starter-derived project, inspect in this order:
|
||||
|
||||
1. `README.md`
|
||||
2. `.env`
|
||||
3. `api/config.yml`
|
||||
4. `api/config.yml.env`
|
||||
5. `api/hooks/config-client.js`
|
||||
6. `docker-compose-local.yml` and `Makefile`
|
||||
7. whether the current stack is Path A (local starter Docker) or Path B (shared/external tibi-server)
|
||||
8. whether website, admin, and API URLs all respond
|
||||
|
||||
This avoids the common mistake of treating setup as a naming exercise instead of a full stack-registration task.
|
||||
|
||||
@@ -226,3 +226,67 @@ curl "http://tibiserver:8080/api/v1/_/<namespace>/ssr?url=/de/ueber-uns"
|
||||
- **Navigation is part of SSR**: if header/footer are missing, the SSR setup is still incomplete even when the page body renders.
|
||||
- **SSR cache can go stale**: Always ensure `clear_cache.js` covers every collection that affects rendered output.
|
||||
- **Do not overfit the skill to demo content**: the skill should explain the architecture and where to inspect project-specific route/content rules, not freeze one content model as universal.
|
||||
|
||||
## SSR data loading pattern
|
||||
|
||||
In Svelte 5, SSR data loading works via **top-level `loadData()` calls** (NOT inside `$effect`):
|
||||
|
||||
```typescript
|
||||
// ✅ Richtig: Top-Level-Aufruf für SSR + Browser
|
||||
loadData()
|
||||
|
||||
async function loadData() {
|
||||
const data = await getCachedEntries(...)
|
||||
state = data
|
||||
}
|
||||
|
||||
// ❌ Falsch: $effect wird für SSR nicht rechtzeitig abgearbeitet
|
||||
$effect(() => { loadData() })
|
||||
```
|
||||
|
||||
**Warum das funktioniert:**
|
||||
- `loadData()` läuft während der Component-Initialisierung (vor Template-Auswertung)
|
||||
- `getCachedEntries` → `apiRequest` → SSR-Pfad → `context.ssrRequest()` → blockierender HTTP-Fetch in goja
|
||||
- goja's `await` auf einem bereits aufgelösten Promise läuft synchron weiter (kein Microtask-Hickhack)
|
||||
- State-Änderungen sind vor der Template-Auswertung sichtbar
|
||||
|
||||
**Browser-Reaktivität:** Wenn Props sich ändern (z.B. Navigation zu anderer Kategorie), wird die Component via `{#if}`/`{#key}` neu erstellt → `loadData()` läuft erneut.
|
||||
|
||||
## SSR-Cache in der Entwicklung
|
||||
|
||||
Der SSR-Cache ist das häufigste Debugging-Hindernis. Der Proxy in `esbuild.config.js` MUSS `&noCache=1` an den SSR-Request anhängen:
|
||||
|
||||
```javascript
|
||||
// esbuild.config.js – SSR-Proxy
|
||||
pathRewrite: function (path, req) {
|
||||
return "/ssr?url=" + encodeURIComponent(path) + "&noCache=1"
|
||||
}
|
||||
```
|
||||
|
||||
Ohne `noCache` wird die erste SSR-Antwort gecached und bei Code-Änderungen nicht invalidiert. Der Entwickler sieht immer den alten Stand. **Immer zuerst den Cache-Bypass prüfen, bevor SSR-Fehler gesucht werden.**
|
||||
|
||||
**Erkennungsmerkmale für veralteten SSR-Cache:**
|
||||
- `X-SSR-Cache: true` im Response-Header
|
||||
- `<!--COMMENT--><!--SSR.ERROR-->` im HTML
|
||||
- `__SSR_CACHE__` enthält nicht die erwarteten Daten
|
||||
- Neustart von tibi-server nötig nach `app.server.js`-Änderungen (`docker restart <tibiserver>`)
|
||||
|
||||
## Build-Arbeitsschritte bei SSR-Änderungen
|
||||
|
||||
Nach jeder Änderung an Svelte-Komponenten oder `api.ts` ist folgendes nötig:
|
||||
|
||||
```bash
|
||||
# 1. Frontend-Bundle bauen
|
||||
yarn build
|
||||
|
||||
# 2. SSR-Bundle bauen (app.server.js)
|
||||
yarn build:server
|
||||
|
||||
# 3. tibi-server neustarten (lädt neues app.server.js)
|
||||
docker restart <tibiserver>
|
||||
|
||||
# 4. Frontend neustarten (für Entwicklungs-Proxy)
|
||||
make docker-restart-frontend
|
||||
```
|
||||
|
||||
**Wichtig:** `yarn build:server` allein reicht nicht – der tibi-server cached das Modul im Speicher und lädt es nur beim Start neu.
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
---
|
||||
name: troubleshooting-and-debugging
|
||||
description: Diagnose common tibi website-project failures. Covers config loading, auth and permission mistakes, hook/goja errors, upload and CORS issues, and a practical debugging order so later agents do not thrash across unrelated layers.
|
||||
---
|
||||
|
||||
# troubleshooting-and-debugging
|
||||
|
||||
## When to use this skill
|
||||
|
||||
Use this skill when:
|
||||
|
||||
- a tibi project is failing in a way that is not obviously tied to one file
|
||||
- hooks, config, permissions, uploads, or routing behave unexpectedly
|
||||
- a project worked before and now fails after configuration or integration changes
|
||||
- you need a practical debugging order instead of ad-hoc guessing
|
||||
|
||||
## Goal
|
||||
|
||||
Help later agents debug the stack systematically.
|
||||
|
||||
The main value of this skill is not “more tips”. It is the order of operations: isolate the failing layer first, then inspect the right tool surface for that layer.
|
||||
|
||||
## Source of truth
|
||||
|
||||
Use these sources when debugging:
|
||||
|
||||
- `tibi-server/docs/15-troubleshooting.md`
|
||||
- `tibi-server/docs/06-hooks.md`
|
||||
- `tibi-server/docs/05-authentication.md`
|
||||
- `tibi-server/docs/17-field-level-permissions.md`
|
||||
- `tibi-server/internal/models/eval_context.go` when hook API exposure is in doubt
|
||||
- `tibi-server/internal/hook/context_*.go` when helper registration is in doubt
|
||||
- `.agents/skills/tibi-hook-authoring/SKILL.md`
|
||||
- `.agents/skills/security-hardening-and-token-strategy/SKILL.md`
|
||||
- `.agents/skills/tibi-ssr-caching/SKILL.md`
|
||||
|
||||
## Debugging order
|
||||
|
||||
Start in this order unless a more specific failure anchor already exists:
|
||||
|
||||
1. reachability and environment
|
||||
2. auth/token/permission layer
|
||||
3. config loading and YAML shape
|
||||
4. hook/goja behavior
|
||||
5. uploads/media pathing
|
||||
6. SSR/routing/publication behavior
|
||||
7. CORS or browser-integration issues
|
||||
|
||||
This order prevents chasing frontend symptoms when the real issue is project registration, token scope, or a broken config reload.
|
||||
|
||||
## Layer 1 — Reachability and environment
|
||||
|
||||
Check first:
|
||||
|
||||
- website URL responds
|
||||
- admin URL responds
|
||||
- API responds with JSON where expected
|
||||
- Docker services are actually up
|
||||
|
||||
If `/api/...` returns HTML, do not debug application logic yet. Fix the environment/proxy path first.
|
||||
|
||||
## Layer 2 — Auth and permission mistakes
|
||||
|
||||
Common patterns:
|
||||
|
||||
- `401 Unauthorized` → missing/invalid JWT or wrong auth surface
|
||||
- `403 Forbidden` → collection/action permissions wrong, user not assigned correctly, or token permission mismatch
|
||||
- static `Token` works for one surface but not another → wrong header type for the requested operation
|
||||
|
||||
Check:
|
||||
|
||||
- `X-Auth-Token` vs `Token` vs `X-Admin-Token`
|
||||
- collection/action permissions
|
||||
- project/user assignment
|
||||
- field-level readonly/hidden behavior if writes fail unexpectedly
|
||||
|
||||
Important current note:
|
||||
|
||||
- current upstream troubleshooting and auth docs still describe MD5-managed passwords; do not debug login failures by assuming the active username/password flow has already moved to bcrypt
|
||||
|
||||
## Layer 3 — Config and reload problems
|
||||
|
||||
Common patterns:
|
||||
|
||||
- project not loading after config change
|
||||
- env vars not resolving
|
||||
- CORS behaving differently than expected
|
||||
|
||||
Check:
|
||||
|
||||
- YAML syntax
|
||||
- whether the correct config file is being loaded
|
||||
- whether project reload actually ran
|
||||
- whether env placeholders use `${VAR_NAME}` format
|
||||
|
||||
When the problem smells like “the server ignores my config change”, verify reload and active config path before editing more files.
|
||||
|
||||
## Layer 4 — Hook and goja errors
|
||||
|
||||
Common patterns:
|
||||
|
||||
- `require is not defined`
|
||||
- `async`/`await` assumptions in hooks
|
||||
- wrong context object assumptions
|
||||
- timeouts or infinite loops
|
||||
|
||||
Remember:
|
||||
|
||||
- hooks run in goja, not Node.js
|
||||
- no `require()` or npm runtime
|
||||
- no normal async/await model
|
||||
- hook behavior often depends on the exact lifecycle step
|
||||
- tibi hook surfaces are accessed through `context`, including request and registered packages such as `context.request()`, `context.db.find()`, `context.http.fetch()`, `context.smtp.sendMail()`, `context.debug.dump()`, or `context.exec.command()`
|
||||
|
||||
Use the hook skill for step-specific quirks such as `context.data` timing or `context.filter` behavior.
|
||||
|
||||
## Layer 5 — Upload and media failures
|
||||
|
||||
Common patterns:
|
||||
|
||||
- files not being saved
|
||||
- media URLs render incorrectly
|
||||
- image filters return 404
|
||||
|
||||
Check:
|
||||
|
||||
- collection `uploadPath`
|
||||
- file field type
|
||||
- base64/data-URI or multipart expectations
|
||||
- filter names in collection config
|
||||
- whether frontend/admin preview code expects `_lookup` or raw IDs
|
||||
|
||||
## Layer 6 — SSR, routing, and publication failures
|
||||
|
||||
Common patterns:
|
||||
|
||||
- route works in browser but not in SSR
|
||||
- SSR returns empty page or wrong status
|
||||
- unpublished or inactive entries disappear unexpectedly
|
||||
|
||||
Check:
|
||||
|
||||
- route model and language-prefix handling
|
||||
- `ssrValidatePath()`
|
||||
- `publishedFilter`
|
||||
- lookup usage for page-critical relations
|
||||
- cache invalidation after mutations
|
||||
|
||||
Do not debug SSR only through browser navigation. Use the SSR endpoint directly when the failure is SSR-shaped.
|
||||
|
||||
## Layer 7 — Browser integration and CORS
|
||||
|
||||
Common patterns:
|
||||
|
||||
- browser form fails while direct API call works
|
||||
- preflight fails
|
||||
- credentials or auth headers do not cross origins correctly
|
||||
|
||||
Check:
|
||||
|
||||
- which layer owns CORS config
|
||||
- `allowOrigins`, `allowMethods`, `allowHeaders`, `allowCredentials`
|
||||
- whether the real deployment even needs cross-origin calls
|
||||
|
||||
## Useful debugging tools
|
||||
|
||||
### Hook-side debug helpers
|
||||
|
||||
```js
|
||||
context.debug.dump(context.data, "payload")
|
||||
context.debug.dump(context.request().header("Authorization"), "auth-header")
|
||||
```
|
||||
|
||||
### Request inspection
|
||||
|
||||
```js
|
||||
const req = context.request()
|
||||
context.debug.dump({
|
||||
method: req.method,
|
||||
path: req.path,
|
||||
url: req.url,
|
||||
host: req.host,
|
||||
clientIp: req.clientIp,
|
||||
})
|
||||
```
|
||||
|
||||
### Database inspection from hooks
|
||||
|
||||
```js
|
||||
const rows = context.db.find("content", { limit: 5 })
|
||||
context.debug.dump(rows, "sample-content")
|
||||
```
|
||||
|
||||
### Direct endpoint debugging
|
||||
|
||||
Prefer targeted `curl` probes for:
|
||||
|
||||
- API JSON responses
|
||||
- SSR endpoint behavior
|
||||
- auth header behavior
|
||||
- audit endpoint behavior when relevant
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- jumping between frontend, hooks, and config without isolating the failing layer
|
||||
- assuming a browser symptom proves a frontend bug
|
||||
- trying to fix permissions only in the UI
|
||||
- debugging SSR without the SSR endpoint
|
||||
- relying on outdated assumptions from old stack behavior
|
||||
|
||||
## What an LLM should inspect first
|
||||
|
||||
When asked to debug a tibi project with unclear failure ownership, inspect in this order:
|
||||
|
||||
1. current failing command or URL
|
||||
2. environment reachability
|
||||
3. auth/permission boundary
|
||||
4. config/reload state
|
||||
5. hook/runtime layer
|
||||
6. SSR/publication layer if the failure is page-related
|
||||
|
||||
This keeps debugging local and falsifiable instead of turning into broad repo wandering.
|
||||
Reference in New Issue
Block a user