Compare commits
28 Commits
master
...
feature/co
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8571c8854 | ||
|
|
67f8e74f7f | ||
|
|
fba9830368 | ||
|
|
10720ac6ff | ||
|
|
2b400e8cad | ||
|
|
027cdba67d | ||
| 969ebabd18 | |||
|
|
2ab447274a | ||
|
|
258d89d339 | ||
| c723f1e1d4 | |||
|
44270c6187
|
|||
|
e7126b86d6
|
|||
|
d119c39a72
|
|||
|
e3ba15dd6b
|
|||
|
abc657252c
|
|||
|
6c24732380
|
|||
|
46c8119548
|
|||
|
71fd86b376
|
|||
|
45c628fef8
|
|||
|
73bfe07b11
|
|||
|
6f0e4da0d2
|
|||
|
0d05965ddb
|
|||
|
fdadede25f
|
|||
|
a3892ef9e1
|
|||
| d5fcfe2d05 | |||
|
b8810b8bcb
|
|||
|
61ddf2e5d0
|
|||
|
626e83d010
|
@@ -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`.
|
||||
@@ -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
|
||||

|
||||
```
|
||||
@@ -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.
|
||||
@@ -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.**
|
||||
@@ -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 +0,0 @@
|
||||
code:$apr1$AeePIAei$E9E6E6jtFFtwmtGhIEG.Y/
|
||||
@@ -1,2 +0,0 @@
|
||||
code:$apr1$AeePIAei$E9E6E6jtFFtwmtGhIEG.Y/
|
||||
web:$apr1$/zc/TBtD$ZGr3RqPiULYMD0kJUup5E0
|
||||
191
.drone.yml
Normal file
191
.drone.yml
Normal 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
.env
25
.env
@@ -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
.gitattributes
vendored
1
.gitattributes
vendored
@@ -1 +0,0 @@
|
||||
.yarn/cache/** filter=lfs diff=lfs merge=lfs -text
|
||||
@@ -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
|
||||
37
.gitignore
vendored
37
.gitignore
vendored
@@ -1,22 +1,17 @@
|
||||
api/hooks/lib/app.server*
|
||||
api/hooks/lib/buildInfo.js
|
||||
frontend/src/lib/buildInfo.ts
|
||||
node_modules
|
||||
media
|
||||
tmp
|
||||
_temp
|
||||
frontend/dist
|
||||
_temp/
|
||||
node_modules/
|
||||
dist/
|
||||
build/
|
||||
build_ssr/
|
||||
stat/
|
||||
yarn-error.log
|
||||
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
|
||||
/media/
|
||||
/test.js
|
||||
/api/templates/spa.html
|
||||
/api/hooks/lib/app.server*
|
||||
cypress/_old
|
||||
cypress/videos
|
||||
cypress/screenshots
|
||||
.~lock.*
|
||||
coverage/
|
||||
.nyc_output/
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"check-parameters"
|
||||
],
|
||||
"no-var-keyword": true,
|
||||
"svelteSortOrder": "scripts-options-markup-styles",
|
||||
"svelteSortOrder": "scripts-markup-styles",
|
||||
"svelteStrictMode": true,
|
||||
"svelteBracketNewLine": true,
|
||||
"svelteAllowShorthand": true,
|
||||
|
||||
15
.vscode/launch.json
vendored
15
.vscode/launch.json
vendored
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
59
.vscode/settings.json
vendored
59
.vscode/settings.json
vendored
@@ -1,39 +1,34 @@
|
||||
{
|
||||
"editor.tabCompletion": "on",
|
||||
"diffEditor.codeLens": true,
|
||||
"eslint.alwaysShowStatus": true,
|
||||
"tslint.autoFixOnSave": 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": {
|
||||
"./../../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"
|
||||
"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"
|
||||
},
|
||||
"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
|
||||
"yaml.customTags": ["!include scalar"]
|
||||
}
|
||||
|
||||
BIN
.yarn/cache/@alloc-quick-lru-npm-5.2.0-eb83517088-bdc35758b5.zip
LFS
vendored
BIN
.yarn/cache/@alloc-quick-lru-npm-5.2.0-eb83517088-bdc35758b5.zip
LFS
vendored
Binary file not shown.
BIN
.yarn/cache/@babel-cli-npm-7.28.6-b4e455ce7e-49279aa65d.zip
LFS
vendored
BIN
.yarn/cache/@babel-cli-npm-7.28.6-b4e455ce7e-49279aa65d.zip
LFS
vendored
Binary file not shown.
BIN
.yarn/cache/@babel-code-frame-npm-7.27.1-4dbcabb137-721b8a6e36.zip
LFS
vendored
BIN
.yarn/cache/@babel-code-frame-npm-7.27.1-4dbcabb137-721b8a6e36.zip
LFS
vendored
Binary file not shown.
BIN
.yarn/cache/@babel-code-frame-npm-7.29.0-6c4947d913-199e15ff89.zip
LFS
vendored
BIN
.yarn/cache/@babel-code-frame-npm-7.29.0-6c4947d913-199e15ff89.zip
LFS
vendored
Binary file not shown.
BIN
.yarn/cache/@babel-compat-data-npm-7.27.7-1eceb4277e-e71bf453a4.zip
LFS
vendored
BIN
.yarn/cache/@babel-compat-data-npm-7.27.7-1eceb4277e-e71bf453a4.zip
LFS
vendored
Binary file not shown.
BIN
.yarn/cache/@babel-compat-data-npm-7.29.0-6b4382e79f-7f21beedb9.zip
LFS
vendored
BIN
.yarn/cache/@babel-compat-data-npm-7.29.0-6b4382e79f-7f21beedb9.zip
LFS
vendored
Binary file not shown.
BIN
.yarn/cache/@babel-core-npm-7.29.0-a74bfc561b-25f4e91688.zip
LFS
vendored
BIN
.yarn/cache/@babel-core-npm-7.29.0-a74bfc561b-25f4e91688.zip
LFS
vendored
Binary file not shown.
BIN
.yarn/cache/@babel-generator-npm-7.27.5-b91f717ed1-f5e6942670.zip
LFS
vendored
BIN
.yarn/cache/@babel-generator-npm-7.27.5-b91f717ed1-f5e6942670.zip
LFS
vendored
Binary file not shown.
BIN
.yarn/cache/@babel-generator-npm-7.29.1-b1bf16fe79-61fe4ddd6e.zip
LFS
vendored
BIN
.yarn/cache/@babel-generator-npm-7.29.1-b1bf16fe79-61fe4ddd6e.zip
LFS
vendored
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@babel-helpers-npm-7.28.6-682df48628-213485cdff.zip
LFS
vendored
BIN
.yarn/cache/@babel-helpers-npm-7.28.6-682df48628-213485cdff.zip
LFS
vendored
Binary file not shown.
BIN
.yarn/cache/@babel-parser-npm-7.27.7-412e710268-ed25ccfc70.zip
LFS
vendored
BIN
.yarn/cache/@babel-parser-npm-7.27.7-412e710268-ed25ccfc70.zip
LFS
vendored
Binary file not shown.
BIN
.yarn/cache/@babel-parser-npm-7.29.0-c605c63e8b-b1576dca41.zip
LFS
vendored
BIN
.yarn/cache/@babel-parser-npm-7.29.0-c605c63e8b-b1576dca41.zip
LFS
vendored
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user