23 Commits

Author SHA1 Message Date
Mario Linz 027cdba67d Prototype Article und Theme Files hinzugefügt 2022-04-01 22:46:54 +02:00
mario 969ebabd18 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 2ab447274a 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 258d89d339 Collections für neue Projekte optimiert...work in progress... 2022-03-18 21:33:01 +01:00
mario c723f1e1d4 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 44270c6187 modrewrite proxy added 2022-03-14 17:18:18 +01:00
apairon e7126b86d6 cypress tsconfig.json fix 2022-02-26 17:43:34 +01:00
apairon d119c39a72 fixed cy:docker: 2022-02-26 11:39:25 +01:00
apairon e3ba15dd6b renamed to tibi-svelte-starter 2022-02-26 11:09:53 +01:00
apairon abc657252c api schema 2022-02-01 19:03:49 +01:00
apairon 6c24732380 ssr 404 2022-01-25 16:21:54 +01:00
apairon 46c8119548 fixed ssr 2022-01-19 18:50:58 +01:00
apairon 71fd86b376 upgrade 2021-12-08 12:56:19 +01:00
apairon 45c628fef8 fixed secret exploit via ssr code sourcemap 2021-09-14 15:51:05 +02:00
apairon 73bfe07b11 cypress and instanbul 2021-09-14 14:45:47 +02:00
apairon 6f0e4da0d2 browsersync 2021-09-14 13:26:35 +02:00
apairon 0d05965ddb sourcemap tests 2021-09-13 18:12:40 +02:00
apairon fdadede25f Merge branch 'master' of ssh://gitbase.de:2222/cms/wmbasic-svelte-starter 2021-08-16 11:08:19 +02:00
apairon a3892ef9e1 using wmbasic-api-types 2021-08-16 11:07:11 +02:00
apairon d5fcfe2d05 „api/hooks/types.d.ts“ ändern 2021-04-29 15:43:03 +02:00
apairon b8810b8bcb readme 2021-03-30 17:45:09 +02:00
apairon 61ddf2e5d0 init 2021-03-22 16:54:31 +01:00
apairon 626e83d010 init 2021-03-22 15:59:05 +01:00
778 changed files with 17215 additions and 19563 deletions
-524
View File
@@ -1,524 +0,0 @@
---
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`.
-325
View File
@@ -1,325 +0,0 @@
---
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`.
@@ -1,360 +0,0 @@
---
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).
@@ -1,25 +0,0 @@
---
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
@@ -1,175 +0,0 @@
---
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).
@@ -1,71 +0,0 @@
---
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.
-173
View File
@@ -1,173 +0,0 @@
---
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.**
-55
View File
@@ -1,55 +0,0 @@
---
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
View File
@@ -1 +0,0 @@
code:$apr1$AeePIAei$E9E6E6jtFFtwmtGhIEG.Y/
-2
View File
@@ -1,2 +0,0 @@
code:$apr1$AeePIAei$E9E6E6jtFFtwmtGhIEG.Y/
web:$apr1$/zc/TBtD$ZGr3RqPiULYMD0kJUup5E0
+191
View File
@@ -0,0 +1,191 @@
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
View File
@@ -1,25 +0,0 @@
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
-1
View File
@@ -1 +0,0 @@
.yarn/cache/** filter=lfs diff=lfs merge=lfs -text
-72
View File
@@ -1,72 +0,0 @@
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
+16 -21
View File
@@ -1,22 +1,17 @@
api/hooks/lib/app.server* _temp/
api/hooks/lib/buildInfo.js node_modules/
frontend/src/lib/buildInfo.ts dist/
node_modules build/
media build_ssr/
tmp stat/
_temp
frontend/dist
yarn-error.log yarn-error.log
test-results/ /media/
playwright-report/ /test.js
playwright/.cache/ /api/templates/spa.html
visual-review/ /api/hooks/lib/app.server*
video-tours/output/ cypress/_old
.playwright-mcp/ cypress/videos
.yarn/* cypress/screenshots
!.yarn/cache .~lock.*
!.yarn/patches coverage/
!.yarn/plugins .nyc_output/
!.yarn/releases
!.yarn/sdks
!.yarn/versions
+1 -1
View File
@@ -10,7 +10,7 @@
"check-parameters" "check-parameters"
], ],
"no-var-keyword": true, "no-var-keyword": true,
"svelteSortOrder": "scripts-options-markup-styles", "svelteSortOrder": "scripts-markup-styles",
"svelteStrictMode": true, "svelteStrictMode": true,
"svelteBracketNewLine": true, "svelteBracketNewLine": true,
"svelteAllowShorthand": true, "svelteAllowShorthand": true,
-15
View File
@@ -1,15 +0,0 @@
{
// 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"
}
]
}
+27 -32
View File
@@ -1,39 +1,34 @@
{ {
"editor.tabCompletion": "on", "eslint.alwaysShowStatus": true,
"diffEditor.codeLens": true, "tslint.autoFixOnSave": true,
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.formatOnPaste": true, "editor.formatOnPaste": true,
"editor.defaultFormatter": "esbenp.prettier-vscode", "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": { "yaml.schemas": {
"./../../cms/tibi-types/schemas/api-config/config.json": "api/config.y*ml", "node_modules/tibi-types/schemas/api-config/config.json": "api/config.y*ml",
"./../../cms/tibi-types/schemas/api-config/collection.json": "api/collections/*.y*ml", "node_modules/tibi-types/schemas/api-config/collection.json": "api/collections/*.y*ml",
"./../../cms/tibi-types/schemas/api-config/field.json": "api/collections/fields/*.y*ml", "node_modules/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.
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