feat: enhance project setup and architecture documentation

- Updated `tibi-project-setup` skill to clarify project initialization goals and steps.
- Improved `tibi-ssr-caching` skill to detail SSR architecture, responsibilities, and caching mechanisms.
- Introduced `website-solution-architecture` skill for translating website requirements into coherent solutions.
- Refined `AGENTS.md` to provide a structured roadmap for project development phases.
- Added `ADMIN_ASSET_VERSION` to `api/config.yml.env` for asset versioning.
- Updated SSR request flow and cache invalidation logic in `api/hooks/ssr/AGENTS.md`.
- Removed obsolete `esbuild.config.admin.js` and integrated asset versioning into the main `esbuild.config.js`.
- Adjusted `api/collections/content.yml` to utilize asset versioning for admin scripts.
This commit is contained in:
2026-05-12 20:01:22 +00:00
parent 4a604bab0b
commit 491f495c66
23 changed files with 3189 additions and 225 deletions
+177 -109
View File
@@ -1,6 +1,6 @@
--- ---
name: admin-ui-config name: admin-ui-config
description: Configure the admin UI for collections — meta labels, views (table/list/cards), field widgets, inputProps, fieldLists, sidebar layout, choices, foreign references, and image handling. Use when setting up or customizing collection admin views. 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.
--- ---
# admin-ui-config # admin-ui-config
@@ -10,7 +10,7 @@ description: Configure the admin UI for collections — meta labels, views (tabl
Use this skill when: Use this skill when:
- Configuring how a collection appears in the tibi-admin UI - Configuring how a collection appears in the tibi-admin UI
- Setting up table/list/card views for a collection - Configuring collection preview and default list presentation
- Configuring field widgets (dropdowns, media pickers, richtext, etc.) - Configuring field widgets (dropdowns, media pickers, richtext, etc.)
- Organizing fields into sidebar groups or sections - Organizing fields into sidebar groups or sections
- Setting up foreign key references between collections - Setting up foreign key references between collections
@@ -18,13 +18,15 @@ Use this skill when:
## Reference source ## Reference source
The canonical type definitions are in `tibi-admin-nova/types/admin.d.ts` (1296 lines). Always consult this file for the full API. This skill provides a practical summary. The canonical type definitions are in `tibi-admin-nova/types/admin.d.ts`. Always consult this file for the full API. This skill provides a practical summary.
Treat this skill as Nova-first. Use current Nova concepts such as `preview`, `singleton: { enabled }`, `drillDown`, `dependsOn`, `containerProps.layout`, `pagebuilder`, `viewHint`, `subNavigation`, and AI media assist.
--- ---
## Collection meta configuration ## Collection meta configuration
The `meta` key in a collection YAML controls how the collection appears in the admin sidebar and list views. The `meta` key in a collection YAML controls how the collection appears in the admin sidebar and collection/list UI.
```yaml ```yaml
name: mycollection name: mycollection
@@ -32,71 +34,55 @@ meta:
label: { de: "Produkte", en: "Products" } # Sidebar label (i18n) label: { de: "Produkte", en: "Products" } # Sidebar label (i18n)
muiIcon: shopping_cart # Material UI icon name muiIcon: shopping_cart # Material UI icon name
group: shop # Group in admin sidebar group: shop # Group in admin sidebar
singleton: false # true = only one entry allowed singleton:
hideInNavigation: false # true = don't show in sidebar enabled: false
defaultSort: "-insertTime" # Default sort (prefix - = descending) hide: false # Set to true to hide the collection for non-admin users
rowIdentTpl: { twig: "{{ name }} ({{ price }})" } # Row display template preview:
label: name
secondary: price
``` ```
### Row identification ### Preview
`rowIdentTpl` uses Twig syntax with field names. Used in admin list to identify entries: Use `meta.preview` as the universal entry representation for Nova lists, breadcrumbs, foreign-key widgets, and search result previews:
```yaml ```yaml
rowIdentTpl: { twig: "{{ name }}" } # Simple preview: name
rowIdentTpl: { twig: "{{ type }} — {{ language }}" } # Combined
preview:
label: name
secondary: slug
badge: status
preview:
eval: "`${$this.firstName} ${$this.lastName}`"
``` ```
--- ## List presentation
## Views: table, simpleList, cardList For current Nova, use `meta.viewHint` plus `meta.preview` for collection/list presentation.
The `views` array defines how entries are displayed in the admin list. Multiple views can coexist (e.g. table for desktop, simpleList for mobile).
### Table view
```yaml ```yaml
meta: meta:
views: viewHint: table
- type: table preview:
columns: label: name
- name # Simple: field name as column secondary: slug
- source: lang # With filter badge: status
filter: true table:
- source: active # Boolean column with filter - name
filter: true - source: status
- source: price # Custom label label: Status
label: { de: "Preis", en: "Price" } - source: author.name
- source: insertTime # Date field label: Author
width: 160 select:
- author.name
``` ```
### Simple list view (mobile) - `meta.viewHint` controls the preferred collection presentation (`table`, `cards`, `media`, or `navigation` object where supported).
- `preview.table` defines explicit list columns for Nova.
```yaml - `preview.select` can reduce lookup work for preview table columns.
meta: - `meta.subNavigation` defines filtered entry tabs in the sidebar.
views:
- type: simpleList
mediaQuery: "(max-width: 600px)" # Show only on small screens
primaryText: name
secondaryText: lang
tertiaryText: path
image: thumbnail # Optional: show image thumbnail
```
### Card list view
```yaml
meta:
views:
- type: cardList
fields:
- source: name
label: Name
- source: price
label: Preis
widget: currency
```
--- ---
@@ -171,18 +157,25 @@ Override the default widget with `meta.widget`:
meta: meta:
widget: richtext # Rich text editor (HTML) widget: richtext # Rich text editor (HTML)
- name: color - name: heroImage
type: string type: file
meta: meta:
widget: color # Color picker widget: image # Image-focused file widget
- name: image - name: relatedPages
type: string type: string[]
meta: meta:
widget: medialib # Media library picker widget: foreignKeyChipArray
``` ```
Common widget types: `text` (default), `richtext`, `color`, `medialib`, `code`, `markdown`, `password`, `hidden`. Common widget types: `text`, `checkbox`, `select`, `chipArray`, `checkboxArray`, `date`, `datetime`, `file`, `image`, `richtext`, `json`, `foreignKey`, `foreignKeyChipArray`, `pagebuilder`, `containerLessObject`, `containerLessObjectArray`.
Important current widgets/features to consider when designing a real website backoffice:
- `pagebuilder` for CMS-driven block/page authoring
- `foreignKeyChipArray` for many-reference editing
- `image` plus `imageEditor` / `downscale` for image-heavy workflows
- `drillDown` editing for complex nested arrays
### Choices — dropdowns/selects ### Choices — dropdowns/selects
@@ -211,7 +204,7 @@ Dynamic choices from API:
choices: choices:
endpoint: categories # Collection name endpoint: categories # Collection name
mapping: mapping:
id: _id id: id
name: name name: name
``` ```
@@ -226,22 +219,25 @@ Link to entries in another collection:
label: { de: "Autor", en: "Author" } label: { de: "Autor", en: "Author" }
foreign: foreign:
collection: users collection: users
id: _id id: id
sort: name sort: name
projection: name,email projection: name,email
render: { twig: "{{ name }} <{{ email }}>" } render:
autoFill: # Auto-fill other fields on selection label: name
- source: email secondary: email
target: authorEmail createDefaults:
role: author
``` ```
Use `foreign.id: id` for the outward FK field identity. Only Mongo-style filters/query conditions use `_id`. Use `foreign.render` or target-collection `meta.preview` so references stay readable. Bare IDs are not acceptable authoring UX for a serious website project.
### Image fields ### Image fields
```yaml ```yaml
- name: image - name: image
type: file type: file
meta: meta:
widget: medialib widget: image
downscale: # Auto-resize on upload downscale: # Auto-resize on upload
maxWidth: 1920 maxWidth: 1920
maxHeight: 1080 maxHeight: 1080
@@ -264,7 +260,7 @@ Link to entries in another collection:
- name: publishDate - name: publishDate
type: date type: date
meta: meta:
position: "sidebar:Veröffentlichung" # Sidebar with group header position: "sidebar:publishing" # Sidebar with group key
``` ```
### Sidebar groups (ordered) ### Sidebar groups (ordered)
@@ -274,9 +270,12 @@ Define sidebar group order in collection meta:
```yaml ```yaml
meta: meta:
sidebar: sidebar:
- Veröffentlichung - group: publishing
- SEO label: { de: "Veröffentlichung", en: "Publishing" }
- Einstellungen - group: seo
label: { de: "SEO", en: "SEO" }
- group: settings
label: { de: "Einstellungen", en: "Settings" }
``` ```
### Sections in main area ### Sections in main area
@@ -313,6 +312,15 @@ Use `containerProps` for multi-column layout:
size: col-6 size: col-6
``` ```
`containerProps.layout` is one of the most important Nova ergonomics features. Use it aggressively to avoid long, single-column forms.
Recommended pattern for real projects:
- sidebar for publication, SEO, flags, relations, admin-only metadata
- main area for editorial content
- 2-column or 3-column layout for short related fields
- section headings for repeated conceptual groups
--- ---
## Nested objects and arrays ## Nested objects and arrays
@@ -340,7 +348,9 @@ Use `containerProps` for multi-column layout:
type: object[] type: object[]
meta: meta:
label: { de: "Inhaltsblöcke", en: "Content Blocks" } label: { de: "Inhaltsblöcke", en: "Content Blocks" }
preview: { eval: "item.type + ': ' + (item.headline || '')" } widget: pagebuilder
preview: { eval: "`${$this.type}: ${$this.headline || ''}`" }
drillDown: true
subFields: subFields:
- name: type - name: type
type: string type: string
@@ -358,6 +368,74 @@ Use `containerProps` for multi-column layout:
The `preview` eval determines what's shown in the collapsed state of each array item. The `preview` eval determines what's shown in the collapsed state of each array item.
### Drill-down arrays
For complex `object[]` data, prefer `drillDown: true` over dense inline editing. This is especially important for:
- nested content blocks
- FAQs / accordions
- team members with nested metadata
- pricing tables / feature matrices
### Pagebuilder fields
Nova supports pagebuilder configuration at both collection and field level.
Typical pattern:
```yaml
meta:
pagebuilder:
blockTypeField: type
defaultViewport: desktop
blockRegistry:
file: /_/assets/dist/admin.mjs
fields:
- name: blocks
type: object[]
meta:
widget: pagebuilder
pagebuilder:
blockTypeField: type
```
Use pagebuilder when editors work with heterogeneous content blocks. Use plain `object[]` only when the structure is uniform and simple.
### dependsOn
Use `dependsOn` to show only fields relevant to the selected block or mode:
```yaml
- name: image
type: file
meta:
dependsOn:
eval: $parent.type == 'hero'
```
This is critical for keeping pagebuilder schemas usable.
### AI-aware media and admin features
Current Nova types support AI-related admin capabilities, especially around media workflows. When appropriate for a project:
- use AI-assisted alt/caption generation for image-heavy collections
- prefer explicit target fields for generated metadata
- keep AI assist opt-in and editorially reviewable
Use AI only where it improves authoring quality; do not force it into every collection.
## Field-level permissions and authoring safety
Current tibi-server supports `readonlyFields`, `hiddenFields`, and eval-based field visibility/readonly rules.
Reflect these server rules in admin design:
- do not put critical computed fields front-and-center if editors may not be allowed to modify them
- use `dependsOn`, `hidden`, and readonly semantics deliberately
- remember that server-side permissions are authoritative even if the UI looks editable
### Drill-down ### Drill-down
For complex nested objects, use `drillDown` to render them as a sub-page: For complex nested objects, use `drillDown` to render them as a sub-page:
@@ -405,28 +483,26 @@ meta:
label: { de: "Produkte", en: "Products" } label: { de: "Produkte", en: "Products" }
muiIcon: inventory_2 muiIcon: inventory_2
group: shop group: shop
defaultSort: "-insertTime" viewHint: table
rowIdentTpl: { twig: "{{ name }} ({{ sku }})" } defaultSort:
field: insertTime
order: DESC
preview:
label: name
secondary: sku
badge: active
table:
- name
- sku
- source: price
label: { de: "Preis", en: "Price" }
- source: category
label: { de: "Kategorie", en: "Category" }
sidebar: sidebar:
- Veröffentlichung - group: publishing
- SEO label: { de: "Veröffentlichung", en: "Publishing" }
- group: seo
views: label: { de: "SEO", en: "SEO" }
- type: simpleList
mediaQuery: "(max-width: 600px)"
primaryText: name
secondaryText: sku
image: image
- type: table
columns:
- name
- sku
- source: price
label: { de: "Preis", en: "Price" }
- source: active
filter: true
- source: category
filter: true
permissions: permissions:
public: public:
@@ -439,18 +515,12 @@ permissions:
put: true put: true
delete: true delete: true
hooks:
beforeRead: |
!include hooks/filter_public.js
afterWrite: |
!include hooks/clear_cache.js
fields: fields:
- name: active - name: active
type: boolean type: boolean
meta: meta:
label: { de: "Aktiv", en: "Active" } label: { de: "Aktiv", en: "Active" }
position: "sidebar:Veröffentlichung" position: "sidebar:publishing"
- name: name - name: name
type: string type: string
meta: meta:
@@ -492,7 +562,7 @@ fields:
type: file type: file
meta: meta:
label: { de: "Produktbild", en: "Product Image" } label: { de: "Produktbild", en: "Product Image" }
widget: medialib widget: image
downscale: downscale:
maxWidth: 1200 maxWidth: 1200
quality: 0.85 quality: 0.85
@@ -500,12 +570,12 @@ fields:
type: string type: string
meta: meta:
label: { de: "SEO Titel", en: "SEO Title" } label: { de: "SEO Titel", en: "SEO Title" }
position: "sidebar:SEO" position: "sidebar:seo"
- name: seoDescription - name: seoDescription
type: string type: string
meta: meta:
label: { de: "SEO Beschreibung", en: "SEO Description" } label: { de: "SEO Beschreibung", en: "SEO Description" }
position: "sidebar:SEO" position: "sidebar:seo"
inputProps: inputProps:
multiline: true multiline: true
rows: 3 rows: 3
@@ -515,10 +585,8 @@ fields:
## Common pitfalls ## Common pitfalls
- **`meta.label` is i18n** — Always provide `{ de: "...", en: "..." }` objects, not plain strings. - **`meta.label` supports both strings and i18n objects** — Use i18n objects only when the collection or field label must be localized.
- **`views` order matters** — First matching view (by `mediaQuery`) is shown. Put mobile views (with `mediaQuery`) before desktop views (without).
- **`choices.id` must match stored value** — The `id` in choices is what gets saved to the database. - **`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. - **`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. - **`position: sidebar` without group** — Fields go to an ungrouped area. Use `position: "sidebar:GroupName"` for grouping.
- **`type: object[]` needs `subFields`** — Forgetting `subFields` renders an empty repeater. - **`type: object[]` needs `subFields`** — Forgetting `subFields` renders an empty repeater.
- **hooks path** — Hook includes are relative to `api/` directory: `!include hooks/myfile.js`.
+88 -25
View File
@@ -19,19 +19,23 @@ Use this skill when:
This project does **NOT** use file-based routing (no SvelteKit router). Instead: This project does **NOT** use file-based routing (no SvelteKit router). Instead:
1. Pages are **CMS entries** in the `content` collection with a `path` field. 1. Pages are **CMS entries** in the `content` collection with a `path` field.
2. `App.svelte` reacts to URL changes → calls `getCachedEntries("content", { lang, path, active: true })`. 2. Public URLs are typically language-prefixed (`/de/...`, `/en/...`), but the DB entry in `content.path` is stored **without** that language prefix.
3. The matching `ContentEntry.blocks[]` array is passed to `BlockRenderer.svelte`. 3. `App.svelte` reacts to URL changes → strips the language prefix → calls `getCachedEntries("content", { lang, path, active: true })`.
4. Each block has a `type` field that maps to a Svelte component. 4. The same loading path is used for browser navigation and SSR.
5. The matching `ContentEntry.blocks[]` array is passed to `BlockRenderer.svelte`.
6. Each block has a `type` field that maps to a Svelte component.
**Implication:** To add a new page, you create a content entry (via Admin UI or API) — no new Svelte file or route config is needed. **Implication:** To add a new page, you create a content entry (via Admin UI or API) — no new Svelte file or route config is needed.
**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.
--- ---
## Adding a new page ## Adding a new page
### Option A: Via Admin UI (preferred for content editors) ### Option A: Via Admin UI (preferred for content editors)
1. Open the tibi-admin at `CODING_URL/_/admin/`. 1. Open the Nova admin at `https://{PROJECT_NAME}-tibiadmin.code.testversion.online/`.
2. Navigate to **Inhalte** (Content) collection. 2. Navigate to **Inhalte** (Content) collection.
3. Click **New** and fill in: 3. Click **New** and fill in:
- `name`: Display name (e.g. "Über uns") - `name`: Display name (e.g. "Über uns")
@@ -43,6 +47,13 @@ This project does **NOT** use file-based routing (no SvelteKit router). Instead:
- `meta.title` / `meta.description`: SEO metadata - `meta.title` / `meta.description`: SEO metadata
4. Save. The page is immediately available at `/{lang}{path}`. 4. Save. The page is immediately available at `/{lang}{path}`.
**Nova authoring guidance:**
- Prefer meaningful `meta.preview` and field `preview` configs so entries and nested blocks are understandable in breadcrumbs, foreign-key widgets, and arrays.
- Use `containerProps.layout.size` to keep editors on one screen instead of stacking every field vertically.
- Use `dependsOn` to hide block-specific fields until the relevant block type is selected.
- Prefer drill-down editing for larger `object[]` structures instead of flat, folded arrays.
### Option B: Via API ### Option B: Via API
```sh ```sh
@@ -74,17 +85,22 @@ To make the page appear in the header/footer menu, edit the corresponding `navig
# Get existing header nav # Get existing header nav
curl "$CODING_URL/api/navigation?filter[type]=header&filter[language]=de" -H "Token: $ADMIN_TOKEN" curl "$CODING_URL/api/navigation?filter[type]=header&filter[language]=de" -H "Token: $ADMIN_TOKEN"
# PUT to update elements array (add your page) # Look up the content entry ID for your page
curl "$CODING_URL/api/content?filter[path]=/ueber-uns&filter[lang]=de" -H "Token: $ADMIN_TOKEN"
# PUT to update elements array (add your page by FK id)
curl -X PUT "$CODING_URL/api/navigation/<id>" \ curl -X PUT "$CODING_URL/api/navigation/<id>" \
-H "Token: $ADMIN_TOKEN" \ -H "Token: $ADMIN_TOKEN" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{ "elements": [ ...existing, { "name": "Über uns", "page": "/ueber-uns" } ] }' -d '{ "elements": [ ...existing, { "name": "Über uns", "page": "<content-id>" } ] }'
``` ```
If navigation drives the public website shell, treat navigation as page-critical SSR data. A page is not fully SSR-ready if only the main content entry exists but header/footer navigation is missing.
### Multi-language pages ### Multi-language pages
- Create one `ContentEntry` per language with the **same `translationKey`** but different `lang` and `path`. - Create one `ContentEntry` per language with the **same `translationKey`** but different `lang` and `path`.
- The language switcher in `App.svelte` uses `currentContentEntry.translationKey` to find the equivalent page. - The language switcher is path-based and derives the target URL from the current route plus `ROUTE_TRANSLATIONS`.
- Add localized route slugs to `ROUTE_TRANSLATIONS` in `frontend/src/lib/i18n.ts` if URLs should differ per language (e.g. `/ueber-uns` vs `/about`). - Add localized route slugs to `ROUTE_TRANSLATIONS` in `frontend/src/lib/i18n.ts` if URLs should differ per language (e.g. `/ueber-uns` vs `/about`).
--- ---
@@ -130,6 +146,8 @@ import MyNewBlock from "./MyNewBlock.svelte"
<MyNewBlock {block} /> <MyNewBlock {block} />
``` ```
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: Extend TypeScript types (if new fields are needed)
Edit `types/global.d.ts` — add fields to `ContentBlockEntry`: Edit `types/global.d.ts` — add fields to `ContentBlockEntry`:
@@ -156,6 +174,9 @@ Edit `api/collections/content.yml` — add subFields under `blocks`:
type: string type: string
- name: myItems - name: myItems
type: object[] type: object[]
meta:
drillDown: true
preview: title
subFields: subFields:
- name: title - name: title
type: string type: string
@@ -163,6 +184,14 @@ Edit `api/collections/content.yml` — add subFields under `blocks`:
type: string type: string
``` ```
Use current Nova patterns when extending block schemas:
- `meta.preview` for entry and block previews
- `meta.drillDown: true` for nested arrays that would otherwise become hard to edit
- `containerProps.layout.size` for dense editor layouts
- `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) ### Step 5: Update mock data (if using MOCK=1)
Add a block with your new type to `frontend/mocking/content.json`. Add a block with your new type to `frontend/mocking/content.json`.
@@ -173,6 +202,13 @@ Add a block with your new type to `frontend/mocking/content.json`.
yarn validate # TypeScript check — must be warning-free yarn validate # TypeScript check — must be warning-free
``` ```
For blocks that appear on SSR pages, also verify:
```sh
yarn build:server
# then request the SSR endpoint directly and check that the block content appears in HTML
```
### Existing block types for reference ### Existing block types for reference
| Type | Component | Purpose | | Type | Component | Purpose |
@@ -188,7 +224,7 @@ yarn validate # TypeScript check — must be warning-free
### Step 1: Create collection YAML ### Step 1: Create collection YAML
Create `api/collections/mycollection.yml`. Use `content.yml` or `navigation.yml` as a template: Create `api/collections/mycollection.yml`. Use `content.yml`, `navigation.yml`, or a current `tibi-admin-nova` example config as a template:
```yaml ```yaml
######################################################################## ########################################################################
@@ -199,17 +235,13 @@ name: mycollection
meta: meta:
label: { de: "Meine Sammlung", en: "My Collection" } label: { de: "Meine Sammlung", en: "My Collection" }
muiIcon: category # Material UI icon name muiIcon: category # Material UI icon name
rowIdentTpl: { twig: "{{ name }}" } # Row display in admin list viewHint: table
preview:
views: label: name
- type: simpleList table:
mediaQuery: "(max-width: 600px)" - name
primaryText: name - source: active
- type: table label: Active
columns:
- name
- source: active
filter: true
permissions: permissions:
public: public:
@@ -234,9 +266,17 @@ fields:
# Add more fields as needed # Add more fields as needed
``` ```
Use current Nova config:
- `preview` for row/foreign/search display
- object-form `singleton`
- `sidebar` groups instead of ad hoc sidebars
- `pagebuilder` defaults when a collection contains pagebuilder fields
- `viewHint` plus `preview.table` for better admin ergonomics
**Field types:** `string`, `number`, `boolean`, `object`, `object[]`, `string[]`, `file`, `file[]`. **Field types:** `string`, `number`, `boolean`, `object`, `object[]`, `string[]`, `file`, `file[]`.
For the full schema reference: `tibi-types/schemas/api-config/collection.json`. For the full schema reference: `tibi-types/schemas/config/collection.schema.json`.
### Step 2: Include in config.yml ### Step 2: Include in config.yml
@@ -285,17 +325,30 @@ type EntryTypeSwitch<T extends string> = T extends "medialib"
Common hook patterns: Common hook patterns:
- **Public filter** — reuse `filter_public.js` to enforce `active: true` for unauthenticated users. - **Public filter** — reuse `filter_public.js` to enforce `active: true` for unauthenticated users.
- **Before-save validation** — create `api/hooks/mycollection_validate.js`. - **Write validation** — add method/step hook files such as `api/hooks/mycollection/post_validate.js` or `api/hooks/mycollection/put_validate.js`.
- **Cache invalidation** — add your collection to `api/hooks/clear_cache.js` if it affects rendered pages. - **Cache invalidation** — add your collection to `api/hooks/clear_cache.js` if it affects rendered pages.
- **Action endpoints** — prefer `actions:` instead of fake collections when you need forms, newsletters, calculators, imports, or other endpoint-like behavior without CRUD storage.
Reference hook in YAML: Reference hook in YAML:
```yaml ```yaml
hooks: hooks:
beforeRead: | get:
!include hooks/filter_public.js read:
afterWrite: | type: javascript
!include hooks/clear_cache.js file: hooks/filter_public.js
put:
update:
type: javascript
file: hooks/clear_cache.js
post:
create:
type: javascript
file: hooks/clear_cache.js
delete:
delete:
type: javascript
file: hooks/clear_cache.js
``` ```
### Step 6: Add mock data (if using MOCK=1) ### Step 6: Add mock data (if using MOCK=1)
@@ -313,6 +366,14 @@ yarn validate # TypeScript check
# If Docker is running, the tibi-server auto-reloads the collection config # If Docker is running, the tibi-server auto-reloads the collection config
``` ```
For collections intended for rich editorial usage, also verify in Nova:
- list/table/card previews are readable
- nested arrays are editable with drill-down where needed
- sidebar groups and layout are usable without scrolling through one long form
- foreign-key displays use meaningful previews
- pagebuilder fields render previews and screenshots correctly
--- ---
## Common pitfalls ## Common pitfalls
@@ -322,3 +383,5 @@ yarn validate # TypeScript check
- **Block `hide` field**: Blocks with `hide: true` are skipped by `BlockRenderer.svelte` — useful for draft blocks. - **Block `hide` field**: Blocks with `hide: true` are skipped by `BlockRenderer.svelte` — useful for draft blocks.
- **Collection YAML indentation**: YAML uses 2-space indentation. Sub-fields under `object[]` require a `subFields` key. - **Collection YAML indentation**: YAML uses 2-space indentation. Sub-fields under `object[]` require a `subFields` key.
- **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`. - **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.
+42 -11
View File
@@ -15,6 +15,7 @@ Use this skill when:
- Adding new Svelte 5 reactive patterns - Adding new Svelte 5 reactive patterns
- Understanding the API layer and error handling - Understanding the API layer and error handling
- Working with i18n / multi-language features - Working with i18n / multi-language features
- Understanding how SSR and SPA loading share one app-level data path
--- ---
@@ -81,6 +82,22 @@ Example: /de/ueber-uns → lang="de", routePath="/ueber-uns"
Root `/` redirects to `/{browserLanguage}/` via `getBrowserLanguage()`. Root `/` redirects to `/{browserLanguage}/` via `getBrowserLanguage()`.
### SSR interaction with routing
This frontend is not just an SPA. The same top-level app also participates in SSR.
- `frontend/src/ssr.ts` is intentionally thin and should mostly bootstrap locale state and call `render(App, { props: { url } })`.
- `App.svelte` owns page loading for both browser and SSR.
- Browser navigation triggers page loading from `$effect`.
- SSR triggers the same page-loading path directly inside `typeof window === "undefined"`.
This means route changes, i18n path handling, and content-loading behavior must be reasoned about together. If a route works in the browser but SSR returns empty content or 404, inspect the mapping between:
- public URL (`/de/...`)
- stripped route path (`/...`)
- `content.path` in the DB
- `api/hooks/config.js` SSR route validation
### Navigation API ### Navigation API
```typescript ```typescript
@@ -130,6 +147,8 @@ export const ROUTE_TRANSLATIONS: Record<string, Record<SupportedLanguage, string
} }
``` ```
Keep in mind that these translations affect the public URL shape and therefore also the SSR route-validation layer. Changing localized slugs is not purely a frontend concern.
--- ---
## State management ## State management
@@ -138,17 +157,17 @@ The project uses **Svelte writable/derived stores** (not a centralized state lib
### Store inventory ### Store inventory
| Store | File | Purpose | | Store | File | Purpose |
| ---------------------- | ---------------------- | ----------------------------------------------------------------------------------- | | ---------------------- | ---------------------- | -------------------------------------------------------------------------------- |
| `location` | `lib/store.ts` | Current URL state (path, search, hash, push/pop flags) | | `location` | `lib/store.ts` | Current URL state (path, search, hash, push/pop flags) |
| `mobileMenuOpen` | `lib/store.ts` | Whether mobile hamburger menu is open | | `mobileMenuOpen` | `lib/store.ts` | Whether mobile hamburger menu is open |
| `currentContentEntry` | `lib/store.ts` | Currently displayed page's `translationKey`, `lang`, `path` (for language switcher) | | `currentContentEntry` | `lib/store.ts` | Currently displayed page entry data such as `translationKey`, `lang`, and `path` |
| `previousPath` | `lib/store.ts` | Previous URL path (for conditional back buttons) | | `previousPath` | `lib/store.ts` | Previous URL path (for conditional back buttons) |
| `apiBaseOverride` | `lib/store.ts` | Override API base URL (used by admin module) | | `apiBaseOverride` | `lib/store.ts` | Override API base URL (used by admin module) |
| `cookieConsentVisible` | `lib/store.ts` | Whether cookie consent banner is showing | | `cookieConsentVisible` | `lib/store.ts` | Whether cookie consent banner is showing |
| `currentLanguage` | `lib/i18n.ts` | Derived from `$location.path` — current language code | | `currentLanguage` | `lib/i18n.ts` | Derived from `$location.path` — current language code |
| `selectedLanguage` | `lib/i18n.ts` | Writable — synced with `currentLanguage` on navigation | | `selectedLanguage` | `lib/i18n.ts` | Writable — synced with `currentLanguage` on navigation |
| `activeRequests` | `lib/requestsStore.ts` | Number of in-flight API requests (drives `LoadingBar`) | | `activeRequests` | `lib/requestsStore.ts` | Number of in-flight API requests (drives `LoadingBar`) |
### Pattern: creating a new store ### Pattern: creating a new store
@@ -244,6 +263,16 @@ Located in `frontend/src/lib/api.ts`. Features:
- **Mock interceptor** — when `__MOCK__` is `true`, routes requests to `frontend/mocking/*.json` - **Mock interceptor** — when `__MOCK__` is `true`, routes requests to `frontend/mocking/*.json`
- **Sentry integration** — span instrumentation (when enabled) - **Sentry integration** — span instrumentation (when enabled)
### Shared browser/SSR transport
The project intentionally shares the low-level API transport between browser and SSR via `api/hooks/lib/ssr`.
- In the browser, it eventually becomes `fetch(...)`.
- In SSR, `apiRequest(...)` delegates to `context.ssrRequest(...)`.
- GET responses reached during SSR are written into `window.__SSR_CACHE__` for hydration.
This is why SSR can preload both content and navigation without building a separate frontend-only data layer.
### Usage patterns ### Usage patterns
```typescript ```typescript
@@ -358,3 +387,5 @@ Return { data, count, buildTime }
- **`_id` not `id` for filters** — API filters use MongoDB's `_id`, but response objects may have both `id` and `_id`. - **`_id` not `id` for filters** — API filters use MongoDB's `_id`, but response objects may have both `id` and `_id`.
- **`$location` strips trailing slashes** — `/about/` becomes `/about` (except root `/`). - **`$location` strips trailing slashes** — `/about/` becomes `/about` (except root `/`).
- **Content cache is 1 hour** — `getCachedEntries` caches in memory for 1h. For admin previews, use `getDBEntries` (uncached). - **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.
+1 -1
View File
@@ -154,7 +154,7 @@ db.content.findOne({ path: "/" })
### Navigation aktualisieren ### Navigation aktualisieren
```js ```js
db.navigation.updateOne({ type: "header", language: "de" }, { $set: { "items.0.label": "Neues Label" } }) db.navigation.updateOne({ type: "header", language: "de" }, { $set: { "elements.0.name": "Neues Label" } })
``` ```
### Dokument-Struktur inspizieren ### Dokument-Struktur inspizieren
@@ -0,0 +1,202 @@
---
name: media-seo-publishing
description: Model media, SEO, and publishing workflows for website projects on this starter. Covers file fields, image validation/filtering, alt texts, social metadata, publication windows, and SSR/cache implications.
---
# media-seo-publishing
## When to use this skill
Use this skill when:
- Designing media-heavy website content models
- Adding image/file fields and image filters
- Modeling SEO fields for pages or reusable content
- Defining publication windows and how they interact with runtime and SSR
- Building authoring workflows around images, metadata, and release control
## Goal
The goal is to make media, SEO, and publishing part of the actual solution design instead of leaving them as late add-ons.
For real website projects, these concerns affect:
- collection schema
- admin ergonomics
- frontend rendering
- SSR/cache validity
- editorial quality
## Source of truth
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`
- `tibi-admin-nova/docs/collection-config.md`
- `api/hooks/config.js`
- `api/hooks/lib/ssr-server.js`
## Media modeling
Use file fields deliberately.
Typical choices:
- `file` for a single image or asset
- `file[]` for galleries or multi-asset attachments
- foreign references to a media collection when assets need their own lifecycle or reuse
Choose between inline file fields and dedicated media references based on reuse and editorial workflow, not just convenience.
## File validation rules
For serious website builds, do not leave file fields unconstrained.
Define validators where appropriate:
- accepted mime types
- max file size
- min/max image dimensions
- whether mixed media is allowed
This should reflect actual content needs. Hero images, logos, documents, and gallery media often need different constraints.
## Image filters
If the project serves resized or transformed assets, define image filters intentionally.
Use filters for:
- thumbnails
- card images
- hero images
- OpenGraph/social images when relevant
Do not leave every consuming component to invent its own ad hoc asset sizes.
## Alt texts and captions
Accessibility and SEO-relevant image metadata should be explicit in the model.
Recommended approach:
- store alt text explicitly
- keep captions separate from alt text
- use localized fields if the site is multilingual
- optionally use AI assistance only as a suggestion flow
Do not treat filenames as acceptable alt text.
## SEO modeling
Page-like collections should usually model SEO explicitly.
Typical fields:
- `meta.title`
- `meta.description`
- social/share image
- optional canonical information if required
- optional index/follow controls for advanced projects
SEO fields should be easy to find in Nova, usually via sidebar groups or clearly named sections.
## Publishing model
If the site uses publication timing, define it intentionally.
Typical concerns:
- draft versus active state
- publication window (`from` / `to`)
- visibility of unpublished content in public reads
- SSR cache validity for time-sensitive content
Publishing is not just a boolean. If publication windows exist, they must influence runtime and cache behavior.
## SSR implications
Media, SEO, and publishing affect SSR directly.
Examples:
- page meta tags must exist in SSR HTML when relevant
- navigation or content with publication windows must invalidate cached HTML correctly
- image-driven blocks must render stable URLs/markup in SSR
If publication timing can make cached HTML stale, the relevant collections must be accounted for in SSR publish-check logic.
## Admin ergonomics
Use current Nova features to make media/SEO workflows usable:
- sidebar groups for SEO/publication fields
- `viewHint.media` for media-focused collections
- previews for image-bearing entities
- layout grouping so editors do not scroll through one long file/SEO form
Media and SEO fields are often technically present but operationally poor if the admin layout is ignored.
## Recommended modeling patterns
### Marketing page
Recommended shape:
- main content blocks
- explicit SEO object or fields
- hero/share image strategy
- publication controls in sidebar
### Media library entry
Recommended shape:
- file field
- title/name
- alt text / caption
- optional copyright/source
- image-focused admin view
### Reusable teaser or card entity
Recommended shape:
- image reference or file
- short label/title
- teaser text
- consistent image filter usage in frontend components
## Anti-patterns
- No file validators on public/editor-uploaded images
- No explicit alt field
- Mixing captions and alt text into one field
- Hardcoding image sizes only in frontend CSS/components
- Treating publication as frontend-only logic
- Forgetting that publish windows can invalidate SSR HTML
## 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.
## What an LLM should inspect first
When asked to work on media, SEO, or publishing on this starter, inspect in this order:
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
This prevents “just add an image field” changes that break runtime, editorial UX, or caching.
@@ -0,0 +1,219 @@
---
name: nova-ai-editor-features
description: Use current AI and LLM capabilities in tibi-admin-nova and tibi-server responsibly. Covers media AI assist, LLM provider setup, token budgets, editor-facing AI workflows, and where AI should or should not be used in website projects.
---
# nova-ai-editor-features
## When to use this skill
Use this skill when:
- A website project should use AI-assisted editor workflows
- You want AI help for media metadata, alt texts, captions, or editorial helper flows
- You need to design LLM-backed actions or admin features on top of tibi-server
- You need to decide whether AI belongs in the admin, in actions, or nowhere
## Goal
The goal is to help an LLM build **useful and controllable** AI features for editors.
Use AI where it improves editorial throughput or content quality. Do not add AI just because it exists.
## Source of truth
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`
- `api/config.yml`
- the project's actual Nova runtime config when such a file exists
## Two AI surfaces in this stack
### 1. Nova media/editor assistance
Nova supports editor-facing AI assistance, especially around media workflows.
Typical pattern:
```yaml
meta:
viewHint:
media:
ai:
targetField: alt
prompt: Beschreibe das Bild kurz und sachlich für einen Alt-Text.
image:
maxWidth: 1280
maxHeight: 1280
quality: 0.82
```
Use this when editors benefit from assisted metadata generation directly in the admin.
### 2. tibi-server LLM proxy and actions
tibi-server provides an LLM proxy with:
- provider configuration
- model whitelisting
- streaming support
- user and org token budgets
- usage logging
This is the right foundation when a project needs controlled backend LLM usage.
## Recommended AI use cases for websites
Good use cases:
- alt text suggestions for uploaded images
- caption or summary suggestions for media-heavy content
- internal editorial helper actions
- controlled rewrite or classification helpers for structured content
- AI support in specialized admin workflows where output is reviewed by humans
Weak or risky use cases:
- auto-publishing public text without review
- replacing the content model with one giant AI prompt field
- hiding important business logic inside opaque prompts
- bypassing permissions or audit trails through AI shortcuts
## AI for media collections
For image-heavy collections, prefer AI as **assistive autofill**, not as a silent overwrite mechanism.
Use Nova media AI when:
- the editor already works inside a media-oriented screen
- the target field is explicit
- the generated text is reviewable
- existing manual values are not overwritten automatically
Prefer explicit target fields such as:
- `alt`
- `caption`
- `localizedCaption.de`
## LLM provider architecture
When enabling server-side LLM usage, define:
- which providers are configured
- which models are allowed
- which model is default
- max tokens per request
- which users or orgs have budgets
Never assume arbitrary models are available. Model choice must stay inside the configured whitelist.
## Token budget design
Use budgets deliberately.
If a project adds editor-facing AI features, define:
- which users may use them
- per-provider token budgets
- org-level budgets if multiple editors share a pool
- expected failure behavior when budgets are exhausted
An editor-facing AI workflow is incomplete if the quota/failure path is not planned.
## Where AI logic should live
Choose the surface intentionally:
- **Nova media AI** for direct editor assistance in image/media workflows
- **Action endpoint** for reusable backend AI workflows with validation and auditing
- **Collection config only** when Nova already provides the needed behavior declaratively
Do not push provider credentials or prompt orchestration into the browser.
## Prompting rules for serious projects
Prompts should be:
- narrow in purpose
- reviewable by humans
- tied to an explicit target field or action contract
- stable enough that editors know what the feature does
Avoid vague prompts such as “improve this content” when the output target and editorial rules are unclear.
## AI + permissions + audit
AI features must still respect:
- field-level permissions
- hidden/readonly fields
- action permissions
- org/user budget boundaries
- logging/auditing expectations
Do not let AI become a side channel around your normal content governance.
## Recommended implementation patterns
### Media alt-text assist
Recommended shape:
- media-oriented collection or view
- `viewHint.media.ai.targetField`
- prompt focused on accessibility and factual image description
- human review before publishing
### Editorial helper action
Recommended shape:
- authenticated action endpoint
- input validation
- provider/model chosen from allowed config
- stable structured response for the admin/frontend
- logging and budget-aware failure handling
### AI-backed enrichment workflow
Recommended shape:
- action reads current entry state
- generates suggestion only
- stores result in explicit reviewable fields or returns suggestion to editor
- never silently mutates unrelated content
## Anti-patterns
- Enabling AI without provider/budget planning
- Using AI for public content generation without editorial review
- Letting AI write into fields that editors should not modify manually
- Hiding core business logic inside prompts instead of code/config
- Treating AI as a replacement for structured content modeling
## Verification checklist
After adding AI-backed editor features, verify all of these:
1. Provider and model configuration are valid.
2. Token budgets and failure modes are defined.
3. The AI target field or action contract is explicit.
4. Editors can review the result before publication when appropriate.
5. Permissions and audit expectations still hold.
6. `yarn validate` stays clean.
## What an LLM should inspect first
When asked to add AI to a website project on this starter, inspect in this order:
1. `tibi-server/docs/09-llm-integration.md`
2. current collection meta for media/admin workflows
3. whether the use case fits Nova media AI, an action, or both
4. user/org budget expectations
5. the exact target field or response contract
This prevents random “AI features” that have no operational boundaries.
@@ -0,0 +1,194 @@
---
name: nova-navigation-modeling
description: Model navigations with current tibi-admin-nova navigation features. Covers recursive trees, declaredTrees, singleton navigation slots, preview design, language-specific trees, and how navigation modeling fits website information architecture.
---
# nova-navigation-modeling
## When to use this skill
Use this skill when:
- Designing header, footer, service, or utility navigation for a website project
- Modeling recursive navigation trees in Nova
- Using `viewHint.navigation` with declared singleton trees
- Refactoring a flat or editor-unfriendly navigation structure
## Goal
The goal is to model navigation as a first-class website structure with current Nova support, not as an afterthought or a plain array field.
On this stack, navigation influences:
- editor workflow
- public rendering
- SSR completeness
- language structure
- information architecture
## Source of truth
Use these sources when implementing or reviewing navigation modeling:
- `tibi-admin-nova/docs/collection-config.md`
- `api/collections/navigation.yml`
- `frontend/src/App.svelte`
- the frontend navigation rendering surface
## Current Nova navigation capability
Nova supports navigation-aware collection rendering through `meta.viewHint.navigation`.
Important building blocks:
- `nodesField`
- `declaredTrees`
- singleton root identifiers
- recursive tree editing for the configured nodes field
This is more than a plain `object[]` form. Use it when the project actually has structured navigation trees.
## Recommended mental model
Model navigation by tree purpose, not by arbitrary document naming.
Typical trees:
- main/header navigation
- footer navigation
- service/navigation variants
- language-specific trees
Each tree should have a clear editorial purpose and runtime consumer.
## Declared trees and singleton slots
Use `declaredTrees` when the project has known required navigation trees.
This gives editors:
- visible expected navigation slots
- stable entry points even when a tree is not created yet
- clearer distinction between intended site structure and accidental extra entries
For website projects, this is usually better than asking editors to create free-form navigation documents manually.
## Language-specific navigation
If the website is multilingual, decide explicitly whether navigation is:
- shared across languages
- separate per language
- partially shared with localized labels
The starter's current navigation collection models separate declared trees per language and per type. That is a good default when localized slugs and labels differ.
## Node schema design
Each navigation node should represent an editorially meaningful choice.
Typical node fields:
- `name`
- internal page reference
- external toggle
- external URL
- hash/anchor
- nested child nodes
Keep the schema focused. Do not overload navigation nodes with unrelated layout or content concerns unless the runtime genuinely needs them.
## Preview design
Navigation authoring depends heavily on preview quality.
Use previews so editors can quickly tell:
- whether a node points to an internal page or external URL
- what label it displays
- which tree they are editing
The current starter navigation config already demonstrates a strong pattern: previewing internal page lookup data and external URLs differently.
## Depth and constraints
Set `maxLevel` intentionally per tree.
Examples:
- header navigation may allow two levels
- footer navigation may allow one level
- service navigation may have different limits
Depth is an information-architecture decision, not only a UI detail.
## Navigation and runtime
Navigation modeling must match the frontend and SSR expectations.
Important checks:
- the frontend knows which tree to load
- language/type keys are stable
- SSR loads navigation as page-critical shell data when needed
- internal page references remain readable in admin and resolvable in runtime
Do not design a navigation schema in admin that the frontend cannot consume cleanly.
## Recommended patterns
### Header/footer split
Recommended shape:
- separate tree purpose via singleton markers such as type/language
- separate max depth per tree
- stable declared trees for required site areas
### Internal/external mixed navigation
Recommended shape:
- explicit external toggle
- page foreign key for internal links
- external URL only when external is true
- preview that makes the choice obvious
### Multilingual navigation
Recommended shape:
- declared trees per language
- clear language field
- editor-visible grouping of the trees
## Anti-patterns
- One generic navigation document with no stable tree identity
- Weak previews that show only IDs or unclear node labels
- No explicit distinction between internal and external targets
- Unlimited nesting without an actual UX reason
- Admin tree design that does not match frontend loading/runtime rules
## Verification checklist
After changing navigation modeling, verify all of these:
1. Editors can find every intended navigation tree quickly.
2. Node previews make internal vs external links obvious.
3. Allowed depth matches the site structure.
4. Frontend loading still resolves the correct trees.
5. SSR still includes required navigation shell data.
6. `yarn validate` stays clean.
## What an LLM should inspect first
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`
3. the frontend navigation loading/rendering path
4. SSR assumptions around header/footer shell data
5. the website's language and information-architecture requirements
This prevents navigation edits that are technically valid but editorially or runtime-wise incoherent.
@@ -0,0 +1,476 @@
---
name: nova-pagebuilder-modeling
description: Model editor-friendly block systems for tibi-admin-nova. Covers pagebuilder structure, block schemas, preview, drillDown, dependsOn, containerProps.layout, and the required alignment between admin config, frontend block registry, and SSR.
---
# nova-pagebuilder-modeling
## When to use this skill
Use this skill when:
- Building a flexible pagebuilder for pages, landing pages, reusable sections, or site settings
- Designing nested `object[]` schemas for blocks in Nova
- Deciding how editors should create, scan, reorder, and edit blocks
- Translating website requirements into maintainable block types
- Refactoring a block system that is technically valid but editor-hostile
## Goal
The goal is not just to make blocks storable. The goal is to model a block system that is:
- understandable for editors
- safe to extend over time
- easy to preview in Nova
- aligned with frontend rendering and SSR
- structured enough that an LLM can add new blocks without inventing ad-hoc patterns
## Source of truth
Use these sources when designing or reviewing the schema:
- `tibi-admin-nova/types/admin.d.ts`
- `tibi-admin-nova/docs/collection-config.md`
- `api/collections/content.yml`
- `frontend/src/blocks/`
- `frontend/src/blocks/BlockRenderer.svelte`
- `types/global.d.ts`
Do not model pagebuilder structures from memory when current Nova types are available.
## Core mental model
In this starter family, a pagebuilder is usually an `object[]` field where each array item represents one block. Each block needs three layers to stay coherent:
1. **Data model** in collection YAML
2. **Render component** in `frontend/src/blocks/`
3. **Type and registry alignment** in TypeScript and `BlockRenderer.svelte`
If one of these layers is missing, the system is incomplete.
## Design rules
### 1. Prefer a small block vocabulary with strong reuse
Do not create a new block type for every tiny content variation.
Prefer:
- `hero`
- `richText`
- `imageText`
- `cta`
- `featureGrid`
- `faq`
- `logos`
- `testimonials`
Avoid block libraries that mirror every page one-to-one. That produces brittle schemas and weak editor UX.
### 2. Every block must be recognizable in lists
Editors should understand an entry without opening each block.
Use current Nova preview capabilities on block objects:
```yaml
meta:
preview:
label: headline
secondary: type
badge: variant
```
If a block has no single identifying field, use a preview `eval` that combines multiple fields.
### 3. Large blocks should open in drill-down editing
If a block contains many fields, nested objects, or repeated items, prefer drill-down editing instead of forcing everything into one long inline form.
Use `drillDown` when the inline view becomes noisy or error-prone.
### 4. Use `dependsOn` to keep block forms focused
Conditional fields are essential in block schemas.
Use `dependsOn` when:
- a field is only relevant for one `variant`
- a CTA only appears when `showCta` is true
- media settings depend on layout choice
- a nested group only matters for one block subtype
Do not dump every optional field into the same visible form.
### 5. Use `containerProps.layout` to model editor flow
Block editing should reflect visual and editorial grouping, not raw storage order.
Use `containerProps.layout` to:
- put related fields side by side
- separate content from appearance controls
- reduce scroll depth
- keep critical fields in the first viewport
### 6. Keep the block model SSR-safe
If a block is page-critical, it must render correctly in SSR too.
That means:
- the block data must come through the same content-loading path as the page
- the Svelte block component must be importable by the SSR bundle
- the renderer must not rely on browser-only APIs during initial render
### 7. Model for migrations, not just first delivery
Blocks evolve. Design schemas so fields can be added without breaking every existing entry.
Prefer additive changes and explicit defaults over brittle implicit assumptions.
## Recommended modeling workflow
### Step 1: Start from editorial jobs, not component names
Define what editors need to do:
- create a page hero
- add structured intro content
- place testimonials
- create CTA sections
- insert FAQs
- reuse site-wide sections
Then derive block types from these jobs.
### Step 2: Decide which data belongs at page level and which belongs inside blocks
Keep page-level fields for concerns that apply to the whole page, such as:
- path
- language
- SEO
- publication
- translation linking
Keep block-level fields for modular content slices.
### Step 3: Define the block array schema
Typical pagebuilder field:
```yaml
- name: blocks
type: object[]
meta:
label: { de: "Blöcke", en: "Blocks" }
widget: pagebuilder
pagebuilder:
blockTypeField: type
preview:
label: headline
secondary: type
badge: variant
drillDown: true
subFields:
- name: type
type: string
meta:
widget: select
choices:
- value: hero
label: Hero
- value: richText
label: Rich text
- value: featureGrid
label: Feature grid
- name: headline
type: string
- name: variant
type: string
meta:
dependsOn:
eval: "$parent.type === 'hero'"
```
The exact shape can vary, but the pattern stays the same: block type first, then a previewable and conditionally focused schema.
### 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`.
The concrete chain is:
1. define the registry in `frontend/src/admin.ts`
2. export it as `blockRegistry`
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`.
Typical starter pattern:
```ts
const blockRegistry = {
hero: {
label: "Hero",
render(container, row, context) {
return {
update(nextRow, nextContext) {},
destroy() {},
}
},
},
}
export { blockRegistry }
```
And the collection wiring:
```yaml
meta:
widget: pagebuilder
pagebuilder:
blockTypeField: type
blockRegistry:
file: /_/assets/dist/admin.mjs?v=${ADMIN_ASSET_VERSION}
```
Important constraints for this starter:
- 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
- after registry changes, run `yarn build` so `frontend/dist/admin.mjs` is regenerated
- 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
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.
### Step 4: Map each block type to a frontend component
Every allowed `type` value in the schema must be handled in `BlockRenderer.svelte`.
Do not leave “temporary” admin-only block types without a renderer unless they are truly non-public and intentionally excluded.
## Frontend preparation requirements
For this starter, pagebuilder work is only half done when the collection schema exists. The frontend must be prepared explicitly so block-based rendering stays maintainable.
### 1. Keep one clear renderer boundary
`frontend/src/blocks/BlockRenderer.svelte` should remain the central registry that maps `block.type` to concrete Svelte components.
That means:
- every public block type in the schema gets one renderer branch
- unknown block handling stays explicit
- block selection logic stays centralized instead of being scattered across many unrelated files
Do not distribute block-type branching across the app shell, page components, and nested helpers at the same time.
### 2. Use a stable component contract
Each block component should receive the block object in a consistent way.
In this starter, the default contract is:
```svelte
<script lang="ts">
let { block }: { block: ContentBlockEntry } = $props()
</script>
```
This matters because the block system becomes much easier to extend when every component follows the same top-level prop contract.
If a block needs additional derived data, derive it inside the component or in a small helper, but do not invent a different top-level prop API for every block.
### 3. Keep `ContentBlockEntry` aligned with real frontend usage
The frontend preparation is incomplete until `types/global.d.ts` can express the fields the block components actually read.
Whenever a new block type or field is added, verify alignment between:
- collection YAML subfields
- `ContentBlockEntry`
- the block component implementation
- `BlockRenderer.svelte`
If a component reads fields that are only implicit or typed as vague leftovers, the pagebuilder is not ready for reliable future extension.
### 4. Plan lookup data together with the block model
If blocks reference media or foreign entities, the frontend must be prepared to receive the resolved lookup data through the page-loading path.
For this starter, that usually means checking the lookup strings used when loading content in `App.svelte`.
For Nova admin previews, treat the incoming row differently from a raw frontend API payload:
- `_lookup` may already be hydrated by the admin preview pipeline
- file/image values may already be absolute URLs
Do not add preview logic that blindly rewrites file URLs or assumes it still has to hydrate foreign references before rendering the admin pagebuilder preview.
Do not add a block that depends on:
- media lookups
- referenced collections
- nested foreign references
without also updating the content-loading layer so the renderer receives the required `_lookup` data.
### 5. Treat SSR compatibility as part of frontend preparation
A pagebuilder block is not frontend-ready if it only works after hydration.
Every public block should render safely during SSR:
- no unconditional `window`/`document` usage at module top level
- browser-only behavior guarded inside `typeof window !== "undefined"`
- meaningful initial markup without waiting for client-only effects
If a block absolutely requires browser APIs, keep the browser-only part small and ensure the surrounding block still renders a stable SSR shell.
### 6. Unknown block handling should help development without hiding errors
`BlockRenderer.svelte` should make unknown block types visible enough during development that schema/frontend drift is caught early.
For this starter, the current renderer already has a development-side unknown-block fallback. Keep a mechanism like that in place when the demo renderer is refactored.
Do not silently swallow unknown block types in a way that makes editor-created content disappear with no signal.
### 7. Keep block components presentation-focused
Pagebuilder block components should mostly render data, not own cross-page application logic.
Prefer:
- block-local formatting and small derived values
- presentational composition
- small helper components inside `frontend/src/blocks/`
Avoid pushing these concerns into block components unless there is a strong reason:
- route loading
- global app state orchestration
- unrelated API fetching
- page-level navigation concerns
### 8. Prepare for styling consistency across blocks
A block system works better when blocks share a few stable layout conventions.
Examples:
- container width choices
- vertical spacing conventions
- anchor/id behavior
- CTA shape and link handling
- media aspect ratio conventions
Do not let every new block invent its own spacing, width, and link semantics from scratch unless the design system really requires it.
## When a block is actually ready in the frontend
A new pagebuilder block should only be considered integrated when all of these are true:
1. The schema contains the block type and required subfields.
2. `ContentBlockEntry` expresses the fields used by the block.
3. A dedicated Svelte block component exists in `frontend/src/blocks/`.
4. `BlockRenderer.svelte` routes the block type to that component.
5. Any required lookup data is loaded by the app content-loading path.
6. The block renders acceptably in SSR and browser navigation.
7. Unknown or stale block types remain debuggable.
### Step 5: Keep types aligned
Update project types when the block model changes.
In this starter family, block schemas usually affect:
- `types/global.d.ts`
- Svelte component props
- block renderer branching
If TypeScript cannot express the new block shape, the schema work is incomplete.
## Practical block design patterns
### Hero block
Use for top-of-page messaging. Keep the editor form short and obvious.
Typical fields:
- eyebrow
- headline
- subline
- image
- cta
- variant
Use `dependsOn` for variant-specific media and CTA settings.
### Rich text block
Use for long-form body content. Avoid mixing it with too many presentational toggles.
Typical fields:
- headline
- body
- maxWidth
### Feature grid block
Use nested repeatable objects for feature items, but make the parent block previewable.
Typical fields:
- headline
- items[]
- columns
- variant
For `items[]`, add its own preview so editors can scan the nested list.
### Reusable section reference
If the same content must appear on many pages, consider a dedicated collection plus foreign reference instead of copy-pasting large pagebuilder blocks.
Use foreign previews so editors understand the referenced entity before opening it.
## Anti-patterns
- One block type per page template fragment with no reuse
- Giant catch-all block with dozens of unrelated optional fields
- No preview on nested objects
- No drill-down for large objects
- Using array order as the only meaning without labels or previews
- Frontend blocks that exist without matching collection schema
- Collection schema values that have no renderer
## Verification checklist
After adding or changing pagebuilder blocks, verify all of these:
1. Editors can identify blocks quickly in Nova.
2. The block form hides irrelevant fields.
3. Reordering works without losing meaning.
4. `BlockRenderer.svelte` handles every public block type.
5. SSR renders the affected page correctly.
6. `yarn validate` stays clean.
## What an LLM should inspect first
When asked to extend a pagebuilder on this starter, inspect in this order:
1. `api/collections/content.yml`
2. `frontend/src/blocks/BlockRenderer.svelte`
3. existing files in `frontend/src/blocks/`
4. `types/global.d.ts`
5. `tibi-admin-nova/types/admin.d.ts`
This order prevents schema-only or frontend-only changes.
@@ -0,0 +1,181 @@
---
name: permissions-and-editor-workflows
description: Design safe editor workflows with current tibi-server permissions and Nova authoring patterns. Covers collection permissions, field-level readonly/hidden rules, roles, tokens, and how admin UX should reflect real editorial boundaries.
---
# permissions-and-editor-workflows
## When to use this skill
Use this skill when:
- A project needs more than one editor/admin role
- Collections or fields should be restricted by role or token
- You need readonly/hidden field logic for real editorial workflows
- You want Nova UX to reflect actual server-side permissions instead of pretending every field is editable
## Goal
The goal is to design permissions as part of the editorial workflow, not as a last-minute access check.
On this stack, permissions affect:
- API methods
- field visibility
- field editability
- collection visibility
- token-based integrations
- admin usability
## Source of truth
Use these sources when implementing or reviewing permissions:
- `tibi-server/docs/17-field-level-permissions.md`
- `tibi-server/docs/05-authentication.md`
- relevant collection YAML files
- `tibi-admin-nova/types/admin.d.ts`
## Permission layers
At minimum, reason about permissions on these levels:
- collection methods (`get`, `post`, `put`, `delete`)
- field-level `readonlyFields`
- field-level `hiddenFields`
- field-definition overrides (`readonly`, `hidden`)
- dynamic eval-based field rules
- collection `meta.hide` for sidebar visibility
Do not flatten all of this into one vague notion of “editor access”.
## Collection-level workflow design
Before implementing permissions, define who does what.
Typical roles/workflows:
- public readers
- editors creating and updating content
- reviewers or restricted staff
- admins configuring structure and sensitive fields
- token-based integrations
Then map those responsibilities to explicit permission sets.
## Field-level permissions
Current tibi-server field permissions are strong and should be used deliberately.
Important behavior:
- `readonlyFields`: writes fail with `400` if those fields are sent
- `hiddenFields`: writes fail with `400`, reads strip the fields from responses
- field-level `readonly` / `hidden` can override or dynamically extend behavior
This means field permissions are not mere UI hints. They are enforced server-side.
## Dynamic field rules
Use eval-based field rules when permissions depend on document state.
Typical examples:
- a field becomes readonly after approval
- an internal note is hidden from non-admin roles
- a billing field is editable only before status changes
Use these rules to model real editorial transitions, not to create confusing surprises.
## 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.
Recommended patterns:
- keep critical restricted fields out of primary editorial flow
- place admin-only or system-managed fields in sidebars or dedicated sections
- avoid forms whose main content becomes unusable when half the fields are hidden by role
- design previews so editors can still identify entries even when some internal fields are hidden
Server permissions are authoritative, but poor admin layout can still create a bad workflow.
## Tokens and integrations
Remember that token-based integrations can have their own permission sets.
Use this for:
- inbound integrations
- service accounts
- controlled automation
- frontend-to-backend machine use cases when appropriate
Do not reuse broad admin permissions for integrations if a narrow token permission set is enough.
## Permission-driven architecture decisions
Permissions can change the correct data model.
Examples:
- if sensitive internal notes should never be visible to normal editors, consider whether they belong in the same collection or a separate one
- if a public form creates internal records, the public action and the internal collection should have separate permission boundaries
- if a workflow has approval stages, model status transitions and readonly behavior explicitly
## Recommended patterns
### Editorial content workflow
Recommended shape:
- editors can create and update content fields
- publication/system fields may be restricted or conditionally readonly
- admin-only technical fields are hidden or isolated in the UI
### Sensitive internal data
Recommended shape:
- hide internal-only fields from normal editors
- prefer explicit server-side rules over relying on UI omission
- ensure previews do not depend on hidden-only data
### Approval-style workflow
Recommended shape:
- status field controls editability of specific fields
- post-approval fields become readonly via eval rules
- admin or reviewer roles retain the intended override path
## Anti-patterns
- Treating permissions as frontend-only display logic
- Leaving sensitive fields visible and merely asking editors not to touch them
- Using one broad admin token for every integration
- Designing forms that depend on fields many roles cannot access
- Adding dynamic readonly/hidden logic without explaining the editorial workflow it represents
## Verification checklist
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.
## What an LLM should inspect first
When asked to design permissions on this starter, inspect in this order:
1. the relevant collection YAML
2. the intended human roles and machine integrations
3. field-level readonly/hidden needs
4. whether the current Nova layout still makes sense under those restrictions
5. any workflow states that require dynamic eval rules
This prevents access rules that are technically correct but operationally unusable.
@@ -0,0 +1,213 @@
---
name: realtime-and-live-workflows
description: Use tibi-server SSE channels for live website and admin workflows. Covers channel design, subscription hooks, replay/TTL/buffer behavior, permission boundaries, and when realtime fits a website project.
---
# realtime-and-live-workflows
## When to use this skill
Use this skill when:
- A website or admin feature needs live updates
- You want SSE-based notifications, preview refreshes, status feeds, or dashboards
- Hooks or jobs should push messages to connected clients
- You need to decide whether realtime is actually appropriate for the feature
## Goal
The goal is to model realtime as a deliberate workflow, not as a random event stream.
On this stack, realtime means:
- SSE transport
- in-memory per-project channels
- server-side send from hooks/jobs
- subscription endpoints implemented in hooks
## Source of truth
Use these sources when implementing or reviewing realtime behavior:
- `tibi-server/docs/07-realtime.md`
- `tibi-server/docs/11-jobs.md`
- the relevant hook files under `api/hooks/`
## Core architecture
tibi-server realtime is based on per-project in-memory pub/sub channels.
Important characteristics:
- channels are created on demand
- channels are isolated per project
- the transport is SSE, not WebSockets
- messages are not durable across restarts
- hooks subscribe, hooks or jobs send
This makes realtime useful for live UX, but not for durable messaging.
## Good use cases for website projects
Good fits:
- live status or progress streams
- lightweight admin notifications
- system messages pushed from jobs
- preview or refresh signals after mutations
- dashboards with current in-memory activity
Weak fits:
- business-critical guaranteed delivery
- cross-instance distributed eventing
- durable queue semantics
- workflows that require replay beyond a bounded in-memory buffer
## Subscription design
Realtime subscriptions should be exposed intentionally through dedicated read hooks that hold the SSE connection open.
Design the endpoint around:
- who may subscribe
- which channel names exist
- what event shape clients receive
- how replay and freshness should work
Do not expose a generic raw event hose unless the project truly needs that.
## Channel options that matter
When modeling realtime behavior, decide these explicitly:
- `bufferSize`
- `onFull`
- `messageTTL`
- `lastN`
- `maxAge`
These are product decisions, not low-level afterthoughts.
### Buffer size
Use a larger buffer only when reconnecting clients should receive some recent history. Do not overestimate it as persistence.
### On-full behavior
- `drop-oldest` favors receiving the newest state, even if some history is lost
- `drop-newest` preserves older pending messages for the subscriber and skips the new one
For most live UI use cases, `drop-oldest` is the more natural choice.
### Replay and freshness
Use `lastN` or `maxAge` only when reconnecting clients genuinely benefit from recent context.
For notification-like channels, some replay can help.
For pure live status indicators, it may be better to show only new events.
## Permission boundaries
Channels do not carry independent auth rules. Access is controlled by the hook/collection permission layer that exposes the SSE endpoint.
That means:
- secure the subscription endpoint, not just the client code
- do not assume channel names themselves protect access
- be explicit about who may connect and what data is safe to send
## Event design
Prefer small, explicit event shapes.
Good event payloads usually include:
- event `type`
- relevant identifier
- minimal status or message fields
- timestamp when useful
Avoid pushing whole documents unless the live client truly needs them.
## Hooks vs. jobs
Use hooks to send events when changes happen immediately in response to requests.
Use jobs to send events when the trigger is scheduled or background-driven.
Typical patterns:
- hook sends `content-updated`
- job sends `maintenance-warning`
- hook sends `import-finished`
- job sends `daily-report-ready`
## Operational limits
This realtime system is intentionally lightweight.
Important limits:
- messages are lost on server restart
- no cross-server synchronization
- no durable backlog
- slow subscribers can miss messages due to ring-buffer behavior
If the feature cannot tolerate these limits, this realtime system is the wrong abstraction.
## Recommended modeling patterns
### Live admin notifications
Recommended shape:
- authenticated SSE endpoint
- narrow event schema
- optional short replay via `lastN`
### Preview refresh signal
Recommended shape:
- hook emits lightweight invalidation or refresh event
- client decides whether to refetch
- do not stream full content when a simple signal is enough
### Scheduled status feed
Recommended shape:
- job emits events to a system channel
- UI listens and renders current status
- TTL keeps stale messages from resurfacing after reconnect
## Anti-patterns
- Using realtime as a replacement for persistence
- Publishing sensitive data because “the UI needs it quickly”
- Creating one generic catch-all channel for unrelated features
- Ignoring replay/TTL/buffer behavior and assuming delivery guarantees
## Verification checklist
After adding realtime behavior, verify all of these:
1. The subscription endpoint is permissioned correctly.
2. The event shape is explicit and minimal.
3. Replay/TTL/buffer settings match the intended UX.
4. Disconnect/reconnect behavior is acceptable.
5. The feature still behaves sensibly after a server restart.
6. `yarn validate` stays clean.
## What an LLM should inspect first
When asked to add realtime to this starter, inspect in this order:
1. `tibi-server/docs/07-realtime.md`
2. the hook that should expose or emit the events
3. whether a job is also part of the workflow
4. the permission boundary of the SSE endpoint
5. the exact event contract the client needs
This prevents building live features with unclear delivery or security assumptions.
@@ -0,0 +1,187 @@
---
name: scheduled-jobs-and-automation
description: Build scheduled background workflows with tibi-server jobs. Covers cron design, job context, safe automation patterns, reporting/cleanup/sync use cases, and how jobs interact with hooks, audit, and realtime.
---
# scheduled-jobs-and-automation
## When to use this skill
Use this skill when:
- A project needs scheduled cleanup, reporting, imports, syncs, reminders, or maintenance tasks
- You want automation without an incoming HTTP request
- Jobs should update data, send mail, call APIs, or emit realtime events
- You need to decide whether logic belongs in a hook, an action, or a job
## Goal
The goal is to design jobs as reliable background workflows, not as miscellaneous scripts.
Jobs on this stack are:
- cron-triggered
- goja-based JavaScript programs
- independent of HTTP requests
- able to use many of the same server-side packages as hooks
## Source of truth
Use these sources when implementing or reviewing jobs:
- `tibi-server/docs/11-jobs.md`
- `tibi-server/docs/10-audit.md`
- `tibi-server/docs/07-realtime.md`
- `api/config.yml`
## Hook vs. action vs. job
Choose the right execution surface.
- **Hook**: request-coupled logic around CRUD or actions
- **Action**: endpoint-style business workflow triggered by an explicit call
- **Job**: scheduled background automation without an incoming request
Do not place scheduled logic into hooks just because the code already exists there.
## Good use cases
Strong job use cases:
- cleanup of old documents or temp data
- periodic report generation
- scheduled API synchronization
- cache warming or maintenance tasks
- reminder and digest emails
- scheduled realtime announcements
Weak use cases:
- workflows that must run immediately on user action
- logic that depends on live request/response objects
- features that need interactive user feedback during execution
## Job configuration
Every job should define:
- cron schedule
- file path
- timeout when appropriate
- optional metadata via `meta`
Treat cron frequency as a product and operations decision. Do not set aggressive schedules without a real need.
## Job context limits
Jobs have broad server-side access, but they are not request-driven.
Important consequences:
- no `request`
- no `response`
- no `user.*`
- no `cookie.*`
- no `channel.subscribe`
- `channel.send` is available
If the logic depends on request context, it does not belong in a job.
## Safe automation patterns
Jobs should be:
- idempotent where possible
- bounded in runtime
- explicit in filters and update scope
- observable through logs or downstream effects
Avoid “run and mutate everything” jobs without clear selection criteria.
## Interaction with audit and realtime
Jobs are not isolated from other system behavior.
- DB operations from jobs appear in audit with `source.type: "job"`
- jobs can emit realtime events through `channel.send`
This makes jobs useful for background workflows that should still be visible operationally.
## Recommended job patterns
### Cleanup job
Recommended shape:
- explicit age threshold
- narrow filter
- bounded timeout
- optional reporting of removed count
### Scheduled reporting
Recommended shape:
- aggregate counts or summaries
- render a report template if needed
- send mail or store result
### External sync
Recommended shape:
- pull from external API
- normalize data
- update local records idempotently
- log enough context for troubleshooting
### Scheduled notifications
Recommended shape:
- compute upcoming or due events
- send mail, action-like side effect, or realtime signal
- avoid duplicate sends through clear state checks
## Operational concerns
When adding a job, decide:
- how often it runs
- what timeout it needs
- whether reruns are safe
- how failure is detected
- whether a manual rerun path exists
Jobs should not become invisible critical infrastructure.
## Anti-patterns
- Using jobs for logic that belongs in request-time hooks or actions
- Overly frequent cron schedules for expensive tasks
- No timeout on potentially slow network-heavy jobs
- Broad destructive updates without precise filters
- Silent failures with no observable output or effect
## Verification checklist
After adding a job, verify all of these:
1. The cron schedule matches the real business need.
2. The job logic does not rely on request-only APIs.
3. Timeout and runtime expectations are reasonable.
4. Repeated execution does not corrupt data.
5. Any audit/realtime side effects are intentional.
6. `yarn validate` stays clean.
## What an LLM should inspect first
When asked to automate something on this starter, inspect in this order:
1. `tibi-server/docs/11-jobs.md`
2. whether the trigger is scheduled, request-driven, or manual
3. whether the logic needs audit visibility or realtime side effects
4. the project config area where the job will be declared
5. the exact data mutation scope
This prevents turning cron tasks into unbounded background risk.
@@ -0,0 +1,183 @@
---
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.
---
# security-hardening-and-token-strategy
## When to use this skill
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
## Goal
The goal is to keep projects on this starter aligned with the current tibi-server security model and known risk areas.
This skill is not a generic web security guide. It is about the concrete operator and implementation choices this stack exposes.
## Source of truth
Use these sources when implementing or reviewing security-related decisions:
- `tibi-server/docs/05-authentication.md`
- `tibi-server/docs/14-security.md`
- relevant collection/action permissions in `api/`
- project environment/config files
## Authentication surfaces
This stack exposes multiple auth mechanisms:
- JWT user auth
- refresh token cookie flow
- admin tokens
- token-based permissions for API-style access
Do not mix them casually. Each one has a different operational purpose.
## Recommended token strategy
Use:
- **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
Avoid using broad admin tokens where a narrow project or collection-level token permission is enough.
## Secrets handling
Do not keep production secrets as plain literals in committed config.
Prefer environment-variable substitution for:
- JWT secrets
- SMTP credentials
- LLM API keys
- external API tokens
- admin token values
If a project ships real secrets in config, treat that as a structural problem, not a cosmetic cleanup.
## Bulk permission safety
Bulk mutations are explicitly 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.
## Cookie and session hardening
For refresh-token flows, ensure the deployment matches secure cookie expectations.
Important considerations:
- 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
## Hook risk surfaces
Current tibi-server exposes powerful server-side capabilities. Some of them require explicit restraint.
Particularly important:
- `http.fetch` / `http.fetchStream` can create SSRF risk
- `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
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.
## Secure implementation patterns
### Public form endpoint
Recommended shape:
- public action with narrow allowed methods
- server-side validation
- no broad admin tokens in the browser
- no unnecessary collection write permissions exposed publicly
### Integration token
Recommended shape:
- dedicated narrow token permission set
- minimal collection/action scope
- header-based transport preferred
### Hook that calls external services
Recommended shape:
- fixed or validated destination URLs
- no arbitrary user-controlled internal target fetching
- minimal capability needed for the feature
## 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
## 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.
## What an LLM should inspect first
When asked to harden or design secure access on this starter, inspect in this order:
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
This prevents “working” implementations that quietly widen the attack surface.
@@ -0,0 +1,288 @@
---
name: tibi-actions-and-forms
description: Build endpoint-style website features with tibi-server actions. Covers when to use actions instead of collections, action hook flow, validation, permissions, CORS, mail/webhook patterns, and frontend integration for forms.
---
# tibi-actions-and-forms
## When to use this skill
Use this skill when:
- Building contact forms, newsletter forms, quote requests, callbacks, or booking requests
- Adding endpoint-like backend logic without CRUD storage
- Replacing old collection hacks that only existed to accept POST requests
- Designing frontend form submissions against tibi-server actions
- Deciding whether a feature should be an action, a collection, or both
## Goal
The goal is to teach an LLM how to build website workflows that behave like real endpoints.
A form feature is complete only when all of these are coherent:
- action config
- hook flow
- validation
- permissions and CORS
- optional persistence or side effects
- frontend submission and error handling
## Source of truth
Use these sources when implementing or reviewing action-based features:
- `tibi-server/docs/19-actions.md`
- `tibi-server/docs/06-hooks.md`
- `api/config.yml`
- `api/hooks/`
- `frontend/src/lib/api.ts`
- `frontend/src/App.svelte` or the relevant frontend form surface
## Core decision: action or collection?
Use an **action** when the feature is primarily an endpoint or workflow.
Typical action cases:
- contact form
- newsletter signup
- quote request
- callback request
- webhook receiver
- utility endpoint
- AI-assisted helper endpoint
Use a **collection** when the feature is primarily stored content or stored records with CRUD semantics.
Typical collection cases:
- products
- team members
- events
- testimonials
- persisted inquiries that editors must browse/edit in admin
Use **action + collection** when you need a public workflow plus internal persistence.
Example:
- a contact form submits to an action
- the action validates, sends mail, and optionally creates an `inquiries` entry for staff follow-up
Do not fake endpoint logic with empty collections unless there is a very specific reason.
## Routing model
Actions are exposed under:
```text
POST /api/v1/_/:project/_actions/:action
GET /api/v1/_/:project/_actions/:action
```
The `_actions` prefix is part of the contract. Frontend form code should treat actions as explicit API endpoints, not as collection writes.
## Where actions are configured
Actions are declared in `api/config.yml` under `actions:` and typically point to files under:
- `api/actions/` for YAML configs
- `api/hooks/<action-name>/` for hook files
Typical config shape:
```yaml
actions:
- !include actions/contact-form.yml
```
```yaml
name: contact-form
meta:
label: { de: "Kontaktformular", en: "Contact Form" }
permissions:
public:
methods:
post: true
hooks:
post:
bind:
type: javascript
file: hooks/contact-form/post_bind.js
validate:
type: javascript
file: hooks/contact-form/post_validate.js
handle:
type: javascript
file: hooks/contact-form/post_handle.js
return:
type: javascript
file: hooks/contact-form/post_return.js
```
## Action lifecycle
### POST flow
`bind``validate``handle``return`
Use the steps deliberately:
- `bind`: normalize the request body, derive helper data, prepare context
- `validate`: enforce required fields, anti-spam checks, shape checks, consent checks
- `handle`: execute the business logic
- `return`: normalize the response payload for the frontend
### GET flow
`handle``return`
GET actions are useful for utility endpoints, signed links, status endpoints, or controlled data retrieval that is not a collection read.
## Recommended website patterns
### Contact form
Recommended behavior:
- public `POST` action
- validate required fields, email format, consent, and anti-spam signal
- send mail or queue message handling
- optionally persist a normalized inquiry record
- return a small stable payload for the frontend
### Newsletter signup
Recommended behavior:
- public `POST` action
- validate email and consent
- call external provider or create local opt-in entry
- keep provider-specific logic in the action, not in the frontend
### Quote or booking request
Recommended behavior:
- public `POST` action
- transform raw form data into a normalized structure in `bind`
- validate business rules in `validate`
- persist or forward the request in `handle`
### Webhook receiver
Recommended behavior:
- restricted `POST` action
- verify secret/signature before any state change
- keep webhook-specific logic isolated from public website forms
## Validation rules
Validation belongs server-side even when the frontend already validates.
Always validate:
- required fields
- string lengths
- email/phone formats when applicable
- consent flags
- expected enums or modes
- anti-spam or rate-limit conditions
Do not trust the frontend form shape.
## Permissions and CORS
Actions use the same permission model as collections.
For public website forms:
- keep only the needed methods public
- avoid opening `GET` unless the use case needs it
- use action-level CORS only when the frontend origin truly differs from the project default
Public form access should be narrow, explicit, and auditable.
## Persistence strategy
Not every form submission belongs in a collection.
Choose persistence deliberately:
- no persistence: mail, webhook, or third-party API only
- minimal persistence: store the normalized request for internal staff
- full persistence: store and manage lifecycle in a dedicated collection
If editors must browse, triage, export, or annotate the data in Nova, add a dedicated collection instead of overloading the action itself.
## Frontend integration
Frontend forms should submit to actions through the normal API layer.
Keep the frontend responsible for:
- collecting input
- disabled/loading state
- optimistic or conservative UX
- success and error messages
Keep the backend responsible for:
- real validation
- side effects
- persistence
- normalization of response shape
## Response design
Return a small stable payload that the frontend can rely on.
Typical response examples:
```json
{ "ok": true, "message": "Danke für Ihre Nachricht." }
```
```json
{ "ok": true, "nextStep": "confirm-email" }
```
Avoid leaking internal implementation details or raw provider responses to the frontend.
## Anti-patterns
- Creating empty fake collections just to receive POST requests
- Moving validation only into the browser
- Sending third-party API credentials from the frontend
- Returning unstable error shapes
- Mixing public forms and internal admin workflows into one hook without boundaries
- Persisting everything by default without a real editorial or operational need
## Verification checklist
After adding an action-based workflow, verify all of these:
1. The action is declared in `api/config.yml`.
2. The hook chain exists and has the intended steps.
3. Invalid submissions fail with useful status and message.
4. Valid submissions trigger the intended side effects.
5. Public permissions are no broader than necessary.
6. The frontend handles success and failure predictably.
7. `yarn validate` stays clean.
## What an LLM should inspect first
When asked to add a website form or endpoint on this starter, inspect in this order:
1. `tibi-server/docs/19-actions.md`
2. `api/config.yml`
3. related hooks in `api/hooks/`
4. the frontend form/API surface
5. whether persistence belongs in a collection too
This prevents the common mistake of starting with a fake collection when the feature is really an action.
+62 -1
View File
@@ -5,6 +5,8 @@ description: Write and debug server-side hooks for tibi-server (goja Go JS runti
# tibi-hook-authoring # tibi-hook-authoring
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.
## Hook file structure ## Hook file structure
Wrap every hook in an IIFE: Wrap every hook in an IIFE:
@@ -22,6 +24,8 @@ Wrap every hook in an IIFE:
Always return a `HookResponse` or throw a `HookException`. Always return a `HookResponse` or throw a `HookException`.
For many hooks, throwing is the normal control flow, especially in SSR hooks where HTML/status are returned via a thrown object.
## Type safety ## Type safety
- Use inline JSDoc type casting: `/** @type {TypeName} */ (value)`. - Use inline JSDoc type casting: `/** @type {TypeName} */ (value)`.
@@ -53,6 +57,15 @@ For `GET /:collection/:id`, the Go server sets `_id` automatically from the URL
GET read hooks should **not** set their own `_id` filter for `req.param("id")`. Only add authorization filters (e.g. `{ userId: userId }`). GET read hooks should **not** set their own `_id` filter for `req.param("id")`. Only add authorization filters (e.g. `{ userId: userId }`).
## Current hook surfaces that matter for website projects
- Collection CRUD hooks under `get`, `post`, `put`, `delete`
- Bulk hooks for optimized bulk operations
- `audit.return` hooks for stripping sensitive data from audit output
- `actions:` hook chains for endpoint-like behavior without a backing CRUD collection
For website builds on this starter, do not force everything into collections. Contact forms, newsletter signups, webhook receivers, import jobs, calculators, or other endpoint-style logic often belong into `actions:` instead.
## HookResponse fields (GET hooks) ## HookResponse fields (GET hooks)
| Field | Purpose | | Field | Purpose |
@@ -68,4 +81,52 @@ GET read hooks should **not** set their own `_id` filter for `req.param("id")`.
- `context.data` can be an array for bulk operations — always guard with `!Array.isArray(context.data)`. - `context.data` can be an array for bulk operations — always guard with `!Array.isArray(context.data)`.
- For POST hooks, `context.data.id` may contain the new entry ID. - For POST hooks, `context.data.id` may contain the new entry ID.
- For PUT/PATCH, `req.param("id")` gives the entry ID. - For PUT, `req.param("id")` gives the entry ID.
## Bulk and optimized paths
- tibi-server supports optimized bulk paths.
- In bulk scenarios, `bind` still runs once at the start.
- Per-document validation/update/delete hooks may be skipped depending on the chosen bulk path.
If a website feature depends on per-entry logic, do not assume a bulk update behaves exactly like N single updates. Check whether a dedicated bulk hook exists or whether the optimized path changes the behavior you rely on.
## Action hooks
Actions are first-class endpoints and should be part of the skill set for complete website builds.
Typical action steps:
- `post.bind`
- `post.validate`
- `post.handle`
- `post.return`
- `get.handle`
- `get.return`
Use actions when the website needs business logic without a CRUD collection.
Typical website use cases:
- contact forms
- newsletter signups
- quote/order requests
- webhook receivers
- utility endpoints
- AI-assisted helper endpoints
## Practical hook patterns for this starter family
- public read filtering for `active`/publication state
- SSR cache invalidation after writes
- route-level SSR validation
- mutation safeguards for readonly/system-managed fields
- custom form/action validation
- audit-output sanitizing for sensitive fields
## 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.
+81 -14
View File
@@ -13,6 +13,8 @@ Use this skill when:
- Onboarding into a freshly cloned starter project where placeholders haven't been replaced yet - 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 - The user asks to "set up", "initialize", or "bootstrap" a new tibi project
Goal: a new website project should end up as a **fully working tibi-server + tibi-admin-nova project**, not just a renamed starter clone.
## Prerequisites ## Prerequisites
- Code-Server environment at `*.code.testversion.online` - Code-Server environment at `*.code.testversion.online`
@@ -34,23 +36,25 @@ git remote rename origin template
**Verify:** `git remote -v` shows `template` pointing to the starter and optionally `origin` pointing to the new repo. **Verify:** `git remote -v` shows `template` pointing to the starter and optionally `origin` pointing to the new repo.
## Step 2 — Replace placeholders ## Step 2 — Replace placeholders and starter values
Three placeholders must be replaced in the correct files: Replace the starter placeholders and starter-derived values in the correct files:
| Placeholder | Files | Format | Example | | Placeholder | Files | Format | Example |
| -------------------- | ---------------------------------------------- | --------------------------------------------------------- | ------------ | | -------------------- | ---------------------------------------------- | --------------------------------------------------------- | ------------ |
| `__PROJECT_NAME__` | `.env` | kebab-case (used for URLs, Docker containers, subdomains) | `my-project` | | `__PROJECT_NAME__` | `.env` | kebab-case (used for URLs, Docker containers, subdomains) | `my-project` |
| `__TIBI_NAMESPACE__` | `.env`, `api/config.yml`, `frontend/.htaccess` | snake_case (used as DB prefix and in API URLs) | `my_project` | | `__TIBI_NAMESPACE__` | `.env`, `api/config.yml`, `frontend/.htaccess` | kebab-case, same value as `PROJECT_NAME` | `my-project` |
```sh ```sh
PROJECT=my-project # kebab-case PROJECT=my-project # kebab-case
NAMESPACE=my_project # snake_case NAMESPACE=my-project # same kebab-case value as PROJECT
sed -i "s/__PROJECT_NAME__/$PROJECT/g" .env sed -i "s/__PROJECT_NAME__/$PROJECT/g" .env
sed -i "s/__TIBI_NAMESPACE__/$NAMESPACE/g" .env api/config.yml frontend/.htaccess sed -i "s/__TIBI_NAMESPACE__/$NAMESPACE/g" .env api/config.yml frontend/.htaccess
``` ```
Also update the starter-derived values that are not placeholder tokens anymore, especially `STAGING_PATH`, `STAGING_URL`, `CODING_URL`, `api/hooks/config-client.js`, and starter metadata in `package.json`.
**Verify each replacement:** **Verify each replacement:**
```sh ```sh
@@ -62,14 +66,14 @@ grep -n '__PROJECT_NAME__\|__TIBI_NAMESPACE__' .env api/config.yml frontend/.hta
```dotenv ```dotenv
PROJECT_NAME=my-project PROJECT_NAME=my-project
TIBI_NAMESPACE=my_project TIBI_NAMESPACE=my-project
CODING_URL=https://my-project.code.testversion.online CODING_URL=https://my-project.code.testversion.online
STAGING_URL=https://dev-my-project.staging.testversion.online STAGING_URL=https://dev-my-project.staging.testversion.online
``` ```
### Common mistakes ### Common mistakes
- **Mixing formats**: `PROJECT` must be kebab-case (`my-project`), `NAMESPACE` must be snake_case (`my_project`). Never use kebab-case where snake_case is expected or vice versa. - **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 `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. - **Forgetting `api/config.yml`**: First line is `namespace: __TIBI_NAMESPACE__`. If not replaced, tibi-server won't start correctly.
@@ -77,29 +81,30 @@ STAGING_URL=https://dev-my-project.staging.testversion.online
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`. 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 ## Step 4 — Admin token
`api/config.yml.env` ships with a default `ADMIN_TOKEN`. For production projects, generate a secure one: `api/config.yml.env` ships with a default `ADMIN_TOKEN`. For production projects, generate a secure one:
```sh ```sh
echo "ADMIN_TOKEN=$(openssl rand -hex 20)" > api/config.yml.env token=$(openssl rand -hex 20) && sed -i "s/^ADMIN_TOKEN=.*/ADMIN_TOKEN=$token/" api/config.yml.env
``` ```
**Verify:** `cat api/config.yml.env` shows a 40-character hex token. 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`.
## Step 5 — Install, upgrade, and start ## Step 5 — Install, upgrade, and start
```sh ```sh
yarn install yarn install
yarn upgrade # Update all deps to latest versions within package.json ranges
make docker-up # Start stack in background make docker-up # Start stack in background
# or # or
make docker-start # Start stack in foreground (CTRL-C to stop) make docker-start # Start stack in foreground (CTRL-C to stop)
``` ```
`yarn upgrade` is safe here because the project is freshly cloned and nothing is running yet. The template's `yarn.lock` may be months old — upgrading ensures you start with the latest compatible (semver-range) versions. 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.
**Do NOT run `yarn upgrade` on an existing, running project without testing.** Even patch-level updates can introduce regressions. For running projects, upgrade targeted packages with `yarn upgrade <package>` and verify with `yarn validate` + `yarn build`.
**Verify containers are running:** **Verify containers are running:**
@@ -154,6 +159,17 @@ For a real project, remove or replace the demo files:
Then adapt `frontend/src/App.svelte` (header, footer, content loading) to your own data model. Then adapt `frontend/src/App.svelte` (header, footer, content loading) to your own data model.
But do not delete starter structures blindly. For a serious project build-out, first decide which parts remain useful foundations:
- SSR pipeline
- i18n route model
- pagebuilder-based content collection
- navigation collection
- media library and image handling
- tests / tours as scaffolding
The goal is not "delete the demo". The goal is "reshape the starter into a project architecture that editors can use productively".
**Decision guide:** **Decision guide:**
- **Keep demo content** if you want to use it as a reference while building your own components. - **Keep demo content** if you want to use it as a reference while building your own components.
@@ -164,8 +180,59 @@ Then adapt `frontend/src/App.svelte` (header, footer, content loading) to your o
```sh ```sh
yarn build # Frontend bundle for modern browsers yarn build # Frontend bundle for modern browsers
yarn build:server # SSR bundle (for tibi-server goja hooks) yarn build:server # SSR bundle (for tibi-server goja hooks)
yarn build # Frontend + admin module
yarn validate # TypeScript + Svelte checks (must show 0 errors and 0 warnings) yarn validate # TypeScript + Svelte checks (must show 0 errors and 0 warnings)
``` ```
**All four commands must succeed with exit code 0 before the project is considered set up.** **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
## 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 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
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.
+156 -18
View File
@@ -5,13 +5,28 @@ description: Implement and debug server-side rendering with goja (Go JS runtime)
# tibi-ssr-caching # tibi-ssr-caching
This skill should teach the **SSR architecture and implementation pattern** used in this repo family, not just describe one demo content setup. The important question is: **how is SSR built here, where is responsibility split, and which parts must be adapted per project?**
## SSR request flow ## SSR request flow
1. `ssr/get_read.js` receives a page request and calls `lib/ssr-server.js`. 1. `ssr/get_read.js` receives a page request and calls `lib/ssr-server.js`.
2. `ssr-server.js` loads `lib/app.server.js` (the Svelte SSR bundle) and renders the page. 2. `ssr/get_read.js` loads `lib/app.server.js` and calls `app.default.render({ url })`.
3. During rendering, API calls are tracked as **dependencies** (collection + entry ID). 3. `frontend/src/ssr.ts` only initializes i18n and delegates rendering to `svelte/server`.
4. The rendered HTML + dependencies are stored in the `ssr` collection. 4. `frontend/src/App.svelte` owns the actual data loading for both browser and SSR.
5. On the client, `lib/ssr.js` hydrates using `window.__SSR_CACHE__` injected by the server. 5. During SSR, the app calls its normal page-loading path directly inside a `typeof window === "undefined"` guard.
6. During browser navigation, the same page-loading path is triggered from `$effect`.
7. API calls made during SSR are tracked as dependency strings (`col:id` or `col:*`) and cached in `window.__SSR_CACHE__`.
8. The rendered HTML + dependency list are stored in the `ssr` collection.
## Responsibility split
- `frontend/src/ssr.ts` should stay minimal.
- `frontend/src/ssr.ts` is responsible for SSR bootstrapping only: locale setup, SSR-safe render wrapper, and calling `render(App, { props: { url } })`.
- The app component should own data loading.
- Hooks under `api/hooks/ssr/` should own caching, cache lookup, and cache persistence.
- `api/hooks/lib/ssr.js` should own the shared API helper that works in both browser and SSR.
If these responsibilities get mixed together, SSR usually becomes harder to reason about and harder for an LLM to modify safely.
## Building the SSR bundle ## Building the SSR bundle
@@ -20,36 +35,159 @@ yarn build:server
``` ```
- Output: `api/hooks/lib/app.server.js` - Output: `api/hooks/lib/app.server.js`
- Uses `babel.config.server.json` to transform async/await to generators (goja doesn't support async). - The project no longer uses Babel for SSR.
- Add `--banner:js='// @ts-nocheck'` to suppress type errors in the generated bundle. - The goja-compatible transform happens in `esbuild.config.server.js` via `supported`:
- `async-await: false`
- `async-generator: false`
- `dynamic-import: false`
- The SSR build writes directly to `api/hooks/lib/app.server.js`.
- Remove splitting-related frontend options (`outdir`, `splitting`, `entryNames`, `chunkNames`, `outExtension`) from the server build, otherwise esbuild will fail with `outfile`/`outdir` conflicts.
## Core design rule
- Prefer **one shared data-loading path** for browser and SSR.
- The browser should trigger it reactively.
- SSR should call that same path explicitly before rendering completes.
- Avoid maintaining a separate SSR-only content-loading implementation unless there is no viable alternative.
In this repo family, the practical pattern is:
- browser: `$effect(() => loadContent(...))`
- SSR: call the same `loadContent(...)` once inside a server guard
The main trap is assuming `$effect` alone is enough for SSR. It is not.
## Dependency-based cache invalidation ## Dependency-based cache invalidation
When content changes, `clear_cache.js` only invalidates SSR entries that depend on the changed collection/entry: When content changes, `clear_cache.js` only invalidates SSR entries that depend on the changed collection/entry:
```js ```js
// Each SSR cache entry stores its dependencies: // Each SSR cache entry stores dependency strings:
{ {
url: "/some-page", path: "/de/ueber-uns",
html: "...", content: "...",
dependencies: [ dependencies: ["content:abc123", "navigation:*", "medialib:*"]
{ collection: "content", id: "abc123" },
{ collection: "medialib", id: "def456" }
]
} }
``` ```
The hook queries the `ssr` collection for entries whose `dependencies` array matches the changed collection (and optionally entry ID), then deletes only those cached pages. - `col:id` means a detail dependency.
- `col:*` means a list dependency.
- `clear_cache.js` must handle `DELETE` robustly, because `context.data.id` and route params may be missing. Fallback to the last path segment if needed.
- `utils.clearSSRCache()` must clear:
- `col:*` on `POST`
- `col:id` OR `col:*` on `PUT`/`DELETE`
- everything on manual clear (`POST /ssr?clear=1` with no collection context)
## How SSR data loading is supposed to work
- Keep `frontend/src/ssr.ts` thin. It should set up locale state and call `render(App, { props: { url } })`.
- Do not move application-specific prefetch logic into `ssr.ts` unless absolutely necessary.
- The app itself should own the page-loading behavior.
- In projects using this starter architecture, the correct pattern is:
- browser: `$effect(() => loadContent(...))`
- SSR: call the same `loadContent(...)` once inside `typeof window === "undefined"`
- This keeps SSR and client navigation on one shared code path.
- `loadContent(...)` must load **all data required for a fully rendered page**. In this repo that includes both navigation and page content. SSR is incomplete if only the main content entry is loaded.
- Because goja runs the transformed async path synchronously enough for this setup, the direct SSR call works. The problem was the reactive `$effect`, not the shared async loader itself.
## What is project-specific vs. architecture-specific
Architecture-specific rules:
- SSR entry goes through `api/hooks/ssr/get_read.js`
- HTML caching lives in the `ssr` collection
- SSR API calls are tracked through `context.ssrRequest`
- Client hydration reuses `window.__SSR_CACHE__`
- The app owns its own data-loading logic
Project-specific rules that an LLM must inspect before changing SSR:
- which collections contribute to rendered pages
- which routes should SSR vs. skip SSR
- whether URLs are language-prefixed
- whether DB paths are stored with or without language prefix
- which lookups are required to make a page fully render
- which collections need publication-aware invalidation
- whether there are canonical/alias paths
Do not hardcode demo assumptions into the skill. Instead, use the architecture rules above and inspect the current project's route model, collections, and page-loading code.
## SSR route validation ## SSR route validation
Route validation in `config.js` controls which paths get SSR treatment. Return: Route validation in `config.js` controls which paths get SSR treatment. Return:
- A positive number to enable SSR for that route - `1` to render the requested path as-is
- `-1` to disable SSR (current default in the starter template) - a string to rewrite to the canonical cache path
- `-1` for not found
For projects following this setup, route validation must understand the public URL shape used by the frontend router:
- `/` and `/{lang}` are valid SSR roots.
- Public content URLs are language-prefixed (`/de/...`, `/en/...`).
- Content entries in the DB are stored **without** the language prefix in `content.path`.
- `ssrValidatePath()` therefore needs to:
- extract the language prefix from the URL
- strip it before querying `content.path`
- include `{ lang }` in the content query
- support `alternativePaths.path`
- return a canonical language-prefixed URL when the request matched via an alternative path
If this mapping is wrong, SSR may appear to work for root pages while returning 404 or empty content for real CMS pages.
## Publication-aware SSR caching
- `config.js` exports `publishedFilter` and `ssrPublishCheckCollections`.
- `ssrPublishCheckCollections` should include every collection whose publication window can make cached HTML stale.
- In this starter, `content` is currently included.
- `ssr-server.js` uses `publication.from` / `publication.to` to compute `context.ssrCacheValidUntil`.
- `get_read.js` must reject expired cache entries and delete them before rendering anew.
## Hydration cache behavior
- `api/hooks/lib/ssr.js` uses the same API helper for browser and SSR.
- On the server, `apiRequest(...)` delegates to `context.ssrRequest(...)`.
- On the client, `window.__SSR_CACHE__` is checked first for GET requests.
- This means SSR is not just HTML prerendering; it also primes client-side data access.
- If HTML renders but `window.__SSR_CACHE__` is missing, the SSR pipeline is incomplete.
## What an LLM should inspect first when changing SSR
1. `api/hooks/ssr/get_read.js` to understand cache lookup, route validation, and template injection.
2. `api/hooks/lib/ssr-server.js` to understand dependency tracking and SSR-side API behavior.
3. `frontend/src/ssr.ts` to confirm how the SSR render wrapper is bootstrapped.
4. The top-level app/page-loading surface (for example `frontend/src/App.svelte`) to see where data is actually loaded.
5. `api/hooks/config.js` to understand route validation, canonicalization, and publication-aware collections.
6. `api/hooks/clear_cache.js` plus `api/hooks/lib/utils.js` to understand invalidation behavior.
This order helps an LLM separate infrastructure problems from app-loading problems.
## How to verify SSR correctly
- Do not rely only on the BrowserSync/frontend proxy when debugging SSR.
- Test the SSR API endpoint directly, for example:
```bash
curl "http://tibiserver:8080/api/v1/_/<namespace>/ssr?url=/de/ueber-uns"
```
- Verify all of the following:
- HTTP status is correct
- expected page content is present in the HTML
- all page-critical content is present in the HTML
- navigation labels are present in the HTML when navigation is part of the app shell
- `window.__SSR_CACHE__` exists
- no `error:` comment was injected into the template
- second request returns `X-SSR-Cache: true`
- `POST /ssr?clear=1` removes cache entries and the next request is a miss again
## Common pitfalls ## Common pitfalls
- **goja has no async/await**: The babel server config transforms these, but avoid top-level await. - **Do not document Babel anymore**: the current SSR build is esbuild-only.
- **goja does not parse every modern syntax feature**: dynamic import must be downlevelled in the server build.
- **Do not leave frontend build options on the server build**: `splitting`/`outdir` inherited from the frontend config will break `build:server`.
- **No browser globals**: `window`, `document`, `localStorage` etc. don't exist in goja. Guard with `typeof window !== "undefined"`. - **No browser globals**: `window`, `document`, `localStorage` etc. don't exist in goja. Guard with `typeof window !== "undefined"`.
- **SSR cache can go stale**: Always ensure `clear_cache.js` covers any new collection that affects rendered output. - **`$effect` does not solve SSR loading**: server-side content must be loaded outside browser-only reactive effects.
- **SSR can look healthy while content is missing**: a 200 response plus app shell is not enough; always verify actual DB content in the HTML.
- **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.
@@ -0,0 +1,303 @@
---
name: website-solution-architecture
description: Translate website requirements into a complete tibi-svelte-starter solution. Covers solution decomposition across collections, pagebuilder, navigation, SSR, actions, permissions, media, admin UX, and validation.
---
# website-solution-architecture
## When to use this skill
Use this skill when:
- The user wants a complete website built on this starter
- Requirements exist, but the data model and project structure do not yet
- A feature request spans frontend, admin, hooks, SSR, and content modeling together
- An LLM needs to decide what belongs in collections, blocks, actions, settings, or frontend code
## Goal
The goal is to teach an LLM how to convert requirements into a coherent website solution on this stack.
That means choosing the right shape for:
- content model
- admin authoring UX
- frontend rendering
- SSR behavior
- actions and workflows
- media handling
- permissions
- verification
This skill is about architecture decisions, not isolated file edits.
## Core principle
Do not start by adding components. Start by modeling the system.
On this starter, a complete website usually spans these layers:
1. collections and actions in `api/`
2. shared types in `types/`
3. app shell, routing, and rendering in `frontend/src/`
4. SSR validation and caching in `api/hooks/`
5. admin ergonomics in collection meta/field config
6. tests or direct validation steps
If the work starts at the UI layer without a content/admin model, the solution usually drifts.
## Canonical architecture areas
### 1. Content model
Decide early which collections exist and why.
Typical website collections:
- `content` for pages
- `navigation` for header/footer/site menus
- `medialib` for media assets
- site settings or global content singleton
- optional domain collections such as team, jobs, events, products, references
Do not create collections just because the frontend has a section. Create them when the data needs its own lifecycle, relations, searchability, or editorial ownership.
### 2. Pagebuilder model
Decide whether pages are best modeled as:
- page entries with `blocks[]`
- references to reusable sections
- a mix of page-local blocks and reusable records
Use pagebuilder structures when editors need flexible composition. Use separate collections when content is reused across multiple pages or needs its own workflows.
### 3. Navigation model
Navigation is not an afterthought. It is often page-critical runtime and SSR data.
Design:
- header navigation
- footer navigation
- utility navigation if needed
- relation to localized paths
If navigation is part of the shell, it must be loaded and rendered coherently in browser and SSR.
### 4. Route model
This starter uses content-driven routing, not file-based routing.
Architectural decisions must account for:
- language-prefixed public URLs
- DB paths stored without language prefix
- route translations
- canonical paths and alias paths
- SSR route validation in `api/hooks/config.js`
If the route model is unclear, the frontend and SSR will diverge.
### 5. Admin authoring model
Every serious website on this stack needs an editor-friendly Nova model.
Use:
- `preview`
- `sidebar`
- `containerProps.layout`
- `dependsOn`
- `drillDown`
- `pagebuilder`
- `subNavigation`
- `singleton`
- foreign previews
Do not treat admin config as optional polish. It is part of the solution architecture.
### 6. Actions and workflows
Decide whether non-page features belong in collections, actions, or both.
Typical action-based website workflows:
- contact form
- newsletter signup
- booking or quote request
- webhook receiver
- AI helper endpoint
Do not force endpoint logic into fake CRUD collections.
### 7. SSR and caching
If the project uses SSR, the architecture must define:
- which routes are SSR-valid
- which collections influence rendered HTML
- how invalidation happens
- which page-critical data must be loaded during SSR
On this starter, SSR is architecture, not a plugin. It must be considered while modeling routing, navigation, page content, and mutation hooks.
### 8. Media and SEO
Most website projects need explicit decisions for:
- media library usage
- image fields and filters
- alt texts and captions
- SEO metadata per page
- social/share metadata where required
- publication windows
These decisions often belong partly in content collections and partly in admin ergonomics.
### 9. Permissions and editorial safety
Before implementing, decide:
- who may edit which collection
- which fields are readonly or hidden
- which collections are public or internal
- whether actions are public, authenticated, or internal only
Permissions are part of the architecture, not only a final hardening step.
## Recommended planning flow
### Step 1: Extract the website capabilities
Turn the brief into concrete capability buckets:
- page types
- reusable sections
- navigation
- forms/workflows
- media/SEO
- localization
- editor roles
- SSR/publication needs
### Step 2: Map capabilities to runtime surfaces
For each capability, decide whether it belongs in:
- collection schema
- action endpoint
- pagebuilder block
- site settings singleton
- frontend-only presentation
- hook-based server logic
### Step 3: Shape the editor workflows
Before building components, decide how editors will:
- create pages
- compose blocks
- edit navigation
- manage reusable entities
- preview references
- find the right entries in Nova
If this step is skipped, the content model often becomes technically correct but operationally poor.
### Step 4: Define the frontend boundaries
Clarify:
- app shell responsibilities
- which parts are pure presentation blocks
- where data loading lives
- how route parameters map to content queries
- which features must be SSR-safe
### Step 5: Define server responsibilities
Clarify:
- route validation
- public read filtering
- cache invalidation
- action validation and side effects
- publication behavior
### Step 6: Define verification before implementation expands
At minimum, define how to verify:
- pages load in the browser
- pages render in SSR when applicable
- admin authoring is usable
- actions/forms behave correctly
- build and validate stay clean
## Typical solution patterns
### Marketing website
Typical shape:
- `content` collection with pagebuilder blocks
- `navigation` collection
- global site settings singleton
- SSR enabled for public pages
- one or more public actions for forms
### Content-heavy editorial website
Typical shape:
- `content` plus additional domain collections
- stronger use of relations and reusable entities
- richer preview/search ergonomics in Nova
- publication-aware SSR invalidation
### Product or service website with lead generation
Typical shape:
- structured domain collections for offers/services
- pagebuilder pages for marketing presentation
- public actions for inquiry flows
- staff-facing inquiry persistence when follow-up is needed
## Anti-patterns
- Starting from Svelte components before defining collections and flows
- Treating admin ergonomics as a later cleanup step
- Mixing page data, workflow data, and settings without clear boundaries
- Creating one-off block types for every page variation
- Using collections where actions are the better model
- Forgetting SSR implications while changing route or content shape
- Leaving types, renderer, and collection schema out of sync
## Architecture checklist
Before calling a website solution on this starter coherent, verify that all of these are answered:
1. Which collections exist and why?
2. Which content is page-local versus reusable?
3. How are routes, language prefixes, and canonical paths modeled?
4. Which authoring workflows exist in Nova?
5. Which non-CRUD workflows require actions?
6. Which data is page-critical for SSR?
7. Which permissions protect content and workflows?
8. How will success be validated technically and functionally?
## What an LLM should inspect first
When asked to build a complete website on this starter, inspect in this order:
1. `api/collections/content.yml`
2. `api/collections/navigation.yml`
3. `frontend/src/App.svelte`
4. `frontend/src/blocks/BlockRenderer.svelte`
5. `types/global.d.ts`
6. `api/hooks/config.js`
7. existing actions/hooks if the project already has workflows
This order exposes the actual project architecture before the LLM starts generating new code.
+87 -16
View File
@@ -10,28 +10,44 @@ Tibi CMS starter template — Svelte 5 SPA with esbuild, SSR via goja, and Playw
- **Tests**: Playwright for E2E, API, mobile, and visual regression tests in `tests/`. - **Tests**: Playwright for E2E, API, mobile, and visual regression tests in `tests/`.
- **Types**: Shared TypeScript types in `types/global.d.ts`. Keep `tibi-types/` read-only. - **Types**: Shared TypeScript types in `types/global.d.ts`. Keep `tibi-types/` read-only.
## Project bootstrap
Before treating this repo as a real project, replace the starter placeholders and initial project values.
Derive these values from the real repo path `gitbase.de/ORG/REPO`:
- `PROJECT_NAME`: use the repo name in kebab-case.
- `TIBI_NAMESPACE`: set it equal to `PROJECT_NAME`, i.e. use the same repo name in kebab-case.
- `STAGING_PATH`: use the real repo org and repo name, i.e. `/staging/ORG/REPO/dev`.
- `.env`: replace `PROJECT_NAME=__PROJECT_NAME__`, `TIBI_NAMESPACE=__TIBI_NAMESPACE__`, `STAGING_PATH=/staging/__ORG__/__PROJECT__/dev`, `STAGING_URL=https://dev-__PROJECT_NAME__.staging.testversion.online`, and `CODING_URL=https://__PROJECT_NAME__.code.testversion.online`.
- `api/config.yml`: replace `namespace: __TIBI_NAMESPACE__`.
- `frontend/.htaccess`: replace both `__TIBI_NAMESPACE__` proxy targets.
- `api/hooks/config-client.js`: replace `https://__PROJECT__.code.testversion.online` with the real origin URL.
- `package.json`: adapt starter metadata like `name` and `repository` when creating the real project repo.
- Docker and local URLs derive from `.env`, so `PROJECT_NAME` and `TIBI_NAMESPACE` must be correct before `make docker-up`.
- Recommended check: search for remaining starter placeholders with `rg '__[A-Z0-9_]+__' .`.
## Setup commands ## Setup commands
- Install deps: `yarn install` - Install deps: `yarn install`
- Start dev: `make docker-up && make docker-start` - Start dev: `make docker-up` or `make docker-start`
- Start with mock data: set `MOCK=1` in `.env`, then `make docker-up && make docker-start` - Restart frontend watcher/dev-server: `make docker-restart-frontend`
- Build frontend: `yarn build` - View logs: `make docker-logs` or `make docker-logs-X`
- Start with mock data: set `MOCK=1` in `.env`, then use the normal Docker start command
- Build frontend/admin bundle: `yarn build`
- Build SSR bundle: `yarn build:server` - Build SSR bundle: `yarn build:server`
- Validate types: `yarn validate` - Validate types: `yarn validate`
## Development workflow ## Development workflow
- **Dev servers always run in Docker** — never use `yarn dev` or `yarn start` locally; web access only works through the Docker reverse proxy. - **Dev servers always run in Docker** — never use `yarn dev` or `yarn start` locally; web access only works through the Docker reverse proxy.
- Docker/Makefile commands: `make docker-up`, `make docker-start`, `make docker-logs`, `make docker-restart-frontend`.
- Local `yarn` is only for standalone tasks: `yarn build`, `yarn build:server`, `yarn validate`. - Local `yarn` is only for standalone tasks: `yarn build`, `yarn build:server`, `yarn validate`.
- **Mock mode**: Set `MOCK=1` to run the frontend without a tibi-server. API calls are served from JSON files in `frontend/mocking/`. Enable in Docker via `MOCK=1` in `.env`, then `make docker-up && make docker-start`. Missing mock endpoints return 404. - **Mock mode**: Set `MOCK=1` to run the frontend without a tibi-server. API calls are served from JSON files in `frontend/mocking/`. Missing mock endpoints return 404.
- Frontend code is automatically built by watcher and BrowserSync; backend hooks are automatically reloaded on change. - Frontend code is automatically rebuilt by the watcher/BrowserSync stack; backend hooks reload on change.
- Read `.env` for environment URLs and secrets. - Read `.env` for environment URLs and secrets.
- `webserver/` is for staging/ops only; use BrowserSync/esbuild for day-to-day dev. - `webserver/` is for staging/ops only; use BrowserSync/esbuild for day-to-day dev.
- If development environment is running, access the website at: `https://${PROJECT_NAME}.code.testversion.online/`. - If development environment is running, access the website at: `https://${PROJECT_NAME}.code.testversion.online/`.
- To force a restart of the frontend build and dev-server: `make docker-restart-frontend`.
- To show last X lines of docker logs: `make docker-logs-X`.
- Esbuild watches for file changes and rebuilds automatically.
- For a11y testing use MCP a11y tools if available. - For a11y testing use MCP a11y tools if available.
- For quick interactive browser testing, ask the user to connect Playwright MCP (preferred) or Browser MCP (only in non-autonomous/chat mode). - For quick interactive browser testing, ask the user to connect Playwright MCP (preferred) or Browser MCP (only in non-autonomous/chat mode).
@@ -57,20 +73,18 @@ Tibi CMS starter template — Svelte 5 SPA with esbuild, SSR via goja, and Playw
## API access ## API access
- API access to collections uses the reverse proxy: `CODING_URL/api/<collection>` (e.g. `CODING_URL/api/content`). - API access to collections uses the reverse proxy: `CODING_URL/api/<collection>` (e.g. `CODING_URL/api/content`).
- Auth via `Token` header with ADMIN_TOKEN from `api/config.yml.env`. - Auth via `Token` header with `ADMIN_TOKEN` from `api/config.yml.env` when a configured token with the required permissions is needed.
## Required secrets and credentials ## Required secrets and credentials
| Secret | Location | Purpose | | Secret | Location | Purpose |
| --- | --- | --- | | --- | --- | --- |
| `ADMIN_TOKEN` | `api/config.yml.env` | API admin auth + tibi-admin login | | `ADMIN_TOKEN` | `api/config.yml.env` | Access token for configured API/admin permissions |
| `SENTRY_AUTH_TOKEN` | Gitea repo secrets | Sourcemap upload to Sentry (CI only) | | `SENTRY_AUTH_TOKEN` | Gitea repo secrets | Sourcemap upload to Sentry (CI only) |
| `.basic-auth-web` | project root (git-ignored) | Basic auth for BrowserSync dev server | | `.basic-auth-web` | project root (git-ignored) | Basic auth for BrowserSync dev server |
| `.basic-auth-code` | project root (git-ignored) | Basic auth for Code-Server / admin | | `.basic-auth-code` | project root (git-ignored) | Basic auth for Code-Server / admin |
| `RSYNC_PASS` | Gitea secrets (`github.token`) | rsync deployment password (CI only) | | `RSYNC_PASS` | Gitea secrets (`github.token`) | rsync deployment password (CI only) |
**Note:** `.basic-auth-web` and `.basic-auth-code` are plain text files in `user:password` format (htpasswd). They are required by Docker Traefik labels for dev environment access. Curl and Playwright requests are let through without basic auth via Traefik label configuration.
## Infrastructure prerequisites ## Infrastructure prerequisites
- **Code-Server environment** — This project is designed for development on a Code-Server instance at `*.code.testversion.online` with a **Traefik reverse proxy** managing HTTPS and auto-routing via Docker labels. - **Code-Server environment** — This project is designed for development on a Code-Server instance at `*.code.testversion.online` with a **Traefik reverse proxy** managing HTTPS and auto-routing via Docker labels.
@@ -123,16 +137,73 @@ Project-specific types (e.g. `Ssr`, `ApiOptions`, `ContentEntry`) live in `types
### Architecture skills (loaded on demand) ### Architecture skills (loaded on demand)
These skills provide deep-dive documentation. Use them when working on the respective area: These skills provide deep-dive documentation. Use them by phase instead of treating them as an unsorted reference list.
#### 1. Project start and solution design
| Skill | When to use |
| --- | --- |
| `tibi-project-setup` | Setting up a new project from scratch |
| `website-solution-architecture` | Translating website requirements into a complete solution across content, admin, SSR, and workflows |
| `security-hardening-and-token-strategy` | Applying secure token, secret, permission, and hook-capability decisions |
#### 2. Content model and editor UX
| Skill | When to use | | Skill | When to use |
| --- | --- | | --- | --- |
| `content-authoring` | Adding new pages, content blocks, or collections | | `content-authoring` | Adding new pages, content blocks, or collections |
| `frontend-architecture` | Routing, state management, Svelte 5 patterns, API layer, error handling | | `nova-pagebuilder-modeling` | Designing editor-friendly block systems, nested block schemas, and pagebuilder UX |
| `nova-navigation-modeling` | Modeling multilingual header/footer/navigation trees with current Nova navigation features |
| `admin-ui-config` | Configuring collection admin views, field widgets, layouts | | `admin-ui-config` | Configuring collection admin views, field widgets, layouts |
| `media-seo-publishing` | Modeling media, SEO, and publication workflows for website projects |
| `permissions-and-editor-workflows` | Designing safe editorial permissions, field rules, and role-aware admin workflows |
| `nova-ai-editor-features` | Applying AI and LLM capabilities in editor workflows and media authoring responsibly |
#### 3. Backend behavior and integrations
| Skill | When to use |
| --- | --- |
| `tibi-hook-authoring` | Writing or debugging server-side hooks | | `tibi-hook-authoring` | Writing or debugging server-side hooks |
| `tibi-actions-and-forms` | Building contact forms, workflow endpoints, and other action-based website features |
| `scheduled-jobs-and-automation` | Building cron-based background tasks, cleanups, reports, and sync workflows |
| `realtime-and-live-workflows` | Designing SSE-based live updates, notifications, previews, and status workflows |
#### 4. Frontend runtime and delivery
| Skill | When to use |
| --- | --- |
| `frontend-architecture` | Routing, state management, Svelte 5 patterns, API layer, error handling |
| `tibi-ssr-caching` | SSR rendering and cache invalidation | | `tibi-ssr-caching` | SSR rendering and cache invalidation |
| `tibi-project-setup` | Setting up a new project from scratch |
### Quickstart roadmap for a new website
Use this order when building a project from scratch on this starter:
1. Foundation.
Start with `tibi-project-setup` until Docker, URLs, build, and validate are green.
2. Solution design.
Use `website-solution-architecture` first, then `security-hardening-and-token-strategy`, before writing components or hooks.
3. Content and admin model.
Use the skills from **Content model and editor UX** to shape `api/collections/content.yml`, `api/collections/navigation.yml`, reusable domain collections, and Nova authoring UX.
4. Frontend and runtime.
Use `frontend-architecture` plus `nova-pagebuilder-modeling` when wiring routing, i18n, content loading, `frontend/src/blocks/BlockRenderer.svelte`, and shared types.
5. Backend behavior.
Use the skills from **Backend behavior and integrations** for hooks, forms/actions, jobs, and realtime only where the project actually needs them.
6. SSR and publishing.
Use `tibi-ssr-caching` once routes, navigation, and page-critical data are defined, and use `media-seo-publishing` plus `permissions-and-editor-workflows` where publication, SEO, and editorial restrictions matter.
7. Optional AI editor features.
Use `nova-ai-editor-features` only when there is a concrete editorial workflow for it.
8. Final verification.
Confirm public site, admin authoring, pagebuilder rendering, navigation/media resolution, forms/actions, and SSR all work, then run `yarn build`, `yarn build:server`, and `yarn validate`.
For one-off tasks, use the phase tables above as the lookup index instead of maintaining a second skill matrix here.
### Tailwind CSS 4 canonical classes ### Tailwind CSS 4 canonical classes
+1 -1
View File
@@ -95,7 +95,7 @@ fields:
blockTypeField: type blockTypeField: type
defaultViewport: desktop defaultViewport: desktop
blockRegistry: blockRegistry:
file: /_/assets/dist/admin.mjs file: /_/assets/dist/admin.mjs?v=${ADMIN_ASSET_VERSION}
subFields: subFields:
- name: hide - name: hide
type: boolean type: boolean
+1
View File
@@ -1 +1,2 @@
ADMIN_TOKEN=5bdfjc78hdxn338cuhSJ ADMIN_TOKEN=5bdfjc78hdxn338cuhSJ
ADMIN_ASSET_VERSION=4a604ba-dirty-1778608358546
+13 -6
View File
@@ -5,20 +5,27 @@ Server-side rendering via goja (Go JS runtime) with HTML caching.
## Request flow ## Request flow
1. `get_read.js` receives the request and calls `lib/ssr-server.js`. 1. `get_read.js` receives the request and calls `lib/ssr-server.js`.
2. `ssr-server.js` renders the Svelte app via `lib/app.server.js` and injects `window.__SSR_CACHE__`. 2. `get_read.js` loads `lib/app.server.js` and calls `app.default.render({ url })`.
3. On the client, `lib/ssr.js` hydrates using the injected cache data. 3. `frontend/src/ssr.ts` stays thin and only initializes locale state before rendering `App.svelte`.
4. Rendered HTML is stored in the `ssr` collection with dependency tracking. 4. `frontend/src/App.svelte` is responsible for actual page data loading for both browser and SSR.
5. During SSR, `App.svelte` calls the same `loadContent(...)` path directly inside `typeof window === "undefined"`.
6. Rendered HTML is stored in the `ssr` collection together with dependency tracking strings.
## Build ## Build
- SSR bundle is built via `yarn build:server` and outputs to `lib/app.server.js`. - SSR bundle is built via `yarn build:server` and outputs to `lib/app.server.js`.
- The build uses `--banner:js='// @ts-nocheck'` to suppress type errors in the generated bundle. - The project no longer uses Babel for SSR.
- goja-compatible transforms are configured in `esbuild.config.server.js` via `supported`.
- The server build must remove frontend-only splitting/outdir options inherited from the shared esbuild config.
## Cache invalidation ## Cache invalidation
- `clear_cache.js` hook invalidates SSR cache entries based on collection dependencies. - `clear_cache.js` hook invalidates SSR cache entries based on collection dependencies.
- When content changes, only SSR entries that depend on the changed collection/entry are cleared. - Dependencies are stored as strings like `content:<id>` or `content:*`.
- `DELETE` invalidation must be robust even when `context.data.id` is missing.
## Route validation ## Route validation
SSR route validation is currently disabled and returns -1 in `config.js`; update this when enabling SSR per route. - SSR route validation is active in `config.js`.
- Public page URLs are language-prefixed (`/de/...`, `/en/...`), while `content.path` in the DB is stored without that prefix.
- `ssrValidatePath()` must strip the language prefix before querying content and return a canonical language-prefixed URL when needed.
-21
View File
@@ -1,21 +0,0 @@
const config = require("./esbuild.config.js")
const svelteConfig = require("./svelte.config")
config.options.minify = false
config.options.entryPoints = ["./frontend/src/admin.ts"]
config.options.outfile = "./" + config.distDir + "/admin.mjs"
delete config.options.outdir
config.options.splitting = false
config.options.plugins = [
config.sveltePlugin({
compilerOptions: {
css: "external",
dev: (process.argv?.length > 2 ? process.argv[2] : "build") !== "build",
},
preprocess: svelteConfig.preprocess,
cache: true,
}),
config.resolvePlugin,
]
module.exports = config
+34 -2
View File
@@ -2,6 +2,36 @@ const fs = require("fs")
const { execSync } = require("child_process") const { execSync } = require("child_process")
const postcssPlugin = require("esbuild-postcss") const postcssPlugin = require("esbuild-postcss")
function upsertEnvVar(filePath, key, value) {
const nextLine = `${key}=${value}`
const content = fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : ""
const lines = content ? content.split(/\r?\n/) : []
const nextLines = []
let replaced = false
for (const line of lines) {
if (!line || line.startsWith(`${key}=`)) {
if (line.startsWith(`${key}=`)) {
nextLines.push(nextLine)
replaced = true
} else {
nextLines.push(line)
}
continue
}
nextLines.push(line)
}
if (!replaced) {
if (nextLines.length && nextLines[nextLines.length - 1] !== "") {
nextLines.push("")
}
nextLines.push(nextLine)
}
fs.writeFileSync(filePath, `${nextLines.join("\n").replace(/\n*$/, "")}\n`)
}
// Resolve version at build time via git describe (tag + hash), fallback to env var or "dev" // Resolve version at build time via git describe (tag + hash), fallback to env var or "dev"
let gitHash = "dev" let gitHash = "dev"
try { try {
@@ -15,16 +45,18 @@ function writeBuildInfo() {
const info = { const info = {
gitHash, gitHash,
buildTime: new Date().toISOString(), buildTime: new Date().toISOString(),
assetVersion: `${gitHash}-${Date.now()}`,
} }
fs.writeFileSync( fs.writeFileSync(
__dirname + "/frontend/src/lib/buildInfo.ts", __dirname + "/frontend/src/lib/buildInfo.ts",
`// AUTO-GENERATED by esbuild.config.js \u2013 do not edit\nexport const gitHash = ${JSON.stringify(info.gitHash)}\nexport const buildTime = ${JSON.stringify(info.buildTime)}\n` `// AUTO-GENERATED by esbuild.config.js \u2013 do not edit\nexport const gitHash = ${JSON.stringify(info.gitHash)}\nexport const buildTime = ${JSON.stringify(info.buildTime)}\nexport const assetVersion = ${JSON.stringify(info.assetVersion)}\n`
) )
// Write same buildInfo for backend hooks (X-Build-Time / X-Release headers) // Write same buildInfo for backend hooks (X-Build-Time / X-Release headers)
fs.writeFileSync( fs.writeFileSync(
__dirname + "/api/hooks/lib/buildInfo.js", __dirname + "/api/hooks/lib/buildInfo.js",
`// AUTO-GENERATED by esbuild.config.js \u2013 do not edit\nmodule.exports = { gitHash: ${JSON.stringify(info.gitHash)}, buildTime: ${JSON.stringify(info.buildTime)} }\n` `// AUTO-GENERATED by esbuild.config.js \u2013 do not edit\nmodule.exports = { gitHash: ${JSON.stringify(info.gitHash)}, buildTime: ${JSON.stringify(info.buildTime)}, assetVersion: ${JSON.stringify(info.assetVersion)} }\n`
) )
upsertEnvVar(__dirname + "/api/config.yml.env", "ADMIN_ASSET_VERSION", info.assetVersion)
} }
// NOTE: writeBuildInfo() is NOT called here at top-level. // NOTE: writeBuildInfo() is NOT called here at top-level.
// It is called by esbuild-wrapper.js before each build. // It is called by esbuild-wrapper.js before each build.