135 Commits

Author SHA1 Message Date
apairon 2d52272b2e feat: enhance search capabilities and indexing across collections 2026-05-17 20:33:22 +00:00
apairon 8cbf0db14f feat: add image filter configuration and update related collections 2026-05-17 18:54:44 +00:00
apairon f407946c19 feat: add comment and tag collections with metadata and mock data 2026-05-17 18:26:26 +00:00
apairon d74964d078 feat: update collection metadata and add tags collection with test data 2026-05-17 17:59:06 +00:00
apairon b0af8ba329 feat: add protected fields to comments and enhance comments aggregation logic 2026-05-17 15:23:48 +00:00
apairon 349fb9b2da feat(ssr-server): enhance lookup and aggregate handling for SSR cache invalidation
- Improved parsing of `lookup` and `aggregate` options to support JSON strings and arrays.
- Added support for object format in `lookup` and `aggregate` to specify collections.
- Simplified dependency tracking for SSR cache invalidation based on new formats.
2026-05-17 15:16:32 +00:00
apairon bd8d413850 feat: unify API options structure and enhance lookup handling across collections 2026-05-17 14:53:52 +00:00
apairon f332c707b7 feat: implement new feature for enhanced user experience 2026-05-17 14:19:45 +00:00
apairon db968ab318 feat: optimize SSR cache dependencies and improve API query precision 2026-05-17 12:30:19 +00:00
apairon 819147f518 feat: enhance validation rules and improve content structure across collections 2026-05-17 12:25:28 +00:00
apairon 4020ad62c5 feat: enhance medialib image handling and add asset URL resolution
- Implemented `resolveApiAssetUrl` function to normalize asset URLs based on API base.
- Updated `MedialibImage` component to utilize new asset URL resolution and added support for alt text and class properties.
- Enhanced image loading behavior with improved width measurement and focal point handling.
- Added placeholder image handling and improved accessibility with alt text.
- Introduced new test script for auditing broken links in skill documentation.
- Expanded seeded test content to include medialib entries and updated related tests for pagebuilder previews.
- Improved global setup and teardown logging for clarity on seeded content management.
2026-05-17 00:52:41 +00:00
apairon 958b45272d feat: enhance admin UI configuration and SSR handling
- Add support for number chip arrays and JSON editor in admin UI config.
- Introduce pagebuilder block registry for Svelte components in admin previews.
- Implement custom role names and a 3-layer cascade model for field-level permissions.
- Add CORS configuration hierarchy for better API security.
- Update project setup instructions for admin token and config management.
- Improve SSR 404 signaling with proper context handling in NotFound component.
- Refactor routing structure to separate NotFound page into its own route.
2026-05-12 23:20:31 +00:00
apairon 60d5920132 feat: add navigation and media library tests with structured controls and visibility checks 2026-05-12 22:43:27 +00:00
apairon 53ad012657 feat: add admin smoke tests and enhance testing documentation with new strategies and configurations 2026-05-12 21:01:25 +00:00
apairon c058ec760f feat: add playwright-testing skill documentation and enhance AGENTS.md with testing references 2026-05-12 20:42:15 +00:00
apairon 1b24bb2157 feat: enhance admin API helpers with CRUD operations for collections and seed data management
- Added functions for creating, updating, deleting, and listing collection entries in admin API.
- Introduced seed data management for consistent test content across tests.
- Updated global setup and teardown processes to ensure seeded content is created and cleaned up.
- Refactored existing tests to utilize seeded content for improved reliability and maintainability.
2026-05-12 20:36:06 +00:00
apairon 491f495c66 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.
2026-05-12 20:01:22 +00:00
apairon 4a604bab0b refactor: streamline SSR setup and remove Babel configuration
- Updated import path for app.server module in SSR hook.
- Removed babel.config.server.json as Babel is no longer needed for async/await transformation.
- Adjusted esbuild configuration to target ESNext and modified output paths.
- Enhanced App.svelte to handle initial content loading during SSR.
- Updated SSR script to load messages synchronously before rendering.
- Simplified build:server script in package.json by removing Babel step.
2026-05-12 15:47:53 +00:00
apairon e84b87ed16 feat: enhance accessibility with skip to main content button and improve navigation handling
🔧 fix: update navigation href resolution to include localized paths

🆕 feat: add new FeatureIcon component for feature boxes

🎨 style: improve styling for prose elements in richtext blocks

🛠️ refactor: streamline medialib image loading and caching logic

📦 chore: update mock data handling to support new medialib entries

🔄 chore: synchronize i18n initialization and locale management

📝 docs: update video tour descriptions to reflect recent changes
2026-05-12 13:55:32 +00:00
apairon 8fb26fdeba feat: upgrade http-proxy-middleware to version 4.0.0 and update related dependencies 2026-05-11 17:05:39 +00:00
apairon 106efb5d6e feat: implement new feature for enhanced user experience 2026-05-11 16:56:57 +00:00
apairon 0be4852f74 feat: add console error monitoring for Playwright tests; enhance page fixture with error assertions 2026-03-08 15:36:50 +00:00
apairon a9a13a6b5b feat: add admin-ui-config, content-authoring, and frontend-architecture skills documentation
- Introduced `admin-ui-config` skill for configuring admin UI for collections.
- Added `content-authoring` skill detailing page and block creation in the CMS.
- Included `frontend-architecture` skill explaining custom SPA routing and state management.
- Updated `AGENTS.md` to reference new skills and provide infrastructure prerequisites.
- Enhanced `frontend/AGENTS.md` with routing details and SPA navigation information.
2026-03-07 16:16:19 +00:00
apairon 18b5af5617 feat: enhance HeroBlock with SPA navigation for anchor links and update feature card styles 2026-02-27 13:58:46 +00:00
apairon d1ef9800f1 🔧 fix: enhance watch mode to reload browser on build completion 2026-02-27 13:29:44 +00:00
apairon 2170bf761e feat: add project setup skill documentation; provide step-by-step guide for initializing new tibi projects 2026-02-26 13:02:02 +00:00
apairon 5707eb30dd feat: update deployment scripts and configuration; enhance CI/CD process with new scripts for staging and production 2026-02-26 12:36:53 +00:00
apairon 965a505e15 feat: enhance SSR support with language extraction, dynamic page titles, and updated styles; adjust color theme 2026-02-26 11:09:42 +00:00
apairon 40ffa8207e feat: add new contact form, hero, features, and richtext blocks; implement scroll-reveal action and update styles
- Introduced ContactFormBlock, FeaturesBlock, HeroBlock, and RichtextBlock components.
- Implemented a scroll-reveal action for animations on element visibility.
- Enhanced CSS styles for better theming and prose formatting.
- Added localization support for new components and updated existing translations.
- Created e2e tests for demo pages including contact form validation and navigation.
- Added a video tour showcasing the demo pages and interactions.
2026-02-26 03:54:07 +00:00
apairon e8fd38e98a feat: add video tour functionality with helpers, configuration, and homepage walkthrough 2026-02-26 02:42:16 +00:00
apairon 20eaa50935 feat: implement mock data support with API interceptor and update documentation 2026-02-26 02:37:01 +00:00
apairon 30501f5f4c feat: add SKILL documentation for Gitea issue attachments, tibi hook authoring, and SSR caching 2026-02-25 21:18:12 +00:00
apairon 3c3e70b474 🗑️ chore: remove outdated instructions and migration documentation 2026-02-25 20:45:46 +00:00
apairon 602fd6101f feat: Add new input, select, and tooltip components with validation and accessibility features
- Introduced Input component with support for various input types, validation, and error handling.
- Added MedialibImage component for displaying images with lazy loading and caption support.
- Implemented Pagination component for navigating through pages with ellipsis for large page sets.
- Created SearchableSelect component allowing users to search and select options from a dropdown.
- Developed Select component with integrated styling and validation.
- Added Tooltip component for displaying additional information on hover/focus.
2026-02-25 20:15:23 +00:00
apairon 74bb860d4f feat: refine type definitions and improve request handling in API layer 2026-02-25 17:44:49 +00:00
apairon 3b84e49383 feat: enhance SSR cache management with dependency tracking and entry-level invalidation 2026-02-25 17:35:10 +00:00
apairon 3886eb9f34 feat: implement new feature for enhanced user experience 2026-02-25 16:50:10 +00:00
apairon b41d12f257 feat: implement new API layer with request deduplication, caching, and Sentry integration 2026-02-25 16:48:37 +00:00
apairon fdeeac88e2 feat: add loading bar and toast notification system with responsive design 2026-02-25 16:30:45 +00:00
apairon e13e696253 feat: implement build version check and update build info handling 2026-02-25 15:53:00 +00:00
apairon f6f565bbcb feat: add Svelte actions and global stores for enhanced functionality 2026-02-25 13:10:52 +00:00
apairon dc00d24899 feat: implement new feature for enhanced user experience 2026-02-11 16:36:56 +00:00
apairon 62f1906276 🔧 fix: update Traefik router rule for MCP/curl access to include host condition
 feat: enable sending default PII in Sentry initialization
2026-02-11 14:25:26 +00:00
apairon b9a455d1b9 yarn upgrade 2026-02-11 13:06:05 +00:00
apairon 18d5e977e5 🔧 fix: update API path in docker-compose-staging.yml for correct endpoint configuration 2025-10-30 09:44:17 +00:00
apairon ae39987c7d 🔧 fix: update spa.html handling to ensure symlink removal and prevent errors 2025-10-30 09:36:53 +00:00
apairon 4893d925c5 🔧 fix: comment out unused SSR path validation logic and update collection check 2025-10-30 09:32:06 +00:00
apairon 66225b731a feat: enhance deployment workflow with reload functionality and update SSR cache handling 2025-10-30 09:27:23 +00:00
apairon 50b6f4a6e5 feat: add initial webserver setup with Express and proxy middleware 2025-10-30 09:16:07 +00:00
apairon 2025a0a71f 🔧 fix: update template handling and svelte compiler options for improved build process 2025-10-30 09:11:44 +00:00
apairon 1ae34d6a18 feat: add Copilot instructions and enhance Docker Compose configuration for improved routing 2025-10-30 08:14:44 +00:00
apairon 4756eab175 🔧 fix: reorder plugins in esbuild configuration for proper execution 2025-10-30 08:02:59 +00:00
apairon 55263a49be fix: update build context for tibiserver-dev service in docker-compose 2025-08-26 09:20:30 +00:00
apairon 39caf6f7d6 Refactor code structure for improved readability and maintainability 2025-07-03 11:37:21 +00:00
apairon 037b3d5a89 feat: add Tailwind CSS and PostCSS configuration
- Created postcss.config.js to configure PostCSS with Tailwind CSS and Autoprefixer.
- Updated svelte.config.js to enable PostCSS preprocessing.
- Added tailwind.config.js for Tailwind CSS configuration.
- Updated yarn.lock to include new dependencies for Tailwind CSS, PostCSS, and related plugins.
2025-07-03 11:32:23 +00:00
apairon 4bbbfc5fee chore: disable ghostMode in esbuild configuration 2025-07-01 12:53:12 +00:00
apairon b4204da0a4 yarn upgrade 2025-07-01 12:50:13 +00:00
apairon ffcded42f3 Aktualisiere Metriken-Logik in App.svelte zur Verwendung von $effect und verbessere die Lesbarkeit 2025-03-27 17:32:55 +00:00
apairon a72780873a Aktualisiere Konfigurationen und entferne nicht benötigte Skripte aus der Umgebung 2025-03-27 17:23:39 +00:00
apairon f66c1fc078 Füge Initialisierungsziel zu Makefile hinzu und aktualisiere Docker-Befehle 2025-03-27 17:07:33 +00:00
apairon cf1acc1d80 Aktualisiere Docker-Images auf die neuesten Versionen und passe die Pfade in der esbuild-Konfiguration an 2025-03-27 16:59:52 +00:00
apairon 7a6a2cbd22 Füge Docker- und Babel-Konfigurationen hinzu, aktualisiere Svelte- und Esbuild-Setups, erweitere Typdefinitionen und aktualisiere die README-Datei 2025-03-27 13:52:13 +00:00
apairon 77cb64b260 yarn 4 2025-03-27 13:26:28 +00:00
apairon 2037953000 yarn upgrade 2025-03-27 12:34:04 +00:00
apairon 3a6ff3fa8e Füge ein neues Deployment-Workflow-Skript hinzu und entferne veraltete Skripte 2025-03-27 12:12:55 +00:00
apairon 212a9720cf first clean up 2025-03-26 17:44:06 +00:00
apairon 4a8864c7b9 CustomTags 2022-11-18 11:47:44 +00:00
mario 30c05143fe Update and rework project structure with new pagebuilder concept. (based on RK Architekten and SFU Politik configs and sources) 2022-11-17 16:01:52 +00:00
apairon 825dfc18f9 yarn upgrade 2022-10-11 14:10:53 +00:00
apairon c8443f4d11 yarn upgrade 2022-09-15 16:02:24 +00:00
apairon ac7eec418c docker:start 2022-09-15 15:56:54 +00:00
mario fef4d3b023 Doppelter Aufruf für das Holen der Page-Contents nach Sprachwechsel gefixed. 2022-07-12 08:45:38 +02:00
mario 1bfa0d8b1b Artikel rendern slug für eventuelle Anker Links. Navigation vereinfacht und Item in eigene Komponente ausgelagert, um später schöner eine Multi-Level Navigation erstellen zu können. Label in Navigation-Collection umbenannt. Home-Page Komponente vereinfacht. Content-Komponente für eventuelles Animated-Ancher-Scrolling erweitert. 2022-07-08 14:44:23 +02:00
mario 345ecb6177 Aktualisieren der auszugebenden Daten-Spalten (fieldviews) in der neuen LinkedEntries Komponente. 2022-07-01 09:08:56 +02:00
mario dbbd7c63ed Update SLUG field in server side post_create and put_update hooks. 2022-06-30 15:00:18 +02:00
mario 49896d6978 Tags Collection nach unten verschoben. Icon für external Collection geändert. Content Collection erweitert um neues linkedEntries field. 2022-06-30 12:19:10 +02:00
mario d8e4f9c902 Konfiguration von Seiten-Verknüpfungen nach Gespräch mit Marc geändert. 2022-06-16 09:48:18 +02:00
mario 706ec88576 Kleiner Fix in Content Komponente - Hinzufügen des End-Slash (/) in geändertem Pfad nach Sprach-Wechsel. 2022-06-15 14:18:07 +02:00
mario 0d06a61c7f Typ Select für Artikel hinzugefügt. Artikel-Details Komponenten sind nun für jeden Typ verfügbar, wenn benötigt. 2022-06-15 13:06:43 +02:00
mario 652f15830d Möglichkeit, in die Artikel-Details zu springen implementiert, ohne einen extra URL Pfad zu benötigen. 2022-06-15 11:23:56 +02:00
mario 65e8b7ffc8 Verlinkung zu externen URLs für Article hinzugefügt. 2022-06-15 08:57:51 +02:00
mario 25a39dfac0 Meta Angaben für Content-Seiten hinzugefügt. 2022-06-15 08:40:12 +02:00
mario ee7ed0db1f Zuweisung von Artikeln zu Seiten möglich. Somit werden keine Content-Blocks mehr benötigt und alle Artikel können über die articles-Collection gepflegt udn zugewiesen werden. 2022-06-14 10:30:40 +02:00
Mario Linz 05fe698af6 Neue Collection für eine einfache Listung von Dateien mit generierung der externen URL. 2022-06-13 18:59:59 +02:00
Mario Linz 2737872396 Activate git lfs. New test collection for external content. 2022-06-13 15:32:05 +02:00
mario 7c712ee7c8 neue Komponente für Artikellisten. Möglichkeit, Artikel Seiten zuzuweisen. 2022-06-13 13:58:04 +02:00
mario 60bd5d21c9 ArticleDetails page vorbereitet. 2022-06-09 15:52:58 +02:00
mario ddd236af06 Vereinheitlichen der API Parameter für Articles und Content Api Methoden. Interfaces etwas angepasst. 2022-06-09 15:06:16 +02:00
mario 9cf3a814e3 Article-Collection etwas aufgeräumt und in einzelne Files ausgelagert. 2022-06-09 14:24:32 +02:00
mario de6968f3d8 Generelle Angaben zum Veröffentlichungs-Datum und dessen Check-Interval werden nun berücksichtigt auf der Seite mit abgefragt. 2022-06-09 10:57:55 +02:00
mario 4824effccb Neue Komponente für Article-Media-Image sowie eine allgemeine Image Komponente. Article-Komponente WorkInProgress. 2022-06-09 08:23:39 +02:00
mario d872767845 Update der starter collections. 2022-06-03 14:43:59 +02:00
mario 432d21daeb Redirekt auf HOME, wenn keine Contentseite gefunden wurde. 2022-06-03 11:12:35 +02:00
mario cabaaef456 Content Page überarbeitet. Inhalte werden nun auch erneut geholt, wenn sich die URL durch die History ändert. Aktuell statische Texte sind in localization files für en und de ausgelagert. 2022-06-02 11:04:37 +02:00
mario aaf2860714 svelte-i18n dem Projekt hinzugefügt für die Möglichkeit, statische Texte sauberer an einer zentralen Stelle zu pflegen. Über den Language-Chooser wird die aktuelle locale der Übersetzungen auch gleich mit umgeschaltet. 2022-06-01 08:02:50 +02:00
mario 87aa1689f3 Content-Seiten und Language Chooser + Collections so überarbeitet, dass ein Umschalten zwischen Sprachen und Pages möglich ist. Collection der Seiten wurde um eine Priorität erweitert. Navigation zeigt aktives Item an. Entsprechende CSS Klassen angepasst und für neue Projekte sauberer strukturiert. 2022-05-31 15:55:35 +02:00
mario fcf5490d5a Content Collection um Tags ChipArray erweitert, um eine Möglichkeit zu schaffen, Stichworte zum Verknüpfen der Seiten zwischen den Sprachen zu schaffen...(WIP) 2022-05-30 16:52:12 +02:00
mario 5d08a96327 Kleinere Anpassungen für Seiten- und Navigations- Bearbeitung. 2022-05-30 15:57:11 +02:00
mario c67f712280 Collection für Content angepasst. Image Components erweitert. Verschiedene CSS Apassungen der Demo Page 2022-05-30 13:11:51 +02:00
Mario Linz a2bd10453b Neue -tablist- Property für Collectons. 2022-05-29 22:21:03 +02:00
Mario Linz de7ddc1097 Collection general > tabs config angepasst. Referenzen werden nun über die subFields Property definiert. (mehr flexibel) 2022-05-27 23:12:27 +02:00
mario 9188148fe7 Navigation und Content-Pages so angepasst, dass über einen Language-Chooser die Sprache der Seite gewechselt werden kann und automatisch auch der reload der Content-Page angestoßen wird. 2022-05-25 15:37:15 +02:00
mario 516c35dcb4 Weitere Verbesserungen des Starter Projekts für neue Projekte. 2022-05-25 12:10:29 +02:00
mario 47fdee2396 Starter Projekt angefangen, etwas aufzubohren und ein paar grundlegend benötigte Collections, Teheming-Styles und Komponenten hinzugefügt. (WIP) 2022-05-24 16:44:55 +02:00
mario f4b6bb17ca Möglichkeit, einen Typ für eine ViewColumn anzugeben. Dieser Typ überschreibt den Typ des Fields. 2022-05-24 11:33:23 +02:00
mario 87fd3c6148 Neue Tabs beispielhaft in general Collection konfiguriert. 2022-05-24 10:25:56 +02:00
mario d0c6ad4092 Neue Collections für Navigations und Sprachen angelegt. MediaLib erstmal aus Config entfernt. Unbenutzte Files entfernt. 2022-05-16 08:53:08 +02:00
mario eedb794251 Beispiel Collection der Media-Library committet. 2022-05-02 10:39:13 +02:00
apairon c1894528b7 Merge branch 'master' of ssh://gitbase.de:2222/cms/tibi-svelte-starter 2022-04-26 11:15:53 +02:00
apairon 09a7688e29 yarn 2, package upgrade 2022-04-26 11:14:37 +02:00
mario 75534213e8 Erste Collections für eine Media-Library. Weitere Collections für spätere neue Projekte hinzugefügt. (alles WorkInProgress) 2022-04-14 12:23:21 +02:00
apairon fd613e5a7d fixed publishLocation 2022-04-12 17:07:21 +02:00
Mario Linz ef8d571ac5 Prototype Article und Theme Files hinzugefügt 2022-04-01 22:46:54 +02:00
mario 4aec1bd712 XXErste Collections für eine Media-Library. Weitere Collections für spätere neue Projekte hinzugefügt. (alles WorkInProgress) 2022-04-01 14:16:41 +02:00
Mario Linz c00f5a9fb3 Prototyp - Neue allgemeine Collection für Artikel. Durch das Svend-Walter Projekt und ein paaar Gesprächen mit Daniela, was in einem Projekt typischerwise für typische Inhalts-Artikel benötigt wird, ist diese Collection entstanden. 2022-03-20 15:43:08 +01:00
Mario Linz 88c5147363 Collections für neue Projekte optimiert...work in progress... 2022-03-18 21:33:01 +01:00
mario 5caa62eb7e Erste kleine Anpassungen am Tibi-Svelte-Starter um später mehr Zeit in neuen Projekten zu sparen. Hier werden noch weitere Anpassungen folgen, die grundlegend in den meisten Projekten benötigt werden. 2022-03-17 11:12:06 +01:00
apairon 75a8906d4a modrewrite proxy added 2022-03-14 17:18:18 +01:00
apairon 0ac8817805 cypress tsconfig.json fix 2022-02-26 17:43:34 +01:00
apairon ff7441f3c5 fixed cy:docker: 2022-02-26 11:39:25 +01:00
apairon 5843680e14 renamed to tibi-svelte-starter 2022-02-26 11:09:53 +01:00
apairon 66d8313316 api schema 2022-02-01 19:03:49 +01:00
apairon 24d2aaaa50 ssr 404 2022-01-25 16:21:54 +01:00
apairon 85fd41ce58 fixed ssr 2022-01-19 18:50:58 +01:00
apairon 9c559f7020 upgrade 2021-12-08 12:56:19 +01:00
apairon bd087ae658 fixed secret exploit via ssr code sourcemap 2021-09-14 15:51:05 +02:00
apairon f9fe8fd735 cypress and instanbul 2021-09-14 14:45:47 +02:00
apairon 786fd12f34 browsersync 2021-09-14 13:26:35 +02:00
apairon 95c2950193 sourcemap tests 2021-09-13 18:12:40 +02:00
apairon 0bf64b1031 Merge branch 'master' of ssh://gitbase.de:2222/cms/wmbasic-svelte-starter 2021-08-16 11:08:19 +02:00
apairon f6ac48daab using wmbasic-api-types 2021-08-16 11:07:11 +02:00
apairon a232a0119e „api/hooks/types.d.ts“ ändern 2021-04-29 15:43:03 +02:00
apairon 82903a8029 readme 2021-03-30 17:45:09 +02:00
apairon 0aca310a5e init 2021-03-22 16:54:31 +01:00
apairon 2ee7f650db init 2021-03-22 15:59:05 +01:00
534 changed files with 39965 additions and 2 deletions
+649
View File
@@ -0,0 +1,649 @@
# Build Checklist — Autonomous Website Project
> Navigate this checklist in order when building a complete website project from the starter.
>
> This file is the project-delivery checklist. It is not the maintenance plan for aligning the starter itself against upstream tibi changes or a stronger reference project. For starter-maintenance work, begin with .agents/STARTER_ALIGNMENT_STATUS.md and .agents/STARTER_ALIGNMENT_PLAN.md.
---
## How to use this checklist
For every phase, complete all five parts:
1. Required skills
2. Required project artifacts
3. Implementation checks
4. Validation commands
5. Exit criteria
Do not mark a phase done only because code exists. A phase is done only when its outputs and validations are both complete.
## Phase 0 — Bootstrap and project registration
**Required skills:** `tibi-project-setup`
**Required project artifacts:**
- `README.md`
- `.env`
- `api/config.yml`
- `api/config.yml.env`
- `api/hooks/config-client.js`
- `frontend/.htaccess` when the deployment path uses the shipped Apache rewrite/proxy file
- `package.json`
- optional operator-owned root `config.yml` for the tibi-server instance when the current stack expects server-level config outside the project config
**Implementation checks:**
- [ ] Replace all starter placeholders in every affected file:
- `.env`: `__PROJECT_NAME__`, `__TIBI_NAMESPACE__`, `__ORG__`, `__PROJECT__`
- `api/config.yml`: `namespace`
- `frontend/.htaccess`: namespace placeholders when Apache rewrite/proxy is actually used
- `api/hooks/config-client.js`: project placeholder
- [ ] Replace or remove visible starter identity leftovers in `README.md`, `package.json`, and other project-facing metadata.
- [ ] Set all required project identity and URL values in `.env`:
- `PROJECT_NAME`
- `TIBI_NAMESPACE`
- `LIVE_URL`
- `CODING_URL`
- `STAGING_URL`
- `CODING_TIBIADMIN_URL`
- `CODING_TIBISERVER_URL` only if the current setup exposes a dedicated raw tibi-server host
- [ ] Update `package.json` project metadata such as package name and repository/default starter references.
- [ ] Generate a real `ADMIN_TOKEN` in `api/config.yml.env`.
- [ ] Ensure `ADMIN_ASSET_VERSION` exists in `api/config.yml.env`.
- [ ] Decide explicitly which bootstrap path applies:
- local starter Docker stack from `docker-compose-local.yml` and `Makefile`
- shared or operator-managed tibi-server with explicit server-level config/project registration
- [ ] For the local starter Docker path, confirm the repo is mounted into `/data` and the project serves through the repo-local `api/config.yml`; do not invent a separate root `config.yml` or `/api/v1/project` step.
- [ ] For the shared/external tibi-server path, create the root `config.yml`, register the project, and reload it.
- [ ] Confirm local/dev assumptions for Docker, reverse proxy, and any required basic-auth files only when the current environment actually uses them.
- [ ] If audit logging or transaction-sensitive features are planned, confirm the MongoDB/replica-set prerequisite for the target environment instead of assuming the local Docker setup is enough.
**Validation commands:**
```bash
rg '__[A-Z0-9_]+__' . --glob '*.{yml,js,env,htaccess,json,md,ts,svelte}'
yarn install
make docker-up
curl -I "$CODING_URL"
curl -I "$CODING_TIBIADMIN_URL"
curl -I "$CODING_URL/api/content?limit=1"
if [ -n "${CODING_TIBISERVER_URL:-}" ]; then curl -I "$CODING_TIBISERVER_URL/api/v1/version"; fi
yarn build
yarn build:server
yarn validate
```
**Exit criteria:**
- [ ] No unresolved placeholders remain.
- [ ] The project no longer advertises starter-default identity in visible metadata.
- [ ] The active operator path is explicit and complete.
- [ ] If the current stack requires project registration, that registration succeeded.
- [ ] Build, SSR build, and validate succeed with 0 warnings.
- [ ] Website, admin, and API all respond on the expected URLs.
## Phase 1 — Solution architecture and delivery decisions
**Required skills:** `website-solution-architecture`, `security-hardening-and-token-strategy`
**Required project artifacts:**
- `docs/solution-architecture.md` or `plans/solution-architecture.md`
- optional companion notes such as `docs/permissions.md` or `docs/content-model.md` when the project is large enough to need them
**Implementation checks:**
- [ ] Create one primary architecture document with explicit sections for:
- context and scope
- route and i18n model
- collections and ownership
- navigation and pagebuilder
- forms, actions, jobs, and realtime
- SSR, publication, and cache behavior
- permissions and integrations
- decision matrix
- [ ] Document the content model:
- page types
- reusable entities
- singleton/config collections
- page-local vs reusable data
- [ ] Document the route model:
- language prefix strategy
- `content.path` expectations
- route translations
- alias/canonical handling
- [ ] Document the i18n strategy explicitly:
- single-language or multilingual
- field-level i18n or entry-level i18n
- default language
- supported languages
- localized slug/path strategy
- [ ] Document forms and workflows:
- which features are CRUD collections
- which features are actions
- whether jobs or realtime are required
- whether persistence is required
- [ ] Document SSR requirements:
- which routes must SSR
- which collections are page-critical
- whether publication windows exist
- [ ] Document permissions strategy:
- human roles
- token-based integrations
- hidden/readonly needs
- [ ] Make an explicit decision whether the project is single-tenant or needs org/team support.
- [ ] Make an explicit decision whether AI/editor assistance, classic search, or embeddings/search are in scope.
- [ ] Record a searchable yes/no decision matrix for at least:
- single-language vs multilingual
- field-level vs entry-level i18n
- single-tenant vs org/team
- AI/editor assistance
- classic search or embeddings
- actions/jobs/realtime usage
- publication scheduling
**Validation commands:**
```bash
arch_doc=$(test -f docs/solution-architecture.md && echo docs/solution-architecture.md || echo plans/solution-architecture.md)
test -f "$arch_doc"
rg 'Route and i18n model|Collections and ownership|Navigation and pagebuilder|SSR, publication, and cache behavior|Permissions and integrations|Decision matrix' "$arch_doc"
rg 'single-language|multilingual|field-level i18n|entry-level i18n' "$arch_doc"
rg 'single-tenant|org/team|AI|search|embedding|actions|jobs|realtime' "$arch_doc"
```
**Exit criteria:**
- [ ] A later agent can open the architecture document and answer the route, content, SSR, permissions, and i18n questions without guessing.
- [ ] The architecture document contains explicit recorded choices for single-language vs multilingual and field-level vs entry-level i18n.
- [ ] The project has an explicit yes/no decision recorded for org/team support, AI/editor assistance, and search/embeddings scope.
## Phase 2 — Collection model and Nova admin ergonomics
**Required skills:** `content-authoring`, `admin-ui-config`, `nova-pagebuilder-modeling`, `nova-navigation-modeling`, `media-seo-publishing`
**Required project artifacts:**
- `api/config.yml`
- `api/collections/content.yml`
- `api/collections/navigation.yml`
- `api/collections/medialib.yml`
- domain collection YAML files
**Implementation checks:**
- [ ] Create or update all required collections in `api/collections/`.
- [ ] Include every collection in `api/config.yml`.
- [ ] Configure meaningful `meta.preview` for each collection.
- [ ] Configure the right `meta.viewHint` for each collection.
- [ ] Configure usable forms with layout, sidebar groups, drillDown, dependsOn, and widget overrides where needed.
- [ ] Use `pagebuilder` plus `blockRegistry` for block-driven collections.
- [ ] Use readable foreign-key previews instead of raw IDs.
- [ ] Configure field validators, file acceptance, and image constraints.
- [ ] If the project benefits from grouped collection navigation or project-level admin i18n, configure those contracts deliberately instead of leaving them implicit.
- [ ] If a collection is effectively single-document config, use `singleton` deliberately.
**Validation commands:**
```bash
yarn validate
```
Admin validation should also cover:
- collection sidebar labels and icons
- list previews and columns
- entry-form usability
- foreign-reference readability
- pagebuilder block chooser availability
- pagebuilder preview rendering for at least one representative block when pagebuilder is used
**Exit criteria:**
- [ ] Every collection has a clear admin presentation model.
- [ ] Editors can identify and edit entries without raw-ID workflows.
- [ ] Pagebuilder-driven collections have a complete block registry and editable block forms.
## Phase 3 — Type contracts and API typing
**Required skills:** `content-authoring`
**Required project artifacts:**
- `types/global.d.ts`
- `frontend/src/lib/api.ts`
**Implementation checks:**
- [ ] Model all block and domain-entry types in `types/global.d.ts`.
- [ ] Add or update `EntryTypeSwitch` coverage in `frontend/src/lib/api.ts`.
- [ ] When a block or collection participates in public rendering or admin preview, update all affected types in the same change.
- [ ] Keep API and block types aligned with the collection YAML definitions.
- [ ] Avoid type drift between the CMS config and the frontend assumptions.
**Validation commands:**
```bash
yarn validate
```
**Exit criteria:**
- [ ] The current project types describe the collection and block model accurately.
- [ ] Validation passes cleanly.
## Phase 4 — Frontend blocks, routing, and admin registry
**Required skills:** `frontend-architecture`, `nova-pagebuilder-modeling`
**Required project artifacts:**
- `frontend/src/blocks/*`
- `frontend/src/blocks/BlockRenderer.svelte`
- `frontend/src/admin.ts`
- routing/i18n files under `frontend/src/lib/`
**Implementation checks:**
- [ ] Implement every required block component.
- [ ] Register every block in `BlockRenderer.svelte`.
- [ ] Register every pagebuilder block in `frontend/src/admin.ts`.
- [ ] Keep blocks SSR-safe.
- [ ] Ensure the route layer, i18n layer, and content lookup logic agree on the public URL model.
- [ ] If blocks or pages render foreign media/references, confirm lookup expectations explicitly instead of assuming resolved data is present.
- [ ] Treat public rendering and admin-preview rendering as the same contract whenever possible.
**Validation commands:**
```bash
yarn build
yarn validate
```
**Exit criteria:**
- [ ] All configured block types render in the site and in the admin registry.
- [ ] Navigation, i18n, and media references behave correctly in the browser.
## Phase 5 — SSR, publication model, and cache invalidation
**Required skills:** `tibi-ssr-caching`, `tibi-hook-authoring`
**Required project artifacts:**
- `api/hooks/config.js`
- `api/hooks/clear_cache.js`
- SSR-related hook files under `api/hooks/ssr/`
**Implementation checks:**
- [ ] Update `ssrValidatePath()` for the real public route model.
- [ ] Ensure `publishedFilter` matches the actual publication model.
- [ ] Ensure `ssrPublishCheckCollections` covers all time-sensitive collections.
- [ ] Confirm page-critical collections are loaded in an SSR-safe way.
- [ ] Confirm mutations to content, navigation, media, and publication-critical data invalidate SSR as intended.
- [ ] Verify at least one representative mutation path against the SSR response instead of only checking static SSR HTML.
- [ ] If lookups are needed for page-critical references, ensure SSR data loading uses them deliberately.
**Validation commands:**
```bash
yarn build:server
curl "$CODING_TIBISERVER_URL/api/v1/_/<namespace>/ssr?url=/de/..."
curl -I "$CODING_TIBISERVER_URL/api/v1/_/<namespace>/ssr?url=/de/..."
```
Check all of these on the SSR response:
- HTTP status is correct.
- HTML contains the expected page content.
- HTML contains navigation labels.
- `window.__SSR_CACHE__` is present.
- a repeated request can return an SSR cache hit where expected.
- after a representative mutation, the next SSR response reflects the change or expected invalidation behavior.
**Exit criteria:**
- [ ] SSR returns correct HTML for valid routes.
- [ ] Publication and invalidation behavior is verified, not assumed.
## Phase 6 — Hooks, actions, jobs, and realtime
**Required skills:** `tibi-hook-authoring`, `tibi-actions-and-forms`, `scheduled-jobs-and-automation`, `realtime-and-live-workflows`
**Required project artifacts:**
- collection hook files in `api/hooks/`
- action files in `api/actions/` when applicable
- job config/hooks when applicable
**Implementation checks:**
- [ ] Implement public read hooks where public filtering differs from raw CRUD reads.
- [ ] Implement cache invalidation hooks for page-critical mutations.
- [ ] Use actions for endpoint-like workflows instead of fake CRUD collections.
- [ ] For each endpoint-style workflow, record why it is an action and not a collection.
- [ ] Respect current action-hook behavior, especially `context.data` timing.
- [ ] If jobs or realtime are used, document why they belong in the project and what they affect.
- [ ] Register every hook/action/job in the project config.
**Validation commands:**
```bash
yarn validate
```
Add targeted API or Playwright checks for:
- anonymous vs token-backed public filtering
- valid vs invalid action submissions
- cache-clear side effects
- job/realtime behavior when those features exist
**Exit criteria:**
- [ ] Backend behavior matches the modeled workflows.
- [ ] Endpoint-style features are implemented as actions when appropriate.
## Phase 7 — Permissions and security hardening
**Required skills:** `permissions-and-editor-workflows`, `security-hardening-and-token-strategy`
**Required project artifacts:**
- collection/action permission sections in YAML
- security-relevant config in server/project config
- optional `docs/permissions.md` for larger projects
**Implementation checks:**
- [ ] Configure collection permissions for `public`, `user`, and any custom roles.
- [ ] Add explicit token permission sets where machine access is required.
- [ ] Use bulk permissions only when there is a real operational need.
- [ ] Write down representative permission actors and workflow states before finalizing the YAML.
- [ ] Configure `readonlyFields`, `hiddenFields`, and any field-level overrides deliberately.
- [ ] Use eval-based field rules where editorial state transitions require them.
- [ ] Review CORS for real cross-origin requirements instead of weakening it by default.
- [ ] Review login rate limiting and secure-cookie expectations for the target environment.
- [ ] Review risky hook capabilities such as outbound fetch or command execution and document why they are necessary when used.
- [ ] Ensure production secrets come from proper sources rather than committed literals.
**Validation commands:**
```bash
yarn validate
```
Add targeted API checks for:
- public vs authenticated vs token-backed access
- readonly/hidden enforcement on read and write
- eval-based permission behavior for representative entry states
- at least one allowed and one denied write for each important workflow state
**Exit criteria:**
- [ ] Permissions reflect the real editor/integration model.
- [ ] Security-sensitive config and risky capabilities were reviewed explicitly.
## Phase 8 — Audit and compliance readiness
**Required skills:** `audit-and-compliance`, `tibi-hook-authoring`
**Required project artifacts:**
- audit config in the active tibi-server config
- collection-level audit settings when relevant
- `audit.return` hooks where sensitive data must be stripped
**Implementation checks:**
- [ ] Decide whether audit logging is required for the project.
- [ ] If enabled, configure server-level audit settings deliberately.
- [ ] If sensitive fields can land in snapshots, add `audit.return` hooks.
- [ ] If hooks/jobs/actions mutate important collections, account for the resulting audit source semantics.
- [ ] If audit is required in production, confirm retention/TTL expectations with operations.
**Validation commands:**
```bash
curl -H "X-Auth-Token: <jwt-token>" "$CODING_TIBISERVER_URL/api/v1/audit?limit=5"
```
**Exit criteria:**
- [ ] The project has an explicit audit decision: enabled with rules, or deliberately not used.
- [ ] Sensitive audit exposure has been considered, not ignored.
## Phase 9 — Media, SEO, and publication
**Required skills:** `media-seo-publishing`, `nova-ai-editor-features` when AI-assisted media workflows are in scope
**Required project artifacts:**
- `api/collections/medialib.yml`
- SEO/publication fields in content or domain collections
- any image-filter configuration used by the frontend or admin
**Implementation checks:**
- [ ] Configure medialib fields, filters, alt/caption handling, and admin widgets.
- [ ] Treat the shared media widget/helper boundary as canonical for public, SSR, and admin-preview image rendering.
- [ ] Add SEO fields with sensible admin placement.
- [ ] Configure social/share image handling where needed.
- [ ] Configure the publication model explicitly.
- [ ] If publication windows exist, define representative current, future, and expired states.
- [ ] Ensure the chosen publication model matches `publishedFilter`, SSR logic, and editor workflows.
**Validation commands:**
```bash
yarn validate
curl "$CODING_TIBISERVER_URL/api/v1/_/<namespace>/ssr?url=/de/..."
```
Check for:
- expected media URLs
- expected SEO/meta output in SSR HTML
- expected social/share metadata when used
- expected publication visibility behavior
- representative current/future/expired publication states when timing is used
**Exit criteria:**
- [ ] Media and SEO behavior is verified from schema through SSR/public output.
- [ ] Publication state is enforced consistently across schema, public reads, and SSR output.
## Phase 10 — Optional AI, search, and enterprise branches
**Required skills:** `nova-ai-editor-features`, `search-and-embeddings`, `multi-tenancy-and-orgs`
**Required project artifacts:**
- explicit architecture note even when the answer is “not used”
- AI/action config when enabled
- org/team config or rollout notes when enabled
- embedding/search config when enabled
**Implementation checks:**
- [ ] Record whether editor AI is enabled.
- [ ] Record whether embeddings/search are enabled.
- [ ] Record whether org/team support is enabled.
- [ ] Record whether the project is explicitly single-tenant or org/team-aware.
- [ ] If AI is enabled, define provider, model, budget, target fields or action contracts, and failure behavior.
- [ ] If org/team support is enabled, define org visibility, team working rights, project assignment rules, and permission ownership.
- [ ] If search/embeddings are enabled, define provider setup, search mode, index/search contracts, regeneration expectations, and operator ownership.
**Validation commands:**
```bash
yarn validate
```
Add feature-specific checks only if the feature is enabled.
Feature-specific checks can include:
- `curl "$CODING_TIBISERVER_URL/api/v1/_/<namespace>/<collection>?q=...&qName=..."`
- representative org/team visibility or permission checks with the intended auth model
**Exit criteria:**
- [ ] The project has an explicit yes/no decision for AI, search/embeddings, and enterprise org/team support.
- [ ] The project has an explicit single-tenant vs org/team-aware decision.
- [ ] Enabled optional branches have concrete contracts and not just ideas.
## Phase 11 — Operations, deployment, and observability
**Required skills:** `deployment`, `monitoring-and-performance`, `mongodb-and-indexes`, `security-hardening-and-token-strategy`
**Required project artifacts:**
- deployment workflow files
- deploy scripts
- environment and secret configuration
- optional monitoring/operations notes
**Implementation checks:**
- [ ] Configure staging and production deployment paths and URLs.
- [ ] Configure CI or other deployment automation.
- [ ] Confirm admin reload and SSR cache clear behavior on deploy.
- [ ] If Sentry or other observability tooling is used, wire it deliberately.
- [ ] If external operators need OpenAPI or metrics, confirm the requirement and the exposure model.
- [ ] Confirm MongoDB version, replica-set, persistence, and backup assumptions for the target environment.
- [ ] Confirm backup/media persistence assumptions and collection upload paths.
**Validation commands:**
```bash
yarn build
yarn build:server
```
Operator checks when applicable:
- staging deploy works
- production deploy flow is documented
- admin reload works
- SSR cache clear works
- health endpoints and metrics/OpenAPI exposure behave as expected
**Exit criteria:**
- [ ] The project has a concrete deploy path, not just local Docker success.
- [ ] Operational dependencies and visibility expectations are documented.
## Phase 12 — Testing and regression coverage
**Required skills:** `playwright-testing`, `tibi-ssr-caching` when SSR-specific checks are needed
**Required project artifacts:**
- seeded data helpers in `tests/api/helpers/`
- Playwright specs in the appropriate test slice
**Implementation checks:**
- [ ] Use deterministic seed data for content and any other collections needed by tests.
- [ ] Keep seed identity explicit, preferably with hidden `_testdata` markers.
- [ ] Record which seed data was reused or extended for the committed test slice.
- [ ] Add API tests for critical collection and action behavior.
- [ ] Add desktop E2E tests for core public journeys.
- [ ] Add admin smoke tests for stable admin contracts.
- [ ] For pagebuilder-driven projects, include committed admin coverage for block chooser/registry behavior and at least one real preview rendering path.
- [ ] If SSR is critical, include SSR-specific verification through targeted checks or dedicated tests.
- [ ] Record whether SSR proof comes from direct endpoint checks, committed tests, or both.
**Validation commands:**
```bash
npx playwright test tests/api/health.spec.ts --project=api
npx playwright test tests/e2e/home.spec.ts --project=chromium
```
Add affected project/spec commands for the current project, for example:
- admin smoke specs
- pagebuilder preview specs
- action-specific API specs
- mobile or visual slices when relevant
Leave behind repo-local evidence of which commands were actually used for sign-off.
**Exit criteria:**
- [ ] The test suite reflects the real project contracts, not demo content.
- [ ] Critical public, admin, and backend paths have executable regression coverage.
- [ ] A later agent can see which specs and SSR checks were used for sign-off.
## Phase 13 — Video tours (optional)
**Required skills:** none mandatory
**Required project artifacts:**
- tour files in `video-tours/tours/` when the project uses them
**Implementation checks:**
- [ ] Add or update tour scripts for the intended training/demo flows.
- [ ] Keep desktop and mobile variants aligned when both exist.
**Validation commands:**
```bash
yarn tour
yarn tour:mobile
```
**Exit criteria:**
- [ ] Tours run successfully when the project uses them.
## Phase 14 — Final verification and production readiness
**Required skills:** `tibi-project-setup`, `playwright-testing`
**Required project artifacts:**
- all previously generated project artifacts
- any remaining project-polish assets
**Implementation checks:**
- [ ] Build and validation are clean.
- [ ] Placeholder scan is clean.
- [ ] Public site loads.
- [ ] Nova admin loads.
- [ ] Pages are creatable and editable.
- [ ] SSR renders real content.
- [ ] Forms/actions work.
- [ ] Project polish is complete when the project is meant for real delivery:
- project image in `api/config.yml` points to a real project-specific asset
- prefer a fresh project screenshot captured via Playwright MCP over placeholders or generic graphics
- resize/compress the chosen project image to a small admin-friendly file size before wiring `meta.imageUrl`
- replace starter or temporary collection icons with project-specific icons
- use thematically fitting free Unsplash/stock images where real customer/project imagery is not yet available; avoid irrelevant placeholders
- [ ] Any intentionally deferred items are documented.
**Validation commands:**
```bash
yarn build
yarn build:server
yarn validate
rg '__[A-Z0-9_]+__' . --glob '*.{yml,js,env,htaccess,json,ts,svelte}'
```
Add the project's affected Playwright command set and any deploy/operator checks required for sign-off.
**Exit criteria:**
- [ ] The project is buildable, testable, and operable.
- [ ] No critical checklist phase was skipped implicitly.
- [ ] A later agent can continue from the project repository without reconstructing hidden assumptions.
+783
View File
@@ -0,0 +1,783 @@
---
name: admin-ui-config
description: Configure the admin UI for collections and project-level Nova behavior — meta labels, preview/viewHint, sidebar layout, collectionGroups, i18n, field widgets, foreign references, and image handling. Use when setting up or customizing admin views.
---
# admin-ui-config
## When to use this skill
Use this skill when:
- Configuring how a collection appears in the tibi-admin UI
- 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
- Customizing the admin module (`frontend/src/admin.ts`)
## Reference source
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.
---
## Project-level admin contracts
Not every important Nova contract lives in a collection YAML file. Some of the most important admin behaviors are configured at project level in `api/config.yml` under `meta:`.
Current starter example:
```yaml
meta:
imageUrl:
eval: "$projectBase + '_/assets/img/admin-pic.svg'"
i18n:
defaultLanguage: de
languages:
- code: de
label: Deutsch
- code: en
label: English
collectionGroups:
- name: content
label: { de: "Inhalte", en: "Content" }
icon: article
- name: media
label: { de: "Medien", en: "Media" }
icon: image_multiple
```
Treat these as part of the admin design, not as optional polish:
- `meta.imageUrl` — project card/preview imagery in the admin
- `meta.i18n` — project-wide language model for field-level and entry-level translation workflows
- `meta.collectionGroups` — ordered collection groups for the sidebar
Important rule:
- collection-level `meta.group` must reference one of the project-level `meta.collectionGroups[].name` values if the collection should appear inside an explicit group
If project-level `meta.i18n` is missing or inconsistent, even well-modeled collections can become confusing in Nova.
---
## Collection meta configuration
The `meta` key in a collection YAML controls how the collection appears in the admin sidebar and collection/list UI.
```yaml
name: mycollection
meta:
label: { de: "Produkte", en: "Products" } # Sidebar label (i18n)
muiIcon: shopping_cart # Material UI icon name
group: shop # Group in admin sidebar
singleton:
enabled: false
hide: false # Set to true to hide the collection for non-admin users
preview:
label: name
secondary: price
```
### Preview
Use `meta.preview` as the universal entry representation for Nova lists, breadcrumbs, foreign-key widgets, and search result previews:
```yaml
preview: name
preview:
label: name
secondary: slug
badge: status
preview:
eval: "`${$this.firstName} ${$this.lastName}`"
```
## List presentation
For current Nova, use `meta.viewHint` plus `meta.preview` for collection/list presentation.
```yaml
meta:
viewHint: table
preview:
label: name
secondary: slug
badge: status
table:
- name
- source: status
label: Status
- source: author.name
label: Author
select:
- author.name
```
- `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.
### Sub-navigation tabs
Use `meta.subNavigation` when one collection needs multiple curated views in the admin without splitting into multiple collections.
```yaml
meta:
subNavigation:
- name: pages
label: { de: "Seiten", en: "Pages" }
muiIcon: article
filter:
type: page
defaultSort:
field: insertTime
order: DESC
setDefault:
field: type
value: page
- name: news
label: { de: "News", en: "News" }
muiIcon: feed
filter:
type: news
setDefault:
field: type
value: news
```
Use sub-navigation when:
- one collection has several stable editorial slices
- the underlying schema is still shared enough to stay one collection
- authors benefit from filtered entry views and sensible defaults
Do not use sub-navigation to hide a bad collection model. If the workflows truly diverge, split the collection instead.
---
## Field configuration
Each field in the `fields` array can have a `meta` section controlling its admin UI behavior.
### Basic field with meta
```yaml
fields:
- name: name
type: string
meta:
label: { de: "Name", en: "Name" }
helperText: { de: "Anzeigename", en: "Display name" }
position: main # "main" (default) or "sidebar"
```
### Field types
| YAML `type` | Admin widget (default) | Notes |
| ----------- | ---------------------- | --------------------------------------------- |
| `string` | Text input | Use `inputProps.multiline: true` for textarea |
| `number` | Number input | |
| `number[]` | Number chip array | Multiple numeric values |
| `boolean` | Toggle/checkbox | |
| `date` | Date picker | |
| `object` | Nested field group | Requires `subFields` |
| `object[]` | Repeatable group | Requires `subFields`, drag-to-reorder |
| `string[]` | Tag input | |
| `file` | File upload | |
| `file[]` | Multi-file upload | |
| `any` | JSON editor | For mixed/arbitrary data |
### inputProps — widget customization
`inputProps` passes props directly to the field widget:
```yaml
# Multiline text (textarea)
- name: description
type: string
meta:
label: { de: "Beschreibung", en: "Description" }
inputProps:
multiline: true
rows: 5
# Number with min/max
- name: price
type: number
meta:
inputProps:
min: 0
max: 99999
step: 0.01
# Placeholder text
- name: email
type: string
meta:
inputProps:
placeholder: "name@example.com"
```
### Widget override
Override the default widget with `meta.widget`:
```yaml
- name: content
type: string
meta:
widget: richtext # Rich text editor (HTML)
- name: heroImage
type: file
meta:
widget: image # Image-focused file widget
- name: relatedPages
type: string[]
meta:
widget: foreignKeyChipArray
```
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
Static choices:
```yaml
- name: type
type: string
meta:
label: { de: "Typ", en: "Type" }
choices:
- id: page
name: { de: "Seite", en: "Page" }
- id: blog
name: { de: "Blog", en: "Blog" }
- id: product
name: { de: "Produkt", en: "Product" }
```
Dynamic choices from API:
```yaml
- name: category
type: string
meta:
choices:
endpoint: categories # Collection name
mapping:
id: id
name: name
```
### Foreign references
Link to entries in another collection:
```yaml
- name: author
type: string
meta:
label: { de: "Autor", en: "Author" }
foreign:
collection: users
id: id
sort: name
projection: name,email
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: image
downscale: # Auto-resize on upload
maxWidth: 1920
maxHeight: 1080
quality: 0.85
imageEditor: true # Enable crop/rotate editor
```
This field config controls the editor widget, not the filesystem target. Configure file storage once at collection level via top-level `uploadPath` (for this starter typically `../media/<collection>`), not on the individual file field.
---
## Layout: position, sections, sidebar
### Sidebar placement
```yaml
- name: active
type: boolean
meta:
position: sidebar # Moves field to sidebar
- name: publishDate
type: date
meta:
position: "sidebar:publishing" # Sidebar with group key
```
### Sidebar groups (ordered)
Define sidebar group order in collection meta:
```yaml
meta:
sidebar:
- 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
```yaml
- name: seoTitle
type: string
meta:
section: SEO # Groups fields under a section header
- name: seoDescription
type: string
meta:
section: SEO
```
### Grid layout (columns)
Use `containerProps` for multi-column layout:
```yaml
- name: firstName
type: string
meta:
containerProps:
layout:
size: col-6 # Half width (12-column grid)
- name: lastName
type: string
meta:
containerProps:
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
### Object (nested group)
```yaml
- name: address
type: object
meta:
label: { de: "Adresse", en: "Address" }
subFields:
- name: street
type: string
- name: city
type: string
- name: zip
type: string
```
### Object array (repeatable blocks)
```yaml
- name: blocks
type: object[]
meta:
label: { de: "Inhaltsblöcke", en: "Content Blocks" }
widget: pagebuilder
preview: { eval: "`${$this.type}: ${$this.headline || ''}`" }
drillDown: true
subFields:
- name: type
type: string
meta:
choices:
- id: hero
name: Hero
- id: richtext
name: Richtext
- name: headline
type: string
- name: hide
type: boolean
```
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:
```yaml
- name: variants
type: object[]
meta:
drillDown: true # Opens as sub-page instead of inline
```
---
## Admin module (frontend/src/admin.ts)
The `admin.ts` file exports the **pagebuilder block registry** and optional custom Svelte components for the tibi-admin UI. This is how the admin preview renders your Svelte blocks.
### Pagebuilder block registry
The current starter uses `createContentBlockDefinition()` to register each block type. This mounts real Svelte block components into Shadow DOM for admin previews:
```typescript
import { mount, unmount, type Component, type SvelteComponent } from "svelte"
import BlockRenderer from "./blocks/BlockRenderer.svelte"
// Creates a block definition that renders the same Svelte component
// used in the public frontend. The block is mounted inside Shadow DOM
// for style isolation.
function createContentBlockDefinition(presentation: { label: string; icon: string; color: string }) {
return {
css: [previewCssUrl], // CSS files to inject into Shadow DOM
label: presentation.label,
icon: presentation.icon,
color: presentation.color,
previewStyles: {
"background-color": "white",
},
render(container, row, context) {
// Mount the Svelte component inside the admin preview
const target = document.createElement("div")
container.appendChild(target)
let mountedComponent = mount(BlockRenderer as Component<any>, {
target,
props: { blocks: [row], isAdminPreview: true },
})
return {
update(nextRow) {
unmount(mountedComponent)
target.innerHTML = ""
mountedComponent = mount(BlockRenderer as Component<any>, {
target,
props: { blocks: [nextRow], isAdminPreview: true },
})
},
destroy() {
unmount(mountedComponent)
target.remove()
},
}
},
}
}
const blockRegistry = {
hero: createContentBlockDefinition({ label: "Hero", icon: "image", color: "#1d4ed8" }),
richtext: createContentBlockDefinition({ label: "Richtext", icon: "article", color: "#7c3aed" }),
// ... add new blocks here
}
export { blockRegistry }
```
**Key points:**
- Each registry entry wraps the Svelte `BlockRenderer` to render the block in the admin preview.
- The `row` object is the block data (same shape as `ContentBlockEntry`).
- Preview data may contain hydrated `_lookup.<fieldPath>` foreign key data and absolute file URLs — do not prepend `apiBase` or attempt re-fetching.
- The `previewCssUrl` loads the project's `index.css` into Shadow DOM so block styles apply.
- After adding blocks to the registry, run `yarn build` so `frontend/dist/admin.mjs` is regenerated.
### Custom Svelte components (advanced)
For custom dashboard widgets, preview components, or field widgets that require Svelte rendering inside the admin UI, use `getRenderedElement()`:
```typescript
import type { SvelteComponent } from "svelte"
function getRenderedElement(
component: typeof SvelteComponent,
options?: { props: { [key: string]: any }; addCss?: string[] },
nestedElements?: { tagName: string; className?: string }[]
) {
// Creates a Shadow DOM container, mounts the Svelte component inside
}
export { getRenderedElement }
```
### Build
Run `yarn build`. The admin module (`frontend/src/admin.ts`) is compiled into `frontend/dist/admin.mjs` as part of the esbuild build pipeline (the same build produces both `index.mjs` for the SPA and `admin.mjs` for the admin module). tibi-admin-nova loads this module from the project's asset path (`/_/assets/dist/admin.mjs`). The `ADMIN_ASSET_VERSION` from `config.yml.env` is appended as a query parameter for cache busting: `admin.mjs?v=${ADMIN_ASSET_VERSION}`.
---
## Complete collection example
```yaml
name: products
meta:
label: { de: "Produkte", en: "Products" }
muiIcon: inventory_2
group: shop
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:
- group: publishing
label: { de: "Veröffentlichung", en: "Publishing" }
- group: seo
label: { de: "SEO", en: "SEO" }
permissions:
public:
methods:
get: true
user:
methods:
get: true
post: true
put: true
delete: false # usually false for real editorial workflows
fields:
- name: active
type: boolean
meta:
label: { de: "Aktiv", en: "Active" }
position: "sidebar:publishing"
- name: name
type: string
meta:
label: { de: "Name", en: "Name" }
- name: sku
type: string
meta:
label: { de: "Artikelnummer", en: "SKU" }
containerProps:
layout:
size: col-6
- name: price
type: number
meta:
label: { de: "Preis", en: "Price" }
inputProps:
min: 0
step: 0.01
containerProps:
layout:
size: col-6
- name: category
type: string
meta:
label: { de: "Kategorie", en: "Category" }
choices:
- id: electronics
name: { de: "Elektronik", en: "Electronics" }
- id: clothing
name: { de: "Kleidung", en: "Clothing" }
- name: description
type: string
meta:
label: { de: "Beschreibung", en: "Description" }
inputProps:
multiline: true
rows: 4
- name: image
type: file
meta:
label: { de: "Produktbild", en: "Product Image" }
widget: image
downscale:
maxWidth: 1200
quality: 0.85
- name: seoTitle
type: string
meta:
label: { de: "SEO Titel", en: "SEO Title" }
position: "sidebar:seo"
- name: seoDescription
type: string
meta:
label: { de: "SEO Beschreibung", en: "SEO Description" }
position: "sidebar:seo"
inputProps:
multiline: true
rows: 3
```
---
## Indexes and search
For production collections with many entries, consider adding indexes in the YAML:
```yaml
name: products
indexes:
- name: price_sort
key: [price]
- name: category_active
key: [category, -active] # -prefix for descending
- name: slug_unique
key: [slug]
unique: true
```
Search configurations can be added for advanced text/vector search:
```yaml
search:
- name: default
mode: text
fields: [name, description]
```
See `tibi-server/docs/04-collections.md` (sections on indexes and search config) for full reference.
## Checklist-facing verification
For a real project, do not stop after writing the YAML. Validate the authoring contract explicitly.
Minimum review points:
1. project-level `meta.i18n` and `meta.collectionGroups` are coherent
2. each collection has a readable `meta.preview`
3. list views show meaningful columns instead of raw IDs or empty rows
4. foreign references render with readable previews
5. sidebars and `containerProps.layout` produce usable forms
6. pagebuilder collections expose both a working chooser and working preview path
Committed admin Playwright coverage is preferred for stable contracts that should not regress.
## Common pitfalls
- **`meta.label` supports both strings and i18n objects** — Use i18n objects only when the collection or field label must be localized.
- **Project-level admin config is easy to forget** — `collectionGroups` and project-level `meta.i18n` live in `api/config.yml`, not in individual collection files.
- **`meta.group` without a matching project group** — The collection still exists, but the sidebar grouping model becomes inconsistent.
- **`choices.id` must match stored value** — The `id` in choices is what gets saved to the database.
- **`inputProps` depends on widget** — Not all props work with all widgets. Check tibi-admin-nova source if unsure.
- **`position: sidebar` without group** — Fields go to an ungrouped area. Use `position: "sidebar:GroupName"` for grouping.
- **`type: object[]` needs `subFields`** — Forgetting `subFields` renders an empty repeater.
@@ -0,0 +1,206 @@
---
name: audit-and-compliance
description: Configure and verify audit logging for tibi website projects. Covers server-level audit config, collection audit actions, audit.return filtering, TTL/retention, source semantics, and what later agents must check before relying on audit trails.
---
# audit-and-compliance
## When to use this skill
Use this skill when:
- a project needs auditable create/update/delete activity
- operators or stakeholders need a trace of who changed content and how
- hooks, jobs, or actions mutate important collections and that history matters
- sensitive data could land in audit snapshots and must be filtered safely
## Goal
Give later agents a concrete workflow for deciding, configuring, and validating audit logging on this stack.
Audit is not just “turn it on”. A usable audit setup must answer:
- what is logged
- how long it is kept
- who can read it
- which sensitive fields must be stripped
- how hook/job/action side effects appear in the audit trail
## Source of truth
Use these sources when implementing or reviewing audit behavior:
- `tibi-server/docs/10-audit.md`
- `tibi-server/docs/06-hooks.md`
- `tibi-server/docs/11-jobs.md`
- `tibi-server/docs/19-actions.md`
- active server/project config
## Core audit model
Audit requires an explicit server-level decision:
```yaml
audit:
enabled: true
defaultTTL: "720h"
defaultLimit: 50
maxLimit: 10000
```
At collection level, audit is controlled separately:
```yaml
name: content
audit:
enabled: true
actions:
- create
- update
- delete
```
Default audited collection actions are `create`, `update`, and `delete`.
Important:
- `get` can also be audited when needed
- system actions such as `login`, `reload`, and `shutdown` are controlled by server-level audit enablement
- bulk API mutations are still audited even though the internal action naming differs (`bulkCreate`, `bulkUpdate`, `bulkDelete`)
## Source semantics matter
Audit entries are not all equal. The `source.type` tells you where the mutation came from:
- `controller` — direct API CRUD request
- `hook` — database mutation performed from a hook
- `job` — database mutation performed from a scheduled job
- `system` — internal system operation
For website projects this matters because content can change through:
- direct editor CRUD
- action-triggered persistence
- hook-side side effects
- cleanup or synchronization jobs
If a project relies on audit trails for governance or debugging, later agents must understand these source types instead of assuming every change came from an editor UI save.
## Authentication context in audit entries
Audit also records how a request was authenticated.
Relevant fields include:
- `authMethod`
- `tokenLabel`
- `tokenPrefix`
- `userId`
- `username`
- `ip`
Practical implication:
- use labeled admin tokens for operator workflows so audit output stays readable
- do not treat all token-based writes as anonymous noise
## Snapshot exposure and `audit.return`
Audit snapshots can contain more than a normal API read would expose.
That means fields that are stripped by normal read hooks can still appear in audit snapshots unless you filter them explicitly.
Use `audit.return` hooks when a collection may contain:
- passwords
- API keys
- internal secrets
- sensitive operator-only notes
Example shape:
```yaml
hooks:
audit:
return:
type: javascript
file: hooks/users/audit_return.js
```
Use this hook to delete or suppress sensitive `snapshot` and `changes` data before the audit response is returned.
## Retention and limits
Audit is an operator/compliance concern, not just a developer concern.
Decide deliberately:
- whether audit is enabled at all
- how long audit logs should be retained (`defaultTTL`)
- what read limits make sense (`defaultLimit`, `maxLimit`)
- whether the target environment expects short-term diagnostics or longer retention
Do not enable audit and leave retention undefined by habit.
## Recommended patterns for website projects
### Content governance
Recommended shape:
- audit enabled for collections whose changes affect public output or editorial accountability
- clear retention decision with operations
- readable token labels for automation or deploy-related writes
### Hook-heavy workflows
Recommended shape:
- understand which hook-side writes will appear as `source.type: "hook"`
- do not assume audit trails point only to controller actions
- document important side effects when hooks fan out into multiple writes
### Jobs and automation
Recommended shape:
- know that job-side DB mutations appear as `source.type: "job"`
- if job behavior matters operationally, make that visible in the job design and ops notes
### Sensitive collections
Recommended shape:
- explicit `audit.return` filtering
- do not expose raw snapshots just because admins technically can read the endpoint
## Anti-patterns
- enabling audit without deciding retention
- treating audit as equivalent to normal collection reads
- forgetting that hooks and jobs create their own audit source types
- relying on unlabeled tokens for important automated writes
- storing sensitive data in snapshots without `audit.return` filtering
## Verification checklist
After audit-related changes, verify all of these:
1. server-level audit settings are deliberate
2. collection-level audited actions match the actual governance need
3. a representative write produces the expected audit entry
4. hook/job/action side effects produce understandable source metadata
5. sensitive fields are filtered from audit output where required
6. read visibility and retention expectations are documented
## What an LLM should inspect first
When asked to design or review audit behavior on this starter, inspect in this order:
1. `tibi-server/docs/10-audit.md`
2. active server audit config
3. collection-level `audit:` sections
4. any hook/job/action workflow that mutates important collections
5. whether `audit.return` filtering is needed
This prevents “audit enabled” setups that are technically on but operationally weak.
+560
View File
@@ -0,0 +1,560 @@
---
name: content-authoring
description: Add new pages, content blocks, and collections to a tibi project. Covers the content-based routing model, block registration in BlockRenderer and frontend/src/admin.ts, lookup-aware reference modeling, collection YAML authoring, and TypeScript type ownership. Use when creating new pages, block types, or collections.
---
# content-authoring
## When to use this skill
Use this skill when:
- Adding a new page to the website
- Creating a new content block type (e.g. testimonials, pricing table, gallery)
- Adding a new collection to the CMS (e.g. products, events, team members)
- Understanding how content is structured and rendered
## Key concept: content-based routing
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. 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.
## Cross-surface ownership rule
For real project work, treat content authoring as a multi-surface contract.
When you add or change blocks, pages, or collections, check these surfaces together:
1. collection YAML in `api/collections/*.yml`
2. type ownership in `types/global.d.ts`
3. typed API mapping in `frontend/src/lib/api.ts` via `EntryTypeSwitch`
4. public rendering in `frontend/src/blocks/BlockRenderer.svelte`
5. admin pagebuilder preview in `frontend/src/admin.ts`
If one of these surfaces is skipped, the project often still looks half-correct until SSR, admin preview, or typed API usage exposes the mismatch.
---
## Adding a new page
### Option A: Via Admin UI (preferred for content editors)
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")
- `path`: URL path without language prefix (e.g. `/ueber-uns`)
- `lang`: Language code (e.g. `de`)
- `active`: `true`
- `translationKey`: Shared key for cross-language linking (e.g. `about`)
- `blocks`: Add content blocks (see below)
- `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
curl -X POST "$CODING_URL/api/content" \
-H "Token: $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"active": true,
"lang": "de",
"name": "Über uns",
"path": "/ueber-uns",
"translationKey": "about",
"blocks": [
{ "type": "hero", "headline": "Über uns", "subline": "Unser Team" }
],
"meta": { "title": "Über uns", "description": "Erfahre mehr über unser Team." }
}'
```
### Option C: Via mock data (for MOCK=1 mode)
Add the entry to `frontend/mocking/content.json` — the mock engine supports MongoDB-style filtering.
### Adding to navigation
To make the page appear in the header/footer menu, edit the corresponding `navigation` entry:
```sh
# Get existing header nav
curl "$CODING_URL/api/navigation?filter[type]=header&filter[language]=de" -H "Token: $ADMIN_TOKEN"
# 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": "<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 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`).
---
## Adding a new content block type
### Step 1: Create the Svelte component
Create `frontend/src/blocks/MyNewBlock.svelte`:
```svelte
<script lang="ts">
let { block }: { block: ContentBlockEntry } = $props()
</script>
<section class="py-16 sm:py-24" id={block.anchorId || undefined}>
<div class="max-w-6xl mx-auto px-6">
{#if block.headline}
<h2 class="text-3xl font-bold mb-6">{block.headline}</h2>
{/if}
<!-- Block-specific content here -->
</div>
</section>
```
**Conventions:**
- Accept `block: ContentBlockEntry` as the single prop.
- Use `block.anchorId` for scroll anchoring.
- Respect `block.containerWidth` (`""` = default, `"wide"`, `"full"`).
- Guard browser-only code with `typeof window !== "undefined"` (SSR safety).
### Step 2: Register in BlockRenderer
Edit `frontend/src/blocks/BlockRenderer.svelte`:
```svelte
<!-- Add import at the top -->
import MyNewBlock from "./MyNewBlock.svelte"
<!-- Add case in the {#each} block -->
{:else if block.type === "my-new-block"}
<MyNewBlock {block} />
```
If block types become numerous, plan for grouping and registry discipline early. A real website built on this starter should not keep extending a demo-style renderer forever without structure.
### Step 3: Register in the admin block registry
If the block is authored through a pagebuilder field, also register it in `frontend/src/admin.ts`.
Example:
```ts
const blockRegistry = {
hero: createContentBlockDefinition({ label: "Hero", icon: "image", color: "#1d4ed8" }),
"my-new-block": createContentBlockDefinition({
label: "My New Block",
icon: "view_compact",
color: "#0f766e",
}),
}
```
Important:
- `BlockRenderer.svelte` controls public rendering
- `frontend/src/admin.ts` controls Nova pagebuilder preview availability
- both should point at the same block contract instead of drifting into separate preview-only logic
### Step 4: Extend TypeScript types (if new fields are needed)
Edit `types/global.d.ts` — add fields to `ContentBlockEntry`:
```typescript
interface ContentBlockEntry {
// ... existing fields ...
// my-new-block fields
myCustomField?: string
myItems?: { title: string; description: string }[]
}
```
If the change also introduces a new collection or new API usage surface, update the corresponding entry interfaces in the same change instead of leaving `Record<string, unknown>` as a long-term placeholder.
### Step 5: Extend collection YAML (if new fields need admin editing)
Edit `api/collections/content.yml` — add subFields under `blocks`:
```yaml
- name: blocks
type: object[]
subFields:
# ... existing subFields ...
- name: myCustomField
type: string
- name: myItems
type: object[]
meta:
drillDown: true
preview: title
subFields:
- name: title
type: string
- name: description
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
When blocks contain foreign references such as medialib images, model the reference path deliberately so later loaders can request the needed `lookup` data.
### Step 6: Update mock data (if using MOCK=1)
Add a block with your new type to `frontend/mocking/content.json`.
### Step 7: Verify
```sh
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
```
For blocks that are authored in pagebuilder and use images or foreign references, also verify:
- the block appears in the admin chooser
- the preview renders in Nova
- image/reference data is present through the intended lookup path
### Existing block types for reference
| Type | Component | Purpose |
| -------------- | ------------------------- | ----------------------------------------- |
| `hero` | `HeroBlock.svelte` | Full-width hero with image, headline, CTA |
| `richtext` | `RichtextBlock.svelte` | Rich text with optional image |
| `accordion` | `AccordionBlock.svelte` | Expandable FAQ/accordion items |
| `contact-form` | `ContactFormBlock.svelte` | Contact form |
---
## Adding a new collection
### Step 1: Create collection YAML
Create `api/collections/mycollection.yml`. Use `content.yml`, `navigation.yml`, or a current `tibi-admin-nova` example config as a template:
```yaml
########################################################################
# MyCollection — description of what this collection stores
########################################################################
name: mycollection
meta:
label: { de: "Meine Sammlung", en: "My Collection" }
muiIcon: category # Material UI icon name
viewHint: table
preview:
label: name
table:
- name
- source: active
label: Active
permissions:
public:
methods:
get: true # Public read access
user:
methods:
get: true
post: true
put: true
delete: true
fields:
- name: active
type: boolean
meta:
label: { de: "Aktiv", en: "Active" }
- name: name
type: string
meta:
label: { de: "Name", en: "Name" }
# 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/config/collection.schema.json`.
### Step 2: Include in config.yml
Edit `api/config.yml`:
```yaml
collections:
- !include collections/content.yml
- !include collections/navigation.yml
- !include collections/ssr.yml
- !include collections/mycollection.yml # ← add this line
```
### Step 3: Add TypeScript types
Edit `types/global.d.ts`:
```typescript
interface MyCollectionEntry {
id?: string
active?: boolean
name?: string
// ... fields matching your YAML
}
```
### Step 4: Configure API layer (optional)
If you need typed helpers, extend the `EntryTypeSwitch` in `frontend/src/lib/api.ts`:
```typescript
type CollectionNameT = "medialib" | "content" | "navigation" | "mycollection" | string
type EntryTypeSwitch<T extends string> = T extends "medialib"
? MedialibEntry
: T extends "content"
? ContentEntry
: T extends "navigation"
? NavigationEntry
: T extends "mycollection"
? MyCollectionEntry
: Record<string, unknown>
```
Do not treat `EntryTypeSwitch` as optional cleanup. If the frontend or tests consume the collection in a typed way, update this mapping in the same change.
### Step 5: Add hooks (optional)
Common hook patterns:
- **Public filter** — reuse `filter_public.js` to enforce `active: true` for unauthenticated users.
- **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:
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)
Create `frontend/mocking/mycollection.json`:
```json
[{ "_id": "1", "active": true, "name": "Example Entry" }]
```
### Step 7: Verify
```sh
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
If the collection feeds public pages or admin block previews, also verify that the typed API helpers and runtime components agree on the same data shape.
---
## Collection Validators
Validatoren definieren Sicherheitsregeln und Typ-Constraints, indem sie als `validator`-Key innerhalb der `fields`-Definitionen einer Collection-YAML (`api/collections/*.yml`) konfiguriert werden.
**Unterschied Client- vs. Serverseitige Validatoren:**
- **Serverseite (`tibi-server`)**: Validatoren werden zentral im Go-Backend bei jedem Datensatz-Schreibvorgang (`POST` / `PUT`) ausgeführt (nach den `validate`-Hooks). Wenn Daten nicht den Constraints entsprechen, erfolgt ein Abbruch (`400 Bad Request`).
- **Clientseite (`tibi-admin-nova`)**: Das CMS-Admin-Interface liest diese Validator-Regeln automatisch über das OpenAPI-Schema ein und wendet sie instant als Client-Side-Validierung in den Formularen an (Rote Markierungen und Check vor dem eigentlichen API-Call). **Validatoren müssen daher nur 1x zentral in der YAML definiert werden.**
**Häufige Validator-Optionen je Feldtyp:**
- **Generell**:
- `required: true` (Zwingendes Pflichtfeld)
- `allowZero: true` (Erlaubt die explizite Eingabe von `""` oder `0`, selbst wenn `required: true` aktiv ist)
- `in: ["wert1", "wert2"]` (Nur dieser exakte Pool an primitiven Werten ist erlaubt)
- `eval: "$this.length >= 3 && $this.length <= 100"` (Serverseitige Javascript-Evaluation für Custom-Logik)
- **Einfache Texte (`string`)**:
- `minLength: X` und `maxLength: Y`
- `pattern: "^[a-zA-Z0-9]+$"` (Prüft Regex-Match des kompletten Werts)
- `format: email` (oder `url`, `uuid`, `slug` für eingebaute Regex-Prüfungen)
- **Zahlen (`number`, `float`)**:
- `min: X` und `max: Y`
- **Datum/Zeit (`date`, `datetime`, `time`)**:
- `minDate: "YYYY-MM-DD"` und `maxDate: "YYYY-MM-DD"` (Zulässige Zeitgrenzen)
- **Listen/Arrays (`string[]`, `object[]`)**:
- `minItems: X` und `maxItems: Y`
- **Dateien/Bilder (`file`, `file[]`)**:
- `maxFileSize: "50MB"` (und `minFileSize`)
- `accept: ["image/png", "image/webp"]` (Erlaubte MIME-Types)
- Constraints für Bildabmessungen konfigurierbar via Sub-Objekt:
```yaml
image:
minWidth: 800
maxWidth: 2400
minHeight: 600
maxHeight: 1800
```
**Beispiel für die Einbindung in einer Collection:**
```yaml
fields:
- name: internalName
type: string
validator:
required: true
maxLength: 100
meta:
label: { de: "Interner Name", en: "Internal Name" }
- name: externalLink
type: string
validator:
format: url
meta:
label: Externe URL
- name: document
type: file
validator:
maxFileSize: "20MB"
accept: ["application/pdf"]
```
## Seed data pattern (Playwright)
Test seed data uses `_testdata: true` as a hidden marker field. **Real content must NEVER use this flag** — otherwise test teardown will delete it.
```yaml
# Last field in every collection schema
- name: _testdata
type: boolean
meta:
hide: true
```
Test setup:
1. `globalSetup` removes entries with `_testdata: true`, then creates new test entries
2. `globalTeardown` removes entries with `_testdata: true`
3. Real editorial content has no `_testdata` field → survives all test runs
## Common pitfalls
- **Path format**: Content paths do NOT include the language prefix. The path `/ueber-uns` becomes `/{lang}/ueber-uns` via the i18n layer.
- **Active flag**: Pages with `active: false` are filtered out by `filter_public.js` for public users. The admin can still see them.
- **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.
## API lookup für aufgelöste Referenzen
Beim Laden von Collections können Fremdschlüssel via `lookup`-Parameter automatisch aufgelöst werden. Der `lookup`-Parameter wird einfach im Optionen-Objekt an `getCachedEntries` übergeben:
```ts
const products = await getCachedEntries<"machines">("machines", {
filter: { active: true, category: catId },
sort: "sortOrder",
lookup: "images:medialib", // lookup: "feld:collection"
})
```
Das Format ist `"feldname:zielcollection"` (z.B. `"images:medialib"`). Die aufgelösten Daten landen in `entry._lookup.feldname` als Array der Ziel-Collection-Objekte. Ohne lookup bleiben `string[]`-Felder reine ID-Arrays.
Wichtig: der `lookup`-Parameter muss auch in `getDBEntries` und `apiRequest` durchgereicht werden (siehe `api.ts`).
Für blockbasierte Inhalte ist der Lookup-Pfad oft verschachtelt, nicht flach. Beispiel:
```ts
const entries = await getCachedEntries<"content">(
"content",
{ active: true, path: "/preview-page" },
"sort",
undefined,
1,
undefined,
undefined,
"blocks.heroImage.image:medialib"
)
```
Merke:
- flache Relationen nutzen Pfade wie `images:medialib`
- block- oder objektverschachtelte Relationen nutzen Dot-Paths wie `blocks.heroImage.image:medialib`
- ohne den passenden Lookup fehlen Admin-Preview, SSR oder Frontend-Rendern oft erst zur Laufzeit
Treat public rendering, SSR rendering, and admin preview as the same reference contract whenever possible. If a block renders a medialib image in the site, the admin preview should usually depend on the same resolved media assumption instead of inventing a separate preview-only data path.
+192
View File
@@ -0,0 +1,192 @@
---
name: deployment
description: Production deployment setup for tibi-projects Basispanel subdomain, .env, CI-Pipeline, Makefile. Use when deploying a new project to production or setting up a staging environment.
---
# Deployment
## Überblick
Ein tibi-Projekt wird per Gitea Actions CI gebaut und via rsync auf den Produktionsserver (dock4) deployed. Davor muss die Subdomain im Basispanel angelegt und der Kunde korrekt konfiguriert sein.
## 1. Basispanel Subdomain anlegen
### Kunde prüfen
```bash
# Domain des Kunden suchen
mcp_call(server="basispanel", tool="bp_list_domains", args={"search": "<kunde>"})
# → liefert Customer-ID, Domain-ID, Company, Username
```
### Subdomain anlegen
```bash
# 1. Config holen (verfügbare Webserver + Storages sehen)
mcp_call(server="basispanel", tool="bp_get_config")
# 2. Subdomain erstellen (ohne Webserver)
mcp_call(server="basispanel", tool="bp_create_subdomain", args={
"domainId": <domain-id>,
"name": "<subdomain>", # oder leer für bare domain
})
# 3. Löschen + neu mit Webserver (wenn Update nicht klappt)
mcp_call(server="basispanel", tool="bp_delete_subdomain", args={"id": <subdomain-id>})
mcp_call(server="basispanel", tool="bp_create_subdomain", args={
"domainId": <domain-id>,
"name": "<subdomain>",
"webserverKey": "dock4_lamp2",
"webserverStorage": "dock4_webroots2",
"webserverSettings": {
"redirectType": "docroot",
"docroot": "/<subdomain>.<domain>/frontend",
"gitbaseRepository": "<org>/<repo>",
"deployRoot": "./..",
"defaultAlias": "wwwAlias",
"defaultSubdomain": "defaultSubdomain",
"wwwRedirect": "wwwRedirect",
"php": "phpDisabled", # tibi-SPA kein PHP
"https": "noHttps", # erstmal aus, später aktivieren
"certbot": "noCertbot",
},
})
```
Wichtige Keys (aus `bp_get_config`):
| Server | Key | Storage |
|--------|-----|---------|
| dock4 | `dock4_lamp2` | `dock4_webroots2` |
| dock1 | `dock1_...` | `dock1_webroots...` |
### Status prüfen
```bash
mcp_call(server="basispanel", tool="bp_get_subdomain_status", args={"id": <subdomain-id>})
```
Achtung: Health-Check zeigt DNS-Warnungen (externe Nameserver) das ist normal solange der Kunde sein DNS selbst verwaltet.
## 2. `.env` konfigurieren
```env
# Basis
PROJECT_NAME=<project>
TIBI_PREFIX=tibi
TIBI_NAMESPACE=<project>
# RSYNC für Deploy
RSYNC_HOST=ftp1.webmakers.de
RSYNC_PORT=22223
PRODUCTION_RSYNC_UID=100<customer-id>00 # z.B. 10051300
PRODUCTION_RSYNC_GID=33
# Production Server
PRODUCTION_SERVER=dock4.basehosts.de
PRODUCTION_TIBI_PREFIX=tibi
PRODUCTION_PATH=/webroots2/customers/<customer-id>/htdocs
# Staging
STAGING_PATH=/staging/<org>/<project>/dev
# URLs
LIVE_URL=http://<subdomain>.<domain>.dock4.basispanel.de # Preview-URL
STAGING_URL=https://dev-<project>.staging.testversion.online
CODING_URL=https://<project>.code.testversion.online
```
## 3. CI-Pipeline (`.gitea/workflows/deploy.yml`)
```yaml
name: deploy to production
on: "push"
jobs:
deploy:
steps:
- checkout + git fetch --tags
- node 22 + yarn install
- yarn validate
- ./scripts/ci-modify-config.sh # injiziert LIVE_URL, release, preview
- yarn build # frontend
- yarn build:server # SSR
- sourcemaps → sentry
- if dev-branch: ./scripts/ci-staging.sh
- if master-branch: ./scripts/ci-deploy.sh
```
**Wichtig:** Das aktuelle Workflow-File führt `yarn validate`, `yarn build` und `yarn build:server` im CI aus. Wenn `validate` dort scheitert, behebe den eigentlichen Typ- oder Pfadfehler statt den Schritt stillschweigend zu entfernen.
## 4. Deploy-Skripte
### `scripts/ci-deploy.sh` (Production)
- Liest `.env` und `api/config.yml.env`
- rsynct `frontend/`, `api/`, `media/` via SSH zu `RSYNC_HOST`
- deshalb muessen Collection-Dateiuploads auf den Repo-Root `media/` zeigen, typischerweise via `uploadPath: ../media/<collection>` in `api/collections/*.yml`
- `api/media` ist in diesem Setup nicht der persistente Deploy-Zielpfad fuer Uploads
- Nutzt `RSYNC_USER` + `RSYNC_PASS` (aus Gitea Secrets)
- Auf master: excludiert `src/` und `*.map`
- Reloadt den projektlokalen Proxy-Endpunkt via `LIVE_URL/api/_/admin/reload` mit `Authorization: Bearer ${ADMIN_TOKEN}`
- Cleared SSR cache via `LIVE_URL/api/ssr?clear=1`
### `scripts/ci-staging.sh` (Dev/Staging)
- rsynct `api/`, `frontend/dist`, und `frontend/assets` nach `/data/${{ github.repository }}/${{ github.ref_name }}`
- Startet `docker-compose-staging.yml`
- Reloadt den projektlokalen Proxy-Endpunkt via `STAGING_URL/api/_/admin/reload` mit `Authorization: Bearer ${ADMIN_TOKEN}`
### `scripts/ci-modify-config.sh`
- Injiziert `LIVE_URL` als `originURL` in `api/hooks/config-client.js`
- Injiziert `LIVE_URL` als `PREVIEW_URL` in `api/config.yml.env`
- Setzt `release` + `buildTime` für Sentry
- Kopiert `frontend/spa.html``api/templates/spa.html` (SSR-Template)
- Ersetzt `__TIMESTAMP__` in spa.html (Cache-Busting)
## 5. Makefile
Wichtige Targets:
```makefile
# Media von Production syncen
media-sync-master-to-local:
rsync -v -e "ssh ... -l $(PRODUCTION_RSYNC_UID),$(PRODUCTION_RSYNC_GID),$(PRODUCTION_PATH)/media" \
-az $(RSYNC_HOST):/ media/
# MongoDB von Production syncen (via Chisel-Tunnel)
mongo-sync-master-to-local:
chisel client --auth coder:$$PASSWORD http://$(PRODUCTION_SERVER):10987 27017:mongo:27017 &
mongodump ... | mongorestore ...
```
## 6. DNS
Der Kunde verwaltet sein DNS selbst (externe Nameserver). Für die Subdomain muss ein A-Record gesetzt werden:
```
<subdomain>.<domain> IN A 45.129.180.102 (IP von dock4)
```
Die Preview-URL `http://<subdomain>.<domain>.dock4.basispanel.de` funktioniert ohne DNS (wird von Basispanel intern aufgelöst).
## 7. HTTPS nachträglich aktivieren
Sobald das Projekt live geht:
1. Im Basispanel Subdomain updaten:
- `https`: `"https"` (statt `"noHttps"`)
- `certbot`: `"certbot"` (automatisches Letsencrypt)
- `httpsRedirect`: `"httpsRedirect"` (HTTP→HTTPS)
2. `.env`: `LIVE_URL` auf `https://www.<domain>` ändern
3. `api/hooks/config-client.js`: `originURL` entsprechend setzen (wird von CI überschrieben)
## 8. Typische Fehler
| Problem | Ursache | Fix |
| ------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------ |
| `invalid webserverKey: dock4` | Falscher Key | Mit `bp_get_config` prüfen → `dock4_lamp2` |
| `subdomain exists` | Doppelt angelegt | Mit `bp_delete_subdomain` löschen, neu anlegen |
| `yarn validate` scheitert in CI | Typen/Submodule/Pfade nicht sauber eingecheckt | Checkout-, Submodule- und Include-Pfade korrigieren; `validate` im Workflow belassen |
| Rsync "Permission denied" | Falscher RSYNC_USER | In Gitea Secrets prüfen |
| 404 auf Subdomain | DNS nicht gesetzt | A-Record beim Kunden-DNS-Provider eintragen |
@@ -0,0 +1,510 @@
---
name: frontend-architecture
description: Understand the frontend architecture — custom SPA routing, state management, Svelte 5 patterns, API layer, error handling, and i18n. Use when working on routing logic, navigation, stores, or understanding how the frontend fits together.
---
# frontend-architecture
## When to use this skill
Use this skill when:
- Understanding or modifying the SPA routing mechanism
- Working with stores or state management
- Debugging navigation issues
- 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
---
## Routing: custom SPA router
This project uses a **custom SPA router** — NOT SvelteKit, NOT file-based routing. Pages are CMS-managed content entries loaded by path.
### Architecture
```
Browser URL change
history.pushState / replaceState (proxied in store.ts)
$location store updates (path, search, hash)
App.svelte $effect reacts to $location.path
loadContent(lang, routePath) → API call: getCachedEntries("content", { lang, path, active: true })
ContentEntry.blocks[] → BlockRenderer.svelte → individual block components
```
### Key files
| File | Responsibility |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `frontend/src/lib/store.ts` | Proxies `history.pushState`/`replaceState` → updates `$location` writable store. Handles `popstate` for back/forward. |
| `frontend/src/lib/navigation.ts` | `spaNavigate(url, options)` — the programmatic navigation API. Also: `initScrollRestoration()`, `spaLink` action, hash parsing. |
| `frontend/src/lib/i18n.ts` | Language routing: `extractLanguageFromPath()`, `stripLanguageFromPath()`, `localizedPath()`, `currentLanguage` derived store, `ROUTE_TRANSLATIONS`. |
| `frontend/src/App.svelte` | Reacts to `$location.path` + `$currentLanguage`, loads content via API, passes blocks to `BlockRenderer`. |
| `frontend/src/blocks/BlockRenderer.svelte` | Maps `block.type` to Svelte components. |
### How the location store works
`store.ts` wraps `history.pushState` and `history.replaceState` with a `Proxy`:
```typescript
// Simplified — see store.ts for full implementation
history.pushState = new Proxy(history.pushState, {
apply: (target, thisArg, args) => {
// Update $location store BEFORE the actual pushState
publishLocation(args[2]) // args[2] = URL
Reflect.apply(target, thisArg, args)
},
})
```
This means **any** `pushState`/`replaceState` call (from `spaNavigate`, `<a>` clicks, or third-party code) automatically updates `$location`.
The `popstate` event (back/forward buttons) also triggers `publishLocation()`.
### URL structure
```
/{lang}/{path}
↓ ↓
de /ueber-uns
Example: /de/ueber-uns → lang="de", routePath="/ueber-uns"
/en/about → lang="en", routePath="/about"
/de/ → lang="de", routePath="/"
```
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
import { spaNavigate } from "./lib/navigation"
// Basic navigation (creates history entry, scrolls to top)
spaNavigate("/de/kontakt")
// Replace current entry (no back button)
spaNavigate("/de/suche", { replace: true })
// Keep scroll position
spaNavigate("/de/produkte#filter=shoes", { noScroll: true })
// With state object
spaNavigate("/de/produkt/123", { state: { from: "search" } })
```
### SPA link action
For `<a>` elements, use the `spaLink` action instead of `spaNavigate`:
```svelte
<script>
import { spaLink } from "../lib/navigation"
</script>
<a href="/de/kontakt" use:spaLink>Kontakt</a>
<a href="/de/suche" use:spaLink={{ replace: true }}>Suche</a>
```
The action intercepts clicks (respecting modifier keys, external links, `target="_blank"`) and calls `spaNavigate` internally.
### BrowserSync SPA fallback
In development, BrowserSync uses `connect-history-api-fallback` to serve `index.html` for all routes, enabling client-side routing. In production, the webserver or tibi-server handles this.
### Localized route translations
For translated URL slugs (e.g. `/ueber-uns``/about`), configure `ROUTE_TRANSLATIONS` in `frontend/src/lib/i18n.ts`:
```typescript
export const ROUTE_TRANSLATIONS: Record<string, Record<SupportedLanguage, string>> = {
about: { de: "ueber-uns", en: "about" },
contact: { de: "kontakt", en: "contact" },
// Add more as needed
}
```
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
The project uses **Svelte writable/derived stores** (not a centralized state library).
### 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 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
```typescript
// In lib/store.ts or a dedicated file
import { writable, derived } from "svelte/store"
// Simple writable
export const myStore = writable<MyType>(initialValue)
// Derived from other stores
export const myDerived = derived(location, ($loc) => {
return computeFromPath($loc.path)
})
```
---
## Svelte 5 patterns used in this project
This project uses **Svelte 5 with Runes**. Key patterns:
### Component props
```svelte
<script lang="ts">
// Rune syntax — replaces export let
let { block, className = "" }: { block: ContentBlockEntry; className?: string } = $props()
</script>
```
### Reactive state
```svelte
<script lang="ts">
// Local reactive state (replaces let x; with $: reactivity)
let count = $state(0)
let items = $state<Item[]>([])
// Computed/derived values (replaces $: derived = ...)
let total = $derived(items.reduce((sum, i) => sum + i.price, 0))
// Side effects (replaces $: { ... } reactive blocks)
$effect(() => {
// Runs when dependencies change
console.log("count changed:", count)
})
</script>
```
### SSR-safe code
```svelte
<script lang="ts">
import { untrack } from "svelte"
// Guard browser-only APIs
if (typeof window !== "undefined") {
window.addEventListener("scroll", handleScroll, { passive: true })
}
// untrack: capture initial value without creating reactive dependency
// Used in App.svelte for SSR initial URL
untrack(() => {
if (url) { /* set initial location */ }
})
</script>
```
### Svelte stores in Svelte 5
Stores (`writable`, `derived`) still work in Svelte 5. Use `$storeName` syntax in components:
```svelte
<script lang="ts">
import { location } from "./lib/store"
// $location is reactive — auto-subscribes in Svelte 5
</script>
<p>Current path: {$location.path}</p>
```
---
## API layer
### Core function: `api()`
Located in `frontend/src/lib/api.ts`. Features:
- **Request deduplication** — identical concurrent GETs share one promise
- **Loading indicator** — drives `activeRequests` store → `LoadingBar`
- **Build-version check** — auto-reloads page when server build is newer
- **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
import { api, getCachedEntries, getCachedEntry, getDBEntries, postDBEntry } from "./lib/api"
// Cached (1h TTL, for read-heavy data)
const pages = await getCachedEntries<"content">("content", { lang: "de", active: true })
const page = await getCachedEntry<"content">("content", { path: "/about" })
// Uncached
const items = await getDBEntries<"content">("content", { type: "blog" }, "sort", 10)
// Write
const result = await postDBEntry("content", { name: "New Page", active: true })
// Raw API call
const { data, count } = await api<MyType[]>("mycollection", { filter: { active: true }, limit: 20 })
```
### `aggregate` for sub-queries
The server supports an `aggregate` parameter to compute reverse aggregates against another collection and store the result under `_aggregate`. This efficiently calculates counts, sums, existence, etc. without embedding the target documents.
```typescript
const res = await api<MyEntry[]>("mycollection", {
filter: { active: true },
params: {
// String syntax: "collection:foreignField:op:valueField:as"
aggregate: "posts:categoryId:count",
// JSON syntax for advanced use cases (custom source field, filtering)
aggregate: JSON.stringify({
collection: "comments",
foreignField: "entryId",
op: "count",
filter: { approved: true },
as: "approvedComments",
}),
},
})
// Result in res.data[0]._aggregate.postsCount and res.data[0]._aggregate.approvedComments
```
Available operations: `count` (default), `exists`, `sum`, `avg`, `min`, `max`.
### Error handling
```typescript
try {
const result = await api<ContentEntry[]>("content", { filter: { path: "/missing" } })
} catch (err) {
// err has shape: { response: Response, data: { error: string } }
const status = (err as any)?.response?.status // e.g. 404
const message = (err as any)?.data?.error // e.g. "Not found"
// For user-visible errors:
import { addToast } from "./lib/toast"
addToast({ type: "error", message: "Seite nicht gefunden" })
// For debugging:
console.error("[MyComponent] API error:", err)
}
```
### `aggregate` for sub-queries
The server supports an `aggregate` parameter to compute reverse aggregates against another collection and store the result under `_aggregate`. This efficiently calculates counts, sums, existence, etc. without embedding the target documents.
```typescript
const res = await api<MyEntry[]>("mycollection", {
filter: { active: true },
params: {
// String syntax: "collection:foreignField:op:valueField:as"
aggregate: "posts:categoryId:count",
// JSON syntax for advanced use cases (custom source field, filtering)
aggregate: JSON.stringify({
collection: "comments",
foreignField: "entryId",
op: "count",
filter: { approved: true },
as: "approvedComments",
}),
},
})
// Result in res.data[0]._aggregate.postsCount and res.data[0]._aggregate.approvedComments
```
Available operations: `count` (default), `exists`, `sum`, `avg`, `min`, `max`.
### Error handling guidelines
| Scenario | Approach |
| --------------------------------- | ------------------------------------------------- |
| API error the user should see | `addToast({ type: "error", message })` |
| API error that's silently handled | `console.error(...)` for dev logging |
| Unexpected error in production | Sentry captures automatically (when enabled) |
| Missing content / 404 | Set `notFound = true` → renders `NotFound.svelte` |
| Network error / offline | Loading bar stays visible; user can retry |
### API request flow (client-side)
```
Component calls api() / getCachedEntries()
Deduplication check (skip if signal provided)
incrementRequests() → LoadingBar appears
__MOCK__? → mockApiRequest() (in-memory JSON filtering)
↓ (else)
apiRequest() from api/hooks/lib/ssr (shared with SSR bundle)
fetch("${apiBaseURL}${endpoint}?filter=...&sort=...&limit=...")
Parse response → check X-Build-Time header
decrementRequests() → LoadingBar disappears
Return { data, count, buildTime }
```
### `_count` endpoint
Der tibi-server stellt einen dedizierten `_count`-Endpoint bereit, der **nur** `{"count": N}` zurückgibt kein Data-Transfer:
```
GET /api/{collection}/_count?filter={"active":true,"category":"<id>"}
→ {"count": 8}
```
Der Endpoint wird durch den BrowserSync-Proxy korrekt geroutet (`/api``/api/v1/_/{namespace}`).
**Frontend-Aufruf:**
```ts
const res = await api<{ count: number }>("machines/_count", {
filter: { active: true, category: catId },
})
// res.data.count === 8
```
Das ist effizienter als `count=1&limit=1`, weil keine Collection-Objekte serialisiert/übertragen werden.
### `select` für schlanke Queries
Der tibi-server unterstützt einen `select`-Parameter als Komma-Liste der gewünschten Felder. Nicht gelistete Felder werden nicht übertragen:
```ts
const res = await api<MachineEntry[]>("machines", {
filter: { active: true, category: catId },
sort: "sortOrder",
limit: 20,
params: {
lookup: "images:medialib",
select: "name,slug,tagline,priceFrom,weight,sku,images",
},
})
```
Nicht aufgeführte Felder (z.B. `description`, `specs`) entfallen spart Bandbreite bei Listen/Grids. `_lookup` und `id` werden automatisch ergänzt.
**Wichtig:** `select` muss als String im `params`-Objekt übergeben werden (der `apiRequest` hängt es als Query-Parameter an). Es wird direkt an den tibi-server durchgereicht.
---
## i18n system
### Architecture
- **svelte-i18n** for translation strings (`$_("key")`)
- **URL-based language routing** (`/{lang}/...`)
- **Lazy-loaded locale files** in `frontend/src/lib/i18n/locales/{lang}.json`
- **Route translations** for localized URL slugs
### Adding a new language
1. Create locale file: `frontend/src/lib/i18n/locales/fr.json`
2. Add to `SUPPORTED_LANGUAGES` in `frontend/src/lib/i18n.ts`:
```typescript
export const SUPPORTED_LANGUAGES = ["de", "en", "fr"] as const
```
3. Add label: `export const LANGUAGE_LABELS = { ..., fr: "Français" }`
4. Add route translations for the new language in `ROUTE_TRANSLATIONS`.
5. Register in `frontend/src/lib/i18n/index.ts` (lazy loader).
6. Create content entries with `lang: "fr"` in the CMS.
### Translation usage
```svelte
<script>
import { _ } from "./lib/i18n/index"
</script>
<h1>{$_("hero.title")}</h1>
<p>{$_("hero.subtitle", { values: { name: "World" } })}</p>
```
---
## Common pitfalls
- **Never `spaNavigate()` in SSR** — always guard with `typeof window !== "undefined"`.
- **Store subscriptions in modules** — if subscribing to stores outside components, remember to unsubscribe to prevent memory leaks.
- **API PUT returns only changed fields** — don't expect a full object back from PUT requests.
- **`_id` not `id` for filters** — API filters use MongoDB's `_id`, but response objects only have `id` as string via API.
- **`$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.
## Cart persistence (localStorage)
For SSR-safe cart/inquiry persistence:
```ts
let cartItems = $state<any[]>([])
// Laden
$effect(() => {
try {
if (typeof localStorage !== "undefined") {
const saved = localStorage.getItem("cart_key")
if (saved) cartItems = JSON.parse(saved)
}
} catch {}
})
// Speichern
$effect(() => {
try {
if (typeof localStorage !== "undefined") {
localStorage.setItem("cart_key", JSON.stringify(cartItems))
}
} catch {}
})
```
Immer mit `typeof localStorage !== "undefined"` für SSR-Sicherheit.
@@ -0,0 +1,25 @@
---
name: gitea-issue-attachments
description: Upload files (screenshots, logs, etc.) to Gitea issues as attachments via the REST API. Use when attaching any file to a Gitea issue or comment.
---
# Gitea Issue Attachments
Attach files to Gitea issues via the REST API:
1. Get the Gitea API token from the running MCP docker process:
```bash
GITEA_PID=$(ps aux | grep 'gitea-mcp-server' | grep -v grep | awk '{print $2}')
GITEA_TOKEN=$(cat /proc/$GITEA_PID/environ | tr '\0' '\n' | grep GITEA_ACCESS_TOKEN | cut -d= -f2)
```
2. Upload the file as an issue attachment:
```bash
curl -s -X POST "https://gitbase.de/api/v1/repos/{owner}/{repo}/issues/{index}/assets" \
-H "Authorization: token $GITEA_TOKEN" \
-F "attachment=@path/to/file"
```
This returns JSON with a `uuid` field.
3. Reference the attachment in the issue or comment body:
```markdown
![Description](attachments/{uuid})
```
+175
View File
@@ -0,0 +1,175 @@
---
name: live-mongodb
description: Connect to the live (production) MongoDB via chisel tunnel and perform read/write operations. Use this skill when you need to inspect or update live data directly in the production database.
---
# Live MongoDB Access via Chisel Tunnel
## Overview
Die Produktions-MongoDB läuft auf dem Server aus `PRODUCTION_SERVER` in `.env`. Der Zugang erfolgt über einen **Chisel-Tunnel**, der den Remote-MongoDB-Port auf localhost mapped. Damit kann man dann entweder über `mongosh`, `mongodump`/`mongorestore`, oder den **MongoDB MCP Server** auf die Live-Daten zugreifen.
## Umgebungsvariablen (aus .env)
| Variable | Beschreibung |
| ------------------------ | ----------------------------------------------- |
| `PRODUCTION_SERVER` | Produktionsserver (z.B. `dock4.basehosts.de`) |
| `PRODUCTION_TIBI_PREFIX` | DB-Prefix auf Produktion (z.B. `tibi`) |
| `TIBI_NAMESPACE` | Projekt-Namespace |
| **Live DB Name** | = `${PRODUCTION_TIBI_PREFIX}_${TIBI_NAMESPACE}` |
| Chisel-Port (Remote) | `10987` — Chisel-Server auf dem Produktionshost |
## Schritt 1: Chisel-Tunnel starten
Das Chisel-Passwort muss vom User bereitgestellt werden. Tunnel starten:
```bash
# Passwort vom User erfragen oder aus Umgebung nehmen
read -s -p "Chisel-Passwort: " CHISEL_PASSWORD
# Tunnel starten (mappt remote mongo:27017 → localhost:27017)
chisel client --auth "coder:${CHISEL_PASSWORD}" \
http://${PRODUCTION_SERVER}:10987 \
27017:mongo:27017 &
# Kurz warten, bis der Tunnel steht
sleep 3
```
**WICHTIG:** Der lokale Docker-Mongo-Container muss gestoppt sein oder auf einem anderen Port laufen, da der Tunnel Port 27017 lokal belegt. Falls der lokale Container läuft:
```bash
# Lokales MongoDB stoppen (belegt sonst Port 27017)
docker compose -f docker-compose-local.yml stop mongo
```
Alternativ kann der Tunnel auf einen anderen lokalen Port gemappt werden:
```bash
chisel client --auth "coder:${CHISEL_PASSWORD}" \
http://${PRODUCTION_SERVER}:10987 \
37017:mongo:27017 &
# → erreichbar unter mongodb://localhost:37017/${PRODUCTION_TIBI_PREFIX}_${TIBI_NAMESPACE}
```
## Schritt 2: Verbinden
### Option A: mongosh (interaktiv)
```bash
mongosh "mongodb://localhost:27017/${PRODUCTION_TIBI_PREFIX}_${TIBI_NAMESPACE}"
```
### Option B: MongoDB MCP Server (für Copilot)
Den MongoDB MCP über die Umgebungsvariable `MDB_MCP_CONNECTION_STRING` auf die Live-DB umleiten.
**Temporär für eine Session** in `.vscode/mcp.json` eine zweite Server-Config eintragen:
```jsonc
{
"servers": {
"mongodb-live": {
"command": "npx",
"args": ["-y", "mongodb-mcp-server@latest"],
"type": "stdio",
"env": {
"MDB_MCP_CONNECTION_STRING": "mongodb://localhost:27017/${PRODUCTION_TIBI_PREFIX}_${TIBI_NAMESPACE}",
"MDB_MCP_READ_ONLY": "false",
"MDB_MCP_TELEMETRY": "disabled",
},
},
},
}
```
> **Achtung:** `MDB_MCP_READ_ONLY=false` erlaubt Schreiboperationen! Nach getaner Arbeit den Server wieder entfernen oder auf `true` setzen.
### Option C: Einmalige Kommandos via Terminal
```bash
DB_NAME="${PRODUCTION_TIBI_PREFIX}_${TIBI_NAMESPACE}"
# Dokument suchen
mongosh "mongodb://localhost:27017/$DB_NAME" \
--eval 'db.content.findOne({path: "/"})'
# Feld updaten
mongosh "mongodb://localhost:27017/$DB_NAME" \
--eval 'db.content.updateOne({path: "/"}, {$set: {"title": "Neuer Titel"}})'
```
## Schritt 3: Tunnel beenden
```bash
killall chisel
```
Falls der lokale Mongo-Container vorher gestoppt wurde, wieder starten:
```bash
docker compose -f docker-compose-local.yml start mongo
```
## Sicherheitsregeln
1. **Immer zuerst lesen, dann schreiben.** Vor jedem Update das betroffene Dokument mit `find`/`findOne` inspizieren.
2. **Backup vor Bulk-Updates.** Bei Massenänderungen vorher ein `mongodump` machen:
```bash
mongodump --uri="mongodb://localhost:27017" \
--db=${PRODUCTION_TIBI_PREFIX}_${TIBI_NAMESPACE} \
--collection=<collection> \
--gzip --archive=backup-<collection>-$(date +%Y%m%d-%H%M%S).gz
```
3. **User muss Chisel-Passwort liefern.** Das Passwort niemals hardcoden oder in Dateien speichern.
4. **Updates immer bestätigen lassen.** Vor jeder Schreiboperation dem User die geplante Query zeigen und explizit nach Bestätigung fragen.
5. **Nach getaner Arbeit Tunnel schließen** und ggf. `mongodb-live` MCP-Server aus der Config entfernen.
6. **Kein Drop/Delete von Collections** ohne explizite User-Anweisung.
7. **SSR-Cache leeren nach Datenänderungen.** Wenn Daten in Collections geändert werden, die auf der Website gerendert werden (z.B. `content`, `navigation`), muss der SSR-Cache invalidiert werden, damit die Änderungen sichtbar werden. Dazu die `ssr`-Collection leeren:
```bash
mongosh "mongodb://localhost:27017/$DB_NAME" \
--eval 'db.ssr.deleteMany({})'
```
Siehe auch den Skill `tibi-ssr-caching` für Details zur Cache-Invalidierung über die API.
## Wichtige Collections
| Collection | Beschreibung |
| ------------ | ------------------------------- |
| `content` | CMS-Inhaltsseiten (Pagebuilder) |
| `navigation` | Navigationsstruktur |
| `medialib` | Medien-Bibliothek |
| `ssr` | SSR-Cache |
> Weitere Collections je nach Projekt — siehe `api/collections/` für die aktuelle Liste.
## Typische Anwendungsfälle
### Content-Eintrag inspizieren
```js
db.content.findOne({ path: "/" })
```
### Navigation aktualisieren
```js
db.navigation.updateOne({ type: "header", language: "de" }, { $set: { "elements.0.name": "Neues Label" } })
```
### Dokument-Struktur inspizieren
```js
// Schema einer Collection anschauen
db.content.findOne()
// Alle Felder eines Dokuments auflisten
Object.keys(db.content.findOne())
```
## Fehlerbehebung
- **"Connection refused" auf Port 27017:** Chisel-Tunnel läuft nicht oder lokaler Mongo blockiert den Port. Prüfen mit `ss -tlnp | grep 27017`.
- **"Authentication failed":** Chisel-Passwort falsch. User erneut fragen.
- **Langsame Queries:** Produktions-DB kann große Collections haben. Immer mit Filtern arbeiten, nie `find({})` ohne Limit.
- **Rate Limiting:** Kein Thema bei direktem DB-Zugang (nur bei API-Calls relevant).
@@ -0,0 +1,450 @@
---
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
## Medialib file serving
Images uploaded to medialib (via base64 or multipart) are stored with a relative `file.src` path such as `"file/example.jpg"`. In this starter, `MedialibImage` resolves that stored path together with the medialib entry ID into the project-local URL:
```
/api/medialib/{id}/file/example.jpg
```
Responsive image filters are then applied by the shared widget as query params:
```
/api/medialib/{id}/file/example.jpg?filter=s-webp
/api/medialib/{id}/file/example.jpg?filter=m-webp
/api/medialib/{id}/file/example.jpg?filter=l-webp
```
Those `/api/...` URLs are starter-local proxy URLs. For frontend rendering in Tibi website projects, the preferred approach is **not** to hand-build medialib URLs in each block or route component.
## Collection uploadPath
For Tibi file fields, the storage root is configured per collection via top-level `uploadPath`.
Important rules:
- `uploadPath` belongs on the collection itself, not on the individual `type: file` field
- in this starter, collection YAML files live in `api/collections/`, while deploy syncs the repo-root `media/` directory
- the current starter collections can omit `uploadPath` and rely on the tibi-server default derived from project config
- if you explicitly override `uploadPath` in this starter, it should normally point to `../media/<collection-name>` so deploy and runtime stay aligned
- do not write uploads into `api/media`; that path is not the persistent deploy target used by the project
Explicit override example:
```yaml
name: medialib
uploadPath: ../media/medialib
fields:
- name: file
type: file
```
For hidden thumbnails stored on the `content` collection, the same rule applies when you choose an explicit override at collection level:
```yaml
name: content
uploadPath: ../media/content
fields:
- name: _pagebuilderThumbnail
type: file
```
Tibi then stores uploads below:
```text
{uploadPath}/{entryId}/{fieldName}/{filename}
```
So an explicitly overridden medialib upload typically lands at `../media/medialib/{entryId}/file/{filename}`, while a content thumbnail lands at `../media/content/{entryId}/_pagebuilderThumbnail/{filename}`.
## Preferred frontend integration
Use a shared media widget such as `MedialibImage` as the frontend boundary for medialib-rendered images.
### MedialibImage with minimal entry (ID only)
When only a medialib ID is available (no resolved `_lookup` entry), pass a minimal entry structure together with the `id` prop:
```svelte
<MedialibImage
id={medialibId}
entry={{ file: { src: "file/example.jpg", type: "image/jpeg" } }}
class="w-full h-full object-cover"
noPlaceholder
/>
```
`resolveFileSrc()` in `MedialibImage` kombiniert `entry.file.src` (zum Beispiel `"file/example.jpg"`) mit der `id` zur korrekten URL: `${apiBase}/medialib/{id}/{src}`. Filter werden im Widget als `?filter=...` angehängt. Wenn `_lookup`-Daten mit vollständigem Entry verfügbar sind, bevorzugt diese verwenden:
```svelte
<MedialibImage entry={resolvedEntry} class="w-full h-full object-cover" noPlaceholder />
```
The preferred flow is:
1. request medialib references with `lookup`
2. pass the resolved `MedialibEntry` to the shared widget
3. let that widget own URL resolution, filter choice, SSR markup, and admin/pagebuilder compatibility
Typical usage:
```svelte
<script lang="ts">
import MedialibImage from "../widgets/MedialibImage.svelte"
let { block }: { block: ContentBlockEntry } = $props()
</script>
{#if block._lookup?.image}
<MedialibImage
entry={block._lookup.image}
class="w-full h-full object-cover"
minWidth={900}
lazy={true}
/>
{/if}
```
For repeated collection data such as galleries, teaser lists, or detail-page image arrays, also request the lookup instead of rendering from raw ID strings:
```ts
const entries = await getCachedEntries<CollectionName>("your-collection", {
filter: { active: true },
sort: "sortOrder",
lookup: "imageField:medialib",
})
```
`getCachedEntries()` expects `lookup` as an option in the second arguments object.
Then consume the resolved entry from `_lookup`, for example `_lookup.imageField` or `_lookup.imageField?.[0]` depending on whether the schema stores one image or an array.
### URL resolution strategy (frontend)
At the shared widget boundary, resolve image data with this priority:
1. Prefer a resolved `MedialibEntry` from `_lookup`
2. If `entry.file.src` is already absolute, use it directly
3. Otherwise construct the file URL from `{apiBase}/medialib/{entryId}/{file.src}` inside the shared widget
4. Only fall back to raw ID-to-URL construction in legacy code paths that cannot yet pass resolved entries
Do not duplicate medialib URL logic in every block. Keep it in one widget/helper layer so SSR, admin preview, and filter behavior stay consistent.
```ts
function resolveFileSrc(src: string | undefined, entryId: string | undefined, apiBase: string): string | null {
if (!src) return null
if (/^(https?:)?\/\//.test(src) || src.startsWith("/")) return src
if (!entryId) return null
return `${apiBase.replace(/\/+$/, "")}/medialib/${entryId}/${src.replace(/^\/+/, "")}`
}
```
### Lookup parameter
Use the `lookup` API parameter to resolve medialib references automatically:
```
/api/{collection}?lookup={fieldPath}:medialib
```
The resolved data is available in the `_lookup` field of the returned object at the corresponding path.
Typical project patterns:
- pagebuilder blocks with nested image refs: `blocks.someImageField:medialib`
- collection entries with a single image field: `image:medialib`
- collection entries with image arrays: `images:medialib`
- attachment arrays or document previews: `attachments:medialib`
Prefer adding the lookup at the data-loading boundary rather than rehydrating IDs later in the component tree.
## Filter sizing strategy
Do not hardcode one fixed filter per component unless the rendered width is truly fixed.
Preferred rules:
1. explicit `filter` prop wins when the caller already knows the right output size
2. otherwise pass a meaningful `minWidth` for layout-stable contexts such as hero, card, or gallery slots
3. let the shared widget derive the final filter from the measured width on the client
4. keep the width-to-filter mapping centralized in the widget instead of repeating `xs/s/m/l/xl` logic in blocks
This keeps image payloads reasonable without forcing each block author to manually guess the correct filter.
Typical examples:
- hero/background image: `minWidth={1600}`
- card image: `minWidth={640}`
- product detail main gallery image: `minWidth={960}`
- thumbnail image: `minWidth={240}`
The exact breakpoints can vary per project, but the sizing logic should remain centralized.
## SSR and no-JS behavior
For raster images, SSR cannot always know the final client width. The shared widget should therefore render deterministically:
1. render a normal `img` with a real `src` in SSR when a filter is explicit, `minWidth` is known, or admin rendering requires a fallback filter
2. emit a `noscript` fallback for raster images so crawlers and JS-disabled clients still receive a concrete image URL
3. avoid unstable per-block SSR hacks that guess widths differently from the client
This is especially important for image-bearing blocks, teasers, galleries, and detail views where HTML must remain stable between SSR, hydration, and no-JS rendering.
## Admin and pagebuilder compatibility
Medialib rendering must also work inside admin/pagebuilder preview contexts, not only on the public website.
Important rules:
- respect `apiBaseOverride` or the admin-provided project base when constructing medialib URLs
- do not prepend frontend public paths blindly when the admin already passes an absolute file URL
- consume hydrated `_lookup` data directly in preview/runtime code instead of trying to re-fetch references inside preview components
- keep placeholder asset resolution admin-safe too, not just medialib file URLs
If a block preview loads collection data in the admin, the preview context must provide the project-specific API base and the frontend code must route requests through that base.
In practice, that means the media widget and any helper used by it should be aware of `apiBaseOverride` or an equivalent admin-provided API base so the same component works in:
- public frontend
- SSR render
- admin pagebuilder preview
- collection-driven block previews
### Base64 file upload via API
Upload images to medialib via JSON API by including base64 data inline:
```json
POST /api/medialib
{
"file": { "src": "data:image/jpeg;base64,..." },
"title": "Image title",
"alt": "Alt text"
}
```
The tibi-server saves the file below the collection's `uploadPath`, for example `../media/medialib/{entryId}/file/{filename}`, and creates the medialib entry.
## Goal
The goal is to make media, SEO, and publishing part of the actual solution design instead of leaving them as late add-ons.
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`
- the project's medialib collection config
- the relevant content or domain collection configs
- `tibi-admin-nova/types/admin.d.ts`
- `tibi-admin-nova/docs/collection-config.md`
- the project's SSR/runtime config hooks
- the project's frontend media widget/helper implementation
## 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
- Building raw `/api/medialib/{id}/{src}` URLs in every block or hardcoding `file/file` instead of using one shared widget
- Repeating filter breakpoint logic in multiple components
- Rendering public image URLs in frontend code that break in admin pagebuilder preview
- Treating publication as frontend-only logic
- Forgetting that publish windows can invalidate SSR HTML
## Publication verification matrix
If the project uses publication timing, do not verify only one happy-path entry.
Check a small matrix deliberately:
1. currently published entry
2. future-scheduled entry
3. expired entry
4. token/admin visibility when editorial or operator access should still exist
This keeps publication modeling tied to the real public-read and SSR behavior instead of to optimistic field design.
## Verification checklist
After changing media/SEO/publishing behavior, verify all of these:
1. Upload validation matches the intended asset type.
2. Image filters are named and used consistently.
3. Shared image widgets receive resolved entries instead of rebuilding URLs ad hoc.
4. Alt/caption/SEO fields are explicit and editor-friendly.
5. Publication state affects public output correctly.
6. SSR HTML still reflects the intended published state.
7. Admin/pagebuilder preview resolves medialib images correctly.
8. `yarn validate` stays clean.
## What an LLM should inspect first
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. the shared media widget/helper layer used by the frontend
4. admin layout and previews for those fields
5. frontend components consuming the media/SEO data
6. SSR publish-check and invalidation logic if timing matters
This prevents “just add an image field” changes that break runtime, editorial UX, or caching.
+170
View File
@@ -0,0 +1,170 @@
---
name: mongodb-and-indexes
description: Design MongoDB prerequisites and index strategy for tibi website projects. Covers replica-set requirements, collection index modeling, text/search index implications, index auto-management on reload, and operator checks for persistence, backups, and regeneration-sensitive features.
---
# mongodb-and-indexes
## When to use this skill
Use this skill when:
- a project moves beyond simple demo data and needs deliberate MongoDB design
- collections need unique, text, sparse, or compound indexes
- search, audit, or larger datasets introduce operational database requirements
- operators need clarity about replica-set, persistence, or backup assumptions
## Goal
Give later agents one place for MongoDB prerequisites and index strategy.
This skill exists because tibi projects do not just “use MongoDB”. They rely on config-driven indexes, reload-time index reconciliation, and environment choices that can help or break production behavior.
## Source of truth
Use these sources when implementing or reviewing MongoDB/index decisions:
- `tibi-server/docs/02-configuration.md`
- `tibi-server/docs/04-collections.md`
- `tibi-server/docs/12-deployment.md`
- active project/server config
## MongoDB prerequisites
Current upstream deployment requirements include:
- MongoDB 4.4+
- replica set for transactions
That means later agents should not promise production-like behavior while ignoring the actual database topology.
For this starter family, check explicitly:
- which MongoDB version is targeted
- whether the environment runs as a replica set
- where persistent data lives
- how backup and restore are expected to work
## Index ownership model
Indexes can be defined in two places.
### Field-level indexes
Use for simple per-field indexes:
```yaml
fields:
- name: email
type: string
index:
- single
- unique
```
Available field-level flags include:
- `single`
- `unique`
- `text`
- `sparse`
### Collection-level indexes
Use for compound or more explicit index definitions:
```yaml
indexes:
- name: category_publish_date
key:
- category
- -publishDate
background: true
- name: title_text
key:
- $text:title
defaultLanguage: german
```
Use collection-level indexes when:
- sort patterns span multiple fields
- uniqueness depends on more than one field
- you need text-index language control
- the index needs a stable explicit name
## Important auto-management behavior
On project setup or reload:
1. configured indexes are created
2. indexes present in MongoDB but no longer in config are dropped
3. `_id_` is never dropped
This is a critical workflow fact.
Do not treat indexes as one-off manual DBA work while also expecting config reload to stay authoritative. In this stack, the YAML config owns the intended index state.
## Search and index interplay
Some search modes depend directly on index strategy:
- `mode: text` requires a text index
- explicit text search can use field-level `index: [text]` or collection-level `$text:` indexes
- regex fallback can become expensive without deliberate field/index choices
- enrichment-based modes such as `ngram` and `vector` add `_search` data and may require regeneration workflows rather than classic MongoDB indexes alone
Keep classic indexes and search config aligned. Search should not be modeled in isolation from database cost and reload behavior.
## Query shape matters
Before adding indexes, inspect the real access patterns:
- which fields appear in permission filters
- which fields appear in list sorting
- which fields appear in frequent admin or public lookups
- whether uniqueness is a real business rule or only a nice-to-have
Index design should follow concrete read and write patterns, not schema aesthetics.
## Operational decisions
At project-delivery level, document these choices explicitly:
- MongoDB version and topology
- replica-set availability
- persistence location for database and uploads
- backup/restore responsibility
- whether audit/search features introduce extra operational expectations
If these decisions are absent, later agents tend to over-focus on schema YAML while missing the operator-critical data layer.
## Anti-patterns
- adding indexes without checking actual query or sort behavior
- relying on manual indexes that the config will later drop
- enabling text search without provisioning the needed text index
- treating replica-set requirements as optional trivia
- shipping production projects without a backup and restore story
## Verification checklist
After MongoDB/index-related changes, verify all of these:
1. the target environment satisfies MongoDB version and replica-set needs
2. configured indexes match real query, sort, and uniqueness requirements
3. project reload succeeds after index changes
4. text/search features have the required index support
5. persistence and backup assumptions are documented
## What an LLM should inspect first
When asked to design or review the data layer on this starter, inspect in this order:
1. `tibi-server/docs/12-deployment.md`
2. `tibi-server/docs/02-configuration.md`
3. `tibi-server/docs/04-collections.md`
4. current collection YAML for indexes/search
5. actual query, sort, audit, and search requirements
This keeps index design tied to real runtime behavior and not just to field definitions.
@@ -0,0 +1,156 @@
---
name: monitoring-and-performance
description: Configure and verify observability for tibi website projects. Covers OpenAPI exposure, Prometheus metrics, Sentry wiring, health/reachability checks, and the operator-facing validation that should exist before a project is considered production-ready.
---
# monitoring-and-performance
## When to use this skill
Use this skill when:
- a project needs operator-facing visibility beyond “the page loads”
- you need OpenAPI output for integrations or documentation
- you need Prometheus/Grafana visibility
- you need Sentry or similar error visibility in frontend or server flows
- you want to define the minimum health and observability checks for deploys
## Goal
Give later agents a concrete workflow for deciding what should be observable, how it is exposed, and how to verify that exposure.
This skill is not about arbitrary performance tuning. It is about making the running system inspectable enough that operators and developers can see whether it is healthy.
## Source of truth
Use these sources when implementing or reviewing observability:
- `tibi-server/docs/13-openapi-metrics.md`
- `tibi-server/docs/02-configuration.md`
- `.agents/skills/deployment/SKILL.md`
- `frontend/src/config.ts`
- relevant deploy scripts and env/config files
## OpenAPI exposure
Tibi-server generates an OpenAPI spec per project:
```text
GET /api/v1/{namespace}/openapi
```
Use this when:
- a project exposes public API surfaces that need documentation
- integrations or client generators benefit from a machine-readable contract
Collection-level OpenAPI customization lives in collection metadata via `meta.openapi`.
Use that metadata deliberately to:
- hide endpoints that should not appear in the spec
- add summaries and descriptions
- keep the public API contract readable
## Metrics exposure
Prometheus metrics are exposed at:
```text
GET /metrics
```
Key upstream metric documented today:
- `tibi_request_duration_seconds`
This is useful for:
- request latency visibility
- collection-level timing comparisons
- basic traffic and error observation in Grafana/Prometheus
Do not enable metrics-like operator expectations in a project and then forget to verify the endpoint actually works in the target environment.
## Sentry and error visibility
This stack can surface errors through Sentry-related configuration.
Relevant surfaces include:
- server-level `sentry` config in tibi-server
- frontend runtime wiring in `frontend/src/config.ts`
- deploy-time release/build metadata injection where the project uses it
Use Sentry deliberately:
- define DSN, environment, and release expectations
- know whether tracing is wanted or only error capture
- make sure deploy scripts and build metadata agree with the runtime setup
Do not leave a half-configured Sentry setup that looks present but produces unusable traces.
## Health and reachability checks
At minimum, operators should be able to verify:
- website URL responds
- admin URL responds
- API responds
- OpenAPI and metrics endpoints respond when they are intended to be used
In this repo family, simple reachability probes are often the first useful health signal. For project delivery, these checks belong next to deploy and sign-off work, not only in ad-hoc troubleshooting.
## Recommended patterns
### Public API projects
Recommended shape:
- expose OpenAPI intentionally
- add `meta.openapi` summaries for meaningful endpoints
- verify the spec against the current collection model
### Operated production projects
Recommended shape:
- metrics endpoint reachable in the target environment
- at least one documented Grafana/Prometheus use case for request timing
- explicit decision whether Sentry is used or intentionally not used
### Basic website deployments
Recommended shape:
- website/admin/API reachability checks are part of deploy verification
- observability is documented enough that later operators know what exists
## Anti-patterns
- treating observability as optional once the build passes
- exposing OpenAPI or metrics accidentally without deciding who uses them
- half-configured Sentry with no useful environment or release handling
- relying on manual browser clicks as the only production health check
## Verification checklist
After observability-related work, verify all of these:
1. intended OpenAPI exposure works and reflects the current collection config
2. intended metrics exposure works in the target environment
3. Sentry/error visibility is either intentionally configured or intentionally absent
4. deploy-time reachability checks cover website, admin, and API
5. `yarn build`, `yarn build:server`, and `yarn validate` still pass when observability wiring touched frontend/server config
## What an LLM should inspect first
When asked to set up monitoring or observability on this starter, inspect in this order:
1. `tibi-server/docs/13-openapi-metrics.md`
2. `tibi-server/docs/02-configuration.md`
3. deploy scripts and env/config files
4. `frontend/src/config.ts`
5. whether the project truly needs OpenAPI, metrics, Sentry, or only reachability checks
This prevents over-documenting features that are not actually wired and under-documenting the ones that matter operationally.
@@ -0,0 +1,150 @@
---
name: multi-tenancy-and-orgs
description: Model org/team-aware tibi projects. Covers the org-team-user hierarchy, visibility vs working rights, permission resolution, project assignment, audit visibility, and how later agents should make an explicit single-tenant vs org/team decision.
---
# multi-tenancy-and-orgs
## When to use this skill
Use this skill when:
- a project might need org/team-aware isolation or working rights
- multiple organizations or departments share one tibi installation
- project visibility and editing rights must be separated cleanly
- LLM budget ownership or audit visibility must follow org boundaries
## Goal
Give later agents a concrete implementation workflow for enterprise-aware projects and a clear off-ramp for single-tenant projects.
The most important decision is not how to model orgs. It is whether the project should use org/team features at all.
## Source of truth
Use these sources when implementing or reviewing org/team-aware projects:
- `tibi-server/docs/18-orgs-teams.md`
- `tibi-server/docs/05-authentication.md`
- `tibi-server/docs/17-field-level-permissions.md`
- `.agents/skills/permissions-and-editor-workflows/SKILL.md`
- `.agents/skills/search-and-embeddings/SKILL.md` when LLM budget or shared AI/search budgets matter
## First decision: single-tenant or org/team-aware
Make this decision explicitly.
### Single-tenant projects
Recommended shape:
- do not model org/team support by default
- document that visibility and permissions are handled without enterprise isolation
- keep the project simpler unless there is a real multi-tenant requirement
### Org/team-aware projects
Use this branch only when you actually need:
- org-scoped project visibility
- team-based working rights
- cross-user governance inside shared organizations
- org-level budget ownership or audit visibility constraints
## Core model
The upstream hierarchy is:
- org = visibility boundary
- team = working-rights unit
- user = member of orgs and teams
Key fields include:
- `project.orgId`
- `project.teams[]`
- `user.orgs[]`
- `user.teams[]`
- `user.primaryOrgId`
Do not flatten these concepts into a generic “role” discussion. Org visibility and team working rights are different concerns.
## Visibility vs working rights
Important rule:
- org membership controls which projects a user can see
- team assignment controls which permission sets a user gets inside those visible projects
This is the main conceptual boundary later agents must keep intact.
If a user can see a project but cannot edit it, that can be correct. Visibility is not the same as edit access.
## Permission resolution
Team permissions map onto collection permission keys.
Example idea:
- team carries `permissions: ["editor"]`
- collection defines an `editor` permission set
- assigned team members inherit that working-rights set on the project
Also important:
- multiple team permissions merge as a union
- admin tokens and system admins still sit above team-based resolution
- token/public/user/team/custom permission order matters
## Project assignment rules
For org/team-aware projects, later agents must design all of these deliberately:
- which org owns the project
- which teams are assigned to the project
- who is allowed to manage those assignments
- how custom collection permission keys map to teams
Half-modeled org/team setups create the worst of both worlds: extra complexity without trustworthy isolation.
## Audit and LLM implications
Enterprise modeling affects more than CRUD visibility.
Relevant side effects:
- audit visibility follows the team → project → collection access chain
- non-admin users do not see system audit entries
- LLM/org budget logic uses `user.primaryOrgId`
That means enterprise design can affect audit reviews and AI cost ownership even if the public website itself looks simple.
## Anti-patterns
- enabling org/team concepts “just in case”
- using teams as visibility boundaries instead of orgs
- mixing custom collection permissions and team permissions without naming discipline
- forgetting that users can belong to multiple teams
- introducing org-aware billing or audit expectations without modeling `primaryOrgId`
## Verification checklist
After org/team-related changes, verify all of these:
1. the project explicitly states single-tenant or org/team-aware
2. org ownership and team assignment are defined for enterprise projects
3. collection permission keys map cleanly to team permissions
4. representative visibility and edit-rights scenarios behave as designed
5. audit and LLM budget implications are understood when those features are in scope
## What an LLM should inspect first
When asked whether a project needs enterprise features, inspect in this order:
1. the project's actual tenancy requirement
2. `tibi-server/docs/18-orgs-teams.md`
3. current permission model in collection configs
4. whether audit visibility or LLM budgets depend on org ownership
5. whether the simpler single-tenant branch is the right answer
This prevents unnecessary enterprise complexity in projects that only need normal editorial permissions.
@@ -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/types/admin.d.ts`
- `tibi-admin-nova/docs/collection-config.md`
- `api/config.yml`
- the project's actual Nova runtime config when such a file exists
## 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,195 @@
---
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/types/admin.d.ts`
- `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/types/admin.d.ts` plus `tibi-admin-nova/docs/collection-config.md` for `viewHint.navigation`
3. the frontend navigation loading/rendering path
4. SSR assumptions around header/footer shell data
5. the website's language and information-architecture requirements
This prevents navigation edits that are technically valid but editorially or runtime-wise incoherent.
@@ -0,0 +1,581 @@
---
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
In current Tibi/Nova projects, the pagebuilder registry is typically not implicit. Nova loads it from the admin bundle via `meta.pagebuilder.blockRegistry.file`.
The concrete chain is:
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 concrete file names vary by project, but the pattern is the same: registry code lives in the admin bundle and collection config points to the built admin asset.
Typical starter pattern:
```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 setup:
- the registry module must be part of the admin bundle, not a random standalone file outside the build pipeline
- the exported registry keys must match the block type values stored in the collection
- 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
- For medialib-based images, prefer the same shared frontend widget used on the public site rather than preview-only URL logic. The widget/helper must honor `apiBaseOverride` so filter URLs, placeholders, and medialib files keep working inside admin preview.
- Nova's `render(container, row, context)` provides API path information. **`context.projectBase`** contains the full project-specific API base including namespace. `context.apiBase` may only contain the generic `/api/` root and is often not sufficient for project-scoped collection endpoints. Blocks that load collection data in preview should therefore use the project-specific base when one is available:
```ts
import { apiBaseOverride } from "./lib/store"
import { get } from "svelte/store"
const prev = get(apiBaseOverride)
if (context?.projectBase) apiBaseOverride.set(String(context.projectBase))
// mount(BlockRenderer, ...)
// in destroy(): apiBaseOverride.set(prev)
```
When a pagebuilder block renders images from medialib, prefer this pattern:
1. request lookup-resolved medialib entries in the data load
2. pass the resolved entry into the shared image widget
3. let that widget decide filter sizing from explicit `filter` or `minWidth`
4. rely on `apiBaseOverride` / `context.projectBase` for admin-safe URL resolution
Do not create a second, preview-only image rendering path that diverges from the public frontend. That usually causes broken placeholders, wrong filter URLs, or SSR/admin mismatches later.
Use collection-level `meta.pagebuilder.blockRegistry.file` when several pagebuilder fields share the same registry. Override at field level only when one field genuinely needs a different registry.
### 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
### 7a. Admin pagebuilder preview: CSS custom properties in shadow DOM
The Nova pagebuilder renders block previews in an isolated DOM context (shadow DOM or detached subtree). Tailwind 4's `@theme` directive generates CSS custom properties on `:root`, but these do **not** cascade into shadow DOM contexts.
**Consequence:** Block previews in the admin can have wrong colors (light text instead of dark, missing brand colors) because `var(--color-ink)` resolves to nothing.
**Fix:** Add a `:host` selector in the project's CSS file that redeclares the theme variables for the shadow DOM context. Also set a hardcoded `color` fallback on `[data-admin-preview]` since the Nova preview container has this attribute.
```css
:host,
[data-admin-preview] {
--color-ink: #2c3e45;
/* … all theme color variables … */
font-family: "Inter Tight", system-ui, sans-serif;
color: #2c3e45; /* hardcoded fallback, not var() */
}
```
Verify by checking admin pagebuilder block preview after any CSS theme changes.
### 7b. Admin pagebuilder preview: API calls from dynamic blocks
Blocks that load data via API (e.g. `CategoryGridBlock` using `getCachedEntries`) need the correct API base URL in the admin preview. The Nova pagebuilder's `render()` callback receives a `context` object with optional `apiBase` and `namespace`, but not all versions provide these.
The `admin.mjs` is loaded by the pagebuilder via dynamic `import()` — the URL is resolved relative to the admin page, NOT relative to the API. So `import.meta.url` contains the admin's page URL (e.g. `/_/assets/dist/admin.mjs`), not the tibi-server's API URL. Regex extraction from `import.meta.url` for the pattern `/api/_/{namespace}/` does NOT work for this reason.
**Reliable approach:** use multiple fallbacks in `admin.ts`, with the admin hostname pattern as the most robust:
1. `context.apiBase` (from Nova when available)
2. `context.namespace` (from Nova)
3. `import.meta.url` regex (works when admin serves admin.mjs through its own API proxy)
4. **Hostname extraction**: admin URL is `{project}-tibiadmin.{domain}` → extract project name
5. DOM scan: find any element with `src`/`href` containing `/api/_/{namespace}/`
```ts
const prevApiBase = get(apiBaseOverride)
let ns: string | null = null
if (context?.apiBase) {
apiBaseOverride.set(String(context.apiBase))
} else {
if (context?.namespace) ns = String(context.namespace)
if (!ns) {
try {
ns = ((import.meta as any).url || "").match(/\/api\/_\/([^/]+)\//)?.[1]
} catch {}
}
// Most reliable: admin hostname is always {namespace}-tibiadmin.{domain}
if (!ns && typeof window !== "undefined") {
const h = window.location.hostname.match(/^(.+?)-tibiadmin\./)
if (h) ns = h[1]
}
// Fallback: scan DOM for API references
if (!ns && typeof document !== "undefined") {
const el = document.querySelector('[src*="/api/_/"], [href*="/api/_/"]')
if (el) {
const a = el.getAttribute("src") || el.getAttribute("href") || ""
ns = a.match(/\/api\/_\/([^/]+)\//)?.[1] || null
}
}
if (ns) apiBaseOverride.set(`/api/_/${ns}/`)
}
```
Set the `apiBaseOverride` store BEFORE mounting the block component so API calls inside `$effect` use the correct base.
The Nova pagebuilder renders block previews in an isolated DOM context (shadow DOM or detached subtree). Tailwind 4's `@theme` directive generates CSS custom properties on `:root`, but these do **not** cascade into shadow DOM contexts.
**Consequence:** Block previews in the admin can have wrong colors (light text instead of dark, missing brand colors) because `var(--color-ink)` resolves to nothing.
**Fix:** Add a `:host` selector in the project's CSS file that redeclares the theme variables for the shadow DOM context. Also set a hardcoded `color` fallback on `[data-admin-preview]` since the Nova preview container has this attribute.
```css
:host,
[data-admin-preview] {
--color-ink: #2c3e45;
--color-ink-2: #3a4d56;
/* … all theme color variables … */
font-family: "Inter Tight", system-ui, sans-serif;
color: #2c3e45; /* hardcoded fallback, not var() */
}
```
Verify by checking admin pagebuilder block preview after any CSS theme changes.
### 8. Prepare for styling consistency across blocks
A block system works better when blocks share a few stable layout conventions.
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,243 @@
---
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”.
**Custom role names:** Permission set keys in collection/action YAML are arbitrary strings. You can define any role name (e.g. `editor`, `reviewer`, `publisher`, `seo-manager`) and assign users with matching permissions. Combined with org/team membership (see `tibi-server/docs/18-orgs-teams.md`), this enables fine-grained editorial workflows beyond the built-in `public` and `user` roles.
### The 3-layer cascade model
Field-level permissions follow a strict 3-layer cascade:
1. **Collection-Level** (`collection.readonlyFields`, `collection.hiddenFields`): Base set applied to all permission sets.
2. **PermissionSet-Level** (`permissions.<role>.readonlyFields`, `permissions.<role>.hiddenFields`): Adds to or removes from the collection-level set. Prefix a field with `-` to negate (e.g. `-createdBy` removes it from the effective set).
3. **Field-Definition Override** (`field.readonly`, `field.hidden`): Absolute override — `true` forces the field into the set, `false` forces it out regardless of upper layers.
**Important:** Field-definition `readonly`/`hidden` also supports **eval expressions** (JS) for per-document dynamic evaluation. Eval rules are evaluated in a separate phase after the static cascade (Phase 1 = static cascade, Phase 2 = per-document eval). Admin role (role=0) bypasses all field-level restrictions.
See `tibi-server/docs/17-field-level-permissions.md` for the full reference with examples and eval expression context variables (`$`, `$this`, `$auth`, `$method`, `$project`, `$namespace`).
## 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.
## Config delivery matters to the admin UX
Field permissions also affect what the client receives from the project config.
Important behavior for non-admin users:
- effective readonly information is exposed through `yourPermissions[collection].readonlyFields`
- statically hidden field definitions are removed from `fields[]`
- `hiddenFields` arrays are not delivered as-is to non-admin clients
- eval-based field rules stay relevant because they depend on document context
Implication:
- Nova and other clients should reflect the real config/permission output instead of pretending every field is always present and editable
If later agents debug “missing” fields in the admin, check permission-shaped config delivery before assuming the admin UI is broken.
## Dynamic field rules
Use eval-based field rules when permissions depend on document state.
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.
For each eval-based rule, later agents should be able to name:
- one allowed write scenario
- one denied write scenario
- the document state that flips the rule
## Admin UX must reflect permission reality
If a field is hidden or readonly for a role, the Nova configuration and layout should support that reality.
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.
## Permission matrix before YAML
Before writing or changing permission sets, write down a small matrix for the real actors.
Typical matrix columns:
- actor or role
- collections they can read
- collections they can write
- fields hidden from them
- fields readonly for them
- machine/token access they need
Typical actors:
- public
- editor
- reviewer or publisher
- admin
- machine token or integration
This avoids permission YAML that is locally correct but globally incoherent.
## Tokens and integrations
Remember that token-based integrations can have their own permission sets.
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. At least one representative allowed write and one denied write were checked for each important workflow state.
5. Non-admin config delivery still makes sense for the admin UI and field layout.
6. Nova forms remain usable for the non-admin roles that actually work there.
7. Token/integration permissions are narrower than admin access when possible.
8. `yarn validate` stays clean.
## What an LLM should inspect first
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.
+546
View File
@@ -0,0 +1,546 @@
---
name: playwright-testing
description: Build, debug, and extend the current Playwright test setup for API, desktop E2E, mobile E2E, and visual checks. Use when changing tests, seeding deterministic content, or validating frontend/API behavior against the reverse-proxied CODING_URL.
---
# playwright-testing
## When to use this skill
Use this skill when:
- Adding or updating Playwright API, E2E, mobile, or visual tests
- Debugging failing tests in `tests/`
- Extending deterministic seed data for frontend or API coverage
- Verifying how `CODING_URL`, `ADMIN_TOKEN`, and collection permissions affect tests
- Deciding where a frontend assertion belongs: API spec, E2E spec, or visual regression spec
---
## Current test architecture
This starter uses Playwright across four slices:
- `tests/api/` for API-level checks
- `tests/e2e/` for desktop browser behavior
- `tests/e2e-admin/` for committed admin smoke coverage
- `tests/e2e-mobile/` for mobile behavior
- `tests/e2e-visual/` for screenshot-based regression tests
The current baseline is deterministic and seed-driven, not demo-content-driven.
### Core files
| File | Responsibility |
| ----------------------------------- | -------------------------------------------------------------------------------- |
| `playwright.config.ts` | Playwright projects, `baseURL`, global setup/teardown, BrowserSync-safe defaults |
| `tests/fixtures/test-constants.ts` | `ADMIN_TOKEN`, `API_BASE`, `TEST_BASE_URL`, seeded route constants |
| `tests/global-setup.ts` | Verifies `TEST_BASE_URL`, probes `/api`, seeds deterministic content |
| `tests/global-teardown.ts` | Cleans seeded content and disposes shared API contexts |
| `tests/api/helpers/admin-api.ts` | Shared admin CRUD helper using the static `Token:` header |
| `tests/api/helpers/seed-data.ts` | Seed definitions and seed cleanup for deterministic content pages |
| `tests/fixtures/console-monitor.ts` | Fails browser-based tests on unexpected page, console, or request errors |
| `tests/e2e/fixtures.ts` | Desktop browser fixtures and SPA helpers |
| `tests/e2e-admin/fixtures.ts` | Admin login helpers and admin smoke fixture setup |
| `tests/e2e-mobile/fixtures.ts` | Mobile browser fixtures and hamburger-menu helpers |
---
## Environment prerequisites
### Always use the configured `CODING_URL`
Playwright uses `TEST_BASE_URL` from `tests/fixtures/test-constants.ts`.
Resolution order:
1. `process.env.CODING_URL`
2. `CODING_URL` from `.env`
3. fallback `http://localhost:3000`
For this project, prefer the reverse-proxied `CODING_URL` from `.env` whenever it serves both:
- `/`
- `/api/...`
If `/api/...` returns HTML instead of JSON, the seeded setup is not usable and `globalSetup` should fail fast.
### Admin host and default credentials
Admin browser tests use `TEST_ADMIN_BASE_URL` from `tests/fixtures/test-constants.ts`.
Resolution order:
1. `process.env.CODING_TIBIADMIN_URL`
2. `CODING_TIBIADMIN_URL` from `.env`
3. fallback `http://localhost:3000`
The current smoke setup assumes the default dev login unless overridden via env vars:
- `ADMIN_UI_USERNAME` default: `admin`
- `ADMIN_UI_PASSWORD` default: `admin`
Keep this only for local/dev smoke coverage. Do not turn production credentials into committed test defaults.
### Static project token vs JWT user auth
This distinction matters for tests:
- `Token:` is the static project/admin token from `api/config.yml.env`
- `X-Auth-Token` is a JWT user token from a login flow
Collection permissions under `user:` do **not** grant access for static `Token:` requests.
If tests seed or mutate a collection through `ADMIN_TOKEN`, that collection must define explicit token permissions like:
```yaml
permissions:
"token:${ADMIN_TOKEN}":
methods:
get: true
post: true
put: true
delete: true
```
This is required for collections the seed helper writes to, such as `content`.
---
## BrowserSync navigation rule
BrowserSync keeps a WebSocket open permanently. Because of that:
- do **not** wait for `networkidle`
- do **not** rely on `load`
- use `domcontentloaded`
The shared fixtures already patch navigation helpers accordingly.
When writing new tests, keep using the project fixtures rather than raw Playwright `test`.
## Console watcher
Browser-based fixtures attach `attachConsoleMonitor(page)` from `tests/fixtures/console-monitor.ts`.
This monitor records and fails tests on unexpected:
- `pageerror`
- `console.error`
- failed network requests except explicitly ignored infrastructure noise
The intent is to catch real frontend/runtime regressions even when visible assertions still pass.
Do not silence app bugs by broadening ignored patterns unless the noise is clearly external infrastructure.
---
## Deterministic seed strategy
The current setup seeds content through the public collection API plus the static `Token:` header.
Use a hidden per-collection marker field as the default seed identity strategy.
In this project the convention is `_testdata: true`.
### Seed lifecycle
1. `globalSetup` probes the configured base URL.
2. `globalSetup` verifies `/api/content` returns JSON.
3. `globalSetup` removes old seeded entries by their hidden test marker before recreating them.
4. `globalSetup` creates deterministic seed entries.
5. Tests run against those seeded routes.
6. `globalTeardown` removes seeded entries again.
Setup cleanup and teardown cleanup are both required.
The setup cleanup handles leftovers from aborted or previously failed runs.
The teardown cleanup keeps the environment clean after successful runs.
### Hidden seed marker pattern
Prefer this pattern for every collection that may receive test-created data:
1. Add a hidden boolean field named `_testdata` as the last field in the collection schema.
2. Set `_testdata: true` on every seeded entry.
3. Let cleanup match `_testdata === true` first.
4. Keep older identifiers such as fixed paths or translation keys only as migration fallbacks when existing seed data already used them.
This is more robust than relying on translation keys because not every collection has a natural grouping field.
It also makes leftovers from aborted runs discoverable across heterogeneous collection shapes.
### Parallel worker rule
Seed creation and seed cleanup must remain run-scoped, not worker-scoped.
- perform seed cleanup and creation in `globalSetup`
- perform final seed cleanup in `globalTeardown`
- do not create or delete shared seeded data in per-test hooks or worker fixtures
- keep seeded identifiers deterministic so many workers can read the same seeded dataset safely
This project runs with many workers.
Parallel safety depends on one shared deterministic seed pass before the suite and one shared cleanup pass after the suite, not on each worker mutating shared fixtures independently.
### Current seeded routes
Defined in `tests/fixtures/test-constants.ts`:
- `SEEDED_TEST_CONTENT.home.path`
- `SEEDED_TEST_CONTENT.contact.path`
- `SEEDED_TEST_CONTENT.inactive.path`
These are backed by DE/EN content entries in `tests/api/helpers/seed-data.ts`.
### What the seed currently covers
- localized page routing
- hero rendering
- features rendering
- richtext rendering
- accordion rendering
- contact-form rendering
- inactive route -> 404 behavior
When adding new deterministic coverage, extend the seed data instead of asserting against editorial demo content.
---
## Which test type to use
## Checklist-facing minimum contract for derived projects
When this starter is used to build a real website project, the testing layer should usually cover these contracts explicitly:
1. deterministic seed setup for the data the suite depends on
2. API smoke coverage for public reads and important write/action behavior
3. desktop E2E coverage for core public journeys such as homepage, navigation, language switching, and 404 behavior
4. admin smoke coverage for stable collection/admin contracts
5. pagebuilder registry plus real preview rendering when the project uses block-based authoring
6. SSR validation through direct endpoint checks, and committed tests where the SSR contract is central and stable enough
Do not treat the test suite as an optional polish step. It is one of the delivery contracts of the project.
### API tests
Use `tests/api/` when validating:
- collection filters
- public vs token-backed API behavior
- seeded content presence or absence
- mutation semantics independent of DOM rendering
Keep them narrow and data-oriented.
### Desktop E2E tests
Use `tests/e2e/` when validating:
- route changes
- language switching
- SPA navigation behavior
- block rendering in the real UI
- keyboard/a11y interactions such as skip links
### Admin smoke tests
Use `tests/e2e-admin/` when validating stable admin contracts such as:
- admin login still works in dev
- the project dashboard opens correctly
- core collections are still reachable
- critical collection views still render their configured labels/columns/actions
- collection lists render meaningful previews instead of broken placeholders
- important field widgets are configured and usable in entry forms
- pagebuilder block choosers, block forms, and live previews load correctly
These tests should stay intentionally narrow. They are regression guards for admin configuration, not full editor journey automation.
### Mobile E2E tests
Use `tests/e2e-mobile/` when validating:
- hamburger menu behavior
- responsive visibility
- mobile-specific navigation or controls
### Visual regression tests
Use `tests/e2e-visual/` only when layout/styling stability matters and a semantic DOM assertion is not enough.
## SSR validation placement
Do not try to prove every SSR property only through browser navigation.
Use direct SSR endpoint checks when:
- validating route acceptance and canonicalization
- validating SSR HTML content
- validating cache-hit / cache-miss behavior
- validating publication-window effects or cache invalidation after mutations
Use committed API/E2E tests when:
- the SSR-related behavior is stable enough to be a long-lived regression contract
- the project depends heavily on SSR for page-critical content
- a browser-level journey would otherwise hide SSR-specific regressions
Preferred rule:
- infrastructure-like SSR checks start as direct endpoint checks
- promote them into committed tests when the behavior is important and deterministic enough
## Admin config coverage strategy
Use a hybrid approach:
- committed Playwright smoke tests for stable, repeatable admin contracts
- one-shot MCP Playwright or VS Code browser checks for exploratory spot checks and ad-hoc audits
Committed tests should cover the admin paths that are expected to stay valid across everyday work, for example:
- login
- opening the Nova project dashboard
- visibility of the core collections
- opening important collection views like `content`
- checking that collection tables expose the intended columns, summaries, and preview thumbnails
- checking that key widgets like selects, foreign/media pickers, sidebars, and pagebuilder controls actually render
- checking that pagebuilder preview updates when block content changes
One-shot live browser checks are useful when:
- reviewing a newly added admin configuration once
- probing a flaky or hard-to-stabilize UI area before deciding what deserves a real test
- checking something highly visual or temporarily environment-specific
Do not rely on one-shot browser checks as the only safeguard for important admin paths. If a check matters repeatedly, promote it into `tests/e2e-admin/`.
---
## Current fixture conventions
### API
Use `tests/api/fixtures.ts`.
Current fixtures:
- `api`
- `adminApi`
`adminApi` is backed by the static `Token:` header from `ADMIN_TOKEN`.
### Desktop E2E
Use `tests/e2e/fixtures.ts`.
Helpers include:
- `waitForSpaReady(page)`
- `navigateToRoute(page, routePath)`
- `clickSpaLink(page, selector)`
- automatic console/page/request error monitoring via `attachConsoleMonitor(page)`
### Admin E2E
Use `tests/e2e-admin/fixtures.ts`.
Helpers include:
- `loginToAdmin(page)`
- `openNovaProjectDashboard(page)`
- automatic console/page/request error monitoring via `attachConsoleMonitor(page)`
### Mobile E2E
Use `tests/e2e-mobile/fixtures.ts`.
Helpers include:
- `waitForSpaReady(page)`
- `navigateToRoute(page, routePath)`
- `openHamburgerMenu(page)`
- `closeHamburgerMenuViaEscape(page)`
- automatic console/page/request error monitoring via `attachConsoleMonitor(page)`
### Visual E2E
Use `tests/e2e-visual/fixtures.ts`.
Helpers include:
- `waitForVisualReady(page)`
- `prepareForScreenshot(page)`
- `expectScreenshot(page, name, opts)`
- automatic console/page/request error monitoring via `attachConsoleMonitor(page)`
Do not reintroduce the old starter `authedPage` / `testUser` assumptions unless the project really needs JWT-user coverage again.
---
## Writing stable selectors
Prefer selectors that reflect current rendered structure and locale:
- scope language links to the relevant container (`header`, `main`, footer)
- avoid ambiguous `getByRole()` selectors when the same link text appears twice
- use the actual locale strings from `frontend/src/lib/i18n/locales/*.json`
- prefer stable block markers like `data-block="hero"`
Examples:
```ts
const header = page.locator("header")
await header.getByRole("link", { name: "en", exact: true }).click()
const homeLink = page.locator("main").getByRole("link", { name: "Zur Startseite" })
```
---
## Recommended commands
Run only the slice you changed.
```bash
/usr/bin/node ./node_modules/playwright/cli.js test tests/api/health.spec.ts --project=api
/usr/bin/node ./node_modules/playwright/cli.js test tests/e2e/home.spec.ts tests/e2e/demo.spec.ts --project=chromium
/usr/bin/node ./node_modules/playwright/cli.js test tests/e2e-admin/smoke.spec.ts --project=admin
/usr/bin/node ./node_modules/playwright/cli.js test tests/e2e-mobile/home.mobile.spec.ts --project=mobile-iphonese
```
If the shell environment is broken, calling the Playwright CLI through `/usr/bin/node` is acceptable in this workspace.
---
## Common failure patterns
### `/api/...` returns HTML
Cause:
- wrong `CODING_URL`
- fallback to BrowserSync without usable API proxy
Fix:
- verify the configured `CODING_URL` serves both `/` and `/api/...`
### `403 {"error":"empty token"}` on POST/PUT/DELETE
Cause:
- collection allows `user:` but not `"token:${ADMIN_TOKEN}":`
Fix:
- add explicit token permissions on that collection
### strict mode violations in role selectors
Cause:
- multiple matching links in header/footer/mobile menu
Fix:
- scope selectors to the intended container
- use `exact: true` where needed
### 404 assertions fail by link text
Cause:
- locale-specific text mismatch
Fix:
- use the real translated string from the locale JSON files
---
## Change workflow
When extending or fixing tests:
1. Start from the failing spec or the exact behavior to cover.
2. Check whether the needed content already exists in `seed-data.ts`.
3. Extend seed data only if the behavior is not already representable.
4. Run only the affected Playwright project/spec files.
5. Fix selectors or seed shape before widening scope.
Keep the test basis deterministic. Do not fall back to existing editorial demo content just because it is already present in the database.
## Admin E2E: Boot abwarten
Admin-SPA lädt Chunks asynchron. Vor Interaktionen auf sichtbares Login-Formular warten:
```ts
await page.goto("/login")
await expect(page.getByLabel(/Benutzername|Username/i)).toBeVisible({ timeout: 20000 })
```
Danach erst fill/click das Formular erscheint erst wenn die App vollständig gebootet ist.
## MailDev E-Mail-Testing
MailDev läuft im Docker-Stack (SMTP Port 25, Web-API Port 1080). Die REST-API erlaubt E-Mails zu lesen und zu löschen:
```ts
const MAILDEV = "https://{project}-maildev.code.testversion.online"
// Alle E-Mails abrufen
const res = await request.get(`${MAILDEV}/email`)
const emails = await res.json()
// Alle löschen (vor Test)
await request.delete(`${MAILDEV}/email/all`)
```
### Polling-Pattern für asynchrone E-Mails
Formular → Action-Hook sendet E-Mail via `context.smtp.sendMail()`. MailDev braucht Zeit zum Verarbeiten:
```ts
// Nach Form-Submit auf E-Mails warten
for (let i = 0; i < 15; i++) {
await new Promise((r) => setTimeout(r, 1000))
const res = await request.get(`${MAILDEV}/email`)
if (res.ok()) {
const emails = await res.json()
if (emails.length >= 2) break // Kunde + Betreiber
}
}
emails.find((e) => e.to.some((t) => t.address === "kunde@test.de"))
```
Wichtig: Tests mit MailDev müssen sequentiell laufen (`--workers=1`), da parallele Tests sich gegenseitig die MailDev-Inbox überschreiben.
## Admin pagebuilder registry coverage
For starter-like projects, committed admin coverage should include both sides of the pagebuilder contract:
1. registry/chooser coverage on a new entry form
2. actual preview rendering on an existing seeded entry
The second check is important because it catches failures that the chooser alone does not see:
- broken `meta.pagebuilder.blockRegistry.file` wiring
- preview components that no longer mount through the shared block renderer
- missing `_lookup` hydration for foreign media fields
- image widgets that work on the public site but fail in admin preview because the API base or URL resolution is wrong
Preferred starter pattern:
- seed one deterministic medialib image through the collection API
- seed one deterministic content entry that references that image in at least one pagebuilder block
- open that entry in `tests/e2e-admin/pagebuilder.spec.ts`
- assert both block text and `img[data-entry-id]` preview rendering
Keep this test generic. Do not tie it to customer-specific block sets unless the project has already diverged from the starter pattern.
## Delivery-checklist alignment
When using this skill together with `.agents/BUILD_CHECKLIST.md`, the testing phase should leave behind explicit evidence for:
- which specs were run
- which seed data was extended or reused
- whether admin smoke coverage exists for the configured collections
- whether pagebuilder preview rendering is covered when pagebuilder is in scope
- whether SSR was verified by direct endpoint checks, committed tests, or both
If that evidence only exists in chat history and not in the repo or task notes, the testing work is too fragile for later agents.
@@ -0,0 +1,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,238 @@
---
name: search-and-embeddings
description: Model search and semantic retrieval for tibi website projects. Covers embedding provider configuration, collection search modes, auto-regeneration, regenerate-search admin flows, and how later agents should decide between no search, classic search, ngram search, and vector search.
---
# search-and-embeddings
## When to use this skill
Use this skill when:
- a project needs explicit search behavior beyond generic CRUD filtering
- search should be typo-tolerant, weighted, or semantic
- embedding providers must be configured
- later agents need a clear yes/no decision for search instead of vague optionality
## Goal
Give later agents a practical workflow for deciding whether search is needed and, if yes, which search mode belongs to the project.
This skill is separate from editor AI features. Search and embeddings affect content retrieval, operational setup, and index/regeneration behavior, not just editor assistance.
## Source of truth
Use these sources when implementing or reviewing search behavior:
- `tibi-server/docs/02-configuration.md`
- `tibi-server/docs/04-collections.md`
- `tibi-server/docs/09-llm-integration.md`
- `.agents/skills/nova-ai-editor-features/SKILL.md`
- `.agents/skills/mongodb-and-indexes/SKILL.md`
## First decision: no search vs explicit search
Do not leave search in an implied state.
Make one explicit decision:
- no search in this project
- classic keyword search only
- fuzzy substring search (`ngram`)
- semantic/vector search
- hybrid search with deliberate ranking behavior
If the answer is “not used”, document that clearly so later agents do not accidentally wire providers or regress into half-configured search.
## Server-level provider setup
Embedding providers are configured server-side:
```yaml
embedding:
providers:
- name: bge-m3
type: native
modelPath: /models/bge-m3
dimensions: 1024
- name: openai-embed
type: openai
model: text-embedding-3-small
apiKey: ${EMBEDDING_OPENAI-EMBED_APIKEY}
baseURL: https://api.openai.com/v1
dimensions: 1536
```
Important:
- collection search config references the provider by name
- embedding secrets and model paths can come from environment variables
- vector search is not only a collection concern; the server must actually provide the embedding backend
## Collection search modes
Tibi supports multiple search modes via collection `search:` config:
- `text`
- `regex`
- `eval`
- `filter`
- `ngram`
- `vector`
- `combined`
Use explicit search configs when search is a real product feature. Auto-fallback is useful, but it is not a substitute for a deliberate retrieval model.
## Choosing the right mode
### `text`
Use when:
- MongoDB text indexing is sufficient
- exact field ownership of the text index is clear
- keyword search is enough
Requires a MongoDB text index (`$text: $**` or specific).
### `regex`
Use when:
- the searchable fields are explicit
- case-insensitive matching is enough
- weighted field scoring is useful (via `regex.weights: { "meta.title": 10, path: 5 }`)
Good for smaller datasets or precise keyed fields. Very easy to configure without external dependencies. Example:
```yaml
search:
- name: default
mode: regex
fields: [title, "alt.de", description]
```
### `filter` or `eval`
Use when:
- search logic depends on auth, project context, or business-specific filtering
- plain keyword matching is not the full contract
Treat these as controlled power tools. The resulting filters are still sanitized against blocked operators.
### `ngram`
Use when:
- typo tolerance or substring matching is needed
- users search codes, names, transliterated terms, or partial inputs
This is enrichment-based search. It stores generated `_search` data and benefits from clear regeneration expectations.
_Note:_ Field weighting is not natively supported inside a single `ngram` mode, because all `fields` are concatenated into one large ngram index block per document.
### `vector`
Use when:
- semantic similarity matters more than literal keyword overlap
- the project can support embedding-provider setup (e.g. `bge-m3` in `api/config.yml`)
- search quality justifies added complexity
Vector mode requires a registered provider.
### `combined` (RRF)
Use when:
- Hybrid search is required (e.g. `vector` + `ngram` to catch typos and semantic meaning).
- You need to simulate field-weighting for `vector` or `ngram` by breaking them up into multiple search blocks and fusing them with different weights.
`mode: combined` uses Reciprocal Rank Fusion (RRF). It delegates execution to other configured search blocks (which should be hidden in admin UI via `meta.hide: true`).
**Field-Weighting Workaround with combined:**
Because `vector` and `ngram` concatenate all fields, you can weight highly important fields (like titles) higher than deep content fields by creating multiple ngram/vector blocks and boosting the important one in the `combined` weights:
```yaml
search:
- name: main_search
mode: combined
rrf:
k: 60
topK: 100
weights:
semantic: 1.5
fuzzy_important: 2.0 # Boosts matches in title/headline
fuzzy_content: 0.5 # Lowers weight for deep text matches
meta:
label: { de: "Suche", en: "Search" }
- name: fuzzy_important
mode: ngram
fields: [name, "meta.title", "blocks.headline"]
autoRegenerate: true
meta: { hide: true }
- name: fuzzy_content
mode: ngram
fields: ["blocks.text", "blocks.items.answer"]
autoRegenerate: true
meta: { hide: true }
- name: semantic
mode: vector
fields: [name, "meta.title", "blocks.text"]
vector: { provider: bge-m3 }
autoRegenerate: true
```
## Auto-regeneration and admin flows
For `ngram` and `vector`, `autoRegenerate: true` can refresh stale enrichment data after config changes.
If regeneration is needed manually, the admin flow depends on project admin tokens with:
- `allowRegenerateSearch: true`
Treat regeneration as part of the search contract, not as an implementation footnote.
## Search and LLM are related but not identical
The LLM system and the embedding system are adjacent, but they are not the same thing.
- `llm.providers` drive chat/completion features
- `embedding.providers` drive vector search enrichment
- org/user budgets affect LLM usage workflows
- search design still needs its own retrieval and operator decisions
Do not assume that enabling editor AI automatically defines a sound search architecture.
## Anti-patterns
- leaving search unspecified and hoping auto-fallback is “good enough”
- enabling vector search without a real provider/runtime plan
- forgetting text indexes for `mode: text`
- enabling enrichment modes without a regeneration story
- mixing editor AI decisions with search decisions until neither is clear
## Verification checklist
After search-related changes, verify all of these:
1. the project has an explicit yes/no search decision
2. server-side embedding providers exist when vector search is configured
3. required text or search indexes exist
4. `?q=` and `?qName=` behavior matches the intended search contract
5. regeneration behavior is defined for enrichment-based modes
## What an LLM should inspect first
When asked to add or review search on this starter, inspect in this order:
1. `tibi-server/docs/04-collections.md`
2. `tibi-server/docs/02-configuration.md`
3. existing collection `search:` config
4. whether the project needs keyword, fuzzy, semantic, or no search
5. operator expectations for regeneration and provider secrets
This prevents over-engineered vector setups and under-specified search behavior.
@@ -0,0 +1,262 @@
---
name: security-hardening-and-token-strategy
description: Apply current tibi-server security practices to website projects. Covers token strategy, secret handling, rate limiting, bulk-permission safety, cookie settings, risky hook capabilities, and secure operator decisions for this stack.
---
# security-hardening-and-token-strategy
## When to use this skill
Use this skill when:
- setting up or reviewing authentication and token usage on this stack
- deciding how admin tokens, JWT auth, and token permissions should be used
- hardening hooks, actions, and project config against current upstream security risks
- reviewing bulk permissions, rate limiting, cookies, secrets, or risky server-side capabilities
## Goal
Keep projects on this starter aligned with the current tibi-server security model and with the security-sensitive operator decisions the stack exposes.
This is not a generic web-security primer. It is the practical security workflow for this repo family.
## Source of truth
Use these sources when implementing or reviewing security decisions:
- `tibi-server/docs/05-authentication.md`
- `tibi-server/docs/14-security.md`
- `tibi-server/docs/17-field-level-permissions.md`
- `tibi-server/docs/02-configuration.md`
- project config and collection/action permission YAML files
## Security review order
When asked to harden a project, inspect in this order:
1. secret sourcing in config/env
2. token type and scope
3. collection/action permissions
4. bulk permission exposure
5. field-level restrictions
6. rate limiting and cookie settings
7. risky hook capabilities such as outbound fetch or exec
This prevents “secure enough” changes that leave the real attack surface untouched.
## Authentication surfaces
This stack exposes multiple auth mechanisms. Do not mix them casually.
- JWT user auth
- refresh-token cookie flow
- admin tokens
- token-based permission sets for narrower machine access
Recommended default:
- use JWT user auth for real users and editor/admin sessions
- use refresh cookies for session continuation where appropriate
- use admin tokens only for system/admin/ops flows that truly need them
- use token permissions for narrow machine integrations
## Token header distinction
Use the right header for the right surface:
- system-level API such as project CRUD, admin reload, shutdown: `X-Admin-Token`
- collection-level CRUD via static project token: `Token`
- JWT-authenticated user flow: `X-Auth-Token`
Do not assume a working `Token` header implies system-level admin rights.
## Secret handling
Do not keep production secrets as committed literals if the deployment can source them from env or operator-managed secrets.
Review at minimum:
- JWT secrets
- SMTP credentials
- admin tokens
- external API keys
- LLM/embedding provider keys
If secrets are hardcoded in committed config, treat that as a structural problem, not as cleanup trivia.
## Bulk permission safety
Bulk operations are more dangerous than single-document mutations.
Important rule:
- boolean `post: true` / `put: true` / `delete: true` does not imply bulk access
- bulk requires object-form permissions with `bulk: true`
Example:
```yaml
permissions:
user:
methods:
post:
allow: true
bulk: true
```
Do not enable bulk access casually in website projects. Most editorial workflows do not need it.
## Field-level security
Security on this stack is not only collection-method based.
Review all of these layers together:
- collection methods
- `readonlyFields`
- `hiddenFields`
- field-level `readonly` / `hidden`
- eval-based field rules
- collection visibility in the admin UI
If a field should not be editable or visible, enforce that on the server. Do not rely on frontend omission.
## Rate limiting and login hardening
Current upstream tibi-server supports login rate limiting with exponential backoff.
Review these config points:
- `ratelimit.enabled`
- `ratelimit.loginInitialDelay`
- `ratelimit.loginMaxDelay`
- `ratelimit.loginResetAfter`
Security implication:
- a project may look fine in normal use while still being too soft against brute-force attempts if rate limiting is not configured as expected
For serious deployments, do not leave this unreviewed just because login works.
## Cookie and session hardening
Refresh-token flows should respect the target environment.
Review:
- `api.secureCookies`
- HTTPS vs local HTTP expectations
- whether debugging shortcuts are accidentally bleeding into production config
Do not weaken secure-cookie behavior globally just to make a dev shortcut work.
## Query-parameter token risk
Token-based permissions can be passed via query parameters in some cases, but this is a documented risk surface.
If query tokens are unavoidable:
- scope them narrowly
- avoid logging full URLs with sensitive query strings
- understand proxy, history, and referrer exposure
Prefer header-based transport whenever possible.
## Risky hook capabilities
Current tibi-server exposes powerful capabilities in hooks. Treat them as explicit design decisions, not utilities.
Particularly important:
- `context.http.fetch()` / `context.http.fetchStream()` can create SSRF risk
- `context.exec.command()` can create command-execution risk
- broad filesystem/network access in hooks should not be treated as harmless
If a feature can be implemented without shell execution or arbitrary internal fetches, prefer the safer path.
When such capabilities are used, document:
- why they are necessary
- what the allowed target surface is
- what the safer rejected alternatives were
## CORS configuration
CORS follows a hierarchy. Configure it deliberately instead of widening it reactively.
Levels:
- server-level `config.yml`
- project-level `api/config.yml`
- collection/action-level YAML overrides
For typical website projects on this starter, the default proxy setup often means no aggressive cross-origin opening is required. Add explicit CORS only when the real deployment needs external origins.
## Recommended implementation patterns
### Public form workflow
Recommended shape:
- public action with narrow methods
- server-side validation
- no admin token in the browser
- separate internal persistence only when truly required
### Integration token
Recommended shape:
- dedicated narrow token permission set
- minimal collection/action scope
- header-based transport preferred
### Sensitive internal fields
Recommended shape:
- use hidden/readonly restrictions explicitly
- keep admin UI aligned with those restrictions
- do not let previews depend on hidden-only data
### Hook that calls external systems
Recommended shape:
- fixed or validated targets
- no user-controlled arbitrary internal fetches
- no shell execution unless unavoidable
## Anti-patterns
- hardcoded production secrets in committed config
- broad admin tokens used for normal frontend or integration traffic
- bulk permissions enabled without a concrete operator need
- risky hook capabilities treated as harmless helpers
- collection security solved in the UI instead of the server
- production cookie or rate-limit settings weakened for convenience
## Verification checklist
After security-relevant changes, verify all of these:
1. secrets are sourced appropriately
2. token type matches the intended actor and scope
3. bulk permissions are not broader than necessary
4. readonly/hidden behavior is correct on the API
5. rate limiting and cookie settings match the environment
6. risky hook capabilities are constrained by design
7. `yarn validate` stays clean
## What an LLM should inspect first
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. `tibi-server/docs/17-field-level-permissions.md`
4. the relevant collection/action permission sets
5. secret sourcing in config/env
6. whether hooks use risky capabilities like outbound fetch or exec
This prevents working implementations that quietly widen the attack surface.
@@ -0,0 +1,376 @@
---
name: tibi-actions-and-forms
description: Build endpoint-style website features with tibi-server actions. Covers when to use actions instead of collections, action hook flow, validation, permissions, CORS, mail/webhook patterns, and frontend integration for forms.
---
# tibi-actions-and-forms
## When to use this skill
Use this skill when:
- Building contact forms, newsletter forms, quote requests, callbacks, or booking requests
- Adding endpoint-like backend logic without CRUD storage
- Replacing old collection hacks that only existed to accept POST requests
- Designing frontend form submissions against tibi-server actions
- Deciding whether a feature should be an action, a collection, or both
## Goal
The goal is to teach an LLM how to build website workflows that behave like real endpoints.
A form feature is complete only when all of these are coherent:
- action config
- hook flow
- validation
- permissions and CORS
- optional persistence or side effects
- frontend submission and error handling
## Source of truth
Use these sources when implementing or reviewing action-based features:
- `tibi-server/docs/19-actions.md`
- `tibi-server/docs/06-hooks.md`
- `api/config.yml`
- `api/hooks/`
- `frontend/src/lib/api.ts`
- `frontend/src/App.svelte` or the relevant frontend form surface
## Core decision: action or collection?
Use an **action** when the feature is primarily an endpoint or workflow.
Typical action cases:
- contact form
- newsletter signup
- quote request
- callback request
- webhook receiver
- utility endpoint
- AI-assisted helper endpoint
Use a **collection** when the feature is primarily stored content or stored records with CRUD semantics.
Typical collection cases:
- products
- team members
- events
- testimonials
- persisted inquiries that editors must browse/edit in admin
Use **action + collection** when you need a public workflow plus internal persistence.
Example:
- a contact form submits to an action
- the action validates, sends mail, and optionally creates an `inquiries` entry for staff follow-up
Do not fake endpoint logic with empty collections unless there is a very specific reason.
## Routing model
Actions are exposed under:
```text
POST /api/v1/_/:project/_actions/:action
GET /api/v1/_/:project/_actions/:action
```
The `_actions` prefix is part of the contract. Frontend form code should treat actions as explicit API endpoints, not as collection writes.
## Where actions are configured
Actions are declared in `api/config.yml` under `actions:` and typically point to files under:
- `api/actions/` for YAML configs
- `api/hooks/<action-name>/` for hook files
Typical config shape:
```yaml
actions:
- !include actions/contact-form.yml
```
```yaml
name: contact-form
meta:
label: { de: "Kontaktformular", en: "Contact Form" }
permissions:
public:
methods:
post: true
hooks:
post:
bind:
type: javascript
file: hooks/contact-form/post_bind.js
validate:
type: javascript
file: hooks/contact-form/post_validate.js
handle:
type: javascript
file: hooks/contact-form/post_handle.js
return:
type: javascript
file: hooks/contact-form/post_return.js
```
## Action lifecycle
### POST flow
`bind``validate``handle``return`
Use the steps deliberately:
- `bind`: normalize the request body, derive helper data, prepare context
- `validate`: enforce required fields, anti-spam checks, shape checks, consent checks
- `handle`: execute the business logic
- `return`: normalize the response payload for the frontend
### GET flow
`handle``return`
GET actions are useful for utility endpoints, signed links, status endpoints, or controlled data retrieval that is not a collection read.
## Recommended website patterns
### Contact form
Recommended behavior:
- public `POST` action
- validate required fields, email format, consent, and anti-spam signal
- send mail or queue message handling
- optionally persist a normalized inquiry record
- return a small stable payload for the frontend
### Newsletter signup
Recommended behavior:
- public `POST` action
- validate email and consent
- call external provider or create local opt-in entry
- keep provider-specific logic in the action, not in the frontend
### Quote or booking request
Recommended behavior:
- public `POST` action
- transform raw form data into a normalized structure in `bind`
- validate business rules in `validate`
- persist or forward the request in `handle`
### Webhook receiver
Recommended behavior:
- restricted `POST` action
- verify secret/signature before any state change
- keep webhook-specific logic isolated from public website forms
## Validation rules
Validation belongs server-side even when the frontend already validates.
Always validate:
- required fields
- string lengths
- email/phone formats when applicable
- consent flags
- expected enums or modes
- anti-spam or rate-limit conditions
Do not trust the frontend form shape.
## Permissions and CORS
Actions use the same permission model as collections.
For public website forms:
- keep only the needed methods public
- avoid opening `GET` unless the use case needs it
- use action-level CORS only when the frontend origin truly differs from the project default
Public form access should be narrow, explicit, and auditable.
## Persistence strategy
Not every form submission belongs in a collection.
Choose persistence deliberately:
- no persistence: mail, webhook, or third-party API only
- minimal persistence: store the normalized request for internal staff
- full persistence: store and manage lifecycle in a dedicated collection
If editors must browse, triage, export, or annotate the data in Nova, add a dedicated collection instead of overloading the action itself.
## Frontend integration
Frontend forms should submit to actions through the normal API layer.
Keep the frontend responsible for:
- collecting input
- disabled/loading state
- optimistic or conservative UX
- success and error messages
Keep the backend responsible for:
- real validation
- side effects
- persistence
- normalization of response shape
## Hook step order: bind → validate → handle → return
Action hooks run in a **fixed step order** in tibi-server:
1. **bind** — runs first. `context.data` is NOT yet set (body not parsed).
2. Body parsing — happens AFTER bind. JSON body is set to `context.data`.
3. **validate**`context.data` is available here for validation.
4. **handle** — main business logic. `context.data` is available.
5. **return** — final response shaping.
**Critical:** The bind hook runs BEFORE the HTTP body is parsed. Do NOT access `context.data` in bind — it will be undefined. Use `handle` or `validate` for data access.
```yaml
# Correct: use handle step for data access
hooks:
post:
handle:
type: javascript
file: hooks/actions/contact/handle.js
```
Action URL pattern (through BrowserSync proxy): `/api/_actions/{name}` — NOT `/api/{name}`. The tibi-server registers actions under `/_actions/`.
```sh
curl -X POST "https://project.code.testversion.online/api/_actions/contact"
```
## Permissions for public actions
Actions need explicit public write permission for unauthenticated access:
```yaml
- name: contact
path: contact
permissions:
public:
methods:
post: true
hooks:
post:
handle:
type: javascript
file: hooks/actions/contact/handle.js
```
## Inline form validation (frontend)
Use `$state` variables for inline errors instead of `alert()`:
```svelte
<script lang="ts">
let startDate = $state("")
let endDate = $state("")
let dateError = $state("")
function handleSubmit() {
if (!startDate) { dateError = "Bitte Mietbeginn wählen"; return }
if (!endDate) { dateError = "Bitte Mietende wählen"; return }
if (startDate > endDate) { dateError = "Mietende muss nach Mietbeginn liegen"; return }
dateError = ""
// submit logic
}
</script>
<form onsubmit={handleSubmit}>
<input type="date" bind:value={startDate} />
<input type="date" bind:value={endDate} />
{#if dateError}
<div class="text-red-600 bg-red-50 px-3 py-2 rounded-lg">{dateError}</div>
{/if}
<button type="submit">Absenden</button>
</form>
```
Errors should appear directly below the relevant field group, not as a browser alert.
## Frontend form submission
Submit to the action endpoint using the correct path:
```ts
fetch("/api/_actions/contact", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name, email, message, consent: true }),
})
```
## Response design
Return a small stable payload that the frontend can rely on.
Typical response examples:
```json
{ "ok": true, "message": "Danke für Ihre Nachricht." }
```
```json
{ "ok": true, "nextStep": "confirm-email" }
```
Avoid leaking internal implementation details or raw provider responses to the frontend.
## Anti-patterns
- Creating empty fake collections just to receive POST requests
- Moving validation only into the browser
- Sending third-party API credentials from the frontend
- Returning unstable error shapes
- Mixing public forms and internal admin workflows into one hook without boundaries
- Persisting everything by default without a real editorial or operational need
## Verification checklist
After adding an action-based workflow, verify all of these:
1. The action is declared in `api/config.yml`.
2. The hook chain exists and has the intended steps.
3. Invalid submissions fail with useful status and message.
4. Valid submissions trigger the intended side effects.
5. Public permissions are no broader than necessary.
6. The frontend handles success and failure predictably.
7. `yarn validate` stays clean.
## What an LLM should inspect first
When asked to add a website form or endpoint on this starter, inspect in this order:
1. `tibi-server/docs/19-actions.md`
2. `api/config.yml`
3. related hooks in `api/hooks/`
4. the frontend form/API surface
5. whether persistence belongs in a collection too
This prevents the common mistake of starting with a fake collection when the feature is really an action.
+369
View File
@@ -0,0 +1,369 @@
---
name: tibi-hook-authoring
description: Write and debug server-side hooks for tibi-server (goja Go JS runtime). Covers IIFE structure, HookResponse/HookException types, context.filter Go-object quirk, single-item vs list retrieval, and MongoDB filter patterns. Use when creating or modifying files in api/hooks/.
---
# 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.
## Source of truth
Use these sources when implementing or reviewing hooks:
- `tibi-server/docs/06-hooks.md`
- `tibi-server/docs/19-actions.md`
- `tibi-server/internal/models/eval_context.go`
- `tibi-server/internal/hook/context_*.go`
- `api/hooks/config.js`
- `api/hooks/filter_public.js`
- `api/hooks/clear_cache.js`
- `.agents/skills/tibi-ssr-caching/SKILL.md`
When hook examples and prose ever disagree about how helpers are exposed, trust the current implementation in `eval_context.go` plus the `context_*.go` registrations.
## First routing decision: collection hook or action
Before writing hook code, decide whether the workflow belongs to CRUD data or to an endpoint.
Use collection hooks when:
- the workflow is about reads or writes on a real collection
- publication filtering belongs to collection reads
- cache invalidation belongs to collection mutations
Use actions when:
- the workflow is endpoint-style business logic
- there is no durable CRUD collection behind it
- validation and side effects matter more than storage
Typical action use cases:
- contact forms
- newsletter signups
- quote or order requests
- webhook receivers
- helper endpoints
Do not implement fake empty collections just to gain a hook surface.
## Action hook context.data quirk
In **action hooks**, the body is NOT parsed before the `bind` step runs. `context.data` is only available starting from the `validate` step. The order is:
1. `bind` — runs, but `context.data` is undefined (body not yet parsed)
2. Body parsing — server parses JSON body into `context.data`
3. `validate``context.data` is now available
4. `handle``context.data` available, this is where main logic goes
5. `return` — final response
**Never access `context.data` in a bind hook** — it will be empty. Use `handle` for data access.
For action config, always use the `handle` step:
```yaml
hooks:
post:
handle:
type: javascript
file: hooks/actions/contact/handle.js
```
Also note: `context.data` can be an array or object depending on the request. Always guard:
```js
const data = (Array.isArray(context.data) ? {} : context.data) || {}
```
## Hook file structure
Wrap every hook in an IIFE:
```js
;(function () {
/** @type {HookResponse} */
const response = { status: 200 }
// ... hook logic ...
return response
})()
```
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)`.
- Reference typed collection entries from `types/global.d.ts`.
- Avoid `@ts-ignore`; use proper casting instead.
- Use `const` and `let` instead of `var` — the goja runtime supports them.
## Hook API exposure model
In hook JavaScript, the server injects one top-level object: `context`.
That means runtime helpers and registered packages are accessed through `context`, for example:
- `context.request()`
- `context.db.find()`
- `context.http.fetch()`
- `context.smtp.sendMail()`
- `context.debug.dump()`
- `context.exec.command()`
Do not silently rewrite these to bare `request()`, `db.find()`, or `http.fetch()` when editing docs or examples for hook code.
## context.filter — Go object quirk
`context.filter` is a Go object, not a regular JS object. Even when empty, it is **truthy**.
Always check with `Object.keys()`:
```js
const requestedFilter =
context.filter &&
typeof context.filter === "object" &&
!Array.isArray(context.filter) &&
Object.keys(context.filter).length > 0
? context.filter
: null
```
**Never** use `context.filter || null` — it is always truthy and produces an empty filter inside `$and`, which crashes the Go server.
## Single-item vs. list retrieval
For `GET /:collection/:id`, the Go server sets `_id` automatically from the URL parameter.
GET read hooks should **not** set their own `_id` filter for `req.param("id")`. Only add authorization filters (e.g. `{ userId: userId }`).
## Interne DB-Lookups in Hooks (Read & Write)
Innerhalb von goja-Hooks hast du über die `context.db`-API Vollzugriff auf die lokale MongoDB. Dies ist essenziell für komplexe Prüfungen (z. B. "Gehört der angemeldete User wirklich zur ID im Foreign-Key des Objekts?").
**Wichtige Konzepte für DB-Calls in Hooks:**
1. **Keine Automatik-Lookups (`_lookup`) in Hook-Queries:** Der Go-Befehl `context.db.find` liefert nur die flachen Datenbank-Dokumente als Array. Die in der REST-API verfügbare `lookup`-Automatik für Foreign-Keys wird in den internen Backend-Hooks *nicht* angewendet. Du musst die verknüpften Collections ggf. manuell nachladen.
2. **Immer Arrays:** `context.db.find` gibt **immer** ein Array zurück, auch wenn du `limit: 1` setzt.
3. **Rechte ignorierend:** Die `context.db.*`-Methoden umgehen alle `permissions` der YAML-Rollen. Du lädst als System-Benutzer!
**Beispiel: Datensatz validieren / verknüpftes Element prüfen**
```javascript
// hooks/my_action/post.before
(function() {
var userId = context.auth().id;
var submittedRefId = context.data.refId;
// 1. Manuell nachladen
var targetList = context.db.find("target_collection", {
filter: { id: submittedRefId },
limit: 1 // Begrenzen für Performance
});
if (targetList.length === 0) {
throw { status: 404, json: { error: "Ziel nicht gefunden" } };
}
var target = targetList[0];
// 2. Custom Security Check
if (target.ownerId !== userId) {
throw { status: 403, json: { error: "Keine Berechtigung für dieses Ziel" } };
}
// ... Hook fortsetzen
})();
```
**Verfügbare DB-Methoden in `context.db`:**
* `context.db.find(collection, { filter: {}, selector: {}, sort: [], limit: 10 })`
* `context.db.count(collection, { filter: {} })`
* `context.db.create(collection, { field: "value" })`
* `context.db.update(collection, "id_string", { field: "new_value" })` (bzw. mit Mongo-Operatoren `$set`, `$inc`, etc.)
* `context.db.delete(collection, "id_string")`
## 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 |
| ------------- | ------------------------------------------------------------------- |
| `filter` | MongoDB filter (list retrieval, or restrict single-item) |
| `selector` | MongoDB projection (`{ field: 0 }` exclude, `{ field: 1 }` include) |
| `offset` | Pagination offset |
| `limit` | Pagination limit |
| `sort` | Sort specification |
| `pipelineMod` | Function to manipulate the aggregation pipeline |
## context.data for write hooks
- `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, `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
## Public filter and publication contract
For website projects, `filter_public.js` and `publishedFilter` are not optional examples. They are part of the public-delivery contract.
Later agents should validate all of these deliberately:
- anonymous public reads see only the intended active/published records
- token-backed or admin-backed reads can still reach records needed for cleanup or operator workflows
- collections that feed navigation or pages do not silently disappear because `active: true` was forgotten
If the public site depends on a collection, a broken public filter is a delivery bug, not only a hook bug.
## Mutation-side SSR invalidation
If a mutation can change rendered HTML, the invalidation belongs in hooks.
Typical SSR-critical mutation domains:
- content
- navigation
- medialib or page-critical referenced media
- publication-relevant fields
For these collections, later agents should verify:
- which mutation steps call cache-clearing behavior
- whether post/put/delete are all covered when needed
- whether a representative mutation actually changes the next SSR response
## Public filter: token bypass for testdata cleanup
`filter_public.js` applies `publishedFilter` (active=true + publication window) to all unauthenticated GET requests. This works well for public traffic but causes a problem: **Playwright test cleanup can't see inactive `_testdata` entries** because `context.user.auth()` returns false even when a static `Token:` header is present. The filter runs, inactive entries are hidden from the API response, and the cleanup never deletes them. Over multiple test runs, stale entries accumulate in MongoDB.
**Fix:** check for any auth header in `filter_public.js` and skip the filter when present:
```js
const req = context.request()
const hasToken =
req.header &&
(req.header("Token") || req.header("X-Admin-Token") || req.header("X-Auth-Token") || req.header("Authorization"))
if (!context.user.auth() && !hasToken) {
// apply publishedFilter
}
```
This way:
- **Anonymous requests** → public filter applies (only active entries visible)
- **Requests with Token header** → no filter → all entries visible → cleanup works
The same fix applies to any collection that uses a public filter hook and also receives testdata writes from Playwright.
## Common pitfalls
- Do not assume browser/Node APIs in hooks. The runtime is goja-based server-side JS.
- Do not treat actions as fake collections unless there is a good reason.
- Do not assume bulk hooks run per document.
- Do not build SSR/cache logic into frontend code when the invalidation belongs in hooks.
## Verification checklist
After hook-related changes, verify all of these:
1. the hook is attached to the right lifecycle step
2. actions are used for endpoint workflows instead of fake collections
3. anonymous vs token-backed reads behave correctly where public filtering exists
4. representative valid and invalid action submissions behave as designed
5. representative SSR-critical mutations invalidate or preserve cache as intended
6. bulk behavior is understood when the workflow depends on per-document logic
## Sending emails from hooks (`context.smtp.sendMail()`)
The `sendMail()` function is registered on `context.smtp` (NOT as a global). Always call via:
```js
context.smtp.sendMail({
to: "recipient@example.com", // string or string[]
cc: "cc@example.com", // optional
bcc: "bcc@example.com", // optional
from: "sender@example.com", // required
fromName: "Sender Name", // optional
replyTo: "reply@example.com", // optional
subject: "Subject line",
plain: "Plain text version",
html: "<h1>HTML version</h1>", // optional
})
```
The SMTP host is configured via:
- Server-level `config.yml`: `mail.host`
- Environment variable: `MAIL_HOST` (e.g. `maildev:1025`)
MailDev (dev SMTP server) runs in the Docker stack at `maildev:25` (SMTP) with a web UI at `:1080`.
## publishedFilter: `active: true` erforderlich
Der `publishedFilter` in `api/hooks/config.js` filtert nach `active: true`:
```js
const publishedFilter = {
active: true,
$or: [ ... ]
}
```
Einträge OHNE `active`-Feld werden bei öffentlichen API-Calls UNSICHTBAR. Das betrifft besonders:
- **Navigationseinträge** werden via `getCachedEntries("navigation", ...)` geladen. Fehlt `active: true`, bleibt `navItems` leer.
- **Manuell via API/MongoDB angelegte Einträge** das `active`-Feld muss explizit gesetzt werden.
Der `filter_public.js`-Hook überspringt den Filter nur wenn ein Token-Header gesetzt ist. Bei öffentlichen API-Calls (z.B. aus dem SPA ohne Token) greift der Filter immer. Daher: alle Einträge in allen Collections müssen `active: true` haben, sonst sind sie auf der Website nicht sichtbar.
+347
View File
@@ -0,0 +1,347 @@
---
name: tibi-project-setup
description: Set up a new tibi project from the tibi-svelte-starter template. Covers placeholder replacement, env/config setup, Docker startup, optional shared-server registration, and build verification. Use when creating a new project or onboarding into this template.
---
# tibi-project-setup
## When to use this skill
Use this skill when:
- creating a new project from `tibi-svelte-starter`
- onboarding into a freshly cloned project where starter placeholders are still present
- fixing a project that was renamed but never fully registered/configured in the current tibi stack
Goal: end with a project that is not only renamed, but actually reachable as a working website, admin, and API project in the current Docker/tibi-server setup.
## Source of truth
Use these sources when bootstrapping or auditing setup:
- `.agents/BUILD_CHECKLIST.md` phase 0
- `AGENTS.md`
- `README.md`
- `Makefile`
- `docker-compose-local.yml`
- `.env`
- `api/config.yml`
- `api/config.yml.env`
- `api/hooks/config-client.js`
- `.gitea/workflows/deploy.yml`
- `scripts/ci-deploy.sh`
- `scripts/ci-staging.sh`
- tibi-server server-level config requirements from `tibi-server/docs/02-configuration.md` when the project does not run on the starter's local Docker stack
## Core setup rule
Do not stop after placeholder replacement.
A project is only set up when all of these are true:
- placeholders and visible starter identity leftovers are gone
- env and token values are present
- Docker stack comes up
- the intended operator path is explicit: local starter Docker stack or shared/external tibi-server stack
- website, admin, and API respond on the expected project URLs
- if the current stack requires server-level config and project registration, that operator flow is completed
- `yarn build`, `yarn build:server`, and `yarn validate` pass
## Prerequisites
- `git`, `yarn`, `make`, `docker compose`, `curl`
- current Code-Server / Docker environment for `*.code.testversion.online`
- reverse proxy/Traefik managed by the host environment
## Step 1 — Clone and prepare remotes
Skip if the project is already cloned.
```sh
git clone https://gitbase.de/cms/tibi-svelte-starter.git my-project
cd my-project
git remote rename origin template
git remote add origin https://gitbase.de/<org>/<repo>.git
```
## Step 2 — Replace starter placeholders and identity surfaces
Replace placeholders in all required files:
- `.env`
- `api/config.yml`
- `frontend/.htaccess` when the deployment path uses the shipped Apache rewrite/proxy file
- `api/hooks/config-client.js`
- `package.json`
- `README.md` or other visible starter naming surfaces when the repo is already project-facing
- any other file that still contains starter markers
Minimum placeholders to replace:
- `__PROJECT_NAME__`
- `__TIBI_NAMESPACE__`
- `__ORG__`
- `__PROJECT__`
Verify with:
```sh
rg '__[A-Z0-9_]+__' . --glob '*.{yml,js,env,htaccess,json,md,ts,svelte}'
```
If anything remains, the setup is not complete.
## Step 3 — Fill project env, token, and metadata files
Set the current project URLs in `.env`:
- `LIVE_URL`
- `CODING_URL`
- `STAGING_URL`
- `CODING_TIBIADMIN_URL`
- `CODING_TIBISERVER_URL` only when the current environment exposes a dedicated raw tibi-server host
Generate `api/config.yml.env` values:
```sh
token=$(openssl rand -hex 20)
cat > api/config.yml.env <<EOF
ADMIN_TOKEN=$token
ADMIN_ASSET_VERSION=$(node -e "process.stdout.write(require('crypto').randomBytes(6).toString('hex'))")-dirty-$(date +%s)
EOF
```
Important:
- `ADMIN_TOKEN` is used for collection-level writes through the header name declared by the collection permission key; in this starter that is typically `Token` via `token:${ADMIN_TOKEN}`
- the current deploy scripts also use the same secret as a bearer token on the project-local reload endpoint
- `ADMIN_ASSET_VERSION` is required so Nova picks up the current admin bundle
- `PROJECT_NAME`, `TIBI_NAMESPACE`, `PRODUCTION_PATH`, and `STAGING_PATH` should be project-specific before the first deploy
- `package.json` should no longer advertise the starter repository or default package name once the project is bootstrapped
## Step 4 — Install and start the Docker stack
Use the Docker targets from the project. Do not try to start the frontend with local dev servers.
```sh
yarn install
make docker-up
```
Notes:
- `make docker-up` already depends on `init`; do not duplicate bootstrap steps unless debugging Make targets directly
- for foreground operation use `make docker-start`
## Step 5 — Choose the active bootstrap path
### Path A — Local starter Docker stack
This repo's default local path is the Docker stack in `docker-compose-local.yml` started via `make docker-up`.
Important characteristics:
- the project is mounted into `tibiserver` as `/data`
- `DB_DIAL`, `DB_PREFIX`, `MAIL_HOST`, and security overrides are injected via container environment
- the project is served from the repo's own `api/config.yml`
- no extra root `config.yml` or `/api/v1/project` registration step is required for basic local startup
Use this path unless the operator environment clearly tells you otherwise.
### Path B — Shared or external tibi-server stack
Only use this path when the project is not started through the local starter compose stack and the operator environment requires explicit server-level config or project registration.
In that case, confirm all of these with the operator first:
- where the server-level `config.yml` lives
- which admin token is valid for raw system-level APIs
- which base URL exposes `/api/v1/project`
- how the project path is mounted into the shared tibi-server instance
Do not invent Path B steps in the local starter Docker stack just because upstream tibi-server docs mention them.
## Step 6 — Optional server-level config and project registration for Path B
Shared or external tibi-server setups may require a server-level `config.yml` outside the project config. That file defines database connection, JWT secret, and admin tokens used for project CRUD and reload.
Create a root-level `config.yml` such as:
```yaml
db:
dial: mongodb://mongo
prefix: tibi
api:
port: 8080
jwtSecret: <random-secret>
adminTokens:
- token: "<ADMIN_TOKEN>"
label: "admin"
permissions:
- project
- project.reload
- user
- namespace.<PROJECT_NAME>
- server.shutdown
mail:
host: localhost:25
security:
allowAbsolutePaths: false
allowUpperPaths: true
```
Then copy it into the tibi-server container and restart that container if the current environment requires this manual step.
## Step 7 — Verify website, admin, and API reachability
Run the project-local checks after startup:
```sh
curl -I "$CODING_URL"
curl -I "$CODING_TIBIADMIN_URL"
curl -I "$CODING_URL/api/content?limit=1"
```
If the current environment also exposes a raw tibi-server host, add:
```sh
curl -I "$CODING_TIBISERVER_URL/api/v1/version"
```
If `/api/...` returns HTML instead of JSON, the reverse-proxy/setup path is still wrong.
## Step 8 — Optional project registration for Path B
Projects are not assumed to exist just because files are present on disk. Register and reload them explicitly when the current stack requires project registration.
```sh
curl -s -X POST "$CODING_TIBISERVER_URL/api/v1/project" \
-H "Content-Type: application/json" \
-H "X-Admin-Token: $ADMIN_TOKEN" \
-d '{
"name": "<PROJECT_NAME>",
"description": "...",
"configFile": "/data/api/config.yml",
"enabled": true
}'
```
Reload after creation or config changes:
```sh
curl -s -X POST "$CODING_TIBISERVER_URL/api/v1/_/<PROJECT_NAME>/_/admin/reload" \
-H "X-Admin-Token: $ADMIN_TOKEN"
```
### Token header distinction
- raw system-level API such as project CRUD or direct admin reload: `X-Admin-Token`
- collection-level CRUD such as content/navigation writes: use the header name from the collection permission key, typically `Token` in this starter via `token:${ADMIN_TOKEN}`
- JWT-authenticated user requests: `X-Auth-Token`
The current starter deploy scripts are a separate case: they call the reverse-proxied reload endpoint on `LIVE_URL` or `STAGING_URL` with `Authorization: Bearer ${ADMIN_TOKEN}`.
Do not mix these headers casually. A working collection token does not imply project-admin access.
## Step 9 — Build and validate
```sh
yarn build
yarn build:server
yarn validate
```
The project is not considered bootstrapped until all three succeed.
## Step 10 — Optional immediate follow-up work
Depending on the project state, continue with:
- seed or create initial content/navigation entries
- remove demo content and demo assets
- update project imagery/icons
- run the first targeted Playwright smoke checks
## Recommended verification sequence
Use this exact order when debugging a broken setup:
1. placeholder scan
2. env/token/metadata presence
3. Docker stack or target operator stack up
4. choose Path A or Path B explicitly
5. if Path B: server-level config and project registration/reload succeed
6. website/admin/API reachability
7. build/SSR build/validate
This prevents wasting time in frontend code when the real issue is project registration or server-level config.
## Common failure modes
### Placeholders still present
Symptom:
- URLs or namespace stay wrong even though the project name was changed manually
Fix:
- rerun the placeholder scan and replace every remaining marker
### Website works but API probes return HTML
Symptom:
- `curl "$CODING_URL/api/content?limit=1"` returns HTML
Fix:
- verify reverse-proxy routing and the configured API/admin URLs
### Files exist but the project is invisible to tibi-server
Symptom:
- project does not show in admin or reload endpoint fails
Fix:
- this is a Path B problem; verify the shared-stack server-level config and project registration flow instead of changing the local starter stack
### Admin bundle changes do not appear
Symptom:
- Nova still loads stale admin assets
Fix:
- regenerate or bump `ADMIN_ASSET_VERSION`
### Build passes locally but operational setup is still broken
Symptom:
- files compile, but website/admin/API are not all reachable
Fix:
- return to the reachability and registration checks instead of continuing with feature work
## What an LLM should inspect first
When asked to bootstrap or audit a starter-derived project, inspect in this order:
1. `README.md`
2. `.env`
3. `api/config.yml`
4. `api/config.yml.env`
5. `api/hooks/config-client.js`
6. `docker-compose-local.yml` and `Makefile`
7. whether the current stack is Path A (local starter Docker) or Path B (shared/external tibi-server)
8. whether website, admin, and API URLs all respond
This avoids the common mistake of treating setup as a naming exercise instead of a full stack-registration task.
+307
View File
@@ -0,0 +1,307 @@
---
name: tibi-ssr-caching
description: Implement and debug server-side rendering with goja (Go JS runtime) and dependency-based HTML cache invalidation for tibi-server. Use when working on SSR hooks, cache clearing, or the server-side Svelte rendering pipeline.
---
# 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/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
```bash
yarn build:server
```
- Output: `api/hooks/lib/app.server.js`
- 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 dependency strings:
{
path: "/de/ueber-uns",
content: "...",
dependencies: ["content:abc123", "navigation:*", "medialib:*"]
}
```
- `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)
## Limit: 1 ensures precise dependencies
By default, an API query for a collection (like `/api/v1/_/content?filter=...`) sets a list dependency `collection:*`. This means _any_ change to ANY entry in that collection will clear the SSR cache for this page.
If you are querying a single document (like a page or article based on its path or slug), you should ALWAYS append `limit: 1` to your API call (or pass `{ filter: {...}, limit: 1 }` to `getDBEntries`).
When `api/hooks/lib/ssr-server.js` intercepts a request with `limit === 1` and exactly one result is returned, it will register a precise `collection:id` dependency instead of a wildcard `collection:*`. This optimizes the cache drastically, because edits to _other_ pages won't invalidate this page.
### Automatic dependency tracking via `lookup` and `aggregate`
When options like `lookup` (e.g. `lookup: "image:medialib"`) or `aggregate` (e.g. `aggregate: "comments:contentId:count"`) are provided to an API call, `ssr-server.js` automatically parses these values and adds wildcard cache dependencies (`medialib:*` or `comments:*`) to the page. This guarantees that if a referenced image or child comment changes, the parent's SSR HTML is correctly flushed.
## 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:
- `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.
## SSR 404 signaling
When a page is not found during SSR, the framework returns the 404 page but with HTTP status **200** unless a 404 signal is set. The SSR hook (`get_read.js`) checks `context.is404` after rendering:
```js
// get_read.js, after app.default.render()
if (context.is404) {
status = 404
}
```
The signal is set from `NotFound.svelte` — when this component is rendered during SSR, it sets the flag directly. This keeps the 404 logic in the component that owns it:
```ts
// NotFound.svelte — top-level script, runs during render:
if (typeof window === "undefined") {
// @ts-ignore - context is the goja global in SSR runtime
context.is404 = true
}
```
**Why this works:**
- The `tibi-types` package declares `var context: HookContext` as a global (available because goja provides it during SSR).
- During SSR, `loadContent()` runs synchronously (goja transforms `async`/`await`).
- By the time `render(App)` returns in `ssr.ts`, `context.is404` is already `true`.
- `get_read.js` reads it, returns HTTP 404, and the rendered 404 page HTML is sent with the correct status.
- Caching is automatically skipped for 404 responses.
**Verification:** Test with a non-existent URL:
```bash
curl -w "\nHTTP Status: %{http_code}\n" "http://tibiserver:8080/api/v1/_/<namespace>/ssr?url=/de/nicht-existierend"
# Expected: HTTP 404, body contains the 404 page HTML
```
## 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
- **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"`.
- **`$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.
## SSR data loading pattern
In Svelte 5, SSR data loading works via **top-level `loadData()` calls** (NOT inside `$effect`):
```typescript
// ✅ Richtig: Top-Level-Aufruf für SSR + Browser
loadData()
async function loadData() {
const data = await getCachedEntries(...)
state = data
}
// ❌ Falsch: $effect wird für SSR nicht rechtzeitig abgearbeitet
$effect(() => { loadData() })
```
**Warum das funktioniert:**
- `loadData()` läuft während der Component-Initialisierung (vor Template-Auswertung)
- `getCachedEntries``apiRequest` → SSR-Pfad → `context.ssrRequest()` → blockierender HTTP-Fetch in goja
- goja's `await` auf einem bereits aufgelösten Promise läuft synchron weiter (kein Microtask-Hickhack)
- State-Änderungen sind vor der Template-Auswertung sichtbar
**Browser-Reaktivität:** Wenn Props sich ändern (z.B. Navigation zu anderer Kategorie), wird die Component via `{#if}`/`{#key}` neu erstellt → `loadData()` läuft erneut.
## SSR-Cache in der Entwicklung
Der SSR-Cache ist das häufigste Debugging-Hindernis. Der Proxy in `esbuild.config.js` MUSS `&noCache=1` an den SSR-Request anhängen:
```javascript
// esbuild.config.js SSR-Proxy
pathRewrite: function (path, req) {
return "/ssr?url=" + encodeURIComponent(path) + "&noCache=1"
}
```
Ohne `noCache` wird die erste SSR-Antwort gecached und bei Code-Änderungen nicht invalidiert. Der Entwickler sieht immer den alten Stand. **Immer zuerst den Cache-Bypass prüfen, bevor SSR-Fehler gesucht werden.**
**Erkennungsmerkmale für veralteten SSR-Cache:**
- `X-SSR-Cache: true` im Response-Header
- `<!--COMMENT--><!--SSR.ERROR-->` im HTML
- `__SSR_CACHE__` enthält nicht die erwarteten Daten
- Neustart von tibi-server nötig nach `app.server.js`-Änderungen (`docker restart <tibiserver>`)
## Build-Arbeitsschritte bei SSR-Änderungen
Nach jeder Änderung an Svelte-Komponenten oder `api.ts` ist folgendes nötig:
```bash
# 1. Frontend-Bundle bauen
yarn build
# 2. SSR-Bundle bauen (app.server.js)
yarn build:server
# 3. tibi-server neustarten (lädt neues app.server.js)
docker restart <tibiserver>
# 4. Frontend neustarten (für Entwicklungs-Proxy)
make docker-restart-frontend
```
**Wichtig:** `yarn build:server` allein reicht nicht der tibi-server cached das Modul im Speicher und lädt es nur beim Start neu.
@@ -0,0 +1,222 @@
---
name: troubleshooting-and-debugging
description: Diagnose common tibi website-project failures. Covers config loading, auth and permission mistakes, hook/goja errors, upload and CORS issues, and a practical debugging order so later agents do not thrash across unrelated layers.
---
# troubleshooting-and-debugging
## When to use this skill
Use this skill when:
- a tibi project is failing in a way that is not obviously tied to one file
- hooks, config, permissions, uploads, or routing behave unexpectedly
- a project worked before and now fails after configuration or integration changes
- you need a practical debugging order instead of ad-hoc guessing
## Goal
Help later agents debug the stack systematically.
The main value of this skill is not “more tips”. It is the order of operations: isolate the failing layer first, then inspect the right tool surface for that layer.
## Source of truth
Use these sources when debugging:
- `tibi-server/docs/15-troubleshooting.md`
- `tibi-server/docs/06-hooks.md`
- `tibi-server/docs/05-authentication.md`
- `tibi-server/docs/17-field-level-permissions.md`
- `tibi-server/internal/models/eval_context.go` when hook API exposure is in doubt
- `tibi-server/internal/hook/context_*.go` when helper registration is in doubt
- `.agents/skills/tibi-hook-authoring/SKILL.md`
- `.agents/skills/security-hardening-and-token-strategy/SKILL.md`
- `.agents/skills/tibi-ssr-caching/SKILL.md`
## Debugging order
Start in this order unless a more specific failure anchor already exists:
1. reachability and environment
2. auth/token/permission layer
3. config loading and YAML shape
4. hook/goja behavior
5. uploads/media pathing
6. SSR/routing/publication behavior
7. CORS or browser-integration issues
This order prevents chasing frontend symptoms when the real issue is project registration, token scope, or a broken config reload.
## Layer 1 — Reachability and environment
Check first:
- website URL responds
- admin URL responds
- API responds with JSON where expected
- Docker services are actually up
If `/api/...` returns HTML, do not debug application logic yet. Fix the environment/proxy path first.
## Layer 2 — Auth and permission mistakes
Common patterns:
- `401 Unauthorized` → missing/invalid JWT or wrong auth surface
- `403 Forbidden` → collection/action permissions wrong, user not assigned correctly, or token permission mismatch
- static `Token` works for one surface but not another → wrong header type for the requested operation
Check:
- `X-Auth-Token` vs `Token` vs `X-Admin-Token`
- collection/action permissions
- project/user assignment
- field-level readonly/hidden behavior if writes fail unexpectedly
Important current note:
- current upstream troubleshooting and auth docs still describe MD5-managed passwords; do not debug login failures by assuming the active username/password flow has already moved to bcrypt
## Layer 3 — Config and reload problems
Common patterns:
- project not loading after config change
- env vars not resolving
- CORS behaving differently than expected
Check:
- YAML syntax
- whether the correct config file is being loaded
- whether project reload actually ran
- whether env placeholders use `${VAR_NAME}` format
When the problem smells like “the server ignores my config change”, verify reload and active config path before editing more files.
## Layer 4 — Hook and goja errors
Common patterns:
- `require is not defined`
- `async`/`await` assumptions in hooks
- wrong context object assumptions
- timeouts or infinite loops
Remember:
- hooks run in goja, not Node.js
- no `require()` or npm runtime
- no normal async/await model
- hook behavior often depends on the exact lifecycle step
- tibi hook surfaces are accessed through `context`, including request and registered packages such as `context.request()`, `context.db.find()`, `context.http.fetch()`, `context.smtp.sendMail()`, `context.debug.dump()`, or `context.exec.command()`
Use the hook skill for step-specific quirks such as `context.data` timing or `context.filter` behavior.
## Layer 5 — Upload and media failures
Common patterns:
- files not being saved
- media URLs render incorrectly
- image filters return 404
Check:
- collection `uploadPath`
- file field type
- base64/data-URI or multipart expectations
- filter names in collection config
- whether frontend/admin preview code expects `_lookup` or raw IDs
## Layer 6 — SSR, routing, and publication failures
Common patterns:
- route works in browser but not in SSR
- SSR returns empty page or wrong status
- unpublished or inactive entries disappear unexpectedly
Check:
- route model and language-prefix handling
- `ssrValidatePath()`
- `publishedFilter`
- lookup usage for page-critical relations
- cache invalidation after mutations
Do not debug SSR only through browser navigation. Use the SSR endpoint directly when the failure is SSR-shaped.
## Layer 7 — Browser integration and CORS
Common patterns:
- browser form fails while direct API call works
- preflight fails
- credentials or auth headers do not cross origins correctly
Check:
- which layer owns CORS config
- `allowOrigins`, `allowMethods`, `allowHeaders`, `allowCredentials`
- whether the real deployment even needs cross-origin calls
## Useful debugging tools
### Hook-side debug helpers
```js
context.debug.dump(context.data, "payload")
context.debug.dump(context.request().header("Authorization"), "auth-header")
```
### Request inspection
```js
const req = context.request()
context.debug.dump({
method: req.method,
path: req.path,
url: req.url,
host: req.host,
clientIp: req.clientIp,
})
```
### Database inspection from hooks
```js
const rows = context.db.find("content", { limit: 5 })
context.debug.dump(rows, "sample-content")
```
### Direct endpoint debugging
Prefer targeted `curl` probes for:
- API JSON responses
- SSR endpoint behavior
- auth header behavior
- audit endpoint behavior when relevant
## Anti-patterns
- jumping between frontend, hooks, and config without isolating the failing layer
- assuming a browser symptom proves a frontend bug
- trying to fix permissions only in the UI
- debugging SSR without the SSR endpoint
- relying on outdated assumptions from old stack behavior
## What an LLM should inspect first
When asked to debug a tibi project with unclear failure ownership, inspect in this order:
1. current failing command or URL
2. environment reachability
3. auth/permission boundary
4. config/reload state
5. hook/runtime layer
6. SSR/publication layer if the failure is page-related
This keeps debugging local and falsifiable instead of turning into broad repo wandering.
@@ -0,0 +1,309 @@
---
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
**I18n field config:** When modeling multilingual content, decide early whether to use:
- **Field-level i18n** — object fields whose subField names match language codes (`de`, `en`, etc.) are auto-detected and rendered with language tabs in Nova. Configured via `api.meta.i18n` in `config.yml` or per-collection `meta.i18n`.
- **Entry-level i18n** — each entry represents one language, linked by a shared `translationGroup` UUID. The `I18nEntryConfig` type (from `tibi-admin-nova/types/admin.d.ts`) defines `languageField`, `groupField`, and `copyFields`/`clearFields` for translation cloning behavior.
See `tibi-admin-nova/types/admin.d.ts` interfaces `I18nFieldConfig` and `I18nEntryConfig` for the full API.
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.
+1
View File
@@ -0,0 +1 @@
code:$apr1$AeePIAei$E9E6E6jtFFtwmtGhIEG.Y/
+2
View File
@@ -0,0 +1,2 @@
code:$apr1$AeePIAei$E9E6E6jtFFtwmtGhIEG.Y/
web:$apr1$/zc/TBtD$ZGr3RqPiULYMD0kJUup5E0
+27
View File
@@ -0,0 +1,27 @@
PROJECT_NAME=__PROJECT_NAME__
TIBI_PREFIX=tibi
TIBI_NAMESPACE=__TIBI_NAMESPACE__
CODER_UID=100
CODER_GID=101
SENTRY_URL=https://sentry.basehosts.de
SENTRY_ORG=webmakers
SENTRY_PROJECT=
RSYNC_HOST=ftp1.webmakers.de
RSYNC_PORT=22223
PRODUCTION_SERVER=dock4.basehosts.de
PRODUCTION_TIBI_PREFIX=tibi
PRODUCTION_PATH=/webroots2/customers/_CUSTOMER_ID_/____
STAGING_PATH=/staging/__ORG__/__PROJECT__/dev
LIVE_URL=https://www
STAGING_URL=https://dev-__PROJECT_NAME__.staging.testversion.online
CODING_URL=https://__PROJECT_NAME__.code.testversion.online
CODING_TIBIADMIN_URL=https://__PROJECT_NAME__-tibiadmin.code.testversion.online
#START_SCRIPT=:ssr
#MOCK=1
+1
View File
@@ -0,0 +1 @@
.yarn/cache/** filter=lfs diff=lfs merge=lfs -text
+70
View File
@@ -0,0 +1,70 @@
name: deploy to production
on: "push"
jobs:
deploy:
name: deploy
runs-on: ubuntu-latest
container:
image: gitbase.de/actions/ubuntu:latest
volumes:
- /data:/data
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
lfs: true
submodules: true
- run: |
git fetch --force --tags
- name: setup node
uses: actions/setup-node@v4
with:
node-version: 22
- name: install dependencies
run: |
npm install -g yarn
yarn install
- name: validate
run: |
yarn validate
- name: modify config
run: ./scripts/ci-modify-config.sh
- name: build
env:
FORCE_COLOR: "true"
run: |
yarn build
- name: build ssr
env:
FORCE_COLOR: "true"
run: |
yarn build:server
- name: upload sourcemaps to sentry
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
run: ./scripts/ci-upload-sourcemaps.sh
- name: staging
if: github.ref == 'refs/heads/dev'
env:
API_BASEDIR: /data/${{ github.repository }}/${{ github.ref_name }}
COMPOSE_PROJECT_NAME: ${{ github.repository }}-${{ github.ref_name }}
run: ./scripts/ci-staging.sh
- name: deploy
if: github.ref == 'refs/heads/master'
env:
RSYNC_USER: ${{ github.repository }}
RSYNC_PASS: ${{ github.token }}
BRANCH: ${{ github.ref_name }}
run: ./scripts/ci-deploy.sh
+24
View File
@@ -0,0 +1,24 @@
api/hooks/lib/app.server*
api/hooks/lib/buildInfo.js
frontend/src/lib/buildInfo.ts
node_modules
media
tmp
_temp
frontend/dist
yarn-error.log
test-results/
playwright-report/
playwright/.cache/
visual-review/
video-tours/output/
.playwright-mcp/
.agents/STARTER_ALIGNMENT_*.md
.yarn/*
!.yarn/cache
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
+18
View File
@@ -0,0 +1,18 @@
{
"printWidth": 120,
"tabWidth": 4,
"singleQuote": false,
"trailingComma": "es5",
"semi": false,
"newline-before-return": true,
"no-duplicate-variable": [
true,
"check-parameters"
],
"no-var-keyword": true,
"svelteSortOrder": "scripts-options-markup-styles",
"svelteStrictMode": true,
"svelteBracketNewLine": true,
"svelteAllowShorthand": true,
"svelteIndentScriptAndStyle": true
}
+15
View File
@@ -0,0 +1,15 @@
{
// Verwendet IntelliSense zum Ermitteln möglicher Attribute.
// Zeigen Sie auf vorhandene Attribute, um die zugehörigen Beschreibungen anzuzeigen.
// Weitere Informationen finden Sie unter https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "pwa-msedge",
"request": "launch",
"name": "Launch Edge against localhost",
"url": "http://localhost:5501/",
"webRoot": "${workspaceFolder}/dist"
}
]
}
+39
View File
@@ -0,0 +1,39 @@
{
"editor.tabCompletion": "on",
"diffEditor.codeLens": true,
"editor.formatOnSave": true,
"editor.formatOnPaste": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"yaml.schemas": {
"./../../cms/tibi-types/schemas/config/project.schema.json": "api/config.y*ml",
"./../../cms/tibi-types/schemas/config/collection.schema.json": "api/collections/*.y*ml",
"./../../cms/tibi-types/schemas/config/field.schema.json": "api/collections/fields/*.y*ml",
"./../../cms/tibi-types/schemas/config/field-list.schema.json": "api/collections/fieldLists/*.y*ml",
"./../../cms/tibi-types/schemas/config/job.schema.json": "api/jobs/*.y*ml",
"./../../cms/tibi-types/schemas/config/asset.schema.json": "api/assets/*.y*ml"
},
"yaml.customTags": ["!include scalar"],
"filewatcher.commands": [
{
"match": "/api/.*(\\.ya?ml|js|env)$",
"isAsync": false,
"cmd": "cd ${currentWorkspace} && scripts/reload-local-tibi.sh",
"event": "onFileChange"
}
],
"i18n-ally.localesPaths": ["frontend/src/lib/i18n/locales"],
"i18n-ally.sourceLanguage": "de",
"i18n-ally.keystyle": "nested",
"i18n-ally.enabledFrameworks": ["svelte"],
"i18n-ally.displayLanguage": "de",
"[svelte]": {
"editor.defaultFormatter": "svelte.svelte-vscode"
},
"files.associations": {
"css": "tailwindcss"
},
"css.validate": true,
"css.lint.unknownAtRules": "ignore",
"playwright.reuseBrowser": false,
"playwright.showTrace": true
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More