55 Commits

Author SHA1 Message Date
0be4852f74 feat: add console error monitoring for Playwright tests; enhance page fixture with error assertions 2026-03-08 15:36:50 +00:00
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
18b5af5617 feat: enhance HeroBlock with SPA navigation for anchor links and update feature card styles 2026-02-27 13:58:46 +00:00
d1ef9800f1 🔧 fix: enhance watch mode to reload browser on build completion 2026-02-27 13:29:44 +00:00
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
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
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
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
e8fd38e98a feat: add video tour functionality with helpers, configuration, and homepage walkthrough 2026-02-26 02:42:16 +00:00
20eaa50935 feat: implement mock data support with API interceptor and update documentation 2026-02-26 02:37:01 +00:00
30501f5f4c feat: add SKILL documentation for Gitea issue attachments, tibi hook authoring, and SSR caching 2026-02-25 21:18:12 +00:00
3c3e70b474 🗑️ chore: remove outdated instructions and migration documentation 2026-02-25 20:45:46 +00:00
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
74bb860d4f feat: refine type definitions and improve request handling in API layer 2026-02-25 17:44:49 +00:00
3b84e49383 feat: enhance SSR cache management with dependency tracking and entry-level invalidation 2026-02-25 17:35:10 +00:00
3886eb9f34 feat: implement new feature for enhanced user experience 2026-02-25 16:50:10 +00:00
b41d12f257 feat: implement new API layer with request deduplication, caching, and Sentry integration 2026-02-25 16:48:37 +00:00
fdeeac88e2 feat: add loading bar and toast notification system with responsive design 2026-02-25 16:30:45 +00:00
e13e696253 feat: implement build version check and update build info handling 2026-02-25 15:53:00 +00:00
f6f565bbcb feat: add Svelte actions and global stores for enhanced functionality 2026-02-25 13:10:52 +00:00
dc00d24899 feat: implement new feature for enhanced user experience 2026-02-11 16:36:56 +00:00
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
b9a455d1b9 yarn upgrade 2026-02-11 13:06:05 +00:00
18d5e977e5 🔧 fix: update API path in docker-compose-staging.yml for correct endpoint configuration 2025-10-30 09:44:17 +00:00
ae39987c7d 🔧 fix: update spa.html handling to ensure symlink removal and prevent errors 2025-10-30 09:36:53 +00:00
4893d925c5 🔧 fix: comment out unused SSR path validation logic and update collection check 2025-10-30 09:32:06 +00:00
66225b731a feat: enhance deployment workflow with reload functionality and update SSR cache handling 2025-10-30 09:27:23 +00:00
50b6f4a6e5 feat: add initial webserver setup with Express and proxy middleware 2025-10-30 09:16:07 +00:00
2025a0a71f 🔧 fix: update template handling and svelte compiler options for improved build process 2025-10-30 09:11:44 +00:00
1ae34d6a18 feat: add Copilot instructions and enhance Docker Compose configuration for improved routing 2025-10-30 08:14:44 +00:00
4756eab175 🔧 fix: reorder plugins in esbuild configuration for proper execution 2025-10-30 08:02:59 +00:00
55263a49be fix: update build context for tibiserver-dev service in docker-compose 2025-08-26 09:20:30 +00:00
39caf6f7d6 Refactor code structure for improved readability and maintainability 2025-07-03 11:37:21 +00:00
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
4bbbfc5fee chore: disable ghostMode in esbuild configuration 2025-07-01 12:53:12 +00:00
b4204da0a4 yarn upgrade 2025-07-01 12:50:13 +00:00
ffcded42f3 Aktualisiere Metriken-Logik in App.svelte zur Verwendung von $effect und verbessere die Lesbarkeit 2025-03-27 17:32:55 +00:00
a72780873a Aktualisiere Konfigurationen und entferne nicht benötigte Skripte aus der Umgebung 2025-03-27 17:23:39 +00:00
f66c1fc078 Füge Initialisierungsziel zu Makefile hinzu und aktualisiere Docker-Befehle 2025-03-27 17:07:33 +00:00
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
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
77cb64b260 yarn 4 2025-03-27 13:26:28 +00:00
2037953000 yarn upgrade 2025-03-27 12:34:04 +00:00
3a6ff3fa8e Füge ein neues Deployment-Workflow-Skript hinzu und entferne veraltete Skripte 2025-03-27 12:12:55 +00:00
212a9720cf first clean up 2025-03-26 17:44:06 +00:00
4a8864c7b9 CustomTags 2022-11-18 11:47:44 +00:00
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
825dfc18f9 yarn upgrade 2022-10-11 14:10:53 +00:00
c8443f4d11 yarn upgrade 2022-09-15 16:02:24 +00:00
ac7eec418c docker:start 2022-09-15 15:56:54 +00:00
fef4d3b023 Doppelter Aufruf für das Holen der Page-Contents nach Sprachwechsel gefixed. 2022-07-12 08:45:38 +02:00
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
345ecb6177 Aktualisieren der auszugebenden Daten-Spalten (fieldviews) in der neuen LinkedEntries Komponente. 2022-07-01 09:08:56 +02:00
dbbd7c63ed Update SLUG field in server side post_create and put_update hooks. 2022-06-30 15:00:18 +02:00
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
1876 changed files with 17168 additions and 23712 deletions

View File

@@ -0,0 +1,524 @@
---
name: admin-ui-config
description: Configure the admin UI for collections — meta labels, views (table/list/cards), field widgets, inputProps, fieldLists, sidebar layout, choices, foreign references, and image handling. Use when setting up or customizing collection admin views.
---
# admin-ui-config
## When to use this skill
Use this skill when:
- Configuring how a collection appears in the tibi-admin UI
- Setting up table/list/card views for a collection
- 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` (1296 lines). Always consult this file for the full API. This skill provides a practical summary.
---
## Collection meta configuration
The `meta` key in a collection YAML controls how the collection appears in the admin sidebar and list views.
```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: false # true = only one entry allowed
hideInNavigation: false # true = don't show in sidebar
defaultSort: "-insertTime" # Default sort (prefix - = descending)
rowIdentTpl: { twig: "{{ name }} ({{ price }})" } # Row display template
```
### Row identification
`rowIdentTpl` uses Twig syntax with field names. Used in admin list to identify entries:
```yaml
rowIdentTpl: { twig: "{{ name }}" } # Simple
rowIdentTpl: { twig: "{{ type }} — {{ language }}" } # Combined
```
---
## Views: table, simpleList, cardList
The `views` array defines how entries are displayed in the admin list. Multiple views can coexist (e.g. table for desktop, simpleList for mobile).
### Table view
```yaml
meta:
views:
- type: table
columns:
- name # Simple: field name as column
- source: lang # With filter
filter: true
- source: active # Boolean column with filter
filter: true
- source: price # Custom label
label: { de: "Preis", en: "Price" }
- source: insertTime # Date field
width: 160
```
### Simple list view (mobile)
```yaml
meta:
views:
- type: simpleList
mediaQuery: "(max-width: 600px)" # Show only on small screens
primaryText: name
secondaryText: lang
tertiaryText: path
image: thumbnail # Optional: show image thumbnail
```
### Card list view
```yaml
meta:
views:
- type: cardList
fields:
- source: name
label: Name
- source: price
label: Preis
widget: currency
```
---
## 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 | |
| `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 | |
### 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: color
type: string
meta:
widget: color # Color picker
- name: image
type: string
meta:
widget: medialib # Media library picker
```
Common widget types: `text` (default), `richtext`, `color`, `medialib`, `code`, `markdown`, `password`, `hidden`.
### 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: { twig: "{{ name }} <{{ email }}>" }
autoFill: # Auto-fill other fields on selection
- source: email
target: authorEmail
```
### Image fields
```yaml
- name: image
type: file
meta:
widget: medialib
downscale: # Auto-resize on upload
maxWidth: 1920
maxHeight: 1080
quality: 0.85
imageEditor: true # Enable crop/rotate editor
```
---
## 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:Veröffentlichung" # Sidebar with group header
```
### Sidebar groups (ordered)
Define sidebar group order in collection meta:
```yaml
meta:
sidebar:
- Veröffentlichung
- SEO
- Einstellungen
```
### 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
```
---
## 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" }
preview: { eval: "item.type + ': ' + (item.headline || '')" }
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
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 custom Svelte components for injection into the tibi-admin UI. Components are rendered inside Shadow DOM to isolate styles.
```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
// addCss: CSS files to inject into Shadow DOM
// nestedElements: wrapper elements inside Shadow DOM
}
export { getRenderedElement }
```
Build with `yarn build:admin`. The output is loaded by tibi-admin-nova as a custom module.
**Use case:** Custom dashboard widgets, preview components, or field widgets that require Svelte rendering inside the admin UI.
---
## Complete collection example
```yaml
name: products
meta:
label: { de: "Produkte", en: "Products" }
muiIcon: inventory_2
group: shop
defaultSort: "-insertTime"
rowIdentTpl: { twig: "{{ name }} ({{ sku }})" }
sidebar:
- Veröffentlichung
- SEO
views:
- type: simpleList
mediaQuery: "(max-width: 600px)"
primaryText: name
secondaryText: sku
image: image
- type: table
columns:
- name
- sku
- source: price
label: { de: "Preis", en: "Price" }
- source: active
filter: true
- source: category
filter: true
permissions:
public:
methods:
get: true
user:
methods:
get: true
post: true
put: true
delete: true
hooks:
beforeRead: |
!include hooks/filter_public.js
afterWrite: |
!include hooks/clear_cache.js
fields:
- name: active
type: boolean
meta:
label: { de: "Aktiv", en: "Active" }
position: "sidebar:Veröffentlichung"
- 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: medialib
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
```
---
## Common pitfalls
- **`meta.label` is i18n** — Always provide `{ de: "...", en: "..." }` objects, not plain strings.
- **`views` order matters** — First matching view (by `mediaQuery`) is shown. Put mobile views (with `mediaQuery`) before desktop views (without).
- **`choices.id` must match stored value** — The `id` in choices is what gets saved to the database.
- **`inputProps` depends on widget** — Not all props work with all widgets. Check tibi-admin-nova source if unsure.
- **`position: sidebar` without group** — Fields go to an ungrouped area. Use `position: "sidebar:GroupName"` for grouping.
- **`type: object[]` needs `subFields`** — Forgetting `subFields` renders an empty repeater.
- **hooks path** — Hook includes are relative to `api/` directory: `!include hooks/myfile.js`.

View File

@@ -0,0 +1,325 @@
---
name: content-authoring
description: Add new pages, content blocks, and collections to a tibi project. Covers the content-based routing model, block registration in BlockRenderer, collection YAML authoring, and TypeScript type definitions. Use when creating new pages, block types, or collections.
---
# 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. `App.svelte` reacts to URL changes → calls `getCachedEntries("content", { lang, path, active: true })`.
3. The matching `ContentEntry.blocks[]` array is passed to `BlockRenderer.svelte`.
4. Each block has a `type` field that maps to a Svelte component.
**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.
---
## Adding a new page
### Option A: Via Admin UI (preferred for content editors)
1. Open the tibi-admin at `CODING_URL/_/admin/`.
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}`.
### 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"
# PUT to update elements array (add your page)
curl -X PUT "$CODING_URL/api/navigation/<id>" \
-H "Token: $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "elements": [ ...existing, { "name": "Über uns", "page": "/ueber-uns" } ] }'
```
### Multi-language pages
- Create one `ContentEntry` per language with the **same `translationKey`** but different `lang` and `path`.
- The language switcher in `App.svelte` uses `currentContentEntry.translationKey` to find the equivalent page.
- 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} />
```
### Step 3: 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 }[]
}
```
### Step 4: 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[]
subFields:
- name: title
type: string
- name: description
type: string
```
### Step 5: Update mock data (if using MOCK=1)
Add a block with your new type to `frontend/mocking/content.json`.
### Step 6: Verify
```sh
yarn validate # TypeScript check — must be warning-free
```
### Existing block types for reference
| Type | Component | Purpose |
| -------------- | ------------------------- | ----------------------------------------- |
| `hero` | `HeroBlock.svelte` | Full-width hero with image, headline, CTA |
| `features` | `FeaturesBlock.svelte` | Feature grid with icons |
| `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` or `navigation.yml` 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
rowIdentTpl: { twig: "{{ name }}" } # Row display in admin list
views:
- type: simpleList
mediaQuery: "(max-width: 600px)"
primaryText: name
- type: table
columns:
- name
- source: active
filter: true
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
```
**Field types:** `string`, `number`, `boolean`, `object`, `object[]`, `string[]`, `file`, `file[]`.
For the full schema reference: `tibi-types/schemas/api-config/collection.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
_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" | "mycollection" | string
type EntryTypeSwitch<T extends string> = T extends "medialib"
? MedialibEntry
: T extends "content"
? ContentEntry
: T extends "mycollection"
? MyCollectionEntry
: Record<string, unknown>
```
### Step 5: Add hooks (optional)
Common hook patterns:
- **Public filter** — reuse `filter_public.js` to enforce `active: true` for unauthenticated users.
- **Before-save validation** — create `api/hooks/mycollection_validate.js`.
- **Cache invalidation** — add your collection to `api/hooks/clear_cache.js` if it affects rendered pages.
Reference hook in YAML:
```yaml
hooks:
beforeRead: |
!include hooks/filter_public.js
afterWrite: |
!include 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
```
---
## 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`.

View File

@@ -0,0 +1,360 @@
---
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
---
## 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()`.
### 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
}
```
---
## 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's `translationKey`, `lang`, `path` (for language switcher) |
| `previousPath` | `lib/store.ts` | Previous URL path (for conditional back buttons) |
| `apiBaseOverride` | `lib/store.ts` | Override API base URL (used by admin module) |
| `cookieConsentVisible` | `lib/store.ts` | Whether cookie consent banner is showing |
| `currentLanguage` | `lib/i18n.ts` | Derived from `$location.path` — current language code |
| `selectedLanguage` | `lib/i18n.ts` | Writable — synced with `currentLanguage` on navigation |
| `activeRequests` | `lib/requestsStore.ts` | Number of in-flight API requests (drives `LoadingBar`) |
### 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)
### 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 })
```
### 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)
}
```
### 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 }
```
---
## 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 may have both `id` and `_id`.
- **`$location` strips trailing slashes** — `/about/` becomes `/about` (except root `/`).
- **Content cache is 1 hour** — `getCachedEntries` caches in memory for 1h. For admin previews, use `getDBEntries` (uncached).

View File

@@ -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})
```

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. `wmbasic`) |
| `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: { "items.0.label": "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).

View File

@@ -0,0 +1,71 @@
---
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
## 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`.
## 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.
## 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 }`).
## 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/PATCH, `req.param("id")` gives the entry ID.

View File

@@ -0,0 +1,173 @@
---
name: tibi-project-setup
description: Set up a new tibi project from the tibi-svelte-starter template. Covers cloning, placeholder replacement, environment config, Docker startup, mock mode, demo cleanup, and build verification. Use when creating a new project or onboarding into this template.
---
# tibi-project-setup
## When to use this skill
Use this skill when:
- Creating a new project from the `tibi-svelte-starter` template
- Onboarding into a freshly cloned starter project where placeholders haven't been replaced yet
- The user asks to "set up", "initialize", or "bootstrap" a new tibi project
## Prerequisites
- Code-Server environment at `*.code.testversion.online`
- `git`, `yarn`, `make`, `docker compose` available
- Traefik reverse proxy running on the host (manages `*.code.testversion.online` subdomains automatically via Docker labels)
## Step 1 — Clone and set up remotes
Skip this step if already inside a cloned project.
```sh
# In the workspace directory (e.g. /WM_Dev/src/gitbase.de/cms/)
git clone https://gitbase.de/cms/tibi-svelte-starter.git my-project
cd my-project
git remote rename origin template
# Create a new remote repo (e.g. on gitbase.de) and add as origin:
# git remote add origin https://gitbase.de/org/my-project.git
```
**Verify:** `git remote -v` shows `template` pointing to the starter and optionally `origin` pointing to the new repo.
## Step 2 — Replace placeholders
Three placeholders must be replaced in the correct files:
| Placeholder | Files | Format | Example |
| -------------------- | -------------------------------------- | --------------------------------------------------------- | ------------ |
| `__PROJECT_NAME__` | `.env` | kebab-case (used for URLs, Docker containers, subdomains) | `my-project` |
| `__TIBI_NAMESPACE__` | `.env` | snake_case (used as DB prefix and in API URLs) | `my_project` |
| `__NAMESPACE__` | `api/config.yml`, `frontend/.htaccess` | snake_case — same value as `TIBI_NAMESPACE` | `my_project` |
```sh
PROJECT=my-project # kebab-case
NAMESPACE=my_project # snake_case
sed -i "s/__PROJECT_NAME__/$PROJECT/g" .env
sed -i "s/__TIBI_NAMESPACE__/$NAMESPACE/g" .env
sed -i "s/__NAMESPACE__/$NAMESPACE/g" api/config.yml frontend/.htaccess
```
**Verify each replacement:**
```sh
grep -n '__PROJECT_NAME__\|__TIBI_NAMESPACE__\|__NAMESPACE__' .env api/config.yml frontend/.htaccess
# Expected: no output (all placeholders replaced)
```
**Result in `.env`:**
```dotenv
PROJECT_NAME=my-project
TIBI_NAMESPACE=my_project
CODING_URL=https://my-project.code.testversion.online
STAGING_URL=https://dev-my-project.staging.testversion.online
```
### Common mistakes
- **Mixing formats**: `PROJECT` must be kebab-case (`my-project`), `NAMESPACE` must be snake_case (`my_project`). Never use kebab-case where snake_case is expected or vice versa.
- **Forgetting `frontend/.htaccess`**: Contains the namespace for API rewrite rules. If missed, API calls from the frontend will fail silently.
- **Forgetting `api/config.yml`**: First line is `namespace: __NAMESPACE__`. If not replaced, tibi-server won't start correctly.
## Step 3 — Page title
The page title is set dynamically via `<svelte:head>` in `frontend/src/App.svelte`. The demo app uses the constant `SITE_NAME` for this. In a new project, `App.svelte` is typically rewritten completely — just make sure `<svelte:head>` with a `<title>` is present. SSR automatically injects it via the `<!--HEAD-->` placeholder in `spa.html`.
## Step 4 — Admin token
`api/config.yml.env` ships with a default `ADMIN_TOKEN`. For production projects, generate a secure one:
```sh
echo "ADMIN_TOKEN=$(openssl rand -hex 20)" > api/config.yml.env
```
**Verify:** `cat api/config.yml.env` shows a 40-character hex token.
## Step 5 — Install, upgrade, and start
```sh
yarn install
yarn upgrade # Update all deps to latest versions within package.json ranges
make docker-up # Start stack in background
# or
make docker-start # Start stack in foreground (CTRL-C to stop)
```
`yarn upgrade` is safe here because the project is freshly cloned and nothing is running yet. The template's `yarn.lock` may be months old — upgrading ensures you start with the latest compatible (semver-range) versions.
**Do NOT run `yarn upgrade` on an existing, running project without testing.** Even patch-level updates can introduce regressions. For running projects, upgrade targeted packages with `yarn upgrade <package>` and verify with `yarn validate` + `yarn build`.
**Verify containers are running:**
```sh
make docker-ps # All containers should be "Up"
make docker-logs # Check for errors
```
## Step 6 — Verify URLs
Traefik picks up Docker labels automatically — no manual config needed. After `make docker-up`, these URLs become available:
| Service | URL |
| --------------------- | ------------------------------------------------------------ |
| Website (BrowserSync) | `https://{PROJECT_NAME}.code.testversion.online/` |
| Tibi Admin | `https://{PROJECT_NAME}-tibiadmin.code.testversion.online/` |
| Tibi Server API | `https://{PROJECT_NAME}-tibiserver.code.testversion.online/` |
| Maildev | `https://{PROJECT_NAME}-maildev.code.testversion.online/` |
The subdomains are registered via the Docker label `online.testversion.code.subdomain=${PROJECT_NAME}`. Traefik watches Docker events and creates routes dynamically.
**Verify:** `curl -sI https://{PROJECT_NAME}.code.testversion.online/ | head -1` returns `HTTP/2 200`.
## Step 7 — Mock mode (optional)
For frontend development without a running tibi-server backend:
```sh
# Set in .env:
MOCK=1
# Then restart:
make docker-up
```
Mock data lives in `frontend/mocking/` as JSON files. Missing endpoints return 404.
**When to use mock mode:** Early UI prototyping, frontend-only work, CI environments without a database.
## Step 8 — Remove demo content
For a real project, remove or replace the demo files:
| File/Folder | Content |
| ---------------------------------- | ------------------------------------------------------ |
| `frontend/src/blocks/` | Demo block components (HeroBlock, FeaturesBlock, etc.) |
| `frontend/mocking/content.json` | Demo mock data for content |
| `frontend/mocking/navigation.json` | Demo mock data for navigation |
| `api/collections/content.yml` | Content collection config |
| `api/collections/navigation.yml` | Navigation collection config |
| `tests/e2e/` | Demo E2E tests |
| `video-tours/tours/` | Demo video tour |
Then adapt `frontend/src/App.svelte` (header, footer, content loading) to your own data model.
**Decision guide:**
- **Keep demo content** if you want to use it as a reference while building your own components.
- **Delete immediately** if you're starting with a completely custom design and the demo files would only cause confusion.
## Step 9 — Build and validate
```sh
yarn build # Frontend bundle for modern browsers
yarn build:server # SSR bundle (for tibi-server goja hooks)
yarn build:admin # Admin modules (optional)
yarn validate # TypeScript + Svelte checks (must show 0 errors and 0 warnings)
```
**All four commands must succeed with exit code 0 before the project is considered set up.**

View File

@@ -0,0 +1,55 @@
---
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
## SSR request flow
1. `ssr/get_read.js` receives a page request and calls `lib/ssr-server.js`.
2. `ssr-server.js` loads `lib/app.server.js` (the Svelte SSR bundle) and renders the page.
3. During rendering, API calls are tracked as **dependencies** (collection + entry ID).
4. The rendered HTML + dependencies are stored in the `ssr` collection.
5. On the client, `lib/ssr.js` hydrates using `window.__SSR_CACHE__` injected by the server.
## Building the SSR bundle
```bash
yarn build:server
```
- Output: `api/hooks/lib/app.server.js`
- Uses `babel.config.server.json` to transform async/await to generators (goja doesn't support async).
- Add `--banner:js='// @ts-nocheck'` to suppress type errors in the generated bundle.
## Dependency-based cache invalidation
When content changes, `clear_cache.js` only invalidates SSR entries that depend on the changed collection/entry:
```js
// Each SSR cache entry stores its dependencies:
{
url: "/some-page",
html: "...",
dependencies: [
{ collection: "content", id: "abc123" },
{ collection: "medialib", id: "def456" }
]
}
```
The hook queries the `ssr` collection for entries whose `dependencies` array matches the changed collection (and optionally entry ID), then deletes only those cached pages.
## SSR route validation
Route validation in `config.js` controls which paths get SSR treatment. Return:
- A positive number to enable SSR for that route
- `-1` to disable SSR (current default in the starter template)
## Common pitfalls
- **goja has no async/await**: The babel server config transforms these, but avoid top-level await.
- **No browser globals**: `window`, `document`, `localStorage` etc. don't exist in goja. Guard with `typeof window !== "undefined"`.
- **SSR cache can go stale**: Always ensure `clear_cache.js` covers any new collection that affects rendered output.

1
.basic-auth-code Normal file
View File

@@ -0,0 +1 @@
code:$apr1$AeePIAei$E9E6E6jtFFtwmtGhIEG.Y/

2
.basic-auth-web Normal file
View File

@@ -0,0 +1,2 @@
code:$apr1$AeePIAei$E9E6E6jtFFtwmtGhIEG.Y/
web:$apr1$/zc/TBtD$ZGr3RqPiULYMD0kJUup5E0

View File

@@ -1,191 +0,0 @@
kind: pipeline
type: docker
name: default
workspace:
path: /drone/workdir
steps:
- name: load dependencies
image: node
pull: if-not-exists
environment:
FORCE_COLOR: "true"
volumes:
- name: cache
path: /cache
commands:
- mkdir -p /cache/node_modules
- mkdir -p /cache/user-cache
- ln -s /cache/node_modules ./node_modules
- ln -s /cache/user-cache ~/.cache
- echo cache=/cache/npm-cache >> .npmrc
- "echo 'enableGlobalCache: false' >> .yarnrc"
- 'echo ''cacheFolder: "/cache/yarn-cache"'' >> .yarnrc'
- 'echo ''yarn-offline-mirror "/cache/npm-packages-offline-cache"'' >> .yarnrc'
- "echo 'yarn-offline-mirror-pruning: true' >> .yarnrc"
- cat .yarnrc
- yarn install --verbose --frozen-lockfile
- name: mongo
image: mongo
pull: if-not-exists
detach: true
- name: maildev
image: node
pull: if-not-exists
volumes:
- name: cache
path: /cache
commands:
- yarn run maildev --web 80 --smtp 25 -v --hide-extensions=STARTTLS
detach: true
- name: liveserver
image: node
pull: if-not-exists
volumes:
- name: cache
path: /cache
commands:
- yarn run -- live-server --no-browser --port=80 --ignore='*' --entry-file=spa.html --no-css-inject --proxy=/api:http://tibi-server:8080/api/v1/_/__NAMESPACE__ dist
detach: true
- name: tibi-server
image: registry.webmakers.de/tibi/tibi-server
pull: never
environment:
DB_DIAL: mongodb://mongo
API_PORT: 8080
MAIL_HOST: maildev:25
detach: true
- name: cypress run
image: cypress/base
pull: if-not-exists
volumes:
- name: cache
path: /cache
environment:
FORCE_COLOR: "true"
CYPRESS_BASE_URL: http://liveserver
CYPRESS_CI: "true"
CYPRESS_mongodbUri: mongodb://mongo
CYPRESS_tibiApiUrl: http://tibi-server:8080/api/v1
CYPRESS_projectApiConfig: /drone/workdir/api/config.yml
commands:
- ln -s /cache/user-cache ~/.cache
- yarn build:instanbul
- yarn cy:run
- yarn run nyc report --exclude-after-remap false
- name: modify master config
image: bash
pull: if-not-exists
commands:
- bash scripts/modify-config.sh master __MASTER_URL__
when:
branch: [master]
- name: modify dev config
image: bash
pull: if-not-exists
commands:
- bash scripts/modify-config.sh dev __DEV_URL__
when:
branch: [dev]
- name: build
image: node
pull: if-not-exists
volumes:
- name: cache
path: /cache
commands:
- yarn build
- name: build ssr
image: node
pull: if-not-exists
volumes:
- name: cache
path: /cache
commands:
- yarn build:server
- name: build legacy
image: node
pull: if-not-exists
volumes:
- name: cache
path: /cache
commands:
- yarn build:legacy
- name: modify html
image: bash
pull: if-not-exists
commands:
- bash scripts/preload-meta.sh public/spa.html
- bash scripts/preload-meta.sh public/spa.html > dist/spa.html
- export stamp=`date +%s`
- echo $$stamp
- sed -i s/__TIMESTAMP__/$$stamp/g dist/spa.html
- sed -i s/__TIMESTAMP__/$$stamp/g dist/serviceworker.js
- cat dist/serviceworker.js
- cp dist/spa.html api/templates/spa.html
- cat dist/spa.html
- name: deploy master
image: instrumentisto/rsync-ssh
pull: if-not-exists
environment:
RSYNC_USER: USER_PROJECT_master
RSYNC_PASS:
from_secret: rsync_master
commands:
- apk add --no-cache sshpass
- scripts/deploy.sh ftp1.webmakers.de $${RSYNC_USER} $${RSYNC_PASS}
when:
branch: [master]
event: [push]
- name: deploy dev
image: instrumentisto/rsync-ssh
pull: if-not-exists
environment:
RSYNC_USER: USER_PROJECT_dev
RSYNC_PASS:
from_secret: rsync_dev
commands:
- apk add --no-cache sshpass
- scripts/deploy.sh ftp1.webmakers.de $${RSYNC_USER} $${RSYNC_PASS}
when:
branch: [dev]
event: [push]
- name: prepare notify
image: cypress/base
pull: if-not-exists
commands:
- find cypress -type f -wholename "cypress/videos/*" -or -wholename "cypress/screenshots/*" | tar -cvf cypress-media.tar -T -
when:
status:
- failure
- name: notify
image: drillster/drone-email
pull: if-not-exists
settings:
from: noreply@ci.gitbase.de
host: smtp.basehosts.de
attachment: cypress-media.tar
when:
status:
- failure
volumes:
- name: cache
host:
path: /tmp/cache/drone/${DRONE_REPO}

25
.env Normal file
View File

@@ -0,0 +1,25 @@
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=wmbasic
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
#START_SCRIPT=:ssr
MOCK=1

View File

@@ -0,0 +1,72 @@
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: modify config
run: ./scripts/ci-modify-config.sh
- name: build
env:
FORCE_COLOR: "true"
run: |
yarn build
- name: build admin
env:
FORCE_COLOR: "true"
run: |
yarn build:admin
- 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

37
.gitignore vendored
View File

@@ -1,17 +1,22 @@
_temp/
node_modules/
dist/
build/
build_ssr/
stat/
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
/media/
/test.js
/api/templates/spa.html
/api/hooks/lib/app.server*
cypress/_old
cypress/videos
cypress/screenshots
.~lock.*
coverage/
.nyc_output/
test-results/
playwright-report/
playwright/.cache/
visual-review/
video-tours/output/
.playwright-mcp/
.yarn/*
!.yarn/cache
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions

View File

@@ -10,7 +10,7 @@
"check-parameters"
],
"no-var-keyword": true,
"svelteSortOrder": "scripts-markup-styles",
"svelteSortOrder": "scripts-options-markup-styles",
"svelteStrictMode": true,
"svelteBracketNewLine": true,
"svelteAllowShorthand": true,

15
.vscode/launch.json vendored Normal file
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"
}
]
}

59
.vscode/settings.json vendored
View File

@@ -1,34 +1,39 @@
{
"eslint.alwaysShowStatus": true,
"tslint.autoFixOnSave": true,
"editor.tabCompletion": "on",
"diffEditor.codeLens": true,
"editor.formatOnSave": true,
"editor.formatOnPaste": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[markdown]": {
"editor.wordWrap": "on",
"editor.defaultFormatter": "vscode.markdown-language-features"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"liveServer.settings.root": "/dist",
"liveServer.settings.file": "spa.html",
"liveServer.settings.port": 5502,
"liveServer.settings.proxy": {
"enable": true,
"baseUri": "/api",
"proxyUri": "http://127.0.0.1:8080/api/v1/_/__NAMESPACE__"
},
"extensions.ignoreRecommendations": true,
"files.autoSave": "off",
"typescript.tsc.autoDetect": "off",
"npm.autoDetect": "off",
"debug.allowBreakpointsEverywhere": true,
"html.autoClosingTags": false,
"yaml.schemas": {
"node_modules/tibi-types/schemas/api-config/config.json": "api/config.y*ml",
"node_modules/tibi-types/schemas/api-config/collection.json": "api/collections/*.y*ml",
"node_modules/tibi-types/schemas/api-config/field.json": "api/collections/fields/*.y*ml"
"./../../cms/tibi-types/schemas/api-config/config.json": "api/config.y*ml",
"./../../cms/tibi-types/schemas/api-config/collection.json": "api/collections/*.y*ml",
"./../../cms/tibi-types/schemas/api-config/field.json": "api/collections/fields/*.y*ml",
"./../../cms/tibi-types/schemas/api-config/fieldArray.json": "api/collections/fieldLists/*.y*ml",
"./../../cms/tibi-types/schemas/api-config/job.json": "api/jobs/*.y*ml",
"./../../cms/tibi-types/schemas/api-config/assets.json": "api/assets/*.y*ml"
},
"yaml.customTags": ["!include scalar"]
"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.

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