forked from cms/tibi-svelte-starter
Compare commits
114 Commits
feature/co
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
0be4852f74
|
|||
|
a9a13a6b5b
|
|||
|
18b5af5617
|
|||
|
d1ef9800f1
|
|||
|
2170bf761e
|
|||
|
5707eb30dd
|
|||
|
965a505e15
|
|||
|
40ffa8207e
|
|||
|
e8fd38e98a
|
|||
|
20eaa50935
|
|||
|
30501f5f4c
|
|||
|
3c3e70b474
|
|||
|
602fd6101f
|
|||
|
74bb860d4f
|
|||
|
3b84e49383
|
|||
|
3886eb9f34
|
|||
|
b41d12f257
|
|||
|
fdeeac88e2
|
|||
|
e13e696253
|
|||
|
f6f565bbcb
|
|||
|
dc00d24899
|
|||
|
62f1906276
|
|||
|
b9a455d1b9
|
|||
|
18d5e977e5
|
|||
|
ae39987c7d
|
|||
|
4893d925c5
|
|||
|
66225b731a
|
|||
|
50b6f4a6e5
|
|||
|
2025a0a71f
|
|||
|
1ae34d6a18
|
|||
|
4756eab175
|
|||
|
55263a49be
|
|||
|
39caf6f7d6
|
|||
|
037b3d5a89
|
|||
|
4bbbfc5fee
|
|||
|
b4204da0a4
|
|||
|
ffcded42f3
|
|||
|
a72780873a
|
|||
|
f66c1fc078
|
|||
|
cf1acc1d80
|
|||
|
7a6a2cbd22
|
|||
|
77cb64b260
|
|||
|
2037953000
|
|||
|
3a6ff3fa8e
|
|||
|
212a9720cf
|
|||
|
4a8864c7b9
|
|||
| 30c05143fe | |||
|
825dfc18f9
|
|||
| c8443f4d11 | |||
| ac7eec418c | |||
| fef4d3b023 | |||
| 1bfa0d8b1b | |||
| 345ecb6177 | |||
| dbbd7c63ed | |||
| 49896d6978 | |||
| d8e4f9c902 | |||
| 706ec88576 | |||
| 0d06a61c7f | |||
| 652f15830d | |||
| 65e8b7ffc8 | |||
| 25a39dfac0 | |||
| ee7ed0db1f | |||
|
|
05fe698af6 | ||
|
|
2737872396 | ||
| 7c712ee7c8 | |||
| 60bd5d21c9 | |||
| ddd236af06 | |||
| 9cf3a814e3 | |||
| de6968f3d8 | |||
| 4824effccb | |||
| d872767845 | |||
| 432d21daeb | |||
| cabaaef456 | |||
| aaf2860714 | |||
| 87aa1689f3 | |||
| fcf5490d5a | |||
| 5d08a96327 | |||
| c67f712280 | |||
|
|
a2bd10453b | ||
|
|
de7ddc1097 | ||
| 9188148fe7 | |||
| 516c35dcb4 | |||
| 47fdee2396 | |||
| f4b6bb17ca | |||
| 87fd3c6148 | |||
| d0c6ad4092 | |||
| eedb794251 | |||
| c1894528b7 | |||
| 09a7688e29 | |||
| 75534213e8 | |||
| fd613e5a7d | |||
|
|
ef8d571ac5 | ||
| 4aec1bd712 | |||
|
|
c00f5a9fb3 | ||
|
|
88c5147363 | ||
| 5caa62eb7e | |||
| 75a8906d4a | |||
| 0ac8817805 | |||
| ff7441f3c5 | |||
| 5843680e14 | |||
| 66d8313316 | |||
| 24d2aaaa50 | |||
| 85fd41ce58 | |||
| 9c559f7020 | |||
| bd087ae658 | |||
| f9fe8fd735 | |||
| 786fd12f34 | |||
| 95c2950193 | |||
| 0bf64b1031 | |||
| f6ac48daab | |||
| a232a0119e | |||
| 82903a8029 | |||
| 0aca310a5e | |||
| 2ee7f650db |
524
.agents/skills/admin-ui-config/SKILL.md
Normal file
524
.agents/skills/admin-ui-config/SKILL.md
Normal file
@@ -0,0 +1,524 @@
|
||||
---
|
||||
name: admin-ui-config
|
||||
description: Configure the admin UI for collections — meta labels, views (table/list/cards), field widgets, inputProps, fieldLists, sidebar layout, choices, foreign references, and image handling. Use when setting up or customizing collection admin views.
|
||||
---
|
||||
|
||||
# admin-ui-config
|
||||
|
||||
## When to use this skill
|
||||
|
||||
Use this skill when:
|
||||
|
||||
- Configuring how a collection appears in the tibi-admin UI
|
||||
- Setting up table/list/card views for a collection
|
||||
- Configuring field widgets (dropdowns, media pickers, richtext, etc.)
|
||||
- Organizing fields into sidebar groups or sections
|
||||
- Setting up foreign key references between collections
|
||||
- Customizing the admin module (`frontend/src/admin.ts`)
|
||||
|
||||
## Reference source
|
||||
|
||||
The canonical type definitions are in `tibi-admin-nova/types/admin.d.ts` (1296 lines). Always consult this file for the full API. This skill provides a practical summary.
|
||||
|
||||
---
|
||||
|
||||
## Collection meta configuration
|
||||
|
||||
The `meta` key in a collection YAML controls how the collection appears in the admin sidebar and list views.
|
||||
|
||||
```yaml
|
||||
name: mycollection
|
||||
meta:
|
||||
label: { de: "Produkte", en: "Products" } # Sidebar label (i18n)
|
||||
muiIcon: shopping_cart # Material UI icon name
|
||||
group: shop # Group in admin sidebar
|
||||
singleton: false # true = only one entry allowed
|
||||
hideInNavigation: false # true = don't show in sidebar
|
||||
defaultSort: "-insertTime" # Default sort (prefix - = descending)
|
||||
rowIdentTpl: { twig: "{{ name }} ({{ price }})" } # Row display template
|
||||
```
|
||||
|
||||
### Row identification
|
||||
|
||||
`rowIdentTpl` uses Twig syntax with field names. Used in admin list to identify entries:
|
||||
|
||||
```yaml
|
||||
rowIdentTpl: { twig: "{{ name }}" } # Simple
|
||||
rowIdentTpl: { twig: "{{ type }} — {{ language }}" } # Combined
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Views: table, simpleList, cardList
|
||||
|
||||
The `views` array defines how entries are displayed in the admin list. Multiple views can coexist (e.g. table for desktop, simpleList for mobile).
|
||||
|
||||
### Table view
|
||||
|
||||
```yaml
|
||||
meta:
|
||||
views:
|
||||
- type: table
|
||||
columns:
|
||||
- name # Simple: field name as column
|
||||
- source: lang # With filter
|
||||
filter: true
|
||||
- source: active # Boolean column with filter
|
||||
filter: true
|
||||
- source: price # Custom label
|
||||
label: { de: "Preis", en: "Price" }
|
||||
- source: insertTime # Date field
|
||||
width: 160
|
||||
```
|
||||
|
||||
### Simple list view (mobile)
|
||||
|
||||
```yaml
|
||||
meta:
|
||||
views:
|
||||
- type: simpleList
|
||||
mediaQuery: "(max-width: 600px)" # Show only on small screens
|
||||
primaryText: name
|
||||
secondaryText: lang
|
||||
tertiaryText: path
|
||||
image: thumbnail # Optional: show image thumbnail
|
||||
```
|
||||
|
||||
### Card list view
|
||||
|
||||
```yaml
|
||||
meta:
|
||||
views:
|
||||
- type: cardList
|
||||
fields:
|
||||
- source: name
|
||||
label: Name
|
||||
- source: price
|
||||
label: Preis
|
||||
widget: currency
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Field configuration
|
||||
|
||||
Each field in the `fields` array can have a `meta` section controlling its admin UI behavior.
|
||||
|
||||
### Basic field with meta
|
||||
|
||||
```yaml
|
||||
fields:
|
||||
- name: name
|
||||
type: string
|
||||
meta:
|
||||
label: { de: "Name", en: "Name" }
|
||||
helperText: { de: "Anzeigename", en: "Display name" }
|
||||
position: main # "main" (default) or "sidebar"
|
||||
```
|
||||
|
||||
### Field types
|
||||
|
||||
| YAML `type` | Admin widget (default) | Notes |
|
||||
| ----------- | ---------------------- | --------------------------------------------- |
|
||||
| `string` | Text input | Use `inputProps.multiline: true` for textarea |
|
||||
| `number` | Number input | |
|
||||
| `boolean` | Toggle/checkbox | |
|
||||
| `date` | Date picker | |
|
||||
| `object` | Nested field group | Requires `subFields` |
|
||||
| `object[]` | Repeatable group | Requires `subFields`, drag-to-reorder |
|
||||
| `string[]` | Tag input | |
|
||||
| `file` | File upload | |
|
||||
| `file[]` | Multi-file upload | |
|
||||
|
||||
### inputProps — widget customization
|
||||
|
||||
`inputProps` passes props directly to the field widget:
|
||||
|
||||
```yaml
|
||||
# Multiline text (textarea)
|
||||
- name: description
|
||||
type: string
|
||||
meta:
|
||||
label: { de: "Beschreibung", en: "Description" }
|
||||
inputProps:
|
||||
multiline: true
|
||||
rows: 5
|
||||
|
||||
# Number with min/max
|
||||
- name: price
|
||||
type: number
|
||||
meta:
|
||||
inputProps:
|
||||
min: 0
|
||||
max: 99999
|
||||
step: 0.01
|
||||
|
||||
# Placeholder text
|
||||
- name: email
|
||||
type: string
|
||||
meta:
|
||||
inputProps:
|
||||
placeholder: "name@example.com"
|
||||
```
|
||||
|
||||
### Widget override
|
||||
|
||||
Override the default widget with `meta.widget`:
|
||||
|
||||
```yaml
|
||||
- name: content
|
||||
type: string
|
||||
meta:
|
||||
widget: richtext # Rich text editor (HTML)
|
||||
|
||||
- name: color
|
||||
type: string
|
||||
meta:
|
||||
widget: color # Color picker
|
||||
|
||||
- name: image
|
||||
type: string
|
||||
meta:
|
||||
widget: medialib # Media library picker
|
||||
```
|
||||
|
||||
Common widget types: `text` (default), `richtext`, `color`, `medialib`, `code`, `markdown`, `password`, `hidden`.
|
||||
|
||||
### Choices — dropdowns/selects
|
||||
|
||||
Static choices:
|
||||
|
||||
```yaml
|
||||
- name: type
|
||||
type: string
|
||||
meta:
|
||||
label: { de: "Typ", en: "Type" }
|
||||
choices:
|
||||
- id: page
|
||||
name: { de: "Seite", en: "Page" }
|
||||
- id: blog
|
||||
name: { de: "Blog", en: "Blog" }
|
||||
- id: product
|
||||
name: { de: "Produkt", en: "Product" }
|
||||
```
|
||||
|
||||
Dynamic choices from API:
|
||||
|
||||
```yaml
|
||||
- name: category
|
||||
type: string
|
||||
meta:
|
||||
choices:
|
||||
endpoint: categories # Collection name
|
||||
mapping:
|
||||
id: _id
|
||||
name: name
|
||||
```
|
||||
|
||||
### Foreign references
|
||||
|
||||
Link to entries in another collection:
|
||||
|
||||
```yaml
|
||||
- name: author
|
||||
type: string
|
||||
meta:
|
||||
label: { de: "Autor", en: "Author" }
|
||||
foreign:
|
||||
collection: users
|
||||
id: _id
|
||||
sort: name
|
||||
projection: name,email
|
||||
render: { twig: "{{ name }} <{{ email }}>" }
|
||||
autoFill: # Auto-fill other fields on selection
|
||||
- source: email
|
||||
target: authorEmail
|
||||
```
|
||||
|
||||
### Image fields
|
||||
|
||||
```yaml
|
||||
- name: image
|
||||
type: file
|
||||
meta:
|
||||
widget: medialib
|
||||
downscale: # Auto-resize on upload
|
||||
maxWidth: 1920
|
||||
maxHeight: 1080
|
||||
quality: 0.85
|
||||
imageEditor: true # Enable crop/rotate editor
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Layout: position, sections, sidebar
|
||||
|
||||
### Sidebar placement
|
||||
|
||||
```yaml
|
||||
- name: active
|
||||
type: boolean
|
||||
meta:
|
||||
position: sidebar # Moves field to sidebar
|
||||
|
||||
- name: publishDate
|
||||
type: date
|
||||
meta:
|
||||
position: "sidebar:Veröffentlichung" # Sidebar with group header
|
||||
```
|
||||
|
||||
### Sidebar groups (ordered)
|
||||
|
||||
Define sidebar group order in collection meta:
|
||||
|
||||
```yaml
|
||||
meta:
|
||||
sidebar:
|
||||
- Veröffentlichung
|
||||
- SEO
|
||||
- Einstellungen
|
||||
```
|
||||
|
||||
### Sections in main area
|
||||
|
||||
```yaml
|
||||
- name: seoTitle
|
||||
type: string
|
||||
meta:
|
||||
section: SEO # Groups fields under a section header
|
||||
|
||||
- name: seoDescription
|
||||
type: string
|
||||
meta:
|
||||
section: SEO
|
||||
```
|
||||
|
||||
### Grid layout (columns)
|
||||
|
||||
Use `containerProps` for multi-column layout:
|
||||
|
||||
```yaml
|
||||
- name: firstName
|
||||
type: string
|
||||
meta:
|
||||
containerProps:
|
||||
layout:
|
||||
size: col-6 # Half width (12-column grid)
|
||||
|
||||
- name: lastName
|
||||
type: string
|
||||
meta:
|
||||
containerProps:
|
||||
layout:
|
||||
size: col-6
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Nested objects and arrays
|
||||
|
||||
### Object (nested group)
|
||||
|
||||
```yaml
|
||||
- name: address
|
||||
type: object
|
||||
meta:
|
||||
label: { de: "Adresse", en: "Address" }
|
||||
subFields:
|
||||
- name: street
|
||||
type: string
|
||||
- name: city
|
||||
type: string
|
||||
- name: zip
|
||||
type: string
|
||||
```
|
||||
|
||||
### Object array (repeatable blocks)
|
||||
|
||||
```yaml
|
||||
- name: blocks
|
||||
type: object[]
|
||||
meta:
|
||||
label: { de: "Inhaltsblöcke", en: "Content Blocks" }
|
||||
preview: { eval: "item.type + ': ' + (item.headline || '')" }
|
||||
subFields:
|
||||
- name: type
|
||||
type: string
|
||||
meta:
|
||||
choices:
|
||||
- id: hero
|
||||
name: Hero
|
||||
- id: richtext
|
||||
name: Richtext
|
||||
- name: headline
|
||||
type: string
|
||||
- name: hide
|
||||
type: boolean
|
||||
```
|
||||
|
||||
The `preview` eval determines what's shown in the collapsed state of each array item.
|
||||
|
||||
### Drill-down
|
||||
|
||||
For complex nested objects, use `drillDown` to render them as a sub-page:
|
||||
|
||||
```yaml
|
||||
- name: variants
|
||||
type: object[]
|
||||
meta:
|
||||
drillDown: true # Opens as sub-page instead of inline
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Admin module (frontend/src/admin.ts)
|
||||
|
||||
The `admin.ts` file exports custom Svelte components for injection into the tibi-admin UI. Components are rendered inside Shadow DOM to isolate styles.
|
||||
|
||||
```typescript
|
||||
import type { SvelteComponent } from "svelte"
|
||||
|
||||
function getRenderedElement(
|
||||
component: typeof SvelteComponent,
|
||||
options?: { props: { [key: string]: any }; addCss?: string[] },
|
||||
nestedElements?: { tagName: string; className?: string }[]
|
||||
) {
|
||||
// Creates a Shadow DOM container, mounts the Svelte component inside
|
||||
// addCss: CSS files to inject into Shadow DOM
|
||||
// nestedElements: wrapper elements inside Shadow DOM
|
||||
}
|
||||
|
||||
export { getRenderedElement }
|
||||
```
|
||||
|
||||
Build with `yarn build:admin`. The output is loaded by tibi-admin-nova as a custom module.
|
||||
|
||||
**Use case:** Custom dashboard widgets, preview components, or field widgets that require Svelte rendering inside the admin UI.
|
||||
|
||||
---
|
||||
|
||||
## Complete collection example
|
||||
|
||||
```yaml
|
||||
name: products
|
||||
meta:
|
||||
label: { de: "Produkte", en: "Products" }
|
||||
muiIcon: inventory_2
|
||||
group: shop
|
||||
defaultSort: "-insertTime"
|
||||
rowIdentTpl: { twig: "{{ name }} ({{ sku }})" }
|
||||
sidebar:
|
||||
- Veröffentlichung
|
||||
- SEO
|
||||
|
||||
views:
|
||||
- type: simpleList
|
||||
mediaQuery: "(max-width: 600px)"
|
||||
primaryText: name
|
||||
secondaryText: sku
|
||||
image: image
|
||||
- type: table
|
||||
columns:
|
||||
- name
|
||||
- sku
|
||||
- source: price
|
||||
label: { de: "Preis", en: "Price" }
|
||||
- source: active
|
||||
filter: true
|
||||
- source: category
|
||||
filter: true
|
||||
|
||||
permissions:
|
||||
public:
|
||||
methods:
|
||||
get: true
|
||||
user:
|
||||
methods:
|
||||
get: true
|
||||
post: true
|
||||
put: true
|
||||
delete: true
|
||||
|
||||
hooks:
|
||||
beforeRead: |
|
||||
!include hooks/filter_public.js
|
||||
afterWrite: |
|
||||
!include hooks/clear_cache.js
|
||||
|
||||
fields:
|
||||
- name: active
|
||||
type: boolean
|
||||
meta:
|
||||
label: { de: "Aktiv", en: "Active" }
|
||||
position: "sidebar:Veröffentlichung"
|
||||
- name: name
|
||||
type: string
|
||||
meta:
|
||||
label: { de: "Name", en: "Name" }
|
||||
- name: sku
|
||||
type: string
|
||||
meta:
|
||||
label: { de: "Artikelnummer", en: "SKU" }
|
||||
containerProps:
|
||||
layout:
|
||||
size: col-6
|
||||
- name: price
|
||||
type: number
|
||||
meta:
|
||||
label: { de: "Preis", en: "Price" }
|
||||
inputProps:
|
||||
min: 0
|
||||
step: 0.01
|
||||
containerProps:
|
||||
layout:
|
||||
size: col-6
|
||||
- name: category
|
||||
type: string
|
||||
meta:
|
||||
label: { de: "Kategorie", en: "Category" }
|
||||
choices:
|
||||
- id: electronics
|
||||
name: { de: "Elektronik", en: "Electronics" }
|
||||
- id: clothing
|
||||
name: { de: "Kleidung", en: "Clothing" }
|
||||
- name: description
|
||||
type: string
|
||||
meta:
|
||||
label: { de: "Beschreibung", en: "Description" }
|
||||
inputProps:
|
||||
multiline: true
|
||||
rows: 4
|
||||
- name: image
|
||||
type: file
|
||||
meta:
|
||||
label: { de: "Produktbild", en: "Product Image" }
|
||||
widget: medialib
|
||||
downscale:
|
||||
maxWidth: 1200
|
||||
quality: 0.85
|
||||
- name: seoTitle
|
||||
type: string
|
||||
meta:
|
||||
label: { de: "SEO Titel", en: "SEO Title" }
|
||||
position: "sidebar:SEO"
|
||||
- name: seoDescription
|
||||
type: string
|
||||
meta:
|
||||
label: { de: "SEO Beschreibung", en: "SEO Description" }
|
||||
position: "sidebar:SEO"
|
||||
inputProps:
|
||||
multiline: true
|
||||
rows: 3
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **`meta.label` is i18n** — Always provide `{ de: "...", en: "..." }` objects, not plain strings.
|
||||
- **`views` order matters** — First matching view (by `mediaQuery`) is shown. Put mobile views (with `mediaQuery`) before desktop views (without).
|
||||
- **`choices.id` must match stored value** — The `id` in choices is what gets saved to the database.
|
||||
- **`inputProps` depends on widget** — Not all props work with all widgets. Check tibi-admin-nova source if unsure.
|
||||
- **`position: sidebar` without group** — Fields go to an ungrouped area. Use `position: "sidebar:GroupName"` for grouping.
|
||||
- **`type: object[]` needs `subFields`** — Forgetting `subFields` renders an empty repeater.
|
||||
- **hooks path** — Hook includes are relative to `api/` directory: `!include hooks/myfile.js`.
|
||||
325
.agents/skills/content-authoring/SKILL.md
Normal file
325
.agents/skills/content-authoring/SKILL.md
Normal file
@@ -0,0 +1,325 @@
|
||||
---
|
||||
name: content-authoring
|
||||
description: Add new pages, content blocks, and collections to a tibi project. Covers the content-based routing model, block registration in BlockRenderer, collection YAML authoring, and TypeScript type definitions. Use when creating new pages, block types, or collections.
|
||||
---
|
||||
|
||||
# content-authoring
|
||||
|
||||
## When to use this skill
|
||||
|
||||
Use this skill when:
|
||||
|
||||
- Adding a new page to the website
|
||||
- Creating a new content block type (e.g. testimonials, pricing table, gallery)
|
||||
- Adding a new collection to the CMS (e.g. products, events, team members)
|
||||
- Understanding how content is structured and rendered
|
||||
|
||||
## Key concept: content-based routing
|
||||
|
||||
This project does **NOT** use file-based routing (no SvelteKit router). Instead:
|
||||
|
||||
1. Pages are **CMS entries** in the `content` collection with a `path` field.
|
||||
2. `App.svelte` reacts to URL changes → calls `getCachedEntries("content", { lang, path, active: true })`.
|
||||
3. The matching `ContentEntry.blocks[]` array is passed to `BlockRenderer.svelte`.
|
||||
4. Each block has a `type` field that maps to a Svelte component.
|
||||
|
||||
**Implication:** To add a new page, you create a content entry (via Admin UI or API) — no new Svelte file or route config is needed.
|
||||
|
||||
---
|
||||
|
||||
## Adding a new page
|
||||
|
||||
### Option A: Via Admin UI (preferred for content editors)
|
||||
|
||||
1. Open the tibi-admin at `CODING_URL/_/admin/`.
|
||||
2. Navigate to **Inhalte** (Content) collection.
|
||||
3. Click **New** and fill in:
|
||||
- `name`: Display name (e.g. "Über uns")
|
||||
- `path`: URL path without language prefix (e.g. `/ueber-uns`)
|
||||
- `lang`: Language code (e.g. `de`)
|
||||
- `active`: `true`
|
||||
- `translationKey`: Shared key for cross-language linking (e.g. `about`)
|
||||
- `blocks`: Add content blocks (see below)
|
||||
- `meta.title` / `meta.description`: SEO metadata
|
||||
4. Save. The page is immediately available at `/{lang}{path}`.
|
||||
|
||||
### Option B: Via API
|
||||
|
||||
```sh
|
||||
curl -X POST "$CODING_URL/api/content" \
|
||||
-H "Token: $ADMIN_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"active": true,
|
||||
"lang": "de",
|
||||
"name": "Über uns",
|
||||
"path": "/ueber-uns",
|
||||
"translationKey": "about",
|
||||
"blocks": [
|
||||
{ "type": "hero", "headline": "Über uns", "subline": "Unser Team" }
|
||||
],
|
||||
"meta": { "title": "Über uns", "description": "Erfahre mehr über unser Team." }
|
||||
}'
|
||||
```
|
||||
|
||||
### Option C: Via mock data (for MOCK=1 mode)
|
||||
|
||||
Add the entry to `frontend/mocking/content.json` — the mock engine supports MongoDB-style filtering.
|
||||
|
||||
### Adding to navigation
|
||||
|
||||
To make the page appear in the header/footer menu, edit the corresponding `navigation` entry:
|
||||
|
||||
```sh
|
||||
# Get existing header nav
|
||||
curl "$CODING_URL/api/navigation?filter[type]=header&filter[language]=de" -H "Token: $ADMIN_TOKEN"
|
||||
|
||||
# PUT to update elements array (add your page)
|
||||
curl -X PUT "$CODING_URL/api/navigation/<id>" \
|
||||
-H "Token: $ADMIN_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{ "elements": [ ...existing, { "name": "Über uns", "page": "/ueber-uns" } ] }'
|
||||
```
|
||||
|
||||
### Multi-language pages
|
||||
|
||||
- Create one `ContentEntry` per language with the **same `translationKey`** but different `lang` and `path`.
|
||||
- The language switcher in `App.svelte` uses `currentContentEntry.translationKey` to find the equivalent page.
|
||||
- Add localized route slugs to `ROUTE_TRANSLATIONS` in `frontend/src/lib/i18n.ts` if URLs should differ per language (e.g. `/ueber-uns` vs `/about`).
|
||||
|
||||
---
|
||||
|
||||
## Adding a new content block type
|
||||
|
||||
### Step 1: Create the Svelte component
|
||||
|
||||
Create `frontend/src/blocks/MyNewBlock.svelte`:
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
let { block }: { block: ContentBlockEntry } = $props()
|
||||
</script>
|
||||
|
||||
<section class="py-16 sm:py-24" id={block.anchorId || undefined}>
|
||||
<div class="max-w-6xl mx-auto px-6">
|
||||
{#if block.headline}
|
||||
<h2 class="text-3xl font-bold mb-6">{block.headline}</h2>
|
||||
{/if}
|
||||
<!-- Block-specific content here -->
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
**Conventions:**
|
||||
|
||||
- Accept `block: ContentBlockEntry` as the single prop.
|
||||
- Use `block.anchorId` for scroll anchoring.
|
||||
- Respect `block.containerWidth` (`""` = default, `"wide"`, `"full"`).
|
||||
- Guard browser-only code with `typeof window !== "undefined"` (SSR safety).
|
||||
|
||||
### Step 2: Register in BlockRenderer
|
||||
|
||||
Edit `frontend/src/blocks/BlockRenderer.svelte`:
|
||||
|
||||
```svelte
|
||||
<!-- Add import at the top -->
|
||||
import MyNewBlock from "./MyNewBlock.svelte"
|
||||
|
||||
<!-- Add case in the {#each} block -->
|
||||
{:else if block.type === "my-new-block"}
|
||||
<MyNewBlock {block} />
|
||||
```
|
||||
|
||||
### Step 3: Extend TypeScript types (if new fields are needed)
|
||||
|
||||
Edit `types/global.d.ts` — add fields to `ContentBlockEntry`:
|
||||
|
||||
```typescript
|
||||
interface ContentBlockEntry {
|
||||
// ... existing fields ...
|
||||
// my-new-block fields
|
||||
myCustomField?: string
|
||||
myItems?: { title: string; description: string }[]
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Extend collection YAML (if new fields need admin editing)
|
||||
|
||||
Edit `api/collections/content.yml` — add subFields under `blocks`:
|
||||
|
||||
```yaml
|
||||
- name: blocks
|
||||
type: object[]
|
||||
subFields:
|
||||
# ... existing subFields ...
|
||||
- name: myCustomField
|
||||
type: string
|
||||
- name: myItems
|
||||
type: object[]
|
||||
subFields:
|
||||
- name: title
|
||||
type: string
|
||||
- name: description
|
||||
type: string
|
||||
```
|
||||
|
||||
### Step 5: Update mock data (if using MOCK=1)
|
||||
|
||||
Add a block with your new type to `frontend/mocking/content.json`.
|
||||
|
||||
### Step 6: Verify
|
||||
|
||||
```sh
|
||||
yarn validate # TypeScript check — must be warning-free
|
||||
```
|
||||
|
||||
### Existing block types for reference
|
||||
|
||||
| Type | Component | Purpose |
|
||||
| -------------- | ------------------------- | ----------------------------------------- |
|
||||
| `hero` | `HeroBlock.svelte` | Full-width hero with image, headline, CTA |
|
||||
| `features` | `FeaturesBlock.svelte` | Feature grid with icons |
|
||||
| `richtext` | `RichtextBlock.svelte` | Rich text with optional image |
|
||||
| `accordion` | `AccordionBlock.svelte` | Expandable FAQ/accordion items |
|
||||
| `contact-form` | `ContactFormBlock.svelte` | Contact form |
|
||||
|
||||
---
|
||||
|
||||
## Adding a new collection
|
||||
|
||||
### Step 1: Create collection YAML
|
||||
|
||||
Create `api/collections/mycollection.yml`. Use `content.yml` or `navigation.yml` as a template:
|
||||
|
||||
```yaml
|
||||
########################################################################
|
||||
# MyCollection — description of what this collection stores
|
||||
########################################################################
|
||||
|
||||
name: mycollection
|
||||
meta:
|
||||
label: { de: "Meine Sammlung", en: "My Collection" }
|
||||
muiIcon: category # Material UI icon name
|
||||
rowIdentTpl: { twig: "{{ name }}" } # Row display in admin list
|
||||
|
||||
views:
|
||||
- type: simpleList
|
||||
mediaQuery: "(max-width: 600px)"
|
||||
primaryText: name
|
||||
- type: table
|
||||
columns:
|
||||
- name
|
||||
- source: active
|
||||
filter: true
|
||||
|
||||
permissions:
|
||||
public:
|
||||
methods:
|
||||
get: true # Public read access
|
||||
user:
|
||||
methods:
|
||||
get: true
|
||||
post: true
|
||||
put: true
|
||||
delete: true
|
||||
|
||||
fields:
|
||||
- name: active
|
||||
type: boolean
|
||||
meta:
|
||||
label: { de: "Aktiv", en: "Active" }
|
||||
- name: name
|
||||
type: string
|
||||
meta:
|
||||
label: { de: "Name", en: "Name" }
|
||||
# Add more fields as needed
|
||||
```
|
||||
|
||||
**Field types:** `string`, `number`, `boolean`, `object`, `object[]`, `string[]`, `file`, `file[]`.
|
||||
|
||||
For the full schema reference: `tibi-types/schemas/api-config/collection.json`.
|
||||
|
||||
### Step 2: Include in config.yml
|
||||
|
||||
Edit `api/config.yml`:
|
||||
|
||||
```yaml
|
||||
collections:
|
||||
- !include collections/content.yml
|
||||
- !include collections/navigation.yml
|
||||
- !include collections/ssr.yml
|
||||
- !include collections/mycollection.yml # ← add this line
|
||||
```
|
||||
|
||||
### Step 3: Add TypeScript types
|
||||
|
||||
Edit `types/global.d.ts`:
|
||||
|
||||
```typescript
|
||||
interface MyCollectionEntry {
|
||||
id?: string
|
||||
_id?: string
|
||||
active?: boolean
|
||||
name?: string
|
||||
// ... fields matching your YAML
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Configure API layer (optional)
|
||||
|
||||
If you need typed helpers, extend the `EntryTypeSwitch` in `frontend/src/lib/api.ts`:
|
||||
|
||||
```typescript
|
||||
type CollectionNameT = "medialib" | "content" | "mycollection" | string
|
||||
|
||||
type EntryTypeSwitch<T extends string> = T extends "medialib"
|
||||
? MedialibEntry
|
||||
: T extends "content"
|
||||
? ContentEntry
|
||||
: T extends "mycollection"
|
||||
? MyCollectionEntry
|
||||
: Record<string, unknown>
|
||||
```
|
||||
|
||||
### Step 5: Add hooks (optional)
|
||||
|
||||
Common hook patterns:
|
||||
|
||||
- **Public filter** — reuse `filter_public.js` to enforce `active: true` for unauthenticated users.
|
||||
- **Before-save validation** — create `api/hooks/mycollection_validate.js`.
|
||||
- **Cache invalidation** — add your collection to `api/hooks/clear_cache.js` if it affects rendered pages.
|
||||
|
||||
Reference hook in YAML:
|
||||
|
||||
```yaml
|
||||
hooks:
|
||||
beforeRead: |
|
||||
!include hooks/filter_public.js
|
||||
afterWrite: |
|
||||
!include hooks/clear_cache.js
|
||||
```
|
||||
|
||||
### Step 6: Add mock data (if using MOCK=1)
|
||||
|
||||
Create `frontend/mocking/mycollection.json`:
|
||||
|
||||
```json
|
||||
[{ "_id": "1", "active": true, "name": "Example Entry" }]
|
||||
```
|
||||
|
||||
### Step 7: Verify
|
||||
|
||||
```sh
|
||||
yarn validate # TypeScript check
|
||||
# If Docker is running, the tibi-server auto-reloads the collection config
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Path format**: Content paths do NOT include the language prefix. The path `/ueber-uns` becomes `/{lang}/ueber-uns` via the i18n layer.
|
||||
- **Active flag**: Pages with `active: false` are filtered out by `filter_public.js` for public users. The admin can still see them.
|
||||
- **Block `hide` field**: Blocks with `hide: true` are skipped by `BlockRenderer.svelte` — useful for draft blocks.
|
||||
- **Collection YAML indentation**: YAML uses 2-space indentation. Sub-fields under `object[]` require a `subFields` key.
|
||||
- **After adding a collection**: The tibi-server auto-reloads hooks on file change, but a new collection in `config.yml` may require `make docker-restart-frontend` or a full `make docker-up`.
|
||||
360
.agents/skills/frontend-architecture/SKILL.md
Normal file
360
.agents/skills/frontend-architecture/SKILL.md
Normal file
@@ -0,0 +1,360 @@
|
||||
---
|
||||
name: frontend-architecture
|
||||
description: Understand the frontend architecture — custom SPA routing, state management, Svelte 5 patterns, API layer, error handling, and i18n. Use when working on routing logic, navigation, stores, or understanding how the frontend fits together.
|
||||
---
|
||||
|
||||
# frontend-architecture
|
||||
|
||||
## When to use this skill
|
||||
|
||||
Use this skill when:
|
||||
|
||||
- Understanding or modifying the SPA routing mechanism
|
||||
- Working with stores or state management
|
||||
- Debugging navigation issues
|
||||
- Adding new Svelte 5 reactive patterns
|
||||
- Understanding the API layer and error handling
|
||||
- Working with i18n / multi-language features
|
||||
|
||||
---
|
||||
|
||||
## Routing: custom SPA router
|
||||
|
||||
This project uses a **custom SPA router** — NOT SvelteKit, NOT file-based routing. Pages are CMS-managed content entries loaded by path.
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
Browser URL change
|
||||
↓
|
||||
history.pushState / replaceState (proxied in store.ts)
|
||||
↓
|
||||
$location store updates (path, search, hash)
|
||||
↓
|
||||
App.svelte $effect reacts to $location.path
|
||||
↓
|
||||
loadContent(lang, routePath) → API call: getCachedEntries("content", { lang, path, active: true })
|
||||
↓
|
||||
ContentEntry.blocks[] → BlockRenderer.svelte → individual block components
|
||||
```
|
||||
|
||||
### Key files
|
||||
|
||||
| File | Responsibility |
|
||||
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `frontend/src/lib/store.ts` | Proxies `history.pushState`/`replaceState` → updates `$location` writable store. Handles `popstate` for back/forward. |
|
||||
| `frontend/src/lib/navigation.ts` | `spaNavigate(url, options)` — the programmatic navigation API. Also: `initScrollRestoration()`, `spaLink` action, hash parsing. |
|
||||
| `frontend/src/lib/i18n.ts` | Language routing: `extractLanguageFromPath()`, `stripLanguageFromPath()`, `localizedPath()`, `currentLanguage` derived store, `ROUTE_TRANSLATIONS`. |
|
||||
| `frontend/src/App.svelte` | Reacts to `$location.path` + `$currentLanguage`, loads content via API, passes blocks to `BlockRenderer`. |
|
||||
| `frontend/src/blocks/BlockRenderer.svelte` | Maps `block.type` to Svelte components. |
|
||||
|
||||
### How the location store works
|
||||
|
||||
`store.ts` wraps `history.pushState` and `history.replaceState` with a `Proxy`:
|
||||
|
||||
```typescript
|
||||
// Simplified — see store.ts for full implementation
|
||||
history.pushState = new Proxy(history.pushState, {
|
||||
apply: (target, thisArg, args) => {
|
||||
// Update $location store BEFORE the actual pushState
|
||||
publishLocation(args[2]) // args[2] = URL
|
||||
Reflect.apply(target, thisArg, args)
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
This means **any** `pushState`/`replaceState` call (from `spaNavigate`, `<a>` clicks, or third-party code) automatically updates `$location`.
|
||||
|
||||
The `popstate` event (back/forward buttons) also triggers `publishLocation()`.
|
||||
|
||||
### URL structure
|
||||
|
||||
```
|
||||
/{lang}/{path}
|
||||
↓ ↓
|
||||
de /ueber-uns
|
||||
|
||||
Example: /de/ueber-uns → lang="de", routePath="/ueber-uns"
|
||||
/en/about → lang="en", routePath="/about"
|
||||
/de/ → lang="de", routePath="/"
|
||||
```
|
||||
|
||||
Root `/` redirects to `/{browserLanguage}/` via `getBrowserLanguage()`.
|
||||
|
||||
### Navigation API
|
||||
|
||||
```typescript
|
||||
import { spaNavigate } from "./lib/navigation"
|
||||
|
||||
// Basic navigation (creates history entry, scrolls to top)
|
||||
spaNavigate("/de/kontakt")
|
||||
|
||||
// Replace current entry (no back button)
|
||||
spaNavigate("/de/suche", { replace: true })
|
||||
|
||||
// Keep scroll position
|
||||
spaNavigate("/de/produkte#filter=shoes", { noScroll: true })
|
||||
|
||||
// With state object
|
||||
spaNavigate("/de/produkt/123", { state: { from: "search" } })
|
||||
```
|
||||
|
||||
### SPA link action
|
||||
|
||||
For `<a>` elements, use the `spaLink` action instead of `spaNavigate`:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { spaLink } from "../lib/navigation"
|
||||
</script>
|
||||
|
||||
<a href="/de/kontakt" use:spaLink>Kontakt</a>
|
||||
<a href="/de/suche" use:spaLink={{ replace: true }}>Suche</a>
|
||||
```
|
||||
|
||||
The action intercepts clicks (respecting modifier keys, external links, `target="_blank"`) and calls `spaNavigate` internally.
|
||||
|
||||
### BrowserSync SPA fallback
|
||||
|
||||
In development, BrowserSync uses `connect-history-api-fallback` to serve `index.html` for all routes, enabling client-side routing. In production, the webserver or tibi-server handles this.
|
||||
|
||||
### Localized route translations
|
||||
|
||||
For translated URL slugs (e.g. `/ueber-uns` ↔ `/about`), configure `ROUTE_TRANSLATIONS` in `frontend/src/lib/i18n.ts`:
|
||||
|
||||
```typescript
|
||||
export const ROUTE_TRANSLATIONS: Record<string, Record<SupportedLanguage, string>> = {
|
||||
about: { de: "ueber-uns", en: "about" },
|
||||
contact: { de: "kontakt", en: "contact" },
|
||||
// Add more as needed
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## State management
|
||||
|
||||
The project uses **Svelte writable/derived stores** (not a centralized state library).
|
||||
|
||||
### Store inventory
|
||||
|
||||
| Store | File | Purpose |
|
||||
| ---------------------- | ---------------------- | ----------------------------------------------------------------------------------- |
|
||||
| `location` | `lib/store.ts` | Current URL state (path, search, hash, push/pop flags) |
|
||||
| `mobileMenuOpen` | `lib/store.ts` | Whether mobile hamburger menu is open |
|
||||
| `currentContentEntry` | `lib/store.ts` | Currently displayed page's `translationKey`, `lang`, `path` (for language switcher) |
|
||||
| `previousPath` | `lib/store.ts` | Previous URL path (for conditional back buttons) |
|
||||
| `apiBaseOverride` | `lib/store.ts` | Override API base URL (used by admin module) |
|
||||
| `cookieConsentVisible` | `lib/store.ts` | Whether cookie consent banner is showing |
|
||||
| `currentLanguage` | `lib/i18n.ts` | Derived from `$location.path` — current language code |
|
||||
| `selectedLanguage` | `lib/i18n.ts` | Writable — synced with `currentLanguage` on navigation |
|
||||
| `activeRequests` | `lib/requestsStore.ts` | Number of in-flight API requests (drives `LoadingBar`) |
|
||||
|
||||
### Pattern: creating a new store
|
||||
|
||||
```typescript
|
||||
// In lib/store.ts or a dedicated file
|
||||
import { writable, derived } from "svelte/store"
|
||||
|
||||
// Simple writable
|
||||
export const myStore = writable<MyType>(initialValue)
|
||||
|
||||
// Derived from other stores
|
||||
export const myDerived = derived(location, ($loc) => {
|
||||
return computeFromPath($loc.path)
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Svelte 5 patterns used in this project
|
||||
|
||||
This project uses **Svelte 5 with Runes**. Key patterns:
|
||||
|
||||
### Component props
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
// Rune syntax — replaces export let
|
||||
let { block, className = "" }: { block: ContentBlockEntry; className?: string } = $props()
|
||||
</script>
|
||||
```
|
||||
|
||||
### Reactive state
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
// Local reactive state (replaces let x; with $: reactivity)
|
||||
let count = $state(0)
|
||||
let items = $state<Item[]>([])
|
||||
|
||||
// Computed/derived values (replaces $: derived = ...)
|
||||
let total = $derived(items.reduce((sum, i) => sum + i.price, 0))
|
||||
|
||||
// Side effects (replaces $: { ... } reactive blocks)
|
||||
$effect(() => {
|
||||
// Runs when dependencies change
|
||||
console.log("count changed:", count)
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
### SSR-safe code
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { untrack } from "svelte"
|
||||
|
||||
// Guard browser-only APIs
|
||||
if (typeof window !== "undefined") {
|
||||
window.addEventListener("scroll", handleScroll, { passive: true })
|
||||
}
|
||||
|
||||
// untrack: capture initial value without creating reactive dependency
|
||||
// Used in App.svelte for SSR initial URL
|
||||
untrack(() => {
|
||||
if (url) { /* set initial location */ }
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
### Svelte stores in Svelte 5
|
||||
|
||||
Stores (`writable`, `derived`) still work in Svelte 5. Use `$storeName` syntax in components:
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { location } from "./lib/store"
|
||||
// $location is reactive — auto-subscribes in Svelte 5
|
||||
</script>
|
||||
<p>Current path: {$location.path}</p>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API layer
|
||||
|
||||
### Core function: `api()`
|
||||
|
||||
Located in `frontend/src/lib/api.ts`. Features:
|
||||
|
||||
- **Request deduplication** — identical concurrent GETs share one promise
|
||||
- **Loading indicator** — drives `activeRequests` store → `LoadingBar`
|
||||
- **Build-version check** — auto-reloads page when server build is newer
|
||||
- **Mock interceptor** — when `__MOCK__` is `true`, routes requests to `frontend/mocking/*.json`
|
||||
- **Sentry integration** — span instrumentation (when enabled)
|
||||
|
||||
### Usage patterns
|
||||
|
||||
```typescript
|
||||
import { api, getCachedEntries, getCachedEntry, getDBEntries, postDBEntry } from "./lib/api"
|
||||
|
||||
// Cached (1h TTL, for read-heavy data)
|
||||
const pages = await getCachedEntries<"content">("content", { lang: "de", active: true })
|
||||
const page = await getCachedEntry<"content">("content", { path: "/about" })
|
||||
|
||||
// Uncached
|
||||
const items = await getDBEntries<"content">("content", { type: "blog" }, "sort", 10)
|
||||
|
||||
// Write
|
||||
const result = await postDBEntry("content", { name: "New Page", active: true })
|
||||
|
||||
// Raw API call
|
||||
const { data, count } = await api<MyType[]>("mycollection", { filter: { active: true }, limit: 20 })
|
||||
```
|
||||
|
||||
### Error handling
|
||||
|
||||
```typescript
|
||||
try {
|
||||
const result = await api<ContentEntry[]>("content", { filter: { path: "/missing" } })
|
||||
} catch (err) {
|
||||
// err has shape: { response: Response, data: { error: string } }
|
||||
const status = (err as any)?.response?.status // e.g. 404
|
||||
const message = (err as any)?.data?.error // e.g. "Not found"
|
||||
|
||||
// For user-visible errors:
|
||||
import { addToast } from "./lib/toast"
|
||||
addToast({ type: "error", message: "Seite nicht gefunden" })
|
||||
|
||||
// For debugging:
|
||||
console.error("[MyComponent] API error:", err)
|
||||
}
|
||||
```
|
||||
|
||||
### Error handling guidelines
|
||||
|
||||
| Scenario | Approach |
|
||||
| --------------------------------- | ------------------------------------------------- |
|
||||
| API error the user should see | `addToast({ type: "error", message })` |
|
||||
| API error that's silently handled | `console.error(...)` for dev logging |
|
||||
| Unexpected error in production | Sentry captures automatically (when enabled) |
|
||||
| Missing content / 404 | Set `notFound = true` → renders `NotFound.svelte` |
|
||||
| Network error / offline | Loading bar stays visible; user can retry |
|
||||
|
||||
### API request flow (client-side)
|
||||
|
||||
```
|
||||
Component calls api() / getCachedEntries()
|
||||
↓
|
||||
Deduplication check (skip if signal provided)
|
||||
↓
|
||||
incrementRequests() → LoadingBar appears
|
||||
↓
|
||||
__MOCK__? → mockApiRequest() (in-memory JSON filtering)
|
||||
↓ (else)
|
||||
apiRequest() from api/hooks/lib/ssr (shared with SSR bundle)
|
||||
↓
|
||||
fetch("${apiBaseURL}${endpoint}?filter=...&sort=...&limit=...")
|
||||
↓
|
||||
Parse response → check X-Build-Time header
|
||||
↓
|
||||
decrementRequests() → LoadingBar disappears
|
||||
↓
|
||||
Return { data, count, buildTime }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## i18n system
|
||||
|
||||
### Architecture
|
||||
|
||||
- **svelte-i18n** for translation strings (`$_("key")`)
|
||||
- **URL-based language routing** (`/{lang}/...`)
|
||||
- **Lazy-loaded locale files** in `frontend/src/lib/i18n/locales/{lang}.json`
|
||||
- **Route translations** for localized URL slugs
|
||||
|
||||
### Adding a new language
|
||||
|
||||
1. Create locale file: `frontend/src/lib/i18n/locales/fr.json`
|
||||
2. Add to `SUPPORTED_LANGUAGES` in `frontend/src/lib/i18n.ts`:
|
||||
```typescript
|
||||
export const SUPPORTED_LANGUAGES = ["de", "en", "fr"] as const
|
||||
```
|
||||
3. Add label: `export const LANGUAGE_LABELS = { ..., fr: "Français" }`
|
||||
4. Add route translations for the new language in `ROUTE_TRANSLATIONS`.
|
||||
5. Register in `frontend/src/lib/i18n/index.ts` (lazy loader).
|
||||
6. Create content entries with `lang: "fr"` in the CMS.
|
||||
|
||||
### Translation usage
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { _ } from "./lib/i18n/index"
|
||||
</script>
|
||||
|
||||
<h1>{$_("hero.title")}</h1>
|
||||
<p>{$_("hero.subtitle", { values: { name: "World" } })}</p>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Never `spaNavigate()` in SSR** — always guard with `typeof window !== "undefined"`.
|
||||
- **Store subscriptions in modules** — if subscribing to stores outside components, remember to unsubscribe to prevent memory leaks.
|
||||
- **API PUT returns only changed fields** — don't expect a full object back from PUT requests.
|
||||
- **`_id` not `id` for filters** — API filters use MongoDB's `_id`, but response objects may have both `id` and `_id`.
|
||||
- **`$location` strips trailing slashes** — `/about/` becomes `/about` (except root `/`).
|
||||
- **Content cache is 1 hour** — `getCachedEntries` caches in memory for 1h. For admin previews, use `getDBEntries` (uncached).
|
||||
25
.agents/skills/gitea-issue-attachments/SKILL.md
Normal file
25
.agents/skills/gitea-issue-attachments/SKILL.md
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
name: gitea-issue-attachments
|
||||
description: Upload files (screenshots, logs, etc.) to Gitea issues as attachments via the REST API. Use when attaching any file to a Gitea issue or comment.
|
||||
---
|
||||
|
||||
# Gitea Issue Attachments
|
||||
|
||||
Attach files to Gitea issues via the REST API:
|
||||
|
||||
1. Get the Gitea API token from the running MCP docker process:
|
||||
```bash
|
||||
GITEA_PID=$(ps aux | grep 'gitea-mcp-server' | grep -v grep | awk '{print $2}')
|
||||
GITEA_TOKEN=$(cat /proc/$GITEA_PID/environ | tr '\0' '\n' | grep GITEA_ACCESS_TOKEN | cut -d= -f2)
|
||||
```
|
||||
2. Upload the file as an issue attachment:
|
||||
```bash
|
||||
curl -s -X POST "https://gitbase.de/api/v1/repos/{owner}/{repo}/issues/{index}/assets" \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-F "attachment=@path/to/file"
|
||||
```
|
||||
This returns JSON with a `uuid` field.
|
||||
3. Reference the attachment in the issue or comment body:
|
||||
```markdown
|
||||

|
||||
```
|
||||
175
.agents/skills/live-mongodb/SKILL.md
Normal file
175
.agents/skills/live-mongodb/SKILL.md
Normal file
@@ -0,0 +1,175 @@
|
||||
---
|
||||
name: live-mongodb
|
||||
description: Connect to the live (production) MongoDB via chisel tunnel and perform read/write operations. Use this skill when you need to inspect or update live data directly in the production database.
|
||||
---
|
||||
|
||||
# Live MongoDB Access via Chisel Tunnel
|
||||
|
||||
## Overview
|
||||
|
||||
Die Produktions-MongoDB läuft auf dem Server aus `PRODUCTION_SERVER` in `.env`. Der Zugang erfolgt über einen **Chisel-Tunnel**, der den Remote-MongoDB-Port auf localhost mapped. Damit kann man dann entweder über `mongosh`, `mongodump`/`mongorestore`, oder den **MongoDB MCP Server** auf die Live-Daten zugreifen.
|
||||
|
||||
## Umgebungsvariablen (aus .env)
|
||||
|
||||
| Variable | Beschreibung |
|
||||
| ------------------------ | ----------------------------------------------- |
|
||||
| `PRODUCTION_SERVER` | Produktionsserver (z.B. `dock4.basehosts.de`) |
|
||||
| `PRODUCTION_TIBI_PREFIX` | DB-Prefix auf Produktion (z.B. `wmbasic`) |
|
||||
| `TIBI_NAMESPACE` | Projekt-Namespace |
|
||||
| **Live DB Name** | = `${PRODUCTION_TIBI_PREFIX}_${TIBI_NAMESPACE}` |
|
||||
| Chisel-Port (Remote) | `10987` — Chisel-Server auf dem Produktionshost |
|
||||
|
||||
## Schritt 1: Chisel-Tunnel starten
|
||||
|
||||
Das Chisel-Passwort muss vom User bereitgestellt werden. Tunnel starten:
|
||||
|
||||
```bash
|
||||
# Passwort vom User erfragen oder aus Umgebung nehmen
|
||||
read -s -p "Chisel-Passwort: " CHISEL_PASSWORD
|
||||
|
||||
# Tunnel starten (mappt remote mongo:27017 → localhost:27017)
|
||||
chisel client --auth "coder:${CHISEL_PASSWORD}" \
|
||||
http://${PRODUCTION_SERVER}:10987 \
|
||||
27017:mongo:27017 &
|
||||
|
||||
# Kurz warten, bis der Tunnel steht
|
||||
sleep 3
|
||||
```
|
||||
|
||||
**WICHTIG:** Der lokale Docker-Mongo-Container muss gestoppt sein oder auf einem anderen Port laufen, da der Tunnel Port 27017 lokal belegt. Falls der lokale Container läuft:
|
||||
|
||||
```bash
|
||||
# Lokales MongoDB stoppen (belegt sonst Port 27017)
|
||||
docker compose -f docker-compose-local.yml stop mongo
|
||||
```
|
||||
|
||||
Alternativ kann der Tunnel auf einen anderen lokalen Port gemappt werden:
|
||||
|
||||
```bash
|
||||
chisel client --auth "coder:${CHISEL_PASSWORD}" \
|
||||
http://${PRODUCTION_SERVER}:10987 \
|
||||
37017:mongo:27017 &
|
||||
# → erreichbar unter mongodb://localhost:37017/${PRODUCTION_TIBI_PREFIX}_${TIBI_NAMESPACE}
|
||||
```
|
||||
|
||||
## Schritt 2: Verbinden
|
||||
|
||||
### Option A: mongosh (interaktiv)
|
||||
|
||||
```bash
|
||||
mongosh "mongodb://localhost:27017/${PRODUCTION_TIBI_PREFIX}_${TIBI_NAMESPACE}"
|
||||
```
|
||||
|
||||
### Option B: MongoDB MCP Server (für Copilot)
|
||||
|
||||
Den MongoDB MCP über die Umgebungsvariable `MDB_MCP_CONNECTION_STRING` auf die Live-DB umleiten.
|
||||
|
||||
**Temporär für eine Session** – in `.vscode/mcp.json` eine zweite Server-Config eintragen:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"servers": {
|
||||
"mongodb-live": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "mongodb-mcp-server@latest"],
|
||||
"type": "stdio",
|
||||
"env": {
|
||||
"MDB_MCP_CONNECTION_STRING": "mongodb://localhost:27017/${PRODUCTION_TIBI_PREFIX}_${TIBI_NAMESPACE}",
|
||||
"MDB_MCP_READ_ONLY": "false",
|
||||
"MDB_MCP_TELEMETRY": "disabled",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
> **Achtung:** `MDB_MCP_READ_ONLY=false` erlaubt Schreiboperationen! Nach getaner Arbeit den Server wieder entfernen oder auf `true` setzen.
|
||||
|
||||
### Option C: Einmalige Kommandos via Terminal
|
||||
|
||||
```bash
|
||||
DB_NAME="${PRODUCTION_TIBI_PREFIX}_${TIBI_NAMESPACE}"
|
||||
|
||||
# Dokument suchen
|
||||
mongosh "mongodb://localhost:27017/$DB_NAME" \
|
||||
--eval 'db.content.findOne({path: "/"})'
|
||||
|
||||
# Feld updaten
|
||||
mongosh "mongodb://localhost:27017/$DB_NAME" \
|
||||
--eval 'db.content.updateOne({path: "/"}, {$set: {"title": "Neuer Titel"}})'
|
||||
```
|
||||
|
||||
## Schritt 3: Tunnel beenden
|
||||
|
||||
```bash
|
||||
killall chisel
|
||||
```
|
||||
|
||||
Falls der lokale Mongo-Container vorher gestoppt wurde, wieder starten:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose-local.yml start mongo
|
||||
```
|
||||
|
||||
## Sicherheitsregeln
|
||||
|
||||
1. **Immer zuerst lesen, dann schreiben.** Vor jedem Update das betroffene Dokument mit `find`/`findOne` inspizieren.
|
||||
2. **Backup vor Bulk-Updates.** Bei Massenänderungen vorher ein `mongodump` machen:
|
||||
```bash
|
||||
mongodump --uri="mongodb://localhost:27017" \
|
||||
--db=${PRODUCTION_TIBI_PREFIX}_${TIBI_NAMESPACE} \
|
||||
--collection=<collection> \
|
||||
--gzip --archive=backup-<collection>-$(date +%Y%m%d-%H%M%S).gz
|
||||
```
|
||||
3. **User muss Chisel-Passwort liefern.** Das Passwort niemals hardcoden oder in Dateien speichern.
|
||||
4. **Updates immer bestätigen lassen.** Vor jeder Schreiboperation dem User die geplante Query zeigen und explizit nach Bestätigung fragen.
|
||||
5. **Nach getaner Arbeit Tunnel schließen** und ggf. `mongodb-live` MCP-Server aus der Config entfernen.
|
||||
6. **Kein Drop/Delete von Collections** ohne explizite User-Anweisung.
|
||||
7. **SSR-Cache leeren nach Datenänderungen.** Wenn Daten in Collections geändert werden, die auf der Website gerendert werden (z.B. `content`, `navigation`), muss der SSR-Cache invalidiert werden, damit die Änderungen sichtbar werden. Dazu die `ssr`-Collection leeren:
|
||||
```bash
|
||||
mongosh "mongodb://localhost:27017/$DB_NAME" \
|
||||
--eval 'db.ssr.deleteMany({})'
|
||||
```
|
||||
Siehe auch den Skill `tibi-ssr-caching` für Details zur Cache-Invalidierung über die API.
|
||||
|
||||
## Wichtige Collections
|
||||
|
||||
| Collection | Beschreibung |
|
||||
| ------------ | ------------------------------- |
|
||||
| `content` | CMS-Inhaltsseiten (Pagebuilder) |
|
||||
| `navigation` | Navigationsstruktur |
|
||||
| `medialib` | Medien-Bibliothek |
|
||||
| `ssr` | SSR-Cache |
|
||||
|
||||
> Weitere Collections je nach Projekt — siehe `api/collections/` für die aktuelle Liste.
|
||||
|
||||
## Typische Anwendungsfälle
|
||||
|
||||
### Content-Eintrag inspizieren
|
||||
|
||||
```js
|
||||
db.content.findOne({ path: "/" })
|
||||
```
|
||||
|
||||
### Navigation aktualisieren
|
||||
|
||||
```js
|
||||
db.navigation.updateOne({ type: "header", language: "de" }, { $set: { "items.0.label": "Neues Label" } })
|
||||
```
|
||||
|
||||
### Dokument-Struktur inspizieren
|
||||
|
||||
```js
|
||||
// Schema einer Collection anschauen
|
||||
db.content.findOne()
|
||||
|
||||
// Alle Felder eines Dokuments auflisten
|
||||
Object.keys(db.content.findOne())
|
||||
```
|
||||
|
||||
## Fehlerbehebung
|
||||
|
||||
- **"Connection refused" auf Port 27017:** Chisel-Tunnel läuft nicht oder lokaler Mongo blockiert den Port. Prüfen mit `ss -tlnp | grep 27017`.
|
||||
- **"Authentication failed":** Chisel-Passwort falsch. User erneut fragen.
|
||||
- **Langsame Queries:** Produktions-DB kann große Collections haben. Immer mit Filtern arbeiten, nie `find({})` ohne Limit.
|
||||
- **Rate Limiting:** Kein Thema bei direktem DB-Zugang (nur bei API-Calls relevant).
|
||||
71
.agents/skills/tibi-hook-authoring/SKILL.md
Normal file
71
.agents/skills/tibi-hook-authoring/SKILL.md
Normal file
@@ -0,0 +1,71 @@
|
||||
---
|
||||
name: tibi-hook-authoring
|
||||
description: Write and debug server-side hooks for tibi-server (goja Go JS runtime). Covers IIFE structure, HookResponse/HookException types, context.filter Go-object quirk, single-item vs list retrieval, and MongoDB filter patterns. Use when creating or modifying files in api/hooks/.
|
||||
---
|
||||
|
||||
# tibi-hook-authoring
|
||||
|
||||
## Hook file structure
|
||||
|
||||
Wrap every hook in an IIFE:
|
||||
|
||||
```js
|
||||
;(function () {
|
||||
/** @type {HookResponse} */
|
||||
const response = { status: 200 }
|
||||
|
||||
// ... hook logic ...
|
||||
|
||||
return response
|
||||
})()
|
||||
```
|
||||
|
||||
Always return a `HookResponse` or throw a `HookException`.
|
||||
|
||||
## Type safety
|
||||
|
||||
- Use inline JSDoc type casting: `/** @type {TypeName} */ (value)`.
|
||||
- Reference typed collection entries from `types/global.d.ts`.
|
||||
- Avoid `@ts-ignore`; use proper casting instead.
|
||||
- Use `const` and `let` instead of `var` — the goja runtime supports them.
|
||||
|
||||
## context.filter — Go object quirk
|
||||
|
||||
`context.filter` is a Go object, not a regular JS object. Even when empty, it is **truthy**.
|
||||
|
||||
Always check with `Object.keys()`:
|
||||
|
||||
```js
|
||||
const requestedFilter =
|
||||
context.filter &&
|
||||
typeof context.filter === "object" &&
|
||||
!Array.isArray(context.filter) &&
|
||||
Object.keys(context.filter).length > 0
|
||||
? context.filter
|
||||
: null
|
||||
```
|
||||
|
||||
**Never** use `context.filter || null` — it is always truthy and produces an empty filter inside `$and`, which crashes the Go server.
|
||||
|
||||
## Single-item vs. list retrieval
|
||||
|
||||
For `GET /:collection/:id`, the Go server sets `_id` automatically from the URL parameter.
|
||||
|
||||
GET read hooks should **not** set their own `_id` filter for `req.param("id")`. Only add authorization filters (e.g. `{ userId: userId }`).
|
||||
|
||||
## HookResponse fields (GET hooks)
|
||||
|
||||
| Field | Purpose |
|
||||
| ------------- | ------------------------------------------------------------------- |
|
||||
| `filter` | MongoDB filter (list retrieval, or restrict single-item) |
|
||||
| `selector` | MongoDB projection (`{ field: 0 }` exclude, `{ field: 1 }` include) |
|
||||
| `offset` | Pagination offset |
|
||||
| `limit` | Pagination limit |
|
||||
| `sort` | Sort specification |
|
||||
| `pipelineMod` | Function to manipulate the aggregation pipeline |
|
||||
|
||||
## context.data for write hooks
|
||||
|
||||
- `context.data` can be an array for bulk operations — always guard with `!Array.isArray(context.data)`.
|
||||
- For POST hooks, `context.data.id` may contain the new entry ID.
|
||||
- For PUT/PATCH, `req.param("id")` gives the entry ID.
|
||||
173
.agents/skills/tibi-project-setup/SKILL.md
Normal file
173
.agents/skills/tibi-project-setup/SKILL.md
Normal file
@@ -0,0 +1,173 @@
|
||||
---
|
||||
name: tibi-project-setup
|
||||
description: Set up a new tibi project from the tibi-svelte-starter template. Covers cloning, placeholder replacement, environment config, Docker startup, mock mode, demo cleanup, and build verification. Use when creating a new project or onboarding into this template.
|
||||
---
|
||||
|
||||
# tibi-project-setup
|
||||
|
||||
## When to use this skill
|
||||
|
||||
Use this skill when:
|
||||
|
||||
- Creating a new project from the `tibi-svelte-starter` template
|
||||
- Onboarding into a freshly cloned starter project where placeholders haven't been replaced yet
|
||||
- The user asks to "set up", "initialize", or "bootstrap" a new tibi project
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Code-Server environment at `*.code.testversion.online`
|
||||
- `git`, `yarn`, `make`, `docker compose` available
|
||||
- Traefik reverse proxy running on the host (manages `*.code.testversion.online` subdomains automatically via Docker labels)
|
||||
|
||||
## Step 1 — Clone and set up remotes
|
||||
|
||||
Skip this step if already inside a cloned project.
|
||||
|
||||
```sh
|
||||
# In the workspace directory (e.g. /WM_Dev/src/gitbase.de/cms/)
|
||||
git clone https://gitbase.de/cms/tibi-svelte-starter.git my-project
|
||||
cd my-project
|
||||
git remote rename origin template
|
||||
# Create a new remote repo (e.g. on gitbase.de) and add as origin:
|
||||
# git remote add origin https://gitbase.de/org/my-project.git
|
||||
```
|
||||
|
||||
**Verify:** `git remote -v` shows `template` pointing to the starter and optionally `origin` pointing to the new repo.
|
||||
|
||||
## Step 2 — Replace placeholders
|
||||
|
||||
Three placeholders must be replaced in the correct files:
|
||||
|
||||
| Placeholder | Files | Format | Example |
|
||||
| -------------------- | -------------------------------------- | --------------------------------------------------------- | ------------ |
|
||||
| `__PROJECT_NAME__` | `.env` | kebab-case (used for URLs, Docker containers, subdomains) | `my-project` |
|
||||
| `__TIBI_NAMESPACE__` | `.env` | snake_case (used as DB prefix and in API URLs) | `my_project` |
|
||||
| `__NAMESPACE__` | `api/config.yml`, `frontend/.htaccess` | snake_case — same value as `TIBI_NAMESPACE` | `my_project` |
|
||||
|
||||
```sh
|
||||
PROJECT=my-project # kebab-case
|
||||
NAMESPACE=my_project # snake_case
|
||||
|
||||
sed -i "s/__PROJECT_NAME__/$PROJECT/g" .env
|
||||
sed -i "s/__TIBI_NAMESPACE__/$NAMESPACE/g" .env
|
||||
sed -i "s/__NAMESPACE__/$NAMESPACE/g" api/config.yml frontend/.htaccess
|
||||
```
|
||||
|
||||
**Verify each replacement:**
|
||||
|
||||
```sh
|
||||
grep -n '__PROJECT_NAME__\|__TIBI_NAMESPACE__\|__NAMESPACE__' .env api/config.yml frontend/.htaccess
|
||||
# Expected: no output (all placeholders replaced)
|
||||
```
|
||||
|
||||
**Result in `.env`:**
|
||||
|
||||
```dotenv
|
||||
PROJECT_NAME=my-project
|
||||
TIBI_NAMESPACE=my_project
|
||||
CODING_URL=https://my-project.code.testversion.online
|
||||
STAGING_URL=https://dev-my-project.staging.testversion.online
|
||||
```
|
||||
|
||||
### Common mistakes
|
||||
|
||||
- **Mixing formats**: `PROJECT` must be kebab-case (`my-project`), `NAMESPACE` must be snake_case (`my_project`). Never use kebab-case where snake_case is expected or vice versa.
|
||||
- **Forgetting `frontend/.htaccess`**: Contains the namespace for API rewrite rules. If missed, API calls from the frontend will fail silently.
|
||||
- **Forgetting `api/config.yml`**: First line is `namespace: __NAMESPACE__`. If not replaced, tibi-server won't start correctly.
|
||||
|
||||
## Step 3 — Page title
|
||||
|
||||
The page title is set dynamically via `<svelte:head>` in `frontend/src/App.svelte`. The demo app uses the constant `SITE_NAME` for this. In a new project, `App.svelte` is typically rewritten completely — just make sure `<svelte:head>` with a `<title>` is present. SSR automatically injects it via the `<!--HEAD-->` placeholder in `spa.html`.
|
||||
|
||||
## Step 4 — Admin token
|
||||
|
||||
`api/config.yml.env` ships with a default `ADMIN_TOKEN`. For production projects, generate a secure one:
|
||||
|
||||
```sh
|
||||
echo "ADMIN_TOKEN=$(openssl rand -hex 20)" > api/config.yml.env
|
||||
```
|
||||
|
||||
**Verify:** `cat api/config.yml.env` shows a 40-character hex token.
|
||||
|
||||
## Step 5 — Install, upgrade, and start
|
||||
|
||||
```sh
|
||||
yarn install
|
||||
yarn upgrade # Update all deps to latest versions within package.json ranges
|
||||
make docker-up # Start stack in background
|
||||
# or
|
||||
make docker-start # Start stack in foreground (CTRL-C to stop)
|
||||
```
|
||||
|
||||
`yarn upgrade` is safe here because the project is freshly cloned and nothing is running yet. The template's `yarn.lock` may be months old — upgrading ensures you start with the latest compatible (semver-range) versions.
|
||||
|
||||
**Do NOT run `yarn upgrade` on an existing, running project without testing.** Even patch-level updates can introduce regressions. For running projects, upgrade targeted packages with `yarn upgrade <package>` and verify with `yarn validate` + `yarn build`.
|
||||
|
||||
**Verify containers are running:**
|
||||
|
||||
```sh
|
||||
make docker-ps # All containers should be "Up"
|
||||
make docker-logs # Check for errors
|
||||
```
|
||||
|
||||
## Step 6 — Verify URLs
|
||||
|
||||
Traefik picks up Docker labels automatically — no manual config needed. After `make docker-up`, these URLs become available:
|
||||
|
||||
| Service | URL |
|
||||
| --------------------- | ------------------------------------------------------------ |
|
||||
| Website (BrowserSync) | `https://{PROJECT_NAME}.code.testversion.online/` |
|
||||
| Tibi Admin | `https://{PROJECT_NAME}-tibiadmin.code.testversion.online/` |
|
||||
| Tibi Server API | `https://{PROJECT_NAME}-tibiserver.code.testversion.online/` |
|
||||
| Maildev | `https://{PROJECT_NAME}-maildev.code.testversion.online/` |
|
||||
|
||||
The subdomains are registered via the Docker label `online.testversion.code.subdomain=${PROJECT_NAME}`. Traefik watches Docker events and creates routes dynamically.
|
||||
|
||||
**Verify:** `curl -sI https://{PROJECT_NAME}.code.testversion.online/ | head -1` returns `HTTP/2 200`.
|
||||
|
||||
## Step 7 — Mock mode (optional)
|
||||
|
||||
For frontend development without a running tibi-server backend:
|
||||
|
||||
```sh
|
||||
# Set in .env:
|
||||
MOCK=1
|
||||
# Then restart:
|
||||
make docker-up
|
||||
```
|
||||
|
||||
Mock data lives in `frontend/mocking/` as JSON files. Missing endpoints return 404.
|
||||
|
||||
**When to use mock mode:** Early UI prototyping, frontend-only work, CI environments without a database.
|
||||
|
||||
## Step 8 — Remove demo content
|
||||
|
||||
For a real project, remove or replace the demo files:
|
||||
|
||||
| File/Folder | Content |
|
||||
| ---------------------------------- | ------------------------------------------------------ |
|
||||
| `frontend/src/blocks/` | Demo block components (HeroBlock, FeaturesBlock, etc.) |
|
||||
| `frontend/mocking/content.json` | Demo mock data for content |
|
||||
| `frontend/mocking/navigation.json` | Demo mock data for navigation |
|
||||
| `api/collections/content.yml` | Content collection config |
|
||||
| `api/collections/navigation.yml` | Navigation collection config |
|
||||
| `tests/e2e/` | Demo E2E tests |
|
||||
| `video-tours/tours/` | Demo video tour |
|
||||
|
||||
Then adapt `frontend/src/App.svelte` (header, footer, content loading) to your own data model.
|
||||
|
||||
**Decision guide:**
|
||||
|
||||
- **Keep demo content** if you want to use it as a reference while building your own components.
|
||||
- **Delete immediately** if you're starting with a completely custom design and the demo files would only cause confusion.
|
||||
|
||||
## Step 9 — Build and validate
|
||||
|
||||
```sh
|
||||
yarn build # Frontend bundle for modern browsers
|
||||
yarn build:server # SSR bundle (for tibi-server goja hooks)
|
||||
yarn build:admin # Admin modules (optional)
|
||||
yarn validate # TypeScript + Svelte checks (must show 0 errors and 0 warnings)
|
||||
```
|
||||
|
||||
**All four commands must succeed with exit code 0 before the project is considered set up.**
|
||||
55
.agents/skills/tibi-ssr-caching/SKILL.md
Normal file
55
.agents/skills/tibi-ssr-caching/SKILL.md
Normal file
@@ -0,0 +1,55 @@
|
||||
---
|
||||
name: tibi-ssr-caching
|
||||
description: Implement and debug server-side rendering with goja (Go JS runtime) and dependency-based HTML cache invalidation for tibi-server. Use when working on SSR hooks, cache clearing, or the server-side Svelte rendering pipeline.
|
||||
---
|
||||
|
||||
# tibi-ssr-caching
|
||||
|
||||
## SSR request flow
|
||||
|
||||
1. `ssr/get_read.js` receives a page request and calls `lib/ssr-server.js`.
|
||||
2. `ssr-server.js` loads `lib/app.server.js` (the Svelte SSR bundle) and renders the page.
|
||||
3. During rendering, API calls are tracked as **dependencies** (collection + entry ID).
|
||||
4. The rendered HTML + dependencies are stored in the `ssr` collection.
|
||||
5. On the client, `lib/ssr.js` hydrates using `window.__SSR_CACHE__` injected by the server.
|
||||
|
||||
## Building the SSR bundle
|
||||
|
||||
```bash
|
||||
yarn build:server
|
||||
```
|
||||
|
||||
- Output: `api/hooks/lib/app.server.js`
|
||||
- Uses `babel.config.server.json` to transform async/await to generators (goja doesn't support async).
|
||||
- Add `--banner:js='// @ts-nocheck'` to suppress type errors in the generated bundle.
|
||||
|
||||
## Dependency-based cache invalidation
|
||||
|
||||
When content changes, `clear_cache.js` only invalidates SSR entries that depend on the changed collection/entry:
|
||||
|
||||
```js
|
||||
// Each SSR cache entry stores its dependencies:
|
||||
{
|
||||
url: "/some-page",
|
||||
html: "...",
|
||||
dependencies: [
|
||||
{ collection: "content", id: "abc123" },
|
||||
{ collection: "medialib", id: "def456" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The hook queries the `ssr` collection for entries whose `dependencies` array matches the changed collection (and optionally entry ID), then deletes only those cached pages.
|
||||
|
||||
## SSR route validation
|
||||
|
||||
Route validation in `config.js` controls which paths get SSR treatment. Return:
|
||||
|
||||
- A positive number to enable SSR for that route
|
||||
- `-1` to disable SSR (current default in the starter template)
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **goja has no async/await**: The babel server config transforms these, but avoid top-level await.
|
||||
- **No browser globals**: `window`, `document`, `localStorage` etc. don't exist in goja. Guard with `typeof window !== "undefined"`.
|
||||
- **SSR cache can go stale**: Always ensure `clear_cache.js` covers any new collection that affects rendered output.
|
||||
1
.basic-auth-code
Normal file
1
.basic-auth-code
Normal file
@@ -0,0 +1 @@
|
||||
code:$apr1$AeePIAei$E9E6E6jtFFtwmtGhIEG.Y/
|
||||
2
.basic-auth-web
Normal file
2
.basic-auth-web
Normal file
@@ -0,0 +1,2 @@
|
||||
code:$apr1$AeePIAei$E9E6E6jtFFtwmtGhIEG.Y/
|
||||
web:$apr1$/zc/TBtD$ZGr3RqPiULYMD0kJUup5E0
|
||||
25
.env
Normal file
25
.env
Normal file
@@ -0,0 +1,25 @@
|
||||
PROJECT_NAME=__PROJECT_NAME__
|
||||
TIBI_PREFIX=tibi
|
||||
TIBI_NAMESPACE=__TIBI_NAMESPACE__
|
||||
CODER_UID=100
|
||||
CODER_GID=101
|
||||
|
||||
SENTRY_URL=https://sentry.basehosts.de
|
||||
SENTRY_ORG=webmakers
|
||||
SENTRY_PROJECT=
|
||||
|
||||
RSYNC_HOST=ftp1.webmakers.de
|
||||
RSYNC_PORT=22223
|
||||
|
||||
PRODUCTION_SERVER=dock4.basehosts.de
|
||||
PRODUCTION_TIBI_PREFIX=wmbasic
|
||||
PRODUCTION_PATH=/webroots2/customers/_CUSTOMER_ID_/____
|
||||
|
||||
STAGING_PATH=/staging/__ORG__/__PROJECT__/dev
|
||||
|
||||
LIVE_URL=https://www
|
||||
STAGING_URL=https://dev-__PROJECT_NAME__.staging.testversion.online
|
||||
CODING_URL=https://__PROJECT_NAME__.code.testversion.online
|
||||
|
||||
#START_SCRIPT=:ssr
|
||||
MOCK=1
|
||||
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.yarn/cache/** filter=lfs diff=lfs merge=lfs -text
|
||||
72
.gitea/workflows/deploy.yml
Normal file
72
.gitea/workflows/deploy.yml
Normal file
@@ -0,0 +1,72 @@
|
||||
name: deploy to production
|
||||
|
||||
on: "push"
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: deploy
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: gitbase.de/actions/ubuntu:latest
|
||||
volumes:
|
||||
- /data:/data
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
lfs: true
|
||||
submodules: true
|
||||
|
||||
- run: |
|
||||
git fetch --force --tags
|
||||
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: install dependencies
|
||||
run: |
|
||||
npm install -g yarn
|
||||
yarn install
|
||||
|
||||
- name: modify config
|
||||
run: ./scripts/ci-modify-config.sh
|
||||
|
||||
- name: build
|
||||
env:
|
||||
FORCE_COLOR: "true"
|
||||
run: |
|
||||
yarn build
|
||||
|
||||
- name: build admin
|
||||
env:
|
||||
FORCE_COLOR: "true"
|
||||
run: |
|
||||
yarn build:admin
|
||||
|
||||
- name: build ssr
|
||||
env:
|
||||
FORCE_COLOR: "true"
|
||||
run: |
|
||||
yarn build:server
|
||||
|
||||
- name: upload sourcemaps to sentry
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
run: ./scripts/ci-upload-sourcemaps.sh
|
||||
|
||||
- name: staging
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
env:
|
||||
API_BASEDIR: /data/${{ github.repository }}/${{ github.ref_name }}
|
||||
COMPOSE_PROJECT_NAME: ${{ github.repository }}-${{ github.ref_name }}
|
||||
run: ./scripts/ci-staging.sh
|
||||
|
||||
- name: deploy
|
||||
if: github.ref == 'refs/heads/master'
|
||||
env:
|
||||
RSYNC_USER: ${{ github.repository }}
|
||||
RSYNC_PASS: ${{ github.token }}
|
||||
BRANCH: ${{ github.ref_name }}
|
||||
run: ./scripts/ci-deploy.sh
|
||||
22
.gitignore
vendored
Normal file
22
.gitignore
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
api/hooks/lib/app.server*
|
||||
api/hooks/lib/buildInfo.js
|
||||
frontend/src/lib/buildInfo.ts
|
||||
node_modules
|
||||
media
|
||||
tmp
|
||||
_temp
|
||||
frontend/dist
|
||||
yarn-error.log
|
||||
test-results/
|
||||
playwright-report/
|
||||
playwright/.cache/
|
||||
visual-review/
|
||||
video-tours/output/
|
||||
.playwright-mcp/
|
||||
.yarn/*
|
||||
!.yarn/cache
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
18
.prettierrc
Normal file
18
.prettierrc
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"tabWidth": 4,
|
||||
"singleQuote": false,
|
||||
"trailingComma": "es5",
|
||||
"semi": false,
|
||||
"newline-before-return": true,
|
||||
"no-duplicate-variable": [
|
||||
true,
|
||||
"check-parameters"
|
||||
],
|
||||
"no-var-keyword": true,
|
||||
"svelteSortOrder": "scripts-options-markup-styles",
|
||||
"svelteStrictMode": true,
|
||||
"svelteBracketNewLine": true,
|
||||
"svelteAllowShorthand": true,
|
||||
"svelteIndentScriptAndStyle": true
|
||||
}
|
||||
15
.vscode/launch.json
vendored
Normal file
15
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
// Verwendet IntelliSense zum Ermitteln möglicher Attribute.
|
||||
// Zeigen Sie auf vorhandene Attribute, um die zugehörigen Beschreibungen anzuzeigen.
|
||||
// Weitere Informationen finden Sie unter https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "pwa-msedge",
|
||||
"request": "launch",
|
||||
"name": "Launch Edge against localhost",
|
||||
"url": "http://localhost:5501/",
|
||||
"webRoot": "${workspaceFolder}/dist"
|
||||
}
|
||||
]
|
||||
}
|
||||
39
.vscode/settings.json
vendored
Normal file
39
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"editor.tabCompletion": "on",
|
||||
"diffEditor.codeLens": true,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnPaste": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"yaml.schemas": {
|
||||
"./../../cms/tibi-types/schemas/api-config/config.json": "api/config.y*ml",
|
||||
"./../../cms/tibi-types/schemas/api-config/collection.json": "api/collections/*.y*ml",
|
||||
"./../../cms/tibi-types/schemas/api-config/field.json": "api/collections/fields/*.y*ml",
|
||||
"./../../cms/tibi-types/schemas/api-config/fieldArray.json": "api/collections/fieldLists/*.y*ml",
|
||||
"./../../cms/tibi-types/schemas/api-config/job.json": "api/jobs/*.y*ml",
|
||||
"./../../cms/tibi-types/schemas/api-config/assets.json": "api/assets/*.y*ml"
|
||||
},
|
||||
"yaml.customTags": ["!include scalar"],
|
||||
"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
|
||||
}
|
||||
BIN
.yarn/cache/@alloc-quick-lru-npm-5.2.0-eb83517088-bdc35758b5.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@alloc-quick-lru-npm-5.2.0-eb83517088-bdc35758b5.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-cli-npm-7.28.6-b4e455ce7e-49279aa65d.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-cli-npm-7.28.6-b4e455ce7e-49279aa65d.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-code-frame-npm-7.27.1-4dbcabb137-721b8a6e36.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-code-frame-npm-7.27.1-4dbcabb137-721b8a6e36.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-code-frame-npm-7.29.0-6c4947d913-199e15ff89.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-code-frame-npm-7.29.0-6c4947d913-199e15ff89.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-compat-data-npm-7.27.7-1eceb4277e-e71bf453a4.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-compat-data-npm-7.27.7-1eceb4277e-e71bf453a4.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-compat-data-npm-7.29.0-6b4382e79f-7f21beedb9.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-compat-data-npm-7.29.0-6b4382e79f-7f21beedb9.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-core-npm-7.29.0-a74bfc561b-25f4e91688.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-core-npm-7.29.0-a74bfc561b-25f4e91688.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-generator-npm-7.27.5-b91f717ed1-f5e6942670.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-generator-npm-7.27.5-b91f717ed1-f5e6942670.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-generator-npm-7.29.1-b1bf16fe79-61fe4ddd6e.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-generator-npm-7.29.1-b1bf16fe79-61fe4ddd6e.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-helper-annotate-as-pure-npm-7.25.9-a0f89e14a0-41edda10df.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-helper-annotate-as-pure-npm-7.25.9-a0f89e14a0-41edda10df.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-helper-annotate-as-pure-npm-7.27.3-d8daa5b949-63863a5c93.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-helper-annotate-as-pure-npm-7.27.3-d8daa5b949-63863a5c93.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-helper-compilation-targets-npm-7.27.2-111dda04b6-bd53c30a74.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-helper-compilation-targets-npm-7.27.2-111dda04b6-bd53c30a74.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-helper-compilation-targets-npm-7.28.6-8880f389c9-f512a5aeee.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-helper-compilation-targets-npm-7.28.6-8880f389c9-f512a5aeee.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-helper-create-class-features-plugin-npm-7.28.6-6b870abc40-11f55607fc.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-helper-create-class-features-plugin-npm-7.28.6-6b870abc40-11f55607fc.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-helper-create-regexp-features-plugin-npm-7.27.0-a1e8c75585-e5734deb62.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-helper-create-regexp-features-plugin-npm-7.27.0-a1e8c75585-e5734deb62.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-helper-create-regexp-features-plugin-npm-7.27.1-76d8a0ecb8-dea272628c.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-helper-create-regexp-features-plugin-npm-7.27.1-76d8a0ecb8-dea272628c.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-helper-create-regexp-features-plugin-npm-7.28.5-bf1c1b99dc-d8791350fe.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-helper-create-regexp-features-plugin-npm-7.28.5-bf1c1b99dc-d8791350fe.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-helper-define-polyfill-provider-npm-0.6.6-6bed657d63-1c725c47ba.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-helper-define-polyfill-provider-npm-0.6.6-6bed657d63-1c725c47ba.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-helper-globals-npm-7.28.0-8d79c12faf-91445f7edf.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-helper-globals-npm-7.28.0-8d79c12faf-91445f7edf.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-helper-member-expression-to-functions-npm-7.27.1-39af2b31f0-533a5a2cf1.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-helper-member-expression-to-functions-npm-7.27.1-39af2b31f0-533a5a2cf1.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-helper-member-expression-to-functions-npm-7.28.5-2fb0be8c55-05e0857cf7.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-helper-member-expression-to-functions-npm-7.28.5-2fb0be8c55-05e0857cf7.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-helper-module-imports-npm-7.27.1-3bf33978f4-58e792ea5d.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-helper-module-imports-npm-7.27.1-3bf33978f4-58e792ea5d.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-helper-module-imports-npm-7.28.6-5b95b9145c-64b1380d74.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-helper-module-imports-npm-7.28.6-5b95b9145c-64b1380d74.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-helper-module-transforms-npm-7.27.3-90dc30d3d9-47abc90ceb.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-helper-module-transforms-npm-7.27.3-90dc30d3d9-47abc90ceb.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-helper-module-transforms-npm-7.28.6-5923cf5a95-2e421c7db7.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-helper-module-transforms-npm-7.28.6-5923cf5a95-2e421c7db7.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-helper-optimise-call-expression-npm-7.27.1-84d2c8f7d3-0fb7ee824a.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-helper-optimise-call-expression-npm-7.27.1-84d2c8f7d3-0fb7ee824a.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-helper-plugin-utils-npm-7.26.5-f9c17c9880-1cc0fd8514.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-helper-plugin-utils-npm-7.26.5-f9c17c9880-1cc0fd8514.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-helper-plugin-utils-npm-7.27.1-4f91e7999b-96136c2428.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-helper-plugin-utils-npm-7.27.1-4f91e7999b-96136c2428.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-helper-plugin-utils-npm-7.28.6-766c984cfe-21c853bbc1.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-helper-plugin-utils-npm-7.28.6-766c984cfe-21c853bbc1.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-helper-remap-async-to-generator-npm-7.27.1-6e89d61aa6-0747397ba0.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-helper-remap-async-to-generator-npm-7.27.1-6e89d61aa6-0747397ba0.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-helper-replace-supers-npm-7.27.1-f784132f4b-72e3f8bef7.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-helper-replace-supers-npm-7.27.1-f784132f4b-72e3f8bef7.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-helper-replace-supers-npm-7.28.6-f11a32993b-ad2724713a.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-helper-replace-supers-npm-7.28.6-f11a32993b-ad2724713a.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-helper-skip-transparent-expression-wrappers-npm-7.27.1-c539e02d36-4f380c5d0e.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-helper-skip-transparent-expression-wrappers-npm-7.27.1-c539e02d36-4f380c5d0e.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-helper-string-parser-npm-7.25.9-eade578078-c28656c52b.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-helper-string-parser-npm-7.25.9-eade578078-c28656c52b.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-helper-string-parser-npm-7.27.1-d1471e0598-0ae29cc200.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-helper-string-parser-npm-7.27.1-d1471e0598-0ae29cc200.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-helper-validator-identifier-npm-7.25.9-2634b947a4-3f9b649be0.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-helper-validator-identifier-npm-7.25.9-2634b947a4-3f9b649be0.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-helper-validator-identifier-npm-7.27.1-2c3cefd5dc-75041904d2.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-helper-validator-identifier-npm-7.27.1-2c3cefd5dc-75041904d2.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-helper-validator-identifier-npm-7.28.5-1953d49d2b-8e5d9b0133.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-helper-validator-identifier-npm-7.28.5-1953d49d2b-8e5d9b0133.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-helper-validator-option-npm-7.27.1-7c563f0423-db73e6a308.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-helper-validator-option-npm-7.27.1-7c563f0423-db73e6a308.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-helper-wrap-function-npm-7.27.1-7c7bc9ac83-effa5ba173.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-helper-wrap-function-npm-7.27.1-7c7bc9ac83-effa5ba173.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-helpers-npm-7.28.6-682df48628-213485cdff.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-helpers-npm-7.28.6-682df48628-213485cdff.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-parser-npm-7.27.7-412e710268-ed25ccfc70.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-parser-npm-7.27.7-412e710268-ed25ccfc70.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-parser-npm-7.29.0-c605c63e8b-b1576dca41.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-parser-npm-7.29.0-c605c63e8b-b1576dca41.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-plugin-bugfix-firefox-class-in-computed-class-key-npm-7.28.5-086662e626-750de98b34.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-plugin-bugfix-firefox-class-in-computed-class-key-npm-7.28.5-086662e626-750de98b34.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-plugin-bugfix-safari-class-field-initializer-scope-npm-7.27.1-168d311408-eb7f4146dc.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-plugin-bugfix-safari-class-field-initializer-scope-npm-7.27.1-168d311408-eb7f4146dc.zip
LFS
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining-npm-7.27.1-1740419cb6-f07aa80272.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining-npm-7.27.1-1740419cb6-f07aa80272.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-plugin-bugfix-v8-static-class-fields-redefine-readonly-npm-7.28.6-3c82220942-9377897aa7.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-plugin-bugfix-v8-static-class-fields-redefine-readonly-npm-7.28.6-3c82220942-9377897aa7.zip
LFS
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@babel-plugin-syntax-import-assertions-npm-7.28.6-b159f02dc0-25017235e1.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-plugin-syntax-import-assertions-npm-7.28.6-b159f02dc0-25017235e1.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-plugin-syntax-import-attributes-npm-7.28.6-05b2209c0a-6c8c6a5988.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-plugin-syntax-import-attributes-npm-7.28.6-05b2209c0a-6c8c6a5988.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-plugin-syntax-unicode-sets-regex-npm-7.18.6-b618a36bfd-a651d700fe.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-plugin-syntax-unicode-sets-regex-npm-7.18.6-b618a36bfd-a651d700fe.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-plugin-transform-arrow-functions-npm-7.27.1-fa40ddd46f-62c2cc0ae2.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-plugin-transform-arrow-functions-npm-7.27.1-fa40ddd46f-62c2cc0ae2.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-plugin-transform-async-generator-functions-npm-7.29.0-20cf975e94-e2c064a5eb.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-plugin-transform-async-generator-functions-npm-7.29.0-20cf975e94-e2c064a5eb.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-plugin-transform-async-to-generator-npm-7.28.6-52e8bb7562-bca5774263.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-plugin-transform-async-to-generator-npm-7.28.6-52e8bb7562-bca5774263.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-plugin-transform-block-scoped-functions-npm-7.27.1-c6d66f6e50-7fb4988ca8.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-plugin-transform-block-scoped-functions-npm-7.27.1-c6d66f6e50-7fb4988ca8.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-plugin-transform-block-scoping-npm-7.28.6-c38d97babf-7ab8a08560.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-plugin-transform-block-scoping-npm-7.28.6-c38d97babf-7ab8a08560.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-plugin-transform-class-properties-npm-7.28.6-87e84c4458-200f30d44b.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-plugin-transform-class-properties-npm-7.28.6-87e84c4458-200f30d44b.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-plugin-transform-class-static-block-npm-7.28.6-69c8a3886f-bea7836846.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-plugin-transform-class-static-block-npm-7.28.6-69c8a3886f-bea7836846.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-plugin-transform-classes-npm-7.28.6-99600ed6ed-9c3278a314.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-plugin-transform-classes-npm-7.28.6-99600ed6ed-9c3278a314.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-plugin-transform-computed-properties-npm-7.28.6-f40893c73e-4a5e270f7e.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-plugin-transform-computed-properties-npm-7.28.6-f40893c73e-4a5e270f7e.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-plugin-transform-destructuring-npm-7.28.5-5a0083928d-9cc67d3377.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-plugin-transform-destructuring-npm-7.28.5-5a0083928d-9cc67d3377.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-plugin-transform-dotall-regex-npm-7.28.6-12e6a9b67b-866ffbbdee.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-plugin-transform-dotall-regex-npm-7.28.6-12e6a9b67b-866ffbbdee.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-plugin-transform-duplicate-keys-npm-7.27.1-0b21c3b329-987b718d2f.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-plugin-transform-duplicate-keys-npm-7.27.1-0b21c3b329-987b718d2f.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-plugin-transform-duplicate-named-capturing-groups-regex-npm-7.29.0-ac722e23a4-7fa7b77325.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-plugin-transform-duplicate-named-capturing-groups-regex-npm-7.29.0-ac722e23a4-7fa7b77325.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-plugin-transform-dynamic-import-npm-7.27.1-ae3564e9cd-7a9fbc8d17.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-plugin-transform-dynamic-import-npm-7.27.1-ae3564e9cd-7a9fbc8d17.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-plugin-transform-explicit-resource-management-npm-7.28.6-832d08c78b-36d638a253.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-plugin-transform-explicit-resource-management-npm-7.28.6-832d08c78b-36d638a253.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-plugin-transform-exponentiation-operator-npm-7.28.6-9c4ecd76db-b232152499.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-plugin-transform-exponentiation-operator-npm-7.28.6-9c4ecd76db-b232152499.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-plugin-transform-export-namespace-from-npm-7.27.1-584dda771c-85082923ec.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-plugin-transform-export-namespace-from-npm-7.27.1-584dda771c-85082923ec.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-plugin-transform-for-of-npm-7.27.1-57bb1bd6d3-705c591d17.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-plugin-transform-for-of-npm-7.27.1-57bb1bd6d3-705c591d17.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-plugin-transform-function-name-npm-7.27.1-ed7f7430eb-26a2a183c3.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-plugin-transform-function-name-npm-7.27.1-ed7f7430eb-26a2a183c3.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-plugin-transform-json-strings-npm-7.28.6-9aa34eb962-69d82a1a0a.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-plugin-transform-json-strings-npm-7.28.6-9aa34eb962-69d82a1a0a.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-plugin-transform-literals-npm-7.27.1-16084b62dc-0a76d12ab1.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-plugin-transform-literals-npm-7.27.1-16084b62dc-0a76d12ab1.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-plugin-transform-logical-assignment-operators-npm-7.28.6-f6632db254-36095d5d1c.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-plugin-transform-logical-assignment-operators-npm-7.28.6-f6632db254-36095d5d1c.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-plugin-transform-member-expression-literals-npm-7.27.1-2d8a23c4c7-804121430a.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-plugin-transform-member-expression-literals-npm-7.27.1-2d8a23c4c7-804121430a.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-plugin-transform-modules-amd-npm-7.27.1-dbd9a5ef9f-5ca9257981.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-plugin-transform-modules-amd-npm-7.27.1-dbd9a5ef9f-5ca9257981.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-plugin-transform-modules-commonjs-npm-7.28.6-5c5a0ea6f2-ec6ea2958e.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-plugin-transform-modules-commonjs-npm-7.28.6-5c5a0ea6f2-ec6ea2958e.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-plugin-transform-modules-systemjs-npm-7.29.0-8a8a03d5d9-b3e64728ee.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-plugin-transform-modules-systemjs-npm-7.29.0-8a8a03d5d9-b3e64728ee.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-plugin-transform-modules-umd-npm-7.27.1-b62536925c-7388932863.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-plugin-transform-modules-umd-npm-7.27.1-b62536925c-7388932863.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-plugin-transform-named-capturing-groups-regex-npm-7.29.0-724a50bbb0-ed8c27699c.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-plugin-transform-named-capturing-groups-regex-npm-7.29.0-724a50bbb0-ed8c27699c.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-plugin-transform-new-target-npm-7.27.1-93bf8bdaef-620d78ee47.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-plugin-transform-new-target-npm-7.27.1-93bf8bdaef-620d78ee47.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-plugin-transform-nullish-coalescing-operator-npm-7.28.6-d32f31ce81-88106952ca.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-plugin-transform-nullish-coalescing-operator-npm-7.28.6-d32f31ce81-88106952ca.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@babel-plugin-transform-numeric-separator-npm-7.28.6-6a6d5b8b1a-4b5ca60e48.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@babel-plugin-transform-numeric-separator-npm-7.28.6-6a6d5b8b1a-4b5ca60e48.zip
LFS
vendored
Normal file
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