✨ 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:
@@ -1,6 +1,6 @@
|
||||
---
|
||||
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
|
||||
@@ -10,7 +10,7 @@ description: Configure the admin UI for collections — meta labels, views (tabl
|
||||
Use this skill when:
|
||||
|
||||
- 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.)
|
||||
- Organizing fields into sidebar groups or sections
|
||||
- Setting up foreign key references between collections
|
||||
@@ -18,13 +18,15 @@ Use this skill when:
|
||||
|
||||
## 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
|
||||
|
||||
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
|
||||
name: mycollection
|
||||
@@ -32,71 +34,55 @@ meta:
|
||||
label: { de: "Produkte", en: "Products" } # Sidebar label (i18n)
|
||||
muiIcon: shopping_cart # Material UI icon name
|
||||
group: shop # Group in admin sidebar
|
||||
singleton: false # true = only one entry allowed
|
||||
hideInNavigation: false # true = don't show in sidebar
|
||||
defaultSort: "-insertTime" # Default sort (prefix - = descending)
|
||||
rowIdentTpl: { twig: "{{ name }} ({{ price }})" } # Row display template
|
||||
singleton:
|
||||
enabled: false
|
||||
hide: false # Set to true to hide the collection for non-admin users
|
||||
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
|
||||
rowIdentTpl: { twig: "{{ name }}" } # Simple
|
||||
rowIdentTpl: { twig: "{{ type }} — {{ language }}" } # Combined
|
||||
preview: name
|
||||
|
||||
preview:
|
||||
label: name
|
||||
secondary: slug
|
||||
badge: status
|
||||
|
||||
preview:
|
||||
eval: "`${$this.firstName} ${$this.lastName}`"
|
||||
```
|
||||
|
||||
---
|
||||
## List presentation
|
||||
|
||||
## Views: table, simpleList, cardList
|
||||
|
||||
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
|
||||
For current Nova, use `meta.viewHint` plus `meta.preview` for collection/list presentation.
|
||||
|
||||
```yaml
|
||||
meta:
|
||||
views:
|
||||
- type: table
|
||||
columns:
|
||||
- name # Simple: field name as column
|
||||
- source: lang # With filter
|
||||
filter: true
|
||||
- source: active # Boolean column with filter
|
||||
filter: true
|
||||
- source: price # Custom label
|
||||
label: { de: "Preis", en: "Price" }
|
||||
- source: insertTime # Date field
|
||||
width: 160
|
||||
viewHint: table
|
||||
preview:
|
||||
label: name
|
||||
secondary: slug
|
||||
badge: status
|
||||
table:
|
||||
- name
|
||||
- source: status
|
||||
label: Status
|
||||
- source: author.name
|
||||
label: Author
|
||||
select:
|
||||
- author.name
|
||||
```
|
||||
|
||||
### Simple list view (mobile)
|
||||
|
||||
```yaml
|
||||
meta:
|
||||
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
|
||||
```
|
||||
- `meta.viewHint` controls the preferred collection presentation (`table`, `cards`, `media`, or `navigation` object where supported).
|
||||
- `preview.table` defines explicit list columns for Nova.
|
||||
- `preview.select` can reduce lookup work for preview table columns.
|
||||
- `meta.subNavigation` defines filtered entry tabs in the sidebar.
|
||||
|
||||
---
|
||||
|
||||
@@ -171,18 +157,25 @@ Override the default widget with `meta.widget`:
|
||||
meta:
|
||||
widget: richtext # Rich text editor (HTML)
|
||||
|
||||
- name: color
|
||||
type: string
|
||||
- name: heroImage
|
||||
type: file
|
||||
meta:
|
||||
widget: color # Color picker
|
||||
widget: image # Image-focused file widget
|
||||
|
||||
- name: image
|
||||
type: string
|
||||
- name: relatedPages
|
||||
type: string[]
|
||||
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
|
||||
|
||||
@@ -211,7 +204,7 @@ Dynamic choices from API:
|
||||
choices:
|
||||
endpoint: categories # Collection name
|
||||
mapping:
|
||||
id: _id
|
||||
id: id
|
||||
name: name
|
||||
```
|
||||
|
||||
@@ -226,22 +219,25 @@ Link to entries in another collection:
|
||||
label: { de: "Autor", en: "Author" }
|
||||
foreign:
|
||||
collection: users
|
||||
id: _id
|
||||
id: id
|
||||
sort: name
|
||||
projection: name,email
|
||||
render: { twig: "{{ name }} <{{ email }}>" }
|
||||
autoFill: # Auto-fill other fields on selection
|
||||
- source: email
|
||||
target: authorEmail
|
||||
render:
|
||||
label: name
|
||||
secondary: email
|
||||
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
|
||||
|
||||
```yaml
|
||||
- name: image
|
||||
type: file
|
||||
meta:
|
||||
widget: medialib
|
||||
widget: image
|
||||
downscale: # Auto-resize on upload
|
||||
maxWidth: 1920
|
||||
maxHeight: 1080
|
||||
@@ -264,7 +260,7 @@ Link to entries in another collection:
|
||||
- name: publishDate
|
||||
type: date
|
||||
meta:
|
||||
position: "sidebar:Veröffentlichung" # Sidebar with group header
|
||||
position: "sidebar:publishing" # Sidebar with group key
|
||||
```
|
||||
|
||||
### Sidebar groups (ordered)
|
||||
@@ -274,9 +270,12 @@ Define sidebar group order in collection meta:
|
||||
```yaml
|
||||
meta:
|
||||
sidebar:
|
||||
- Veröffentlichung
|
||||
- SEO
|
||||
- Einstellungen
|
||||
- group: publishing
|
||||
label: { de: "Veröffentlichung", en: "Publishing" }
|
||||
- group: seo
|
||||
label: { de: "SEO", en: "SEO" }
|
||||
- group: settings
|
||||
label: { de: "Einstellungen", en: "Settings" }
|
||||
```
|
||||
|
||||
### Sections in main area
|
||||
@@ -313,6 +312,15 @@ Use `containerProps` for multi-column layout:
|
||||
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
|
||||
@@ -340,7 +348,9 @@ Use `containerProps` for multi-column layout:
|
||||
type: object[]
|
||||
meta:
|
||||
label: { de: "Inhaltsblöcke", en: "Content Blocks" }
|
||||
preview: { eval: "item.type + ': ' + (item.headline || '')" }
|
||||
widget: pagebuilder
|
||||
preview: { eval: "`${$this.type}: ${$this.headline || ''}`" }
|
||||
drillDown: true
|
||||
subFields:
|
||||
- name: type
|
||||
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.
|
||||
|
||||
### 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
|
||||
|
||||
For complex nested objects, use `drillDown` to render them as a sub-page:
|
||||
@@ -405,28 +483,26 @@ meta:
|
||||
label: { de: "Produkte", en: "Products" }
|
||||
muiIcon: inventory_2
|
||||
group: shop
|
||||
defaultSort: "-insertTime"
|
||||
rowIdentTpl: { twig: "{{ name }} ({{ sku }})" }
|
||||
viewHint: table
|
||||
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:
|
||||
- Veröffentlichung
|
||||
- SEO
|
||||
|
||||
views:
|
||||
- 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
|
||||
- group: publishing
|
||||
label: { de: "Veröffentlichung", en: "Publishing" }
|
||||
- group: seo
|
||||
label: { de: "SEO", en: "SEO" }
|
||||
|
||||
permissions:
|
||||
public:
|
||||
@@ -439,18 +515,12 @@ permissions:
|
||||
put: true
|
||||
delete: true
|
||||
|
||||
hooks:
|
||||
beforeRead: |
|
||||
!include hooks/filter_public.js
|
||||
afterWrite: |
|
||||
!include hooks/clear_cache.js
|
||||
|
||||
fields:
|
||||
- name: active
|
||||
type: boolean
|
||||
meta:
|
||||
label: { de: "Aktiv", en: "Active" }
|
||||
position: "sidebar:Veröffentlichung"
|
||||
position: "sidebar:publishing"
|
||||
- name: name
|
||||
type: string
|
||||
meta:
|
||||
@@ -492,7 +562,7 @@ fields:
|
||||
type: file
|
||||
meta:
|
||||
label: { de: "Produktbild", en: "Product Image" }
|
||||
widget: medialib
|
||||
widget: image
|
||||
downscale:
|
||||
maxWidth: 1200
|
||||
quality: 0.85
|
||||
@@ -500,12 +570,12 @@ fields:
|
||||
type: string
|
||||
meta:
|
||||
label: { de: "SEO Titel", en: "SEO Title" }
|
||||
position: "sidebar:SEO"
|
||||
position: "sidebar:seo"
|
||||
- name: seoDescription
|
||||
type: string
|
||||
meta:
|
||||
label: { de: "SEO Beschreibung", en: "SEO Description" }
|
||||
position: "sidebar:SEO"
|
||||
position: "sidebar:seo"
|
||||
inputProps:
|
||||
multiline: true
|
||||
rows: 3
|
||||
@@ -515,10 +585,8 @@ fields:
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **`meta.label` is i18n** — Always provide `{ de: "...", en: "..." }` objects, not plain strings.
|
||||
- **`views` order matters** — First matching view (by `mediaQuery`) is shown. Put mobile views (with `mediaQuery`) before desktop views (without).
|
||||
- **`meta.label` supports both strings and i18n objects** — Use i18n objects only when the collection or field label must be localized.
|
||||
- **`choices.id` must match stored value** — The `id` in choices is what gets saved to the database.
|
||||
- **`inputProps` depends on widget** — Not all props work with all widgets. Check tibi-admin-nova source if unsure.
|
||||
- **`position: sidebar` without group** — Fields go to an ungrouped area. Use `position: "sidebar:GroupName"` for grouping.
|
||||
- **`type: object[]` needs `subFields`** — Forgetting `subFields` renders an empty repeater.
|
||||
- **hooks path** — Hook includes are relative to `api/` directory: `!include hooks/myfile.js`.
|
||||
|
||||
@@ -19,19 +19,23 @@ Use this skill when:
|
||||
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.
|
||||
2. `App.svelte` reacts to URL changes → calls `getCachedEntries("content", { lang, path, active: true })`.
|
||||
3. The matching `ContentEntry.blocks[]` array is passed to `BlockRenderer.svelte`.
|
||||
4. Each block has a `type` field that maps to a Svelte component.
|
||||
2. Public URLs are typically language-prefixed (`/de/...`, `/en/...`), but the DB entry in `content.path` is stored **without** that language prefix.
|
||||
3. `App.svelte` reacts to URL changes → strips the language prefix → calls `getCachedEntries("content", { lang, path, active: true })`.
|
||||
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.
|
||||
|
||||
**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
|
||||
|
||||
### 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.
|
||||
3. Click **New** and fill in:
|
||||
- `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
|
||||
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
|
||||
|
||||
```sh
|
||||
@@ -74,17 +85,22 @@ To make the page appear in the header/footer menu, edit the corresponding `navig
|
||||
# Get existing header nav
|
||||
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>" \
|
||||
-H "Token: $ADMIN_TOKEN" \
|
||||
-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
|
||||
|
||||
- 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`).
|
||||
|
||||
---
|
||||
@@ -130,6 +146,8 @@ import MyNewBlock from "./MyNewBlock.svelte"
|
||||
<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)
|
||||
|
||||
Edit `types/global.d.ts` — add fields to `ContentBlockEntry`:
|
||||
@@ -156,6 +174,9 @@ Edit `api/collections/content.yml` — add subFields under `blocks`:
|
||||
type: string
|
||||
- name: myItems
|
||||
type: object[]
|
||||
meta:
|
||||
drillDown: true
|
||||
preview: title
|
||||
subFields:
|
||||
- name: title
|
||||
type: string
|
||||
@@ -163,6 +184,14 @@ Edit `api/collections/content.yml` — add subFields under `blocks`:
|
||||
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)
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
| Type | Component | Purpose |
|
||||
@@ -188,7 +224,7 @@ yarn validate # TypeScript check — must be warning-free
|
||||
|
||||
### 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
|
||||
########################################################################
|
||||
@@ -199,17 +235,13 @@ name: mycollection
|
||||
meta:
|
||||
label: { de: "Meine Sammlung", en: "My Collection" }
|
||||
muiIcon: category # Material UI icon name
|
||||
rowIdentTpl: { twig: "{{ name }}" } # Row display in admin list
|
||||
|
||||
views:
|
||||
- type: simpleList
|
||||
mediaQuery: "(max-width: 600px)"
|
||||
primaryText: name
|
||||
- type: table
|
||||
columns:
|
||||
- name
|
||||
- source: active
|
||||
filter: true
|
||||
viewHint: table
|
||||
preview:
|
||||
label: name
|
||||
table:
|
||||
- name
|
||||
- source: active
|
||||
label: Active
|
||||
|
||||
permissions:
|
||||
public:
|
||||
@@ -234,9 +266,17 @@ fields:
|
||||
# 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[]`.
|
||||
|
||||
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
|
||||
|
||||
@@ -285,17 +325,30 @@ type EntryTypeSwitch<T extends string> = T extends "medialib"
|
||||
Common hook patterns:
|
||||
|
||||
- **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.
|
||||
- **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:
|
||||
|
||||
```yaml
|
||||
hooks:
|
||||
beforeRead: |
|
||||
!include hooks/filter_public.js
|
||||
afterWrite: |
|
||||
!include hooks/clear_cache.js
|
||||
get:
|
||||
read:
|
||||
type: javascript
|
||||
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)
|
||||
@@ -313,6 +366,14 @@ yarn validate # TypeScript check
|
||||
# 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
|
||||
@@ -322,3 +383,5 @@ yarn validate # TypeScript check
|
||||
- **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.
|
||||
- **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.
|
||||
|
||||
@@ -15,6 +15,7 @@ Use this skill when:
|
||||
- Adding new Svelte 5 reactive patterns
|
||||
- Understanding the API layer and error handling
|
||||
- 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()`.
|
||||
|
||||
### 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
|
||||
|
||||
```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
|
||||
@@ -138,17 +157,17 @@ The project uses **Svelte writable/derived stores** (not a centralized state lib
|
||||
|
||||
### Store inventory
|
||||
|
||||
| Store | File | Purpose |
|
||||
| ---------------------- | ---------------------- | ----------------------------------------------------------------------------------- |
|
||||
| `location` | `lib/store.ts` | Current URL state (path, search, hash, push/pop flags) |
|
||||
| `mobileMenuOpen` | `lib/store.ts` | Whether mobile hamburger menu is open |
|
||||
| `currentContentEntry` | `lib/store.ts` | Currently displayed page's `translationKey`, `lang`, `path` (for language switcher) |
|
||||
| `previousPath` | `lib/store.ts` | Previous URL path (for conditional back buttons) |
|
||||
| `apiBaseOverride` | `lib/store.ts` | Override API base URL (used by admin module) |
|
||||
| `cookieConsentVisible` | `lib/store.ts` | Whether cookie consent banner is showing |
|
||||
| `currentLanguage` | `lib/i18n.ts` | Derived from `$location.path` — current language code |
|
||||
| `selectedLanguage` | `lib/i18n.ts` | Writable — synced with `currentLanguage` on navigation |
|
||||
| `activeRequests` | `lib/requestsStore.ts` | Number of in-flight API requests (drives `LoadingBar`) |
|
||||
| Store | File | Purpose |
|
||||
| ---------------------- | ---------------------- | -------------------------------------------------------------------------------- |
|
||||
| `location` | `lib/store.ts` | Current URL state (path, search, hash, push/pop flags) |
|
||||
| `mobileMenuOpen` | `lib/store.ts` | Whether mobile hamburger menu is open |
|
||||
| `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) |
|
||||
| `apiBaseOverride` | `lib/store.ts` | Override API base URL (used by admin module) |
|
||||
| `cookieConsentVisible` | `lib/store.ts` | Whether cookie consent banner is showing |
|
||||
| `currentLanguage` | `lib/i18n.ts` | Derived from `$location.path` — current language code |
|
||||
| `selectedLanguage` | `lib/i18n.ts` | Writable — synced with `currentLanguage` on navigation |
|
||||
| `activeRequests` | `lib/requestsStore.ts` | Number of in-flight API requests (drives `LoadingBar`) |
|
||||
|
||||
### 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`
|
||||
- **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
|
||||
|
||||
```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`.
|
||||
- **`$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).
|
||||
- **`$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.
|
||||
|
||||
@@ -154,7 +154,7 @@ db.content.findOne({ path: "/" })
|
||||
### Navigation aktualisieren
|
||||
|
||||
```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
|
||||
|
||||
@@ -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.
|
||||
@@ -5,6 +5,8 @@ description: Write and debug server-side hooks for tibi-server (goja Go JS runti
|
||||
|
||||
# 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
|
||||
|
||||
Wrap every hook in an IIFE:
|
||||
@@ -22,6 +24,8 @@ Wrap every hook in an IIFE:
|
||||
|
||||
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
|
||||
|
||||
- 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 }`).
|
||||
|
||||
## 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)
|
||||
|
||||
| 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)`.
|
||||
- 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.
|
||||
|
||||
@@ -13,6 +13,8 @@ Use this skill when:
|
||||
- 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
|
||||
|
||||
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
|
||||
|
||||
- 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.
|
||||
|
||||
## 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 |
|
||||
| -------------------- | ---------------------------------------------- | --------------------------------------------------------- | ------------ |
|
||||
| `__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
|
||||
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/__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:**
|
||||
|
||||
```sh
|
||||
@@ -62,14 +66,14 @@ grep -n '__PROJECT_NAME__\|__TIBI_NAMESPACE__' .env api/config.yml frontend/.hta
|
||||
|
||||
```dotenv
|
||||
PROJECT_NAME=my-project
|
||||
TIBI_NAMESPACE=my_project
|
||||
TIBI_NAMESPACE=my-project
|
||||
CODING_URL=https://my-project.code.testversion.online
|
||||
STAGING_URL=https://dev-my-project.staging.testversion.online
|
||||
```
|
||||
|
||||
### Common mistakes
|
||||
|
||||
- **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 `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`.
|
||||
|
||||
Also verify that SSR still renders meaningful page content and not just the shell after the rewrite.
|
||||
|
||||
## Step 4 — Admin token
|
||||
|
||||
`api/config.yml.env` ships with a default `ADMIN_TOKEN`. For production projects, generate a secure one:
|
||||
|
||||
```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
|
||||
|
||||
```sh
|
||||
yarn install
|
||||
yarn upgrade # Update all deps to latest versions within package.json ranges
|
||||
make docker-up # Start stack in background
|
||||
# or
|
||||
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 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`.
|
||||
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.
|
||||
|
||||
**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.
|
||||
|
||||
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:**
|
||||
|
||||
- **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
|
||||
yarn build # Frontend bundle for modern browsers
|
||||
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)
|
||||
```
|
||||
|
||||
**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.
|
||||
|
||||
@@ -5,13 +5,28 @@ description: Implement and debug server-side rendering with goja (Go JS runtime)
|
||||
|
||||
# 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
|
||||
|
||||
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.
|
||||
3. During rendering, API calls are tracked as **dependencies** (collection + entry ID).
|
||||
4. The rendered HTML + dependencies are stored in the `ssr` collection.
|
||||
5. On the client, `lib/ssr.js` hydrates using `window.__SSR_CACHE__` injected by the server.
|
||||
2. `ssr/get_read.js` loads `lib/app.server.js` and calls `app.default.render({ url })`.
|
||||
3. `frontend/src/ssr.ts` only initializes i18n and delegates rendering to `svelte/server`.
|
||||
4. `frontend/src/App.svelte` owns the actual data loading for both browser and SSR.
|
||||
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
|
||||
|
||||
@@ -20,36 +35,159 @@ yarn build:server
|
||||
```
|
||||
|
||||
- Output: `api/hooks/lib/app.server.js`
|
||||
- Uses `babel.config.server.json` to transform async/await to generators (goja doesn't support async).
|
||||
- Add `--banner:js='// @ts-nocheck'` to suppress type errors in the generated bundle.
|
||||
- The project no longer uses Babel for SSR.
|
||||
- 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
|
||||
|
||||
When content changes, `clear_cache.js` only invalidates SSR entries that depend on the changed collection/entry:
|
||||
|
||||
```js
|
||||
// Each SSR cache entry stores its dependencies:
|
||||
// Each SSR cache entry stores dependency strings:
|
||||
{
|
||||
url: "/some-page",
|
||||
html: "...",
|
||||
dependencies: [
|
||||
{ collection: "content", id: "abc123" },
|
||||
{ collection: "medialib", id: "def456" }
|
||||
]
|
||||
path: "/de/ueber-uns",
|
||||
content: "...",
|
||||
dependencies: ["content:abc123", "navigation:*", "medialib:*"]
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
Route validation in `config.js` controls which paths get SSR treatment. Return:
|
||||
|
||||
- A positive number to enable SSR for that route
|
||||
- `-1` to disable SSR (current default in the starter template)
|
||||
- `1` to render the requested path as-is
|
||||
- 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
|
||||
|
||||
- **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"`.
|
||||
- **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.
|
||||
@@ -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/`.
|
||||
- **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
|
||||
|
||||
- Install deps: `yarn install`
|
||||
- Start dev: `make docker-up && make docker-start`
|
||||
- Start with mock data: set `MOCK=1` in `.env`, then `make docker-up && make docker-start`
|
||||
- Build frontend: `yarn build`
|
||||
- Start dev: `make docker-up` or `make docker-start`
|
||||
- Restart frontend watcher/dev-server: `make docker-restart-frontend`
|
||||
- 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`
|
||||
- Validate types: `yarn validate`
|
||||
|
||||
## 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.
|
||||
- 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`.
|
||||
- **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.
|
||||
- Frontend code is automatically built by watcher and BrowserSync; backend hooks are automatically reloaded on change.
|
||||
- **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 rebuilt by the watcher/BrowserSync stack; backend hooks reload on change.
|
||||
- Read `.env` for environment URLs and secrets.
|
||||
- `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/`.
|
||||
- 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 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 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
|
||||
|
||||
| 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) |
|
||||
| `.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 |
|
||||
| `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
|
||||
|
||||
- **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)
|
||||
|
||||
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 |
|
||||
| --- | --- |
|
||||
| `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 |
|
||||
| `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-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-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
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ fields:
|
||||
blockTypeField: type
|
||||
defaultViewport: desktop
|
||||
blockRegistry:
|
||||
file: /_/assets/dist/admin.mjs
|
||||
file: /_/assets/dist/admin.mjs?v=${ADMIN_ASSET_VERSION}
|
||||
subFields:
|
||||
- name: hide
|
||||
type: boolean
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
ADMIN_TOKEN=5bdfjc78hdxn338cuhSJ
|
||||
ADMIN_ASSET_VERSION=4a604ba-dirty-1778608358546
|
||||
|
||||
+13
-6
@@ -5,20 +5,27 @@ Server-side rendering via goja (Go JS runtime) with HTML caching.
|
||||
## Request flow
|
||||
|
||||
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__`.
|
||||
3. On the client, `lib/ssr.js` hydrates using the injected cache data.
|
||||
4. Rendered HTML is stored in the `ssr` collection with dependency tracking.
|
||||
2. `get_read.js` loads `lib/app.server.js` and calls `app.default.render({ url })`.
|
||||
3. `frontend/src/ssr.ts` stays thin and only initializes locale state before rendering `App.svelte`.
|
||||
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
|
||||
|
||||
- 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
|
||||
|
||||
- `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
|
||||
|
||||
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.
|
||||
|
||||
@@ -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
@@ -2,6 +2,36 @@ const fs = require("fs")
|
||||
const { execSync } = require("child_process")
|
||||
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"
|
||||
let gitHash = "dev"
|
||||
try {
|
||||
@@ -15,16 +45,18 @@ function writeBuildInfo() {
|
||||
const info = {
|
||||
gitHash,
|
||||
buildTime: new Date().toISOString(),
|
||||
assetVersion: `${gitHash}-${Date.now()}`,
|
||||
}
|
||||
fs.writeFileSync(
|
||||
__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)
|
||||
fs.writeFileSync(
|
||||
__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.
|
||||
// It is called by esbuild-wrapper.js before each build.
|
||||
|
||||
Reference in New Issue
Block a user