diff --git a/.agents/skills/admin-ui-config/SKILL.md b/.agents/skills/admin-ui-config/SKILL.md index 531f9ab..36d5dc4 100644 --- a/.agents/skills/admin-ui-config/SKILL.md +++ b/.agents/skills/admin-ui-config/SKILL.md @@ -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`. diff --git a/.agents/skills/content-authoring/SKILL.md b/.agents/skills/content-authoring/SKILL.md index fe65895..263f26c 100644 --- a/.agents/skills/content-authoring/SKILL.md +++ b/.agents/skills/content-authoring/SKILL.md @@ -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/" \ -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": "" } ] }' ``` +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" ``` +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 "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. diff --git a/.agents/skills/frontend-architecture/SKILL.md b/.agents/skills/frontend-architecture/SKILL.md index e929346..c62c4f1 100644 --- a/.agents/skills/frontend-architecture/SKILL.md +++ b/.agents/skills/frontend-architecture/SKILL.md @@ -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