forked from cms/tibi-svelte-starter
Compare commits
76 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2d52272b2e | |||
| 8cbf0db14f | |||
| f407946c19 | |||
| d74964d078 | |||
| b0af8ba329 | |||
| 349fb9b2da | |||
| bd8d413850 | |||
| f332c707b7 | |||
| db968ab318 | |||
| 819147f518 | |||
| 4020ad62c5 | |||
| 958b45272d | |||
| 60d5920132 | |||
| 53ad012657 | |||
| c058ec760f | |||
| 1b24bb2157 | |||
| 491f495c66 | |||
| 4a604bab0b | |||
| e84b87ed16 | |||
| 8fb26fdeba | |||
| 106efb5d6e | |||
|
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 |
@@ -0,0 +1,649 @@
|
||||
# Build Checklist — Autonomous Website Project
|
||||
|
||||
> Navigate this checklist in order when building a complete website project from the starter.
|
||||
>
|
||||
> This file is the project-delivery checklist. It is not the maintenance plan for aligning the starter itself against upstream tibi changes or a stronger reference project. For starter-maintenance work, begin with .agents/STARTER_ALIGNMENT_STATUS.md and .agents/STARTER_ALIGNMENT_PLAN.md.
|
||||
|
||||
---
|
||||
|
||||
## How to use this checklist
|
||||
|
||||
For every phase, complete all five parts:
|
||||
|
||||
1. Required skills
|
||||
2. Required project artifacts
|
||||
3. Implementation checks
|
||||
4. Validation commands
|
||||
5. Exit criteria
|
||||
|
||||
Do not mark a phase done only because code exists. A phase is done only when its outputs and validations are both complete.
|
||||
|
||||
## Phase 0 — Bootstrap and project registration
|
||||
|
||||
**Required skills:** `tibi-project-setup`
|
||||
|
||||
**Required project artifacts:**
|
||||
|
||||
- `README.md`
|
||||
- `.env`
|
||||
- `api/config.yml`
|
||||
- `api/config.yml.env`
|
||||
- `api/hooks/config-client.js`
|
||||
- `frontend/.htaccess` when the deployment path uses the shipped Apache rewrite/proxy file
|
||||
- `package.json`
|
||||
- optional operator-owned root `config.yml` for the tibi-server instance when the current stack expects server-level config outside the project config
|
||||
|
||||
**Implementation checks:**
|
||||
|
||||
- [ ] Replace all starter placeholders in every affected file:
|
||||
- `.env`: `__PROJECT_NAME__`, `__TIBI_NAMESPACE__`, `__ORG__`, `__PROJECT__`
|
||||
- `api/config.yml`: `namespace`
|
||||
- `frontend/.htaccess`: namespace placeholders when Apache rewrite/proxy is actually used
|
||||
- `api/hooks/config-client.js`: project placeholder
|
||||
- [ ] Replace or remove visible starter identity leftovers in `README.md`, `package.json`, and other project-facing metadata.
|
||||
- [ ] Set all required project identity and URL values in `.env`:
|
||||
- `PROJECT_NAME`
|
||||
- `TIBI_NAMESPACE`
|
||||
- `LIVE_URL`
|
||||
- `CODING_URL`
|
||||
- `STAGING_URL`
|
||||
- `CODING_TIBIADMIN_URL`
|
||||
- `CODING_TIBISERVER_URL` only if the current setup exposes a dedicated raw tibi-server host
|
||||
- [ ] Update `package.json` project metadata such as package name and repository/default starter references.
|
||||
- [ ] Generate a real `ADMIN_TOKEN` in `api/config.yml.env`.
|
||||
- [ ] Ensure `ADMIN_ASSET_VERSION` exists in `api/config.yml.env`.
|
||||
- [ ] Decide explicitly which bootstrap path applies:
|
||||
- local starter Docker stack from `docker-compose-local.yml` and `Makefile`
|
||||
- shared or operator-managed tibi-server with explicit server-level config/project registration
|
||||
- [ ] For the local starter Docker path, confirm the repo is mounted into `/data` and the project serves through the repo-local `api/config.yml`; do not invent a separate root `config.yml` or `/api/v1/project` step.
|
||||
- [ ] For the shared/external tibi-server path, create the root `config.yml`, register the project, and reload it.
|
||||
- [ ] Confirm local/dev assumptions for Docker, reverse proxy, and any required basic-auth files only when the current environment actually uses them.
|
||||
- [ ] If audit logging or transaction-sensitive features are planned, confirm the MongoDB/replica-set prerequisite for the target environment instead of assuming the local Docker setup is enough.
|
||||
|
||||
**Validation commands:**
|
||||
|
||||
```bash
|
||||
rg '__[A-Z0-9_]+__' . --glob '*.{yml,js,env,htaccess,json,md,ts,svelte}'
|
||||
yarn install
|
||||
make docker-up
|
||||
curl -I "$CODING_URL"
|
||||
curl -I "$CODING_TIBIADMIN_URL"
|
||||
curl -I "$CODING_URL/api/content?limit=1"
|
||||
if [ -n "${CODING_TIBISERVER_URL:-}" ]; then curl -I "$CODING_TIBISERVER_URL/api/v1/version"; fi
|
||||
yarn build
|
||||
yarn build:server
|
||||
yarn validate
|
||||
```
|
||||
|
||||
**Exit criteria:**
|
||||
|
||||
- [ ] No unresolved placeholders remain.
|
||||
- [ ] The project no longer advertises starter-default identity in visible metadata.
|
||||
- [ ] The active operator path is explicit and complete.
|
||||
- [ ] If the current stack requires project registration, that registration succeeded.
|
||||
- [ ] Build, SSR build, and validate succeed with 0 warnings.
|
||||
- [ ] Website, admin, and API all respond on the expected URLs.
|
||||
|
||||
## Phase 1 — Solution architecture and delivery decisions
|
||||
|
||||
**Required skills:** `website-solution-architecture`, `security-hardening-and-token-strategy`
|
||||
|
||||
**Required project artifacts:**
|
||||
|
||||
- `docs/solution-architecture.md` or `plans/solution-architecture.md`
|
||||
- optional companion notes such as `docs/permissions.md` or `docs/content-model.md` when the project is large enough to need them
|
||||
|
||||
**Implementation checks:**
|
||||
|
||||
- [ ] Create one primary architecture document with explicit sections for:
|
||||
- context and scope
|
||||
- route and i18n model
|
||||
- collections and ownership
|
||||
- navigation and pagebuilder
|
||||
- forms, actions, jobs, and realtime
|
||||
- SSR, publication, and cache behavior
|
||||
- permissions and integrations
|
||||
- decision matrix
|
||||
- [ ] Document the content model:
|
||||
- page types
|
||||
- reusable entities
|
||||
- singleton/config collections
|
||||
- page-local vs reusable data
|
||||
- [ ] Document the route model:
|
||||
- language prefix strategy
|
||||
- `content.path` expectations
|
||||
- route translations
|
||||
- alias/canonical handling
|
||||
- [ ] Document the i18n strategy explicitly:
|
||||
- single-language or multilingual
|
||||
- field-level i18n or entry-level i18n
|
||||
- default language
|
||||
- supported languages
|
||||
- localized slug/path strategy
|
||||
- [ ] Document forms and workflows:
|
||||
- which features are CRUD collections
|
||||
- which features are actions
|
||||
- whether jobs or realtime are required
|
||||
- whether persistence is required
|
||||
- [ ] Document SSR requirements:
|
||||
- which routes must SSR
|
||||
- which collections are page-critical
|
||||
- whether publication windows exist
|
||||
- [ ] Document permissions strategy:
|
||||
- human roles
|
||||
- token-based integrations
|
||||
- hidden/readonly needs
|
||||
- [ ] Make an explicit decision whether the project is single-tenant or needs org/team support.
|
||||
- [ ] Make an explicit decision whether AI/editor assistance, classic search, or embeddings/search are in scope.
|
||||
- [ ] Record a searchable yes/no decision matrix for at least:
|
||||
- single-language vs multilingual
|
||||
- field-level vs entry-level i18n
|
||||
- single-tenant vs org/team
|
||||
- AI/editor assistance
|
||||
- classic search or embeddings
|
||||
- actions/jobs/realtime usage
|
||||
- publication scheduling
|
||||
|
||||
**Validation commands:**
|
||||
|
||||
```bash
|
||||
arch_doc=$(test -f docs/solution-architecture.md && echo docs/solution-architecture.md || echo plans/solution-architecture.md)
|
||||
test -f "$arch_doc"
|
||||
rg 'Route and i18n model|Collections and ownership|Navigation and pagebuilder|SSR, publication, and cache behavior|Permissions and integrations|Decision matrix' "$arch_doc"
|
||||
rg 'single-language|multilingual|field-level i18n|entry-level i18n' "$arch_doc"
|
||||
rg 'single-tenant|org/team|AI|search|embedding|actions|jobs|realtime' "$arch_doc"
|
||||
```
|
||||
|
||||
**Exit criteria:**
|
||||
|
||||
- [ ] A later agent can open the architecture document and answer the route, content, SSR, permissions, and i18n questions without guessing.
|
||||
- [ ] The architecture document contains explicit recorded choices for single-language vs multilingual and field-level vs entry-level i18n.
|
||||
- [ ] The project has an explicit yes/no decision recorded for org/team support, AI/editor assistance, and search/embeddings scope.
|
||||
|
||||
## Phase 2 — Collection model and Nova admin ergonomics
|
||||
|
||||
**Required skills:** `content-authoring`, `admin-ui-config`, `nova-pagebuilder-modeling`, `nova-navigation-modeling`, `media-seo-publishing`
|
||||
|
||||
**Required project artifacts:**
|
||||
|
||||
- `api/config.yml`
|
||||
- `api/collections/content.yml`
|
||||
- `api/collections/navigation.yml`
|
||||
- `api/collections/medialib.yml`
|
||||
- domain collection YAML files
|
||||
|
||||
**Implementation checks:**
|
||||
|
||||
- [ ] Create or update all required collections in `api/collections/`.
|
||||
- [ ] Include every collection in `api/config.yml`.
|
||||
- [ ] Configure meaningful `meta.preview` for each collection.
|
||||
- [ ] Configure the right `meta.viewHint` for each collection.
|
||||
- [ ] Configure usable forms with layout, sidebar groups, drillDown, dependsOn, and widget overrides where needed.
|
||||
- [ ] Use `pagebuilder` plus `blockRegistry` for block-driven collections.
|
||||
- [ ] Use readable foreign-key previews instead of raw IDs.
|
||||
- [ ] Configure field validators, file acceptance, and image constraints.
|
||||
- [ ] If the project benefits from grouped collection navigation or project-level admin i18n, configure those contracts deliberately instead of leaving them implicit.
|
||||
- [ ] If a collection is effectively single-document config, use `singleton` deliberately.
|
||||
|
||||
**Validation commands:**
|
||||
|
||||
```bash
|
||||
yarn validate
|
||||
```
|
||||
|
||||
Admin validation should also cover:
|
||||
|
||||
- collection sidebar labels and icons
|
||||
- list previews and columns
|
||||
- entry-form usability
|
||||
- foreign-reference readability
|
||||
- pagebuilder block chooser availability
|
||||
- pagebuilder preview rendering for at least one representative block when pagebuilder is used
|
||||
|
||||
**Exit criteria:**
|
||||
|
||||
- [ ] Every collection has a clear admin presentation model.
|
||||
- [ ] Editors can identify and edit entries without raw-ID workflows.
|
||||
- [ ] Pagebuilder-driven collections have a complete block registry and editable block forms.
|
||||
|
||||
## Phase 3 — Type contracts and API typing
|
||||
|
||||
**Required skills:** `content-authoring`
|
||||
|
||||
**Required project artifacts:**
|
||||
|
||||
- `types/global.d.ts`
|
||||
- `frontend/src/lib/api.ts`
|
||||
|
||||
**Implementation checks:**
|
||||
|
||||
- [ ] Model all block and domain-entry types in `types/global.d.ts`.
|
||||
- [ ] Add or update `EntryTypeSwitch` coverage in `frontend/src/lib/api.ts`.
|
||||
- [ ] When a block or collection participates in public rendering or admin preview, update all affected types in the same change.
|
||||
- [ ] Keep API and block types aligned with the collection YAML definitions.
|
||||
- [ ] Avoid type drift between the CMS config and the frontend assumptions.
|
||||
|
||||
**Validation commands:**
|
||||
|
||||
```bash
|
||||
yarn validate
|
||||
```
|
||||
|
||||
**Exit criteria:**
|
||||
|
||||
- [ ] The current project types describe the collection and block model accurately.
|
||||
- [ ] Validation passes cleanly.
|
||||
|
||||
## Phase 4 — Frontend blocks, routing, and admin registry
|
||||
|
||||
**Required skills:** `frontend-architecture`, `nova-pagebuilder-modeling`
|
||||
|
||||
**Required project artifacts:**
|
||||
|
||||
- `frontend/src/blocks/*`
|
||||
- `frontend/src/blocks/BlockRenderer.svelte`
|
||||
- `frontend/src/admin.ts`
|
||||
- routing/i18n files under `frontend/src/lib/`
|
||||
|
||||
**Implementation checks:**
|
||||
|
||||
- [ ] Implement every required block component.
|
||||
- [ ] Register every block in `BlockRenderer.svelte`.
|
||||
- [ ] Register every pagebuilder block in `frontend/src/admin.ts`.
|
||||
- [ ] Keep blocks SSR-safe.
|
||||
- [ ] Ensure the route layer, i18n layer, and content lookup logic agree on the public URL model.
|
||||
- [ ] If blocks or pages render foreign media/references, confirm lookup expectations explicitly instead of assuming resolved data is present.
|
||||
- [ ] Treat public rendering and admin-preview rendering as the same contract whenever possible.
|
||||
|
||||
**Validation commands:**
|
||||
|
||||
```bash
|
||||
yarn build
|
||||
yarn validate
|
||||
```
|
||||
|
||||
**Exit criteria:**
|
||||
|
||||
- [ ] All configured block types render in the site and in the admin registry.
|
||||
- [ ] Navigation, i18n, and media references behave correctly in the browser.
|
||||
|
||||
## Phase 5 — SSR, publication model, and cache invalidation
|
||||
|
||||
**Required skills:** `tibi-ssr-caching`, `tibi-hook-authoring`
|
||||
|
||||
**Required project artifacts:**
|
||||
|
||||
- `api/hooks/config.js`
|
||||
- `api/hooks/clear_cache.js`
|
||||
- SSR-related hook files under `api/hooks/ssr/`
|
||||
|
||||
**Implementation checks:**
|
||||
|
||||
- [ ] Update `ssrValidatePath()` for the real public route model.
|
||||
- [ ] Ensure `publishedFilter` matches the actual publication model.
|
||||
- [ ] Ensure `ssrPublishCheckCollections` covers all time-sensitive collections.
|
||||
- [ ] Confirm page-critical collections are loaded in an SSR-safe way.
|
||||
- [ ] Confirm mutations to content, navigation, media, and publication-critical data invalidate SSR as intended.
|
||||
- [ ] Verify at least one representative mutation path against the SSR response instead of only checking static SSR HTML.
|
||||
- [ ] If lookups are needed for page-critical references, ensure SSR data loading uses them deliberately.
|
||||
|
||||
**Validation commands:**
|
||||
|
||||
```bash
|
||||
yarn build:server
|
||||
curl "$CODING_TIBISERVER_URL/api/v1/_/<namespace>/ssr?url=/de/..."
|
||||
curl -I "$CODING_TIBISERVER_URL/api/v1/_/<namespace>/ssr?url=/de/..."
|
||||
```
|
||||
|
||||
Check all of these on the SSR response:
|
||||
|
||||
- HTTP status is correct.
|
||||
- HTML contains the expected page content.
|
||||
- HTML contains navigation labels.
|
||||
- `window.__SSR_CACHE__` is present.
|
||||
- a repeated request can return an SSR cache hit where expected.
|
||||
- after a representative mutation, the next SSR response reflects the change or expected invalidation behavior.
|
||||
|
||||
**Exit criteria:**
|
||||
|
||||
- [ ] SSR returns correct HTML for valid routes.
|
||||
- [ ] Publication and invalidation behavior is verified, not assumed.
|
||||
|
||||
## Phase 6 — Hooks, actions, jobs, and realtime
|
||||
|
||||
**Required skills:** `tibi-hook-authoring`, `tibi-actions-and-forms`, `scheduled-jobs-and-automation`, `realtime-and-live-workflows`
|
||||
|
||||
**Required project artifacts:**
|
||||
|
||||
- collection hook files in `api/hooks/`
|
||||
- action files in `api/actions/` when applicable
|
||||
- job config/hooks when applicable
|
||||
|
||||
**Implementation checks:**
|
||||
|
||||
- [ ] Implement public read hooks where public filtering differs from raw CRUD reads.
|
||||
- [ ] Implement cache invalidation hooks for page-critical mutations.
|
||||
- [ ] Use actions for endpoint-like workflows instead of fake CRUD collections.
|
||||
- [ ] For each endpoint-style workflow, record why it is an action and not a collection.
|
||||
- [ ] Respect current action-hook behavior, especially `context.data` timing.
|
||||
- [ ] If jobs or realtime are used, document why they belong in the project and what they affect.
|
||||
- [ ] Register every hook/action/job in the project config.
|
||||
|
||||
**Validation commands:**
|
||||
|
||||
```bash
|
||||
yarn validate
|
||||
```
|
||||
|
||||
Add targeted API or Playwright checks for:
|
||||
|
||||
- anonymous vs token-backed public filtering
|
||||
- valid vs invalid action submissions
|
||||
- cache-clear side effects
|
||||
- job/realtime behavior when those features exist
|
||||
|
||||
**Exit criteria:**
|
||||
|
||||
- [ ] Backend behavior matches the modeled workflows.
|
||||
- [ ] Endpoint-style features are implemented as actions when appropriate.
|
||||
|
||||
## Phase 7 — Permissions and security hardening
|
||||
|
||||
**Required skills:** `permissions-and-editor-workflows`, `security-hardening-and-token-strategy`
|
||||
|
||||
**Required project artifacts:**
|
||||
|
||||
- collection/action permission sections in YAML
|
||||
- security-relevant config in server/project config
|
||||
- optional `docs/permissions.md` for larger projects
|
||||
|
||||
**Implementation checks:**
|
||||
|
||||
- [ ] Configure collection permissions for `public`, `user`, and any custom roles.
|
||||
- [ ] Add explicit token permission sets where machine access is required.
|
||||
- [ ] Use bulk permissions only when there is a real operational need.
|
||||
- [ ] Write down representative permission actors and workflow states before finalizing the YAML.
|
||||
- [ ] Configure `readonlyFields`, `hiddenFields`, and any field-level overrides deliberately.
|
||||
- [ ] Use eval-based field rules where editorial state transitions require them.
|
||||
- [ ] Review CORS for real cross-origin requirements instead of weakening it by default.
|
||||
- [ ] Review login rate limiting and secure-cookie expectations for the target environment.
|
||||
- [ ] Review risky hook capabilities such as outbound fetch or command execution and document why they are necessary when used.
|
||||
- [ ] Ensure production secrets come from proper sources rather than committed literals.
|
||||
|
||||
**Validation commands:**
|
||||
|
||||
```bash
|
||||
yarn validate
|
||||
```
|
||||
|
||||
Add targeted API checks for:
|
||||
|
||||
- public vs authenticated vs token-backed access
|
||||
- readonly/hidden enforcement on read and write
|
||||
- eval-based permission behavior for representative entry states
|
||||
- at least one allowed and one denied write for each important workflow state
|
||||
|
||||
**Exit criteria:**
|
||||
|
||||
- [ ] Permissions reflect the real editor/integration model.
|
||||
- [ ] Security-sensitive config and risky capabilities were reviewed explicitly.
|
||||
|
||||
## Phase 8 — Audit and compliance readiness
|
||||
|
||||
**Required skills:** `audit-and-compliance`, `tibi-hook-authoring`
|
||||
|
||||
**Required project artifacts:**
|
||||
|
||||
- audit config in the active tibi-server config
|
||||
- collection-level audit settings when relevant
|
||||
- `audit.return` hooks where sensitive data must be stripped
|
||||
|
||||
**Implementation checks:**
|
||||
|
||||
- [ ] Decide whether audit logging is required for the project.
|
||||
- [ ] If enabled, configure server-level audit settings deliberately.
|
||||
- [ ] If sensitive fields can land in snapshots, add `audit.return` hooks.
|
||||
- [ ] If hooks/jobs/actions mutate important collections, account for the resulting audit source semantics.
|
||||
- [ ] If audit is required in production, confirm retention/TTL expectations with operations.
|
||||
|
||||
**Validation commands:**
|
||||
|
||||
```bash
|
||||
curl -H "X-Auth-Token: <jwt-token>" "$CODING_TIBISERVER_URL/api/v1/audit?limit=5"
|
||||
```
|
||||
|
||||
**Exit criteria:**
|
||||
|
||||
- [ ] The project has an explicit audit decision: enabled with rules, or deliberately not used.
|
||||
- [ ] Sensitive audit exposure has been considered, not ignored.
|
||||
|
||||
## Phase 9 — Media, SEO, and publication
|
||||
|
||||
**Required skills:** `media-seo-publishing`, `nova-ai-editor-features` when AI-assisted media workflows are in scope
|
||||
|
||||
**Required project artifacts:**
|
||||
|
||||
- `api/collections/medialib.yml`
|
||||
- SEO/publication fields in content or domain collections
|
||||
- any image-filter configuration used by the frontend or admin
|
||||
|
||||
**Implementation checks:**
|
||||
|
||||
- [ ] Configure medialib fields, filters, alt/caption handling, and admin widgets.
|
||||
- [ ] Treat the shared media widget/helper boundary as canonical for public, SSR, and admin-preview image rendering.
|
||||
- [ ] Add SEO fields with sensible admin placement.
|
||||
- [ ] Configure social/share image handling where needed.
|
||||
- [ ] Configure the publication model explicitly.
|
||||
- [ ] If publication windows exist, define representative current, future, and expired states.
|
||||
- [ ] Ensure the chosen publication model matches `publishedFilter`, SSR logic, and editor workflows.
|
||||
|
||||
**Validation commands:**
|
||||
|
||||
```bash
|
||||
yarn validate
|
||||
curl "$CODING_TIBISERVER_URL/api/v1/_/<namespace>/ssr?url=/de/..."
|
||||
```
|
||||
|
||||
Check for:
|
||||
|
||||
- expected media URLs
|
||||
- expected SEO/meta output in SSR HTML
|
||||
- expected social/share metadata when used
|
||||
- expected publication visibility behavior
|
||||
- representative current/future/expired publication states when timing is used
|
||||
|
||||
**Exit criteria:**
|
||||
|
||||
- [ ] Media and SEO behavior is verified from schema through SSR/public output.
|
||||
- [ ] Publication state is enforced consistently across schema, public reads, and SSR output.
|
||||
|
||||
## Phase 10 — Optional AI, search, and enterprise branches
|
||||
|
||||
**Required skills:** `nova-ai-editor-features`, `search-and-embeddings`, `multi-tenancy-and-orgs`
|
||||
|
||||
**Required project artifacts:**
|
||||
|
||||
- explicit architecture note even when the answer is “not used”
|
||||
- AI/action config when enabled
|
||||
- org/team config or rollout notes when enabled
|
||||
- embedding/search config when enabled
|
||||
|
||||
**Implementation checks:**
|
||||
|
||||
- [ ] Record whether editor AI is enabled.
|
||||
- [ ] Record whether embeddings/search are enabled.
|
||||
- [ ] Record whether org/team support is enabled.
|
||||
- [ ] Record whether the project is explicitly single-tenant or org/team-aware.
|
||||
- [ ] If AI is enabled, define provider, model, budget, target fields or action contracts, and failure behavior.
|
||||
- [ ] If org/team support is enabled, define org visibility, team working rights, project assignment rules, and permission ownership.
|
||||
- [ ] If search/embeddings are enabled, define provider setup, search mode, index/search contracts, regeneration expectations, and operator ownership.
|
||||
|
||||
**Validation commands:**
|
||||
|
||||
```bash
|
||||
yarn validate
|
||||
```
|
||||
|
||||
Add feature-specific checks only if the feature is enabled.
|
||||
|
||||
Feature-specific checks can include:
|
||||
|
||||
- `curl "$CODING_TIBISERVER_URL/api/v1/_/<namespace>/<collection>?q=...&qName=..."`
|
||||
- representative org/team visibility or permission checks with the intended auth model
|
||||
|
||||
**Exit criteria:**
|
||||
|
||||
- [ ] The project has an explicit yes/no decision for AI, search/embeddings, and enterprise org/team support.
|
||||
- [ ] The project has an explicit single-tenant vs org/team-aware decision.
|
||||
- [ ] Enabled optional branches have concrete contracts and not just ideas.
|
||||
|
||||
## Phase 11 — Operations, deployment, and observability
|
||||
|
||||
**Required skills:** `deployment`, `monitoring-and-performance`, `mongodb-and-indexes`, `security-hardening-and-token-strategy`
|
||||
|
||||
**Required project artifacts:**
|
||||
|
||||
- deployment workflow files
|
||||
- deploy scripts
|
||||
- environment and secret configuration
|
||||
- optional monitoring/operations notes
|
||||
|
||||
**Implementation checks:**
|
||||
|
||||
- [ ] Configure staging and production deployment paths and URLs.
|
||||
- [ ] Configure CI or other deployment automation.
|
||||
- [ ] Confirm admin reload and SSR cache clear behavior on deploy.
|
||||
- [ ] If Sentry or other observability tooling is used, wire it deliberately.
|
||||
- [ ] If external operators need OpenAPI or metrics, confirm the requirement and the exposure model.
|
||||
- [ ] Confirm MongoDB version, replica-set, persistence, and backup assumptions for the target environment.
|
||||
- [ ] Confirm backup/media persistence assumptions and collection upload paths.
|
||||
|
||||
**Validation commands:**
|
||||
|
||||
```bash
|
||||
yarn build
|
||||
yarn build:server
|
||||
```
|
||||
|
||||
Operator checks when applicable:
|
||||
|
||||
- staging deploy works
|
||||
- production deploy flow is documented
|
||||
- admin reload works
|
||||
- SSR cache clear works
|
||||
- health endpoints and metrics/OpenAPI exposure behave as expected
|
||||
|
||||
**Exit criteria:**
|
||||
|
||||
- [ ] The project has a concrete deploy path, not just local Docker success.
|
||||
- [ ] Operational dependencies and visibility expectations are documented.
|
||||
|
||||
## Phase 12 — Testing and regression coverage
|
||||
|
||||
**Required skills:** `playwright-testing`, `tibi-ssr-caching` when SSR-specific checks are needed
|
||||
|
||||
**Required project artifacts:**
|
||||
|
||||
- seeded data helpers in `tests/api/helpers/`
|
||||
- Playwright specs in the appropriate test slice
|
||||
|
||||
**Implementation checks:**
|
||||
|
||||
- [ ] Use deterministic seed data for content and any other collections needed by tests.
|
||||
- [ ] Keep seed identity explicit, preferably with hidden `_testdata` markers.
|
||||
- [ ] Record which seed data was reused or extended for the committed test slice.
|
||||
- [ ] Add API tests for critical collection and action behavior.
|
||||
- [ ] Add desktop E2E tests for core public journeys.
|
||||
- [ ] Add admin smoke tests for stable admin contracts.
|
||||
- [ ] For pagebuilder-driven projects, include committed admin coverage for block chooser/registry behavior and at least one real preview rendering path.
|
||||
- [ ] If SSR is critical, include SSR-specific verification through targeted checks or dedicated tests.
|
||||
- [ ] Record whether SSR proof comes from direct endpoint checks, committed tests, or both.
|
||||
|
||||
**Validation commands:**
|
||||
|
||||
```bash
|
||||
npx playwright test tests/api/health.spec.ts --project=api
|
||||
npx playwright test tests/e2e/home.spec.ts --project=chromium
|
||||
```
|
||||
|
||||
Add affected project/spec commands for the current project, for example:
|
||||
|
||||
- admin smoke specs
|
||||
- pagebuilder preview specs
|
||||
- action-specific API specs
|
||||
- mobile or visual slices when relevant
|
||||
|
||||
Leave behind repo-local evidence of which commands were actually used for sign-off.
|
||||
|
||||
**Exit criteria:**
|
||||
|
||||
- [ ] The test suite reflects the real project contracts, not demo content.
|
||||
- [ ] Critical public, admin, and backend paths have executable regression coverage.
|
||||
- [ ] A later agent can see which specs and SSR checks were used for sign-off.
|
||||
|
||||
## Phase 13 — Video tours (optional)
|
||||
|
||||
**Required skills:** none mandatory
|
||||
|
||||
**Required project artifacts:**
|
||||
|
||||
- tour files in `video-tours/tours/` when the project uses them
|
||||
|
||||
**Implementation checks:**
|
||||
|
||||
- [ ] Add or update tour scripts for the intended training/demo flows.
|
||||
- [ ] Keep desktop and mobile variants aligned when both exist.
|
||||
|
||||
**Validation commands:**
|
||||
|
||||
```bash
|
||||
yarn tour
|
||||
yarn tour:mobile
|
||||
```
|
||||
|
||||
**Exit criteria:**
|
||||
|
||||
- [ ] Tours run successfully when the project uses them.
|
||||
|
||||
## Phase 14 — Final verification and production readiness
|
||||
|
||||
**Required skills:** `tibi-project-setup`, `playwright-testing`
|
||||
|
||||
**Required project artifacts:**
|
||||
|
||||
- all previously generated project artifacts
|
||||
- any remaining project-polish assets
|
||||
|
||||
**Implementation checks:**
|
||||
|
||||
- [ ] Build and validation are clean.
|
||||
- [ ] Placeholder scan is clean.
|
||||
- [ ] Public site loads.
|
||||
- [ ] Nova admin loads.
|
||||
- [ ] Pages are creatable and editable.
|
||||
- [ ] SSR renders real content.
|
||||
- [ ] Forms/actions work.
|
||||
- [ ] Project polish is complete when the project is meant for real delivery:
|
||||
- project image in `api/config.yml` points to a real project-specific asset
|
||||
- prefer a fresh project screenshot captured via Playwright MCP over placeholders or generic graphics
|
||||
- resize/compress the chosen project image to a small admin-friendly file size before wiring `meta.imageUrl`
|
||||
- replace starter or temporary collection icons with project-specific icons
|
||||
- use thematically fitting free Unsplash/stock images where real customer/project imagery is not yet available; avoid irrelevant placeholders
|
||||
- [ ] Any intentionally deferred items are documented.
|
||||
|
||||
**Validation commands:**
|
||||
|
||||
```bash
|
||||
yarn build
|
||||
yarn build:server
|
||||
yarn validate
|
||||
rg '__[A-Z0-9_]+__' . --glob '*.{yml,js,env,htaccess,json,ts,svelte}'
|
||||
```
|
||||
|
||||
Add the project's affected Playwright command set and any deploy/operator checks required for sign-off.
|
||||
|
||||
**Exit criteria:**
|
||||
|
||||
- [ ] The project is buildable, testable, and operable.
|
||||
- [ ] No critical checklist phase was skipped implicitly.
|
||||
- [ ] A later agent can continue from the project repository without reconstructing hidden assumptions.
|
||||
@@ -0,0 +1,783 @@
|
||||
---
|
||||
name: admin-ui-config
|
||||
description: Configure the admin UI for collections and project-level Nova behavior — meta labels, preview/viewHint, sidebar layout, collectionGroups, i18n, field widgets, foreign references, and image handling. Use when setting up or customizing admin views.
|
||||
---
|
||||
|
||||
# admin-ui-config
|
||||
|
||||
## When to use this skill
|
||||
|
||||
Use this skill when:
|
||||
|
||||
- Configuring how a collection appears in the tibi-admin UI
|
||||
- Configuring collection preview and default list presentation
|
||||
- Configuring field widgets (dropdowns, media pickers, richtext, etc.)
|
||||
- Organizing fields into sidebar groups or sections
|
||||
- Setting up foreign key references between collections
|
||||
- Customizing the admin module (`frontend/src/admin.ts`)
|
||||
|
||||
## Reference source
|
||||
|
||||
The canonical type definitions are in `tibi-admin-nova/types/admin.d.ts`. Always consult this file for the full API. This skill provides a practical summary.
|
||||
|
||||
Treat this skill as Nova-first. Use current Nova concepts such as `preview`, `singleton: { enabled }`, `drillDown`, `dependsOn`, `containerProps.layout`, `pagebuilder`, `viewHint`, `subNavigation`, and AI media assist.
|
||||
|
||||
---
|
||||
|
||||
## Project-level admin contracts
|
||||
|
||||
Not every important Nova contract lives in a collection YAML file. Some of the most important admin behaviors are configured at project level in `api/config.yml` under `meta:`.
|
||||
|
||||
Current starter example:
|
||||
|
||||
```yaml
|
||||
meta:
|
||||
imageUrl:
|
||||
eval: "$projectBase + '_/assets/img/admin-pic.svg'"
|
||||
i18n:
|
||||
defaultLanguage: de
|
||||
languages:
|
||||
- code: de
|
||||
label: Deutsch
|
||||
- code: en
|
||||
label: English
|
||||
collectionGroups:
|
||||
- name: content
|
||||
label: { de: "Inhalte", en: "Content" }
|
||||
icon: article
|
||||
- name: media
|
||||
label: { de: "Medien", en: "Media" }
|
||||
icon: image_multiple
|
||||
```
|
||||
|
||||
Treat these as part of the admin design, not as optional polish:
|
||||
|
||||
- `meta.imageUrl` — project card/preview imagery in the admin
|
||||
- `meta.i18n` — project-wide language model for field-level and entry-level translation workflows
|
||||
- `meta.collectionGroups` — ordered collection groups for the sidebar
|
||||
|
||||
Important rule:
|
||||
|
||||
- collection-level `meta.group` must reference one of the project-level `meta.collectionGroups[].name` values if the collection should appear inside an explicit group
|
||||
|
||||
If project-level `meta.i18n` is missing or inconsistent, even well-modeled collections can become confusing in Nova.
|
||||
|
||||
---
|
||||
|
||||
## Collection meta configuration
|
||||
|
||||
The `meta` key in a collection YAML controls how the collection appears in the admin sidebar and collection/list UI.
|
||||
|
||||
```yaml
|
||||
name: mycollection
|
||||
meta:
|
||||
label: { de: "Produkte", en: "Products" } # Sidebar label (i18n)
|
||||
muiIcon: shopping_cart # Material UI icon name
|
||||
group: shop # Group in admin sidebar
|
||||
singleton:
|
||||
enabled: false
|
||||
hide: false # Set to true to hide the collection for non-admin users
|
||||
preview:
|
||||
label: name
|
||||
secondary: price
|
||||
```
|
||||
|
||||
### Preview
|
||||
|
||||
Use `meta.preview` as the universal entry representation for Nova lists, breadcrumbs, foreign-key widgets, and search result previews:
|
||||
|
||||
```yaml
|
||||
preview: name
|
||||
|
||||
preview:
|
||||
label: name
|
||||
secondary: slug
|
||||
badge: status
|
||||
|
||||
preview:
|
||||
eval: "`${$this.firstName} ${$this.lastName}`"
|
||||
```
|
||||
|
||||
## List presentation
|
||||
|
||||
For current Nova, use `meta.viewHint` plus `meta.preview` for collection/list presentation.
|
||||
|
||||
```yaml
|
||||
meta:
|
||||
viewHint: table
|
||||
preview:
|
||||
label: name
|
||||
secondary: slug
|
||||
badge: status
|
||||
table:
|
||||
- name
|
||||
- source: status
|
||||
label: Status
|
||||
- source: author.name
|
||||
label: Author
|
||||
select:
|
||||
- author.name
|
||||
```
|
||||
|
||||
- `meta.viewHint` controls the preferred collection presentation (`table`, `cards`, `media`, or `navigation` object where supported).
|
||||
- `preview.table` defines explicit list columns for Nova.
|
||||
- `preview.select` can reduce lookup work for preview table columns.
|
||||
- `meta.subNavigation` defines filtered entry tabs in the sidebar.
|
||||
|
||||
### Sub-navigation tabs
|
||||
|
||||
Use `meta.subNavigation` when one collection needs multiple curated views in the admin without splitting into multiple collections.
|
||||
|
||||
```yaml
|
||||
meta:
|
||||
subNavigation:
|
||||
- name: pages
|
||||
label: { de: "Seiten", en: "Pages" }
|
||||
muiIcon: article
|
||||
filter:
|
||||
type: page
|
||||
defaultSort:
|
||||
field: insertTime
|
||||
order: DESC
|
||||
setDefault:
|
||||
field: type
|
||||
value: page
|
||||
- name: news
|
||||
label: { de: "News", en: "News" }
|
||||
muiIcon: feed
|
||||
filter:
|
||||
type: news
|
||||
setDefault:
|
||||
field: type
|
||||
value: news
|
||||
```
|
||||
|
||||
Use sub-navigation when:
|
||||
|
||||
- one collection has several stable editorial slices
|
||||
- the underlying schema is still shared enough to stay one collection
|
||||
- authors benefit from filtered entry views and sensible defaults
|
||||
|
||||
Do not use sub-navigation to hide a bad collection model. If the workflows truly diverge, split the collection instead.
|
||||
|
||||
---
|
||||
|
||||
## Field configuration
|
||||
|
||||
Each field in the `fields` array can have a `meta` section controlling its admin UI behavior.
|
||||
|
||||
### Basic field with meta
|
||||
|
||||
```yaml
|
||||
fields:
|
||||
- name: name
|
||||
type: string
|
||||
meta:
|
||||
label: { de: "Name", en: "Name" }
|
||||
helperText: { de: "Anzeigename", en: "Display name" }
|
||||
position: main # "main" (default) or "sidebar"
|
||||
```
|
||||
|
||||
### Field types
|
||||
|
||||
| YAML `type` | Admin widget (default) | Notes |
|
||||
| ----------- | ---------------------- | --------------------------------------------- |
|
||||
| `string` | Text input | Use `inputProps.multiline: true` for textarea |
|
||||
| `number` | Number input | |
|
||||
| `number[]` | Number chip array | Multiple numeric values |
|
||||
| `boolean` | Toggle/checkbox | |
|
||||
| `date` | Date picker | |
|
||||
| `object` | Nested field group | Requires `subFields` |
|
||||
| `object[]` | Repeatable group | Requires `subFields`, drag-to-reorder |
|
||||
| `string[]` | Tag input | |
|
||||
| `file` | File upload | |
|
||||
| `file[]` | Multi-file upload | |
|
||||
| `any` | JSON editor | For mixed/arbitrary data |
|
||||
|
||||
### inputProps — widget customization
|
||||
|
||||
`inputProps` passes props directly to the field widget:
|
||||
|
||||
```yaml
|
||||
# Multiline text (textarea)
|
||||
- name: description
|
||||
type: string
|
||||
meta:
|
||||
label: { de: "Beschreibung", en: "Description" }
|
||||
inputProps:
|
||||
multiline: true
|
||||
rows: 5
|
||||
|
||||
# Number with min/max
|
||||
- name: price
|
||||
type: number
|
||||
meta:
|
||||
inputProps:
|
||||
min: 0
|
||||
max: 99999
|
||||
step: 0.01
|
||||
|
||||
# Placeholder text
|
||||
- name: email
|
||||
type: string
|
||||
meta:
|
||||
inputProps:
|
||||
placeholder: "name@example.com"
|
||||
```
|
||||
|
||||
### Widget override
|
||||
|
||||
Override the default widget with `meta.widget`:
|
||||
|
||||
```yaml
|
||||
- name: content
|
||||
type: string
|
||||
meta:
|
||||
widget: richtext # Rich text editor (HTML)
|
||||
|
||||
- name: heroImage
|
||||
type: file
|
||||
meta:
|
||||
widget: image # Image-focused file widget
|
||||
|
||||
- name: relatedPages
|
||||
type: string[]
|
||||
meta:
|
||||
widget: foreignKeyChipArray
|
||||
```
|
||||
|
||||
Common widget types: `text`, `checkbox`, `select`, `chipArray`, `checkboxArray`, `date`, `datetime`, `file`, `image`, `richtext`, `json`, `foreignKey`, `foreignKeyChipArray`, `pagebuilder`, `containerLessObject`, `containerLessObjectArray`.
|
||||
|
||||
Important current widgets/features to consider when designing a real website backoffice:
|
||||
|
||||
- `pagebuilder` for CMS-driven block/page authoring
|
||||
- `foreignKeyChipArray` for many-reference editing
|
||||
- `image` plus `imageEditor` / `downscale` for image-heavy workflows
|
||||
- `drillDown` editing for complex nested arrays
|
||||
|
||||
### Choices — dropdowns/selects
|
||||
|
||||
Static choices:
|
||||
|
||||
```yaml
|
||||
- name: type
|
||||
type: string
|
||||
meta:
|
||||
label: { de: "Typ", en: "Type" }
|
||||
choices:
|
||||
- id: page
|
||||
name: { de: "Seite", en: "Page" }
|
||||
- id: blog
|
||||
name: { de: "Blog", en: "Blog" }
|
||||
- id: product
|
||||
name: { de: "Produkt", en: "Product" }
|
||||
```
|
||||
|
||||
Dynamic choices from API:
|
||||
|
||||
```yaml
|
||||
- name: category
|
||||
type: string
|
||||
meta:
|
||||
choices:
|
||||
endpoint: categories # Collection name
|
||||
mapping:
|
||||
id: id
|
||||
name: name
|
||||
```
|
||||
|
||||
### Foreign references
|
||||
|
||||
Link to entries in another collection:
|
||||
|
||||
```yaml
|
||||
- name: author
|
||||
type: string
|
||||
meta:
|
||||
label: { de: "Autor", en: "Author" }
|
||||
foreign:
|
||||
collection: users
|
||||
id: id
|
||||
sort: name
|
||||
projection: name,email
|
||||
render:
|
||||
label: name
|
||||
secondary: email
|
||||
createDefaults:
|
||||
role: author
|
||||
```
|
||||
|
||||
Use `foreign.id: id` for the outward FK field identity. Only Mongo-style filters/query conditions use `_id`. Use `foreign.render` or target-collection `meta.preview` so references stay readable. Bare IDs are not acceptable authoring UX for a serious website project.
|
||||
|
||||
### Image fields
|
||||
|
||||
```yaml
|
||||
- name: image
|
||||
type: file
|
||||
meta:
|
||||
widget: image
|
||||
downscale: # Auto-resize on upload
|
||||
maxWidth: 1920
|
||||
maxHeight: 1080
|
||||
quality: 0.85
|
||||
imageEditor: true # Enable crop/rotate editor
|
||||
```
|
||||
|
||||
This field config controls the editor widget, not the filesystem target. Configure file storage once at collection level via top-level `uploadPath` (for this starter typically `../media/<collection>`), not on the individual file field.
|
||||
|
||||
---
|
||||
|
||||
## Layout: position, sections, sidebar
|
||||
|
||||
### Sidebar placement
|
||||
|
||||
```yaml
|
||||
- name: active
|
||||
type: boolean
|
||||
meta:
|
||||
position: sidebar # Moves field to sidebar
|
||||
|
||||
- name: publishDate
|
||||
type: date
|
||||
meta:
|
||||
position: "sidebar:publishing" # Sidebar with group key
|
||||
```
|
||||
|
||||
### Sidebar groups (ordered)
|
||||
|
||||
Define sidebar group order in collection meta:
|
||||
|
||||
```yaml
|
||||
meta:
|
||||
sidebar:
|
||||
- group: publishing
|
||||
label: { de: "Veröffentlichung", en: "Publishing" }
|
||||
- group: seo
|
||||
label: { de: "SEO", en: "SEO" }
|
||||
- group: settings
|
||||
label: { de: "Einstellungen", en: "Settings" }
|
||||
```
|
||||
|
||||
### Sections in main area
|
||||
|
||||
```yaml
|
||||
- name: seoTitle
|
||||
type: string
|
||||
meta:
|
||||
section: SEO # Groups fields under a section header
|
||||
|
||||
- name: seoDescription
|
||||
type: string
|
||||
meta:
|
||||
section: SEO
|
||||
```
|
||||
|
||||
### Grid layout (columns)
|
||||
|
||||
Use `containerProps` for multi-column layout:
|
||||
|
||||
```yaml
|
||||
- name: firstName
|
||||
type: string
|
||||
meta:
|
||||
containerProps:
|
||||
layout:
|
||||
size: col-6 # Half width (12-column grid)
|
||||
|
||||
- name: lastName
|
||||
type: string
|
||||
meta:
|
||||
containerProps:
|
||||
layout:
|
||||
size: col-6
|
||||
```
|
||||
|
||||
`containerProps.layout` is one of the most important Nova ergonomics features. Use it aggressively to avoid long, single-column forms.
|
||||
|
||||
Recommended pattern for real projects:
|
||||
|
||||
- sidebar for publication, SEO, flags, relations, admin-only metadata
|
||||
- main area for editorial content
|
||||
- 2-column or 3-column layout for short related fields
|
||||
- section headings for repeated conceptual groups
|
||||
|
||||
---
|
||||
|
||||
## Nested objects and arrays
|
||||
|
||||
### Object (nested group)
|
||||
|
||||
```yaml
|
||||
- name: address
|
||||
type: object
|
||||
meta:
|
||||
label: { de: "Adresse", en: "Address" }
|
||||
subFields:
|
||||
- name: street
|
||||
type: string
|
||||
- name: city
|
||||
type: string
|
||||
- name: zip
|
||||
type: string
|
||||
```
|
||||
|
||||
### Object array (repeatable blocks)
|
||||
|
||||
```yaml
|
||||
- name: blocks
|
||||
type: object[]
|
||||
meta:
|
||||
label: { de: "Inhaltsblöcke", en: "Content Blocks" }
|
||||
widget: pagebuilder
|
||||
preview: { eval: "`${$this.type}: ${$this.headline || ''}`" }
|
||||
drillDown: true
|
||||
subFields:
|
||||
- name: type
|
||||
type: string
|
||||
meta:
|
||||
choices:
|
||||
- id: hero
|
||||
name: Hero
|
||||
- id: richtext
|
||||
name: Richtext
|
||||
- name: headline
|
||||
type: string
|
||||
- name: hide
|
||||
type: boolean
|
||||
```
|
||||
|
||||
The `preview` eval determines what's shown in the collapsed state of each array item.
|
||||
|
||||
### Drill-down arrays
|
||||
|
||||
For complex `object[]` data, prefer `drillDown: true` over dense inline editing. This is especially important for:
|
||||
|
||||
- nested content blocks
|
||||
- FAQs / accordions
|
||||
- team members with nested metadata
|
||||
- pricing tables / feature matrices
|
||||
|
||||
### Pagebuilder fields
|
||||
|
||||
Nova supports pagebuilder configuration at both collection and field level.
|
||||
|
||||
Typical pattern:
|
||||
|
||||
```yaml
|
||||
meta:
|
||||
pagebuilder:
|
||||
blockTypeField: type
|
||||
defaultViewport: desktop
|
||||
blockRegistry:
|
||||
file: /_/assets/dist/admin.mjs
|
||||
|
||||
fields:
|
||||
- name: blocks
|
||||
type: object[]
|
||||
meta:
|
||||
widget: pagebuilder
|
||||
pagebuilder:
|
||||
blockTypeField: type
|
||||
```
|
||||
|
||||
Use pagebuilder when editors work with heterogeneous content blocks. Use plain `object[]` only when the structure is uniform and simple.
|
||||
|
||||
### dependsOn
|
||||
|
||||
Use `dependsOn` to show only fields relevant to the selected block or mode:
|
||||
|
||||
```yaml
|
||||
- name: image
|
||||
type: file
|
||||
meta:
|
||||
dependsOn:
|
||||
eval: $parent.type == 'hero'
|
||||
```
|
||||
|
||||
This is critical for keeping pagebuilder schemas usable.
|
||||
|
||||
### AI-aware media and admin features
|
||||
|
||||
Current Nova types support AI-related admin capabilities, especially around media workflows. When appropriate for a project:
|
||||
|
||||
- use AI-assisted alt/caption generation for image-heavy collections
|
||||
- prefer explicit target fields for generated metadata
|
||||
- keep AI assist opt-in and editorially reviewable
|
||||
|
||||
Use AI only where it improves authoring quality; do not force it into every collection.
|
||||
|
||||
## Field-level permissions and authoring safety
|
||||
|
||||
Current tibi-server supports `readonlyFields`, `hiddenFields`, and eval-based field visibility/readonly rules.
|
||||
|
||||
Reflect these server rules in admin design:
|
||||
|
||||
- do not put critical computed fields front-and-center if editors may not be allowed to modify them
|
||||
- use `dependsOn`, `hidden`, and readonly semantics deliberately
|
||||
- remember that server-side permissions are authoritative even if the UI looks editable
|
||||
|
||||
### Drill-down
|
||||
|
||||
For complex nested objects, use `drillDown` to render them as a sub-page:
|
||||
|
||||
```yaml
|
||||
- name: variants
|
||||
type: object[]
|
||||
meta:
|
||||
drillDown: true # Opens as sub-page instead of inline
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Admin module (frontend/src/admin.ts)
|
||||
|
||||
The `admin.ts` file exports the **pagebuilder block registry** and optional custom Svelte components for the tibi-admin UI. This is how the admin preview renders your Svelte blocks.
|
||||
|
||||
### Pagebuilder block registry
|
||||
|
||||
The current starter uses `createContentBlockDefinition()` to register each block type. This mounts real Svelte block components into Shadow DOM for admin previews:
|
||||
|
||||
```typescript
|
||||
import { mount, unmount, type Component, type SvelteComponent } from "svelte"
|
||||
import BlockRenderer from "./blocks/BlockRenderer.svelte"
|
||||
|
||||
// Creates a block definition that renders the same Svelte component
|
||||
// used in the public frontend. The block is mounted inside Shadow DOM
|
||||
// for style isolation.
|
||||
function createContentBlockDefinition(presentation: { label: string; icon: string; color: string }) {
|
||||
return {
|
||||
css: [previewCssUrl], // CSS files to inject into Shadow DOM
|
||||
label: presentation.label,
|
||||
icon: presentation.icon,
|
||||
color: presentation.color,
|
||||
previewStyles: {
|
||||
"background-color": "white",
|
||||
},
|
||||
render(container, row, context) {
|
||||
// Mount the Svelte component inside the admin preview
|
||||
const target = document.createElement("div")
|
||||
container.appendChild(target)
|
||||
|
||||
let mountedComponent = mount(BlockRenderer as Component<any>, {
|
||||
target,
|
||||
props: { blocks: [row], isAdminPreview: true },
|
||||
})
|
||||
|
||||
return {
|
||||
update(nextRow) {
|
||||
unmount(mountedComponent)
|
||||
target.innerHTML = ""
|
||||
mountedComponent = mount(BlockRenderer as Component<any>, {
|
||||
target,
|
||||
props: { blocks: [nextRow], isAdminPreview: true },
|
||||
})
|
||||
},
|
||||
destroy() {
|
||||
unmount(mountedComponent)
|
||||
target.remove()
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const blockRegistry = {
|
||||
hero: createContentBlockDefinition({ label: "Hero", icon: "image", color: "#1d4ed8" }),
|
||||
richtext: createContentBlockDefinition({ label: "Richtext", icon: "article", color: "#7c3aed" }),
|
||||
// ... add new blocks here
|
||||
}
|
||||
|
||||
export { blockRegistry }
|
||||
```
|
||||
|
||||
**Key points:**
|
||||
|
||||
- Each registry entry wraps the Svelte `BlockRenderer` to render the block in the admin preview.
|
||||
- The `row` object is the block data (same shape as `ContentBlockEntry`).
|
||||
- Preview data may contain hydrated `_lookup.<fieldPath>` foreign key data and absolute file URLs — do not prepend `apiBase` or attempt re-fetching.
|
||||
- The `previewCssUrl` loads the project's `index.css` into Shadow DOM so block styles apply.
|
||||
- After adding blocks to the registry, run `yarn build` so `frontend/dist/admin.mjs` is regenerated.
|
||||
|
||||
### Custom Svelte components (advanced)
|
||||
|
||||
For custom dashboard widgets, preview components, or field widgets that require Svelte rendering inside the admin UI, use `getRenderedElement()`:
|
||||
|
||||
```typescript
|
||||
import type { SvelteComponent } from "svelte"
|
||||
|
||||
function getRenderedElement(
|
||||
component: typeof SvelteComponent,
|
||||
options?: { props: { [key: string]: any }; addCss?: string[] },
|
||||
nestedElements?: { tagName: string; className?: string }[]
|
||||
) {
|
||||
// Creates a Shadow DOM container, mounts the Svelte component inside
|
||||
}
|
||||
|
||||
export { getRenderedElement }
|
||||
```
|
||||
|
||||
### Build
|
||||
|
||||
Run `yarn build`. The admin module (`frontend/src/admin.ts`) is compiled into `frontend/dist/admin.mjs` as part of the esbuild build pipeline (the same build produces both `index.mjs` for the SPA and `admin.mjs` for the admin module). tibi-admin-nova loads this module from the project's asset path (`/_/assets/dist/admin.mjs`). The `ADMIN_ASSET_VERSION` from `config.yml.env` is appended as a query parameter for cache busting: `admin.mjs?v=${ADMIN_ASSET_VERSION}`.
|
||||
|
||||
---
|
||||
|
||||
## Complete collection example
|
||||
|
||||
```yaml
|
||||
name: products
|
||||
meta:
|
||||
label: { de: "Produkte", en: "Products" }
|
||||
muiIcon: inventory_2
|
||||
group: shop
|
||||
viewHint: table
|
||||
defaultSort:
|
||||
field: insertTime
|
||||
order: DESC
|
||||
preview:
|
||||
label: name
|
||||
secondary: sku
|
||||
badge: active
|
||||
table:
|
||||
- name
|
||||
- sku
|
||||
- source: price
|
||||
label: { de: "Preis", en: "Price" }
|
||||
- source: category
|
||||
label: { de: "Kategorie", en: "Category" }
|
||||
sidebar:
|
||||
- group: publishing
|
||||
label: { de: "Veröffentlichung", en: "Publishing" }
|
||||
- group: seo
|
||||
label: { de: "SEO", en: "SEO" }
|
||||
|
||||
permissions:
|
||||
public:
|
||||
methods:
|
||||
get: true
|
||||
user:
|
||||
methods:
|
||||
get: true
|
||||
post: true
|
||||
put: true
|
||||
delete: false # usually false for real editorial workflows
|
||||
|
||||
fields:
|
||||
- name: active
|
||||
type: boolean
|
||||
meta:
|
||||
label: { de: "Aktiv", en: "Active" }
|
||||
position: "sidebar:publishing"
|
||||
- name: name
|
||||
type: string
|
||||
meta:
|
||||
label: { de: "Name", en: "Name" }
|
||||
- name: sku
|
||||
type: string
|
||||
meta:
|
||||
label: { de: "Artikelnummer", en: "SKU" }
|
||||
containerProps:
|
||||
layout:
|
||||
size: col-6
|
||||
- name: price
|
||||
type: number
|
||||
meta:
|
||||
label: { de: "Preis", en: "Price" }
|
||||
inputProps:
|
||||
min: 0
|
||||
step: 0.01
|
||||
containerProps:
|
||||
layout:
|
||||
size: col-6
|
||||
- name: category
|
||||
type: string
|
||||
meta:
|
||||
label: { de: "Kategorie", en: "Category" }
|
||||
choices:
|
||||
- id: electronics
|
||||
name: { de: "Elektronik", en: "Electronics" }
|
||||
- id: clothing
|
||||
name: { de: "Kleidung", en: "Clothing" }
|
||||
- name: description
|
||||
type: string
|
||||
meta:
|
||||
label: { de: "Beschreibung", en: "Description" }
|
||||
inputProps:
|
||||
multiline: true
|
||||
rows: 4
|
||||
- name: image
|
||||
type: file
|
||||
meta:
|
||||
label: { de: "Produktbild", en: "Product Image" }
|
||||
widget: image
|
||||
downscale:
|
||||
maxWidth: 1200
|
||||
quality: 0.85
|
||||
- name: seoTitle
|
||||
type: string
|
||||
meta:
|
||||
label: { de: "SEO Titel", en: "SEO Title" }
|
||||
position: "sidebar:seo"
|
||||
- name: seoDescription
|
||||
type: string
|
||||
meta:
|
||||
label: { de: "SEO Beschreibung", en: "SEO Description" }
|
||||
position: "sidebar:seo"
|
||||
inputProps:
|
||||
multiline: true
|
||||
rows: 3
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Indexes and search
|
||||
|
||||
For production collections with many entries, consider adding indexes in the YAML:
|
||||
|
||||
```yaml
|
||||
name: products
|
||||
indexes:
|
||||
- name: price_sort
|
||||
key: [price]
|
||||
- name: category_active
|
||||
key: [category, -active] # -prefix for descending
|
||||
- name: slug_unique
|
||||
key: [slug]
|
||||
unique: true
|
||||
```
|
||||
|
||||
Search configurations can be added for advanced text/vector search:
|
||||
|
||||
```yaml
|
||||
search:
|
||||
- name: default
|
||||
mode: text
|
||||
fields: [name, description]
|
||||
```
|
||||
|
||||
See `tibi-server/docs/04-collections.md` (sections on indexes and search config) for full reference.
|
||||
|
||||
## Checklist-facing verification
|
||||
|
||||
For a real project, do not stop after writing the YAML. Validate the authoring contract explicitly.
|
||||
|
||||
Minimum review points:
|
||||
|
||||
1. project-level `meta.i18n` and `meta.collectionGroups` are coherent
|
||||
2. each collection has a readable `meta.preview`
|
||||
3. list views show meaningful columns instead of raw IDs or empty rows
|
||||
4. foreign references render with readable previews
|
||||
5. sidebars and `containerProps.layout` produce usable forms
|
||||
6. pagebuilder collections expose both a working chooser and working preview path
|
||||
|
||||
Committed admin Playwright coverage is preferred for stable contracts that should not regress.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **`meta.label` supports both strings and i18n objects** — Use i18n objects only when the collection or field label must be localized.
|
||||
- **Project-level admin config is easy to forget** — `collectionGroups` and project-level `meta.i18n` live in `api/config.yml`, not in individual collection files.
|
||||
- **`meta.group` without a matching project group** — The collection still exists, but the sidebar grouping model becomes inconsistent.
|
||||
- **`choices.id` must match stored value** — The `id` in choices is what gets saved to the database.
|
||||
- **`inputProps` depends on widget** — Not all props work with all widgets. Check tibi-admin-nova source if unsure.
|
||||
- **`position: sidebar` without group** — Fields go to an ungrouped area. Use `position: "sidebar:GroupName"` for grouping.
|
||||
- **`type: object[]` needs `subFields`** — Forgetting `subFields` renders an empty repeater.
|
||||
@@ -0,0 +1,206 @@
|
||||
---
|
||||
name: audit-and-compliance
|
||||
description: Configure and verify audit logging for tibi website projects. Covers server-level audit config, collection audit actions, audit.return filtering, TTL/retention, source semantics, and what later agents must check before relying on audit trails.
|
||||
---
|
||||
|
||||
# audit-and-compliance
|
||||
|
||||
## When to use this skill
|
||||
|
||||
Use this skill when:
|
||||
|
||||
- a project needs auditable create/update/delete activity
|
||||
- operators or stakeholders need a trace of who changed content and how
|
||||
- hooks, jobs, or actions mutate important collections and that history matters
|
||||
- sensitive data could land in audit snapshots and must be filtered safely
|
||||
|
||||
## Goal
|
||||
|
||||
Give later agents a concrete workflow for deciding, configuring, and validating audit logging on this stack.
|
||||
|
||||
Audit is not just “turn it on”. A usable audit setup must answer:
|
||||
|
||||
- what is logged
|
||||
- how long it is kept
|
||||
- who can read it
|
||||
- which sensitive fields must be stripped
|
||||
- how hook/job/action side effects appear in the audit trail
|
||||
|
||||
## Source of truth
|
||||
|
||||
Use these sources when implementing or reviewing audit behavior:
|
||||
|
||||
- `tibi-server/docs/10-audit.md`
|
||||
- `tibi-server/docs/06-hooks.md`
|
||||
- `tibi-server/docs/11-jobs.md`
|
||||
- `tibi-server/docs/19-actions.md`
|
||||
- active server/project config
|
||||
|
||||
## Core audit model
|
||||
|
||||
Audit requires an explicit server-level decision:
|
||||
|
||||
```yaml
|
||||
audit:
|
||||
enabled: true
|
||||
defaultTTL: "720h"
|
||||
defaultLimit: 50
|
||||
maxLimit: 10000
|
||||
```
|
||||
|
||||
At collection level, audit is controlled separately:
|
||||
|
||||
```yaml
|
||||
name: content
|
||||
audit:
|
||||
enabled: true
|
||||
actions:
|
||||
- create
|
||||
- update
|
||||
- delete
|
||||
```
|
||||
|
||||
Default audited collection actions are `create`, `update`, and `delete`.
|
||||
|
||||
Important:
|
||||
|
||||
- `get` can also be audited when needed
|
||||
- system actions such as `login`, `reload`, and `shutdown` are controlled by server-level audit enablement
|
||||
- bulk API mutations are still audited even though the internal action naming differs (`bulkCreate`, `bulkUpdate`, `bulkDelete`)
|
||||
|
||||
## Source semantics matter
|
||||
|
||||
Audit entries are not all equal. The `source.type` tells you where the mutation came from:
|
||||
|
||||
- `controller` — direct API CRUD request
|
||||
- `hook` — database mutation performed from a hook
|
||||
- `job` — database mutation performed from a scheduled job
|
||||
- `system` — internal system operation
|
||||
|
||||
For website projects this matters because content can change through:
|
||||
|
||||
- direct editor CRUD
|
||||
- action-triggered persistence
|
||||
- hook-side side effects
|
||||
- cleanup or synchronization jobs
|
||||
|
||||
If a project relies on audit trails for governance or debugging, later agents must understand these source types instead of assuming every change came from an editor UI save.
|
||||
|
||||
## Authentication context in audit entries
|
||||
|
||||
Audit also records how a request was authenticated.
|
||||
|
||||
Relevant fields include:
|
||||
|
||||
- `authMethod`
|
||||
- `tokenLabel`
|
||||
- `tokenPrefix`
|
||||
- `userId`
|
||||
- `username`
|
||||
- `ip`
|
||||
|
||||
Practical implication:
|
||||
|
||||
- use labeled admin tokens for operator workflows so audit output stays readable
|
||||
- do not treat all token-based writes as anonymous noise
|
||||
|
||||
## Snapshot exposure and `audit.return`
|
||||
|
||||
Audit snapshots can contain more than a normal API read would expose.
|
||||
|
||||
That means fields that are stripped by normal read hooks can still appear in audit snapshots unless you filter them explicitly.
|
||||
|
||||
Use `audit.return` hooks when a collection may contain:
|
||||
|
||||
- passwords
|
||||
- API keys
|
||||
- internal secrets
|
||||
- sensitive operator-only notes
|
||||
|
||||
Example shape:
|
||||
|
||||
```yaml
|
||||
hooks:
|
||||
audit:
|
||||
return:
|
||||
type: javascript
|
||||
file: hooks/users/audit_return.js
|
||||
```
|
||||
|
||||
Use this hook to delete or suppress sensitive `snapshot` and `changes` data before the audit response is returned.
|
||||
|
||||
## Retention and limits
|
||||
|
||||
Audit is an operator/compliance concern, not just a developer concern.
|
||||
|
||||
Decide deliberately:
|
||||
|
||||
- whether audit is enabled at all
|
||||
- how long audit logs should be retained (`defaultTTL`)
|
||||
- what read limits make sense (`defaultLimit`, `maxLimit`)
|
||||
- whether the target environment expects short-term diagnostics or longer retention
|
||||
|
||||
Do not enable audit and leave retention undefined by habit.
|
||||
|
||||
## Recommended patterns for website projects
|
||||
|
||||
### Content governance
|
||||
|
||||
Recommended shape:
|
||||
|
||||
- audit enabled for collections whose changes affect public output or editorial accountability
|
||||
- clear retention decision with operations
|
||||
- readable token labels for automation or deploy-related writes
|
||||
|
||||
### Hook-heavy workflows
|
||||
|
||||
Recommended shape:
|
||||
|
||||
- understand which hook-side writes will appear as `source.type: "hook"`
|
||||
- do not assume audit trails point only to controller actions
|
||||
- document important side effects when hooks fan out into multiple writes
|
||||
|
||||
### Jobs and automation
|
||||
|
||||
Recommended shape:
|
||||
|
||||
- know that job-side DB mutations appear as `source.type: "job"`
|
||||
- if job behavior matters operationally, make that visible in the job design and ops notes
|
||||
|
||||
### Sensitive collections
|
||||
|
||||
Recommended shape:
|
||||
|
||||
- explicit `audit.return` filtering
|
||||
- do not expose raw snapshots just because admins technically can read the endpoint
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- enabling audit without deciding retention
|
||||
- treating audit as equivalent to normal collection reads
|
||||
- forgetting that hooks and jobs create their own audit source types
|
||||
- relying on unlabeled tokens for important automated writes
|
||||
- storing sensitive data in snapshots without `audit.return` filtering
|
||||
|
||||
## Verification checklist
|
||||
|
||||
After audit-related changes, verify all of these:
|
||||
|
||||
1. server-level audit settings are deliberate
|
||||
2. collection-level audited actions match the actual governance need
|
||||
3. a representative write produces the expected audit entry
|
||||
4. hook/job/action side effects produce understandable source metadata
|
||||
5. sensitive fields are filtered from audit output where required
|
||||
6. read visibility and retention expectations are documented
|
||||
|
||||
## What an LLM should inspect first
|
||||
|
||||
When asked to design or review audit behavior on this starter, inspect in this order:
|
||||
|
||||
1. `tibi-server/docs/10-audit.md`
|
||||
2. active server audit config
|
||||
3. collection-level `audit:` sections
|
||||
4. any hook/job/action workflow that mutates important collections
|
||||
5. whether `audit.return` filtering is needed
|
||||
|
||||
This prevents “audit enabled” setups that are technically on but operationally weak.
|
||||
@@ -0,0 +1,560 @@
|
||||
---
|
||||
name: content-authoring
|
||||
description: Add new pages, content blocks, and collections to a tibi project. Covers the content-based routing model, block registration in BlockRenderer and frontend/src/admin.ts, lookup-aware reference modeling, collection YAML authoring, and TypeScript type ownership. Use when creating new pages, block types, or collections.
|
||||
---
|
||||
|
||||
# content-authoring
|
||||
|
||||
## When to use this skill
|
||||
|
||||
Use this skill when:
|
||||
|
||||
- Adding a new page to the website
|
||||
- Creating a new content block type (e.g. testimonials, pricing table, gallery)
|
||||
- Adding a new collection to the CMS (e.g. products, events, team members)
|
||||
- Understanding how content is structured and rendered
|
||||
|
||||
## Key concept: content-based routing
|
||||
|
||||
This project does **NOT** use file-based routing (no SvelteKit router). Instead:
|
||||
|
||||
1. Pages are **CMS entries** in the `content` collection with a `path` field.
|
||||
2. Public URLs are typically language-prefixed (`/de/...`, `/en/...`), but the DB entry in `content.path` is stored **without** that language prefix.
|
||||
3. `App.svelte` reacts to URL changes → strips the language prefix → calls `getCachedEntries("content", { lang, path, active: true })`.
|
||||
4. The same loading path is used for browser navigation and SSR.
|
||||
5. The matching `ContentEntry.blocks[]` array is passed to `BlockRenderer.svelte`.
|
||||
6. Each block has a `type` field that maps to a Svelte component.
|
||||
|
||||
**Implication:** To add a new page, you create a content entry (via Admin UI or API) — no new Svelte file or route config is needed.
|
||||
|
||||
**Important:** When adding new page types, inspect both the frontend route/i18n layer and `api/hooks/config.js` (SSR route validation). A page can exist in the DB and still fail under SSR if the public URL shape and `content.path` mapping are not aligned.
|
||||
|
||||
## Cross-surface ownership rule
|
||||
|
||||
For real project work, treat content authoring as a multi-surface contract.
|
||||
|
||||
When you add or change blocks, pages, or collections, check these surfaces together:
|
||||
|
||||
1. collection YAML in `api/collections/*.yml`
|
||||
2. type ownership in `types/global.d.ts`
|
||||
3. typed API mapping in `frontend/src/lib/api.ts` via `EntryTypeSwitch`
|
||||
4. public rendering in `frontend/src/blocks/BlockRenderer.svelte`
|
||||
5. admin pagebuilder preview in `frontend/src/admin.ts`
|
||||
|
||||
If one of these surfaces is skipped, the project often still looks half-correct until SSR, admin preview, or typed API usage exposes the mismatch.
|
||||
|
||||
---
|
||||
|
||||
## Adding a new page
|
||||
|
||||
### Option A: Via Admin UI (preferred for content editors)
|
||||
|
||||
1. Open the Nova admin at `https://{PROJECT_NAME}-tibiadmin.code.testversion.online/`.
|
||||
2. Navigate to **Inhalte** (Content) collection.
|
||||
3. Click **New** and fill in:
|
||||
- `name`: Display name (e.g. "Über uns")
|
||||
- `path`: URL path without language prefix (e.g. `/ueber-uns`)
|
||||
- `lang`: Language code (e.g. `de`)
|
||||
- `active`: `true`
|
||||
- `translationKey`: Shared key for cross-language linking (e.g. `about`)
|
||||
- `blocks`: Add content blocks (see below)
|
||||
- `meta.title` / `meta.description`: SEO metadata
|
||||
4. Save. The page is immediately available at `/{lang}{path}`.
|
||||
|
||||
**Nova authoring guidance:**
|
||||
|
||||
- Prefer meaningful `meta.preview` and field `preview` configs so entries and nested blocks are understandable in breadcrumbs, foreign-key widgets, and arrays.
|
||||
- Use `containerProps.layout.size` to keep editors on one screen instead of stacking every field vertically.
|
||||
- Use `dependsOn` to hide block-specific fields until the relevant block type is selected.
|
||||
- Prefer drill-down editing for larger `object[]` structures instead of flat, folded arrays.
|
||||
|
||||
### Option B: Via API
|
||||
|
||||
```sh
|
||||
curl -X POST "$CODING_URL/api/content" \
|
||||
-H "Token: $ADMIN_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"active": true,
|
||||
"lang": "de",
|
||||
"name": "Über uns",
|
||||
"path": "/ueber-uns",
|
||||
"translationKey": "about",
|
||||
"blocks": [
|
||||
{ "type": "hero", "headline": "Über uns", "subline": "Unser Team" }
|
||||
],
|
||||
"meta": { "title": "Über uns", "description": "Erfahre mehr über unser Team." }
|
||||
}'
|
||||
```
|
||||
|
||||
### Option C: Via mock data (for MOCK=1 mode)
|
||||
|
||||
Add the entry to `frontend/mocking/content.json` — the mock engine supports MongoDB-style filtering.
|
||||
|
||||
### Adding to navigation
|
||||
|
||||
To make the page appear in the header/footer menu, edit the corresponding `navigation` entry:
|
||||
|
||||
```sh
|
||||
# Get existing header nav
|
||||
curl "$CODING_URL/api/navigation?filter[type]=header&filter[language]=de" -H "Token: $ADMIN_TOKEN"
|
||||
|
||||
# Look up the content entry ID for your page
|
||||
curl "$CODING_URL/api/content?filter[path]=/ueber-uns&filter[lang]=de" -H "Token: $ADMIN_TOKEN"
|
||||
|
||||
# PUT to update elements array (add your page by FK id)
|
||||
curl -X PUT "$CODING_URL/api/navigation/<id>" \
|
||||
-H "Token: $ADMIN_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{ "elements": [ ...existing, { "name": "Über uns", "page": "<content-id>" } ] }'
|
||||
```
|
||||
|
||||
If navigation drives the public website shell, treat navigation as page-critical SSR data. A page is not fully SSR-ready if only the main content entry exists but header/footer navigation is missing.
|
||||
|
||||
### Multi-language pages
|
||||
|
||||
- Create one `ContentEntry` per language with the **same `translationKey`** but different `lang` and `path`.
|
||||
- The language switcher is path-based and derives the target URL from the current route plus `ROUTE_TRANSLATIONS`.
|
||||
- Add localized route slugs to `ROUTE_TRANSLATIONS` in `frontend/src/lib/i18n.ts` if URLs should differ per language (e.g. `/ueber-uns` vs `/about`).
|
||||
|
||||
---
|
||||
|
||||
## Adding a new content block type
|
||||
|
||||
### Step 1: Create the Svelte component
|
||||
|
||||
Create `frontend/src/blocks/MyNewBlock.svelte`:
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
let { block }: { block: ContentBlockEntry } = $props()
|
||||
</script>
|
||||
|
||||
<section class="py-16 sm:py-24" id={block.anchorId || undefined}>
|
||||
<div class="max-w-6xl mx-auto px-6">
|
||||
{#if block.headline}
|
||||
<h2 class="text-3xl font-bold mb-6">{block.headline}</h2>
|
||||
{/if}
|
||||
<!-- Block-specific content here -->
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
**Conventions:**
|
||||
|
||||
- Accept `block: ContentBlockEntry` as the single prop.
|
||||
- Use `block.anchorId` for scroll anchoring.
|
||||
- Respect `block.containerWidth` (`""` = default, `"wide"`, `"full"`).
|
||||
- Guard browser-only code with `typeof window !== "undefined"` (SSR safety).
|
||||
|
||||
### Step 2: Register in BlockRenderer
|
||||
|
||||
Edit `frontend/src/blocks/BlockRenderer.svelte`:
|
||||
|
||||
```svelte
|
||||
<!-- Add import at the top -->
|
||||
import MyNewBlock from "./MyNewBlock.svelte"
|
||||
|
||||
<!-- Add case in the {#each} block -->
|
||||
{:else if block.type === "my-new-block"}
|
||||
<MyNewBlock {block} />
|
||||
```
|
||||
|
||||
If block types become numerous, plan for grouping and registry discipline early. A real website built on this starter should not keep extending a demo-style renderer forever without structure.
|
||||
|
||||
### Step 3: Register in the admin block registry
|
||||
|
||||
If the block is authored through a pagebuilder field, also register it in `frontend/src/admin.ts`.
|
||||
|
||||
Example:
|
||||
|
||||
```ts
|
||||
const blockRegistry = {
|
||||
hero: createContentBlockDefinition({ label: "Hero", icon: "image", color: "#1d4ed8" }),
|
||||
"my-new-block": createContentBlockDefinition({
|
||||
label: "My New Block",
|
||||
icon: "view_compact",
|
||||
color: "#0f766e",
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
Important:
|
||||
|
||||
- `BlockRenderer.svelte` controls public rendering
|
||||
- `frontend/src/admin.ts` controls Nova pagebuilder preview availability
|
||||
- both should point at the same block contract instead of drifting into separate preview-only logic
|
||||
|
||||
### Step 4: Extend TypeScript types (if new fields are needed)
|
||||
|
||||
Edit `types/global.d.ts` — add fields to `ContentBlockEntry`:
|
||||
|
||||
```typescript
|
||||
interface ContentBlockEntry {
|
||||
// ... existing fields ...
|
||||
// my-new-block fields
|
||||
myCustomField?: string
|
||||
myItems?: { title: string; description: string }[]
|
||||
}
|
||||
```
|
||||
|
||||
If the change also introduces a new collection or new API usage surface, update the corresponding entry interfaces in the same change instead of leaving `Record<string, unknown>` as a long-term placeholder.
|
||||
|
||||
### Step 5: Extend collection YAML (if new fields need admin editing)
|
||||
|
||||
Edit `api/collections/content.yml` — add subFields under `blocks`:
|
||||
|
||||
```yaml
|
||||
- name: blocks
|
||||
type: object[]
|
||||
subFields:
|
||||
# ... existing subFields ...
|
||||
- name: myCustomField
|
||||
type: string
|
||||
- name: myItems
|
||||
type: object[]
|
||||
meta:
|
||||
drillDown: true
|
||||
preview: title
|
||||
subFields:
|
||||
- name: title
|
||||
type: string
|
||||
- name: description
|
||||
type: string
|
||||
```
|
||||
|
||||
Use current Nova patterns when extending block schemas:
|
||||
|
||||
- `meta.preview` for entry and block previews
|
||||
- `meta.drillDown: true` for nested arrays that would otherwise become hard to edit
|
||||
- `containerProps.layout.size` for dense editor layouts
|
||||
- `dependsOn` for block-type-specific fields
|
||||
- collection- or field-level `meta.pagebuilder` for registry/default viewport settings
|
||||
|
||||
When blocks contain foreign references such as medialib images, model the reference path deliberately so later loaders can request the needed `lookup` data.
|
||||
|
||||
### Step 6: Update mock data (if using MOCK=1)
|
||||
|
||||
Add a block with your new type to `frontend/mocking/content.json`.
|
||||
|
||||
### Step 7: Verify
|
||||
|
||||
```sh
|
||||
yarn validate # TypeScript check — must be warning-free
|
||||
```
|
||||
|
||||
For blocks that appear on SSR pages, also verify:
|
||||
|
||||
```sh
|
||||
yarn build:server
|
||||
# then request the SSR endpoint directly and check that the block content appears in HTML
|
||||
```
|
||||
|
||||
For blocks that are authored in pagebuilder and use images or foreign references, also verify:
|
||||
|
||||
- the block appears in the admin chooser
|
||||
- the preview renders in Nova
|
||||
- image/reference data is present through the intended lookup path
|
||||
|
||||
### Existing block types for reference
|
||||
|
||||
| Type | Component | Purpose |
|
||||
| -------------- | ------------------------- | ----------------------------------------- |
|
||||
| `hero` | `HeroBlock.svelte` | Full-width hero with image, headline, CTA |
|
||||
| `richtext` | `RichtextBlock.svelte` | Rich text with optional image |
|
||||
| `accordion` | `AccordionBlock.svelte` | Expandable FAQ/accordion items |
|
||||
| `contact-form` | `ContactFormBlock.svelte` | Contact form |
|
||||
|
||||
---
|
||||
|
||||
## Adding a new collection
|
||||
|
||||
### Step 1: Create collection YAML
|
||||
|
||||
Create `api/collections/mycollection.yml`. Use `content.yml`, `navigation.yml`, or a current `tibi-admin-nova` example config as a template:
|
||||
|
||||
```yaml
|
||||
########################################################################
|
||||
# MyCollection — description of what this collection stores
|
||||
########################################################################
|
||||
|
||||
name: mycollection
|
||||
meta:
|
||||
label: { de: "Meine Sammlung", en: "My Collection" }
|
||||
muiIcon: category # Material UI icon name
|
||||
viewHint: table
|
||||
preview:
|
||||
label: name
|
||||
table:
|
||||
- name
|
||||
- source: active
|
||||
label: Active
|
||||
|
||||
permissions:
|
||||
public:
|
||||
methods:
|
||||
get: true # Public read access
|
||||
user:
|
||||
methods:
|
||||
get: true
|
||||
post: true
|
||||
put: true
|
||||
delete: true
|
||||
|
||||
fields:
|
||||
- name: active
|
||||
type: boolean
|
||||
meta:
|
||||
label: { de: "Aktiv", en: "Active" }
|
||||
- name: name
|
||||
type: string
|
||||
meta:
|
||||
label: { de: "Name", en: "Name" }
|
||||
# Add more fields as needed
|
||||
```
|
||||
|
||||
Use current Nova config:
|
||||
|
||||
- `preview` for row/foreign/search display
|
||||
- object-form `singleton`
|
||||
- `sidebar` groups instead of ad hoc sidebars
|
||||
- `pagebuilder` defaults when a collection contains pagebuilder fields
|
||||
- `viewHint` plus `preview.table` for better admin ergonomics
|
||||
|
||||
**Field types:** `string`, `number`, `boolean`, `object`, `object[]`, `string[]`, `file`, `file[]`.
|
||||
|
||||
For the full schema reference: `tibi-types/schemas/config/collection.schema.json`.
|
||||
|
||||
### Step 2: Include in config.yml
|
||||
|
||||
Edit `api/config.yml`:
|
||||
|
||||
```yaml
|
||||
collections:
|
||||
- !include collections/content.yml
|
||||
- !include collections/navigation.yml
|
||||
- !include collections/ssr.yml
|
||||
- !include collections/mycollection.yml # ← add this line
|
||||
```
|
||||
|
||||
### Step 3: Add TypeScript types
|
||||
|
||||
Edit `types/global.d.ts`:
|
||||
|
||||
```typescript
|
||||
interface MyCollectionEntry {
|
||||
id?: string
|
||||
active?: boolean
|
||||
name?: string
|
||||
// ... fields matching your YAML
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Configure API layer (optional)
|
||||
|
||||
If you need typed helpers, extend the `EntryTypeSwitch` in `frontend/src/lib/api.ts`:
|
||||
|
||||
```typescript
|
||||
type CollectionNameT = "medialib" | "content" | "navigation" | "mycollection" | string
|
||||
|
||||
type EntryTypeSwitch<T extends string> = T extends "medialib"
|
||||
? MedialibEntry
|
||||
: T extends "content"
|
||||
? ContentEntry
|
||||
: T extends "navigation"
|
||||
? NavigationEntry
|
||||
: T extends "mycollection"
|
||||
? MyCollectionEntry
|
||||
: Record<string, unknown>
|
||||
```
|
||||
|
||||
Do not treat `EntryTypeSwitch` as optional cleanup. If the frontend or tests consume the collection in a typed way, update this mapping in the same change.
|
||||
|
||||
### Step 5: Add hooks (optional)
|
||||
|
||||
Common hook patterns:
|
||||
|
||||
- **Public filter** — reuse `filter_public.js` to enforce `active: true` for unauthenticated users.
|
||||
- **Write validation** — add method/step hook files such as `api/hooks/mycollection/post_validate.js` or `api/hooks/mycollection/put_validate.js`.
|
||||
- **Cache invalidation** — add your collection to `api/hooks/clear_cache.js` if it affects rendered pages.
|
||||
- **Action endpoints** — prefer `actions:` instead of fake collections when you need forms, newsletters, calculators, imports, or other endpoint-like behavior without CRUD storage.
|
||||
|
||||
Reference hook in YAML:
|
||||
|
||||
```yaml
|
||||
hooks:
|
||||
get:
|
||||
read:
|
||||
type: javascript
|
||||
file: hooks/filter_public.js
|
||||
put:
|
||||
update:
|
||||
type: javascript
|
||||
file: hooks/clear_cache.js
|
||||
post:
|
||||
create:
|
||||
type: javascript
|
||||
file: hooks/clear_cache.js
|
||||
delete:
|
||||
delete:
|
||||
type: javascript
|
||||
file: hooks/clear_cache.js
|
||||
```
|
||||
|
||||
### Step 6: Add mock data (if using MOCK=1)
|
||||
|
||||
Create `frontend/mocking/mycollection.json`:
|
||||
|
||||
```json
|
||||
[{ "_id": "1", "active": true, "name": "Example Entry" }]
|
||||
```
|
||||
|
||||
### Step 7: Verify
|
||||
|
||||
```sh
|
||||
yarn validate # TypeScript check
|
||||
# If Docker is running, the tibi-server auto-reloads the collection config
|
||||
```
|
||||
|
||||
For collections intended for rich editorial usage, also verify in Nova:
|
||||
|
||||
- list/table/card previews are readable
|
||||
- nested arrays are editable with drill-down where needed
|
||||
- sidebar groups and layout are usable without scrolling through one long form
|
||||
- foreign-key displays use meaningful previews
|
||||
- pagebuilder fields render previews and screenshots correctly
|
||||
|
||||
If the collection feeds public pages or admin block previews, also verify that the typed API helpers and runtime components agree on the same data shape.
|
||||
|
||||
---
|
||||
|
||||
## Collection Validators
|
||||
|
||||
Validatoren definieren Sicherheitsregeln und Typ-Constraints, indem sie als `validator`-Key innerhalb der `fields`-Definitionen einer Collection-YAML (`api/collections/*.yml`) konfiguriert werden.
|
||||
|
||||
**Unterschied Client- vs. Serverseitige Validatoren:**
|
||||
|
||||
- **Serverseite (`tibi-server`)**: Validatoren werden zentral im Go-Backend bei jedem Datensatz-Schreibvorgang (`POST` / `PUT`) ausgeführt (nach den `validate`-Hooks). Wenn Daten nicht den Constraints entsprechen, erfolgt ein Abbruch (`400 Bad Request`).
|
||||
- **Clientseite (`tibi-admin-nova`)**: Das CMS-Admin-Interface liest diese Validator-Regeln automatisch über das OpenAPI-Schema ein und wendet sie instant als Client-Side-Validierung in den Formularen an (Rote Markierungen und Check vor dem eigentlichen API-Call). **Validatoren müssen daher nur 1x zentral in der YAML definiert werden.**
|
||||
|
||||
**Häufige Validator-Optionen je Feldtyp:**
|
||||
|
||||
- **Generell**:
|
||||
- `required: true` (Zwingendes Pflichtfeld)
|
||||
- `allowZero: true` (Erlaubt die explizite Eingabe von `""` oder `0`, selbst wenn `required: true` aktiv ist)
|
||||
- `in: ["wert1", "wert2"]` (Nur dieser exakte Pool an primitiven Werten ist erlaubt)
|
||||
- `eval: "$this.length >= 3 && $this.length <= 100"` (Serverseitige Javascript-Evaluation für Custom-Logik)
|
||||
- **Einfache Texte (`string`)**:
|
||||
- `minLength: X` und `maxLength: Y`
|
||||
- `pattern: "^[a-zA-Z0-9]+$"` (Prüft Regex-Match des kompletten Werts)
|
||||
- `format: email` (oder `url`, `uuid`, `slug` für eingebaute Regex-Prüfungen)
|
||||
- **Zahlen (`number`, `float`)**:
|
||||
- `min: X` und `max: Y`
|
||||
- **Datum/Zeit (`date`, `datetime`, `time`)**:
|
||||
- `minDate: "YYYY-MM-DD"` und `maxDate: "YYYY-MM-DD"` (Zulässige Zeitgrenzen)
|
||||
- **Listen/Arrays (`string[]`, `object[]`)**:
|
||||
- `minItems: X` und `maxItems: Y`
|
||||
- **Dateien/Bilder (`file`, `file[]`)**:
|
||||
- `maxFileSize: "50MB"` (und `minFileSize`)
|
||||
- `accept: ["image/png", "image/webp"]` (Erlaubte MIME-Types)
|
||||
- Constraints für Bildabmessungen konfigurierbar via Sub-Objekt:
|
||||
```yaml
|
||||
image:
|
||||
minWidth: 800
|
||||
maxWidth: 2400
|
||||
minHeight: 600
|
||||
maxHeight: 1800
|
||||
```
|
||||
|
||||
**Beispiel für die Einbindung in einer Collection:**
|
||||
|
||||
```yaml
|
||||
fields:
|
||||
- name: internalName
|
||||
type: string
|
||||
validator:
|
||||
required: true
|
||||
maxLength: 100
|
||||
meta:
|
||||
label: { de: "Interner Name", en: "Internal Name" }
|
||||
|
||||
- name: externalLink
|
||||
type: string
|
||||
validator:
|
||||
format: url
|
||||
meta:
|
||||
label: Externe URL
|
||||
|
||||
- name: document
|
||||
type: file
|
||||
validator:
|
||||
maxFileSize: "20MB"
|
||||
accept: ["application/pdf"]
|
||||
```
|
||||
|
||||
## Seed data pattern (Playwright)
|
||||
|
||||
Test seed data uses `_testdata: true` as a hidden marker field. **Real content must NEVER use this flag** — otherwise test teardown will delete it.
|
||||
|
||||
```yaml
|
||||
# Last field in every collection schema
|
||||
- name: _testdata
|
||||
type: boolean
|
||||
meta:
|
||||
hide: true
|
||||
```
|
||||
|
||||
Test setup:
|
||||
|
||||
1. `globalSetup` removes entries with `_testdata: true`, then creates new test entries
|
||||
2. `globalTeardown` removes entries with `_testdata: true`
|
||||
3. Real editorial content has no `_testdata` field → survives all test runs
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Path format**: Content paths do NOT include the language prefix. The path `/ueber-uns` becomes `/{lang}/ueber-uns` via the i18n layer.
|
||||
- **Active flag**: Pages with `active: false` are filtered out by `filter_public.js` for public users. The admin can still see them.
|
||||
- **Block `hide` field**: Blocks with `hide: true` are skipped by `BlockRenderer.svelte` — useful for draft blocks.
|
||||
- **Collection YAML indentation**: YAML uses 2-space indentation. Sub-fields under `object[]` require a `subFields` key.
|
||||
- **After adding a collection**: The tibi-server auto-reloads hooks on file change, but a new collection in `config.yml` may require `make docker-restart-frontend` or a full `make docker-up`.
|
||||
- **Do not fake forms as collections** if they are really endpoint logic. Use `actions:` when no CRUD collection is needed.
|
||||
- **Do not overfit to demo blocks**. Real projects should shape block schemas and admin ergonomics around actual editor workflows.
|
||||
|
||||
## API lookup für aufgelöste Referenzen
|
||||
|
||||
Beim Laden von Collections können Fremdschlüssel via `lookup`-Parameter automatisch aufgelöst werden. Der `lookup`-Parameter wird einfach im Optionen-Objekt an `getCachedEntries` übergeben:
|
||||
|
||||
```ts
|
||||
const products = await getCachedEntries<"machines">("machines", {
|
||||
filter: { active: true, category: catId },
|
||||
sort: "sortOrder",
|
||||
lookup: "images:medialib", // lookup: "feld:collection"
|
||||
})
|
||||
```
|
||||
|
||||
Das Format ist `"feldname:zielcollection"` (z.B. `"images:medialib"`). Die aufgelösten Daten landen in `entry._lookup.feldname` als Array der Ziel-Collection-Objekte. Ohne lookup bleiben `string[]`-Felder reine ID-Arrays.
|
||||
|
||||
Wichtig: der `lookup`-Parameter muss auch in `getDBEntries` und `apiRequest` durchgereicht werden (siehe `api.ts`).
|
||||
|
||||
Für blockbasierte Inhalte ist der Lookup-Pfad oft verschachtelt, nicht flach. Beispiel:
|
||||
|
||||
```ts
|
||||
const entries = await getCachedEntries<"content">(
|
||||
"content",
|
||||
{ active: true, path: "/preview-page" },
|
||||
"sort",
|
||||
undefined,
|
||||
1,
|
||||
undefined,
|
||||
undefined,
|
||||
"blocks.heroImage.image:medialib"
|
||||
)
|
||||
```
|
||||
|
||||
Merke:
|
||||
|
||||
- flache Relationen nutzen Pfade wie `images:medialib`
|
||||
- block- oder objektverschachtelte Relationen nutzen Dot-Paths wie `blocks.heroImage.image:medialib`
|
||||
- ohne den passenden Lookup fehlen Admin-Preview, SSR oder Frontend-Rendern oft erst zur Laufzeit
|
||||
|
||||
Treat public rendering, SSR rendering, and admin preview as the same reference contract whenever possible. If a block renders a medialib image in the site, the admin preview should usually depend on the same resolved media assumption instead of inventing a separate preview-only data path.
|
||||
@@ -0,0 +1,192 @@
|
||||
---
|
||||
name: deployment
|
||||
description: Production deployment setup for tibi-projects – Basispanel subdomain, .env, CI-Pipeline, Makefile. Use when deploying a new project to production or setting up a staging environment.
|
||||
---
|
||||
|
||||
# Deployment
|
||||
|
||||
## Überblick
|
||||
|
||||
Ein tibi-Projekt wird per Gitea Actions CI gebaut und via rsync auf den Produktionsserver (dock4) deployed. Davor muss die Subdomain im Basispanel angelegt und der Kunde korrekt konfiguriert sein.
|
||||
|
||||
## 1. Basispanel – Subdomain anlegen
|
||||
|
||||
### Kunde prüfen
|
||||
|
||||
```bash
|
||||
# Domain des Kunden suchen
|
||||
mcp_call(server="basispanel", tool="bp_list_domains", args={"search": "<kunde>"})
|
||||
# → liefert Customer-ID, Domain-ID, Company, Username
|
||||
```
|
||||
|
||||
### Subdomain anlegen
|
||||
|
||||
```bash
|
||||
# 1. Config holen (verfügbare Webserver + Storages sehen)
|
||||
mcp_call(server="basispanel", tool="bp_get_config")
|
||||
|
||||
# 2. Subdomain erstellen (ohne Webserver)
|
||||
mcp_call(server="basispanel", tool="bp_create_subdomain", args={
|
||||
"domainId": <domain-id>,
|
||||
"name": "<subdomain>", # oder leer für bare domain
|
||||
})
|
||||
|
||||
# 3. Löschen + neu mit Webserver (wenn Update nicht klappt)
|
||||
mcp_call(server="basispanel", tool="bp_delete_subdomain", args={"id": <subdomain-id>})
|
||||
|
||||
mcp_call(server="basispanel", tool="bp_create_subdomain", args={
|
||||
"domainId": <domain-id>,
|
||||
"name": "<subdomain>",
|
||||
"webserverKey": "dock4_lamp2",
|
||||
"webserverStorage": "dock4_webroots2",
|
||||
"webserverSettings": {
|
||||
"redirectType": "docroot",
|
||||
"docroot": "/<subdomain>.<domain>/frontend",
|
||||
"gitbaseRepository": "<org>/<repo>",
|
||||
"deployRoot": "./..",
|
||||
"defaultAlias": "wwwAlias",
|
||||
"defaultSubdomain": "defaultSubdomain",
|
||||
"wwwRedirect": "wwwRedirect",
|
||||
"php": "phpDisabled", # tibi-SPA kein PHP
|
||||
"https": "noHttps", # erstmal aus, später aktivieren
|
||||
"certbot": "noCertbot",
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Wichtige Keys (aus `bp_get_config`):
|
||||
| Server | Key | Storage |
|
||||
|--------|-----|---------|
|
||||
| dock4 | `dock4_lamp2` | `dock4_webroots2` |
|
||||
| dock1 | `dock1_...` | `dock1_webroots...` |
|
||||
|
||||
### Status prüfen
|
||||
|
||||
```bash
|
||||
mcp_call(server="basispanel", tool="bp_get_subdomain_status", args={"id": <subdomain-id>})
|
||||
```
|
||||
|
||||
Achtung: Health-Check zeigt DNS-Warnungen (externe Nameserver) – das ist normal solange der Kunde sein DNS selbst verwaltet.
|
||||
|
||||
## 2. `.env` konfigurieren
|
||||
|
||||
```env
|
||||
# Basis
|
||||
PROJECT_NAME=<project>
|
||||
TIBI_PREFIX=tibi
|
||||
TIBI_NAMESPACE=<project>
|
||||
|
||||
# RSYNC für Deploy
|
||||
RSYNC_HOST=ftp1.webmakers.de
|
||||
RSYNC_PORT=22223
|
||||
PRODUCTION_RSYNC_UID=100<customer-id>00 # z.B. 10051300
|
||||
PRODUCTION_RSYNC_GID=33
|
||||
|
||||
# Production Server
|
||||
PRODUCTION_SERVER=dock4.basehosts.de
|
||||
PRODUCTION_TIBI_PREFIX=tibi
|
||||
PRODUCTION_PATH=/webroots2/customers/<customer-id>/htdocs
|
||||
|
||||
# Staging
|
||||
STAGING_PATH=/staging/<org>/<project>/dev
|
||||
|
||||
# URLs
|
||||
LIVE_URL=http://<subdomain>.<domain>.dock4.basispanel.de # Preview-URL
|
||||
STAGING_URL=https://dev-<project>.staging.testversion.online
|
||||
CODING_URL=https://<project>.code.testversion.online
|
||||
```
|
||||
|
||||
## 3. CI-Pipeline (`.gitea/workflows/deploy.yml`)
|
||||
|
||||
```yaml
|
||||
name: deploy to production
|
||||
on: "push"
|
||||
jobs:
|
||||
deploy:
|
||||
steps:
|
||||
- checkout + git fetch --tags
|
||||
- node 22 + yarn install
|
||||
- yarn validate
|
||||
- ./scripts/ci-modify-config.sh # injiziert LIVE_URL, release, preview
|
||||
- yarn build # frontend
|
||||
- yarn build:server # SSR
|
||||
- sourcemaps → sentry
|
||||
- if dev-branch: ./scripts/ci-staging.sh
|
||||
- if master-branch: ./scripts/ci-deploy.sh
|
||||
```
|
||||
|
||||
**Wichtig:** Das aktuelle Workflow-File führt `yarn validate`, `yarn build` und `yarn build:server` im CI aus. Wenn `validate` dort scheitert, behebe den eigentlichen Typ- oder Pfadfehler statt den Schritt stillschweigend zu entfernen.
|
||||
|
||||
## 4. Deploy-Skripte
|
||||
|
||||
### `scripts/ci-deploy.sh` (Production)
|
||||
|
||||
- Liest `.env` und `api/config.yml.env`
|
||||
- rsynct `frontend/`, `api/`, `media/` via SSH zu `RSYNC_HOST`
|
||||
- deshalb muessen Collection-Dateiuploads auf den Repo-Root `media/` zeigen, typischerweise via `uploadPath: ../media/<collection>` in `api/collections/*.yml`
|
||||
- `api/media` ist in diesem Setup nicht der persistente Deploy-Zielpfad fuer Uploads
|
||||
- Nutzt `RSYNC_USER` + `RSYNC_PASS` (aus Gitea Secrets)
|
||||
- Auf master: excludiert `src/` und `*.map`
|
||||
- Reloadt den projektlokalen Proxy-Endpunkt via `LIVE_URL/api/_/admin/reload` mit `Authorization: Bearer ${ADMIN_TOKEN}`
|
||||
- Cleared SSR cache via `LIVE_URL/api/ssr?clear=1`
|
||||
|
||||
### `scripts/ci-staging.sh` (Dev/Staging)
|
||||
|
||||
- rsynct `api/`, `frontend/dist`, und `frontend/assets` nach `/data/${{ github.repository }}/${{ github.ref_name }}`
|
||||
- Startet `docker-compose-staging.yml`
|
||||
- Reloadt den projektlokalen Proxy-Endpunkt via `STAGING_URL/api/_/admin/reload` mit `Authorization: Bearer ${ADMIN_TOKEN}`
|
||||
|
||||
### `scripts/ci-modify-config.sh`
|
||||
|
||||
- Injiziert `LIVE_URL` als `originURL` in `api/hooks/config-client.js`
|
||||
- Injiziert `LIVE_URL` als `PREVIEW_URL` in `api/config.yml.env`
|
||||
- Setzt `release` + `buildTime` für Sentry
|
||||
- Kopiert `frontend/spa.html` → `api/templates/spa.html` (SSR-Template)
|
||||
- Ersetzt `__TIMESTAMP__` in spa.html (Cache-Busting)
|
||||
|
||||
## 5. Makefile
|
||||
|
||||
Wichtige Targets:
|
||||
|
||||
```makefile
|
||||
# Media von Production syncen
|
||||
media-sync-master-to-local:
|
||||
rsync -v -e "ssh ... -l $(PRODUCTION_RSYNC_UID),$(PRODUCTION_RSYNC_GID),$(PRODUCTION_PATH)/media" \
|
||||
-az $(RSYNC_HOST):/ media/
|
||||
|
||||
# MongoDB von Production syncen (via Chisel-Tunnel)
|
||||
mongo-sync-master-to-local:
|
||||
chisel client --auth coder:$$PASSWORD http://$(PRODUCTION_SERVER):10987 27017:mongo:27017 &
|
||||
mongodump ... | mongorestore ...
|
||||
```
|
||||
|
||||
## 6. DNS
|
||||
|
||||
Der Kunde verwaltet sein DNS selbst (externe Nameserver). Für die Subdomain muss ein A-Record gesetzt werden:
|
||||
|
||||
```
|
||||
<subdomain>.<domain> IN A 45.129.180.102 (IP von dock4)
|
||||
```
|
||||
|
||||
Die Preview-URL `http://<subdomain>.<domain>.dock4.basispanel.de` funktioniert ohne DNS (wird von Basispanel intern aufgelöst).
|
||||
|
||||
## 7. HTTPS nachträglich aktivieren
|
||||
|
||||
Sobald das Projekt live geht:
|
||||
|
||||
1. Im Basispanel Subdomain updaten:
|
||||
- `https`: `"https"` (statt `"noHttps"`)
|
||||
- `certbot`: `"certbot"` (automatisches Letsencrypt)
|
||||
- `httpsRedirect`: `"httpsRedirect"` (HTTP→HTTPS)
|
||||
2. `.env`: `LIVE_URL` auf `https://www.<domain>` ändern
|
||||
3. `api/hooks/config-client.js`: `originURL` entsprechend setzen (wird von CI überschrieben)
|
||||
|
||||
## 8. Typische Fehler
|
||||
|
||||
| Problem | Ursache | Fix |
|
||||
| ------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------ |
|
||||
| `invalid webserverKey: dock4` | Falscher Key | Mit `bp_get_config` prüfen → `dock4_lamp2` |
|
||||
| `subdomain exists` | Doppelt angelegt | Mit `bp_delete_subdomain` löschen, neu anlegen |
|
||||
| `yarn validate` scheitert in CI | Typen/Submodule/Pfade nicht sauber eingecheckt | Checkout-, Submodule- und Include-Pfade korrigieren; `validate` im Workflow belassen |
|
||||
| Rsync "Permission denied" | Falscher RSYNC_USER | In Gitea Secrets prüfen |
|
||||
| 404 auf Subdomain | DNS nicht gesetzt | A-Record beim Kunden-DNS-Provider eintragen |
|
||||
@@ -0,0 +1,510 @@
|
||||
---
|
||||
name: frontend-architecture
|
||||
description: Understand the frontend architecture — custom SPA routing, state management, Svelte 5 patterns, API layer, error handling, and i18n. Use when working on routing logic, navigation, stores, or understanding how the frontend fits together.
|
||||
---
|
||||
|
||||
# frontend-architecture
|
||||
|
||||
## When to use this skill
|
||||
|
||||
Use this skill when:
|
||||
|
||||
- Understanding or modifying the SPA routing mechanism
|
||||
- Working with stores or state management
|
||||
- Debugging navigation issues
|
||||
- Adding new Svelte 5 reactive patterns
|
||||
- Understanding the API layer and error handling
|
||||
- Working with i18n / multi-language features
|
||||
- Understanding how SSR and SPA loading share one app-level data path
|
||||
|
||||
---
|
||||
|
||||
## Routing: custom SPA router
|
||||
|
||||
This project uses a **custom SPA router** — NOT SvelteKit, NOT file-based routing. Pages are CMS-managed content entries loaded by path.
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
Browser URL change
|
||||
↓
|
||||
history.pushState / replaceState (proxied in store.ts)
|
||||
↓
|
||||
$location store updates (path, search, hash)
|
||||
↓
|
||||
App.svelte $effect reacts to $location.path
|
||||
↓
|
||||
loadContent(lang, routePath) → API call: getCachedEntries("content", { lang, path, active: true })
|
||||
↓
|
||||
ContentEntry.blocks[] → BlockRenderer.svelte → individual block components
|
||||
```
|
||||
|
||||
### Key files
|
||||
|
||||
| File | Responsibility |
|
||||
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `frontend/src/lib/store.ts` | Proxies `history.pushState`/`replaceState` → updates `$location` writable store. Handles `popstate` for back/forward. |
|
||||
| `frontend/src/lib/navigation.ts` | `spaNavigate(url, options)` — the programmatic navigation API. Also: `initScrollRestoration()`, `spaLink` action, hash parsing. |
|
||||
| `frontend/src/lib/i18n.ts` | Language routing: `extractLanguageFromPath()`, `stripLanguageFromPath()`, `localizedPath()`, `currentLanguage` derived store, `ROUTE_TRANSLATIONS`. |
|
||||
| `frontend/src/App.svelte` | Reacts to `$location.path` + `$currentLanguage`, loads content via API, passes blocks to `BlockRenderer`. |
|
||||
| `frontend/src/blocks/BlockRenderer.svelte` | Maps `block.type` to Svelte components. |
|
||||
|
||||
### How the location store works
|
||||
|
||||
`store.ts` wraps `history.pushState` and `history.replaceState` with a `Proxy`:
|
||||
|
||||
```typescript
|
||||
// Simplified — see store.ts for full implementation
|
||||
history.pushState = new Proxy(history.pushState, {
|
||||
apply: (target, thisArg, args) => {
|
||||
// Update $location store BEFORE the actual pushState
|
||||
publishLocation(args[2]) // args[2] = URL
|
||||
Reflect.apply(target, thisArg, args)
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
This means **any** `pushState`/`replaceState` call (from `spaNavigate`, `<a>` clicks, or third-party code) automatically updates `$location`.
|
||||
|
||||
The `popstate` event (back/forward buttons) also triggers `publishLocation()`.
|
||||
|
||||
### URL structure
|
||||
|
||||
```
|
||||
/{lang}/{path}
|
||||
↓ ↓
|
||||
de /ueber-uns
|
||||
|
||||
Example: /de/ueber-uns → lang="de", routePath="/ueber-uns"
|
||||
/en/about → lang="en", routePath="/about"
|
||||
/de/ → lang="de", routePath="/"
|
||||
```
|
||||
|
||||
Root `/` redirects to `/{browserLanguage}/` via `getBrowserLanguage()`.
|
||||
|
||||
### SSR interaction with routing
|
||||
|
||||
This frontend is not just an SPA. The same top-level app also participates in SSR.
|
||||
|
||||
- `frontend/src/ssr.ts` is intentionally thin and should mostly bootstrap locale state and call `render(App, { props: { url } })`.
|
||||
- `App.svelte` owns page loading for both browser and SSR.
|
||||
- Browser navigation triggers page loading from `$effect`.
|
||||
- SSR triggers the same page-loading path directly inside `typeof window === "undefined"`.
|
||||
|
||||
This means route changes, i18n path handling, and content-loading behavior must be reasoned about together. If a route works in the browser but SSR returns empty content or 404, inspect the mapping between:
|
||||
|
||||
- public URL (`/de/...`)
|
||||
- stripped route path (`/...`)
|
||||
- `content.path` in the DB
|
||||
- `api/hooks/config.js` SSR route validation
|
||||
|
||||
### Navigation API
|
||||
|
||||
```typescript
|
||||
import { spaNavigate } from "./lib/navigation"
|
||||
|
||||
// Basic navigation (creates history entry, scrolls to top)
|
||||
spaNavigate("/de/kontakt")
|
||||
|
||||
// Replace current entry (no back button)
|
||||
spaNavigate("/de/suche", { replace: true })
|
||||
|
||||
// Keep scroll position
|
||||
spaNavigate("/de/produkte#filter=shoes", { noScroll: true })
|
||||
|
||||
// With state object
|
||||
spaNavigate("/de/produkt/123", { state: { from: "search" } })
|
||||
```
|
||||
|
||||
### SPA link action
|
||||
|
||||
For `<a>` elements, use the `spaLink` action instead of `spaNavigate`:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { spaLink } from "../lib/navigation"
|
||||
</script>
|
||||
|
||||
<a href="/de/kontakt" use:spaLink>Kontakt</a>
|
||||
<a href="/de/suche" use:spaLink={{ replace: true }}>Suche</a>
|
||||
```
|
||||
|
||||
The action intercepts clicks (respecting modifier keys, external links, `target="_blank"`) and calls `spaNavigate` internally.
|
||||
|
||||
### BrowserSync SPA fallback
|
||||
|
||||
In development, BrowserSync uses `connect-history-api-fallback` to serve `index.html` for all routes, enabling client-side routing. In production, the webserver or tibi-server handles this.
|
||||
|
||||
### Localized route translations
|
||||
|
||||
For translated URL slugs (e.g. `/ueber-uns` ↔ `/about`), configure `ROUTE_TRANSLATIONS` in `frontend/src/lib/i18n.ts`:
|
||||
|
||||
```typescript
|
||||
export const ROUTE_TRANSLATIONS: Record<string, Record<SupportedLanguage, string>> = {
|
||||
about: { de: "ueber-uns", en: "about" },
|
||||
contact: { de: "kontakt", en: "contact" },
|
||||
// Add more as needed
|
||||
}
|
||||
```
|
||||
|
||||
Keep in mind that these translations affect the public URL shape and therefore also the SSR route-validation layer. Changing localized slugs is not purely a frontend concern.
|
||||
|
||||
---
|
||||
|
||||
## State management
|
||||
|
||||
The project uses **Svelte writable/derived stores** (not a centralized state library).
|
||||
|
||||
### Store inventory
|
||||
|
||||
| Store | File | Purpose |
|
||||
| ---------------------- | ---------------------- | -------------------------------------------------------------------------------- |
|
||||
| `location` | `lib/store.ts` | Current URL state (path, search, hash, push/pop flags) |
|
||||
| `mobileMenuOpen` | `lib/store.ts` | Whether mobile hamburger menu is open |
|
||||
| `currentContentEntry` | `lib/store.ts` | Currently displayed page entry data such as `translationKey`, `lang`, and `path` |
|
||||
| `previousPath` | `lib/store.ts` | Previous URL path (for conditional back buttons) |
|
||||
| `apiBaseOverride` | `lib/store.ts` | Override API base URL (used by admin module) |
|
||||
| `cookieConsentVisible` | `lib/store.ts` | Whether cookie consent banner is showing |
|
||||
| `currentLanguage` | `lib/i18n.ts` | Derived from `$location.path` — current language code |
|
||||
| `selectedLanguage` | `lib/i18n.ts` | Writable — synced with `currentLanguage` on navigation |
|
||||
| `activeRequests` | `lib/requestsStore.ts` | Number of in-flight API requests (drives `LoadingBar`) |
|
||||
|
||||
### Pattern: creating a new store
|
||||
|
||||
```typescript
|
||||
// In lib/store.ts or a dedicated file
|
||||
import { writable, derived } from "svelte/store"
|
||||
|
||||
// Simple writable
|
||||
export const myStore = writable<MyType>(initialValue)
|
||||
|
||||
// Derived from other stores
|
||||
export const myDerived = derived(location, ($loc) => {
|
||||
return computeFromPath($loc.path)
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Svelte 5 patterns used in this project
|
||||
|
||||
This project uses **Svelte 5 with Runes**. Key patterns:
|
||||
|
||||
### Component props
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
// Rune syntax — replaces export let
|
||||
let { block, className = "" }: { block: ContentBlockEntry; className?: string } = $props()
|
||||
</script>
|
||||
```
|
||||
|
||||
### Reactive state
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
// Local reactive state (replaces let x; with $: reactivity)
|
||||
let count = $state(0)
|
||||
let items = $state<Item[]>([])
|
||||
|
||||
// Computed/derived values (replaces $: derived = ...)
|
||||
let total = $derived(items.reduce((sum, i) => sum + i.price, 0))
|
||||
|
||||
// Side effects (replaces $: { ... } reactive blocks)
|
||||
$effect(() => {
|
||||
// Runs when dependencies change
|
||||
console.log("count changed:", count)
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
### SSR-safe code
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { untrack } from "svelte"
|
||||
|
||||
// Guard browser-only APIs
|
||||
if (typeof window !== "undefined") {
|
||||
window.addEventListener("scroll", handleScroll, { passive: true })
|
||||
}
|
||||
|
||||
// untrack: capture initial value without creating reactive dependency
|
||||
// Used in App.svelte for SSR initial URL
|
||||
untrack(() => {
|
||||
if (url) { /* set initial location */ }
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
### Svelte stores in Svelte 5
|
||||
|
||||
Stores (`writable`, `derived`) still work in Svelte 5. Use `$storeName` syntax in components:
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { location } from "./lib/store"
|
||||
// $location is reactive — auto-subscribes in Svelte 5
|
||||
</script>
|
||||
<p>Current path: {$location.path}</p>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API layer
|
||||
|
||||
### Core function: `api()`
|
||||
|
||||
Located in `frontend/src/lib/api.ts`. Features:
|
||||
|
||||
- **Request deduplication** — identical concurrent GETs share one promise
|
||||
- **Loading indicator** — drives `activeRequests` store → `LoadingBar`
|
||||
- **Build-version check** — auto-reloads page when server build is newer
|
||||
- **Mock interceptor** — when `__MOCK__` is `true`, routes requests to `frontend/mocking/*.json`
|
||||
- **Sentry integration** — span instrumentation (when enabled)
|
||||
|
||||
### Shared browser/SSR transport
|
||||
|
||||
The project intentionally shares the low-level API transport between browser and SSR via `api/hooks/lib/ssr`.
|
||||
|
||||
- In the browser, it eventually becomes `fetch(...)`.
|
||||
- In SSR, `apiRequest(...)` delegates to `context.ssrRequest(...)`.
|
||||
- GET responses reached during SSR are written into `window.__SSR_CACHE__` for hydration.
|
||||
|
||||
This is why SSR can preload both content and navigation without building a separate frontend-only data layer.
|
||||
|
||||
### Usage patterns
|
||||
|
||||
```typescript
|
||||
import { api, getCachedEntries, getCachedEntry, getDBEntries, postDBEntry } from "./lib/api"
|
||||
|
||||
// Cached (1h TTL, for read-heavy data)
|
||||
const pages = await getCachedEntries<"content">("content", { lang: "de", active: true })
|
||||
const page = await getCachedEntry<"content">("content", { path: "/about" })
|
||||
|
||||
// Uncached
|
||||
const items = await getDBEntries<"content">("content", { type: "blog" }, "sort", 10)
|
||||
|
||||
// Write
|
||||
const result = await postDBEntry("content", { name: "New Page", active: true })
|
||||
|
||||
// Raw API call
|
||||
const { data, count } = await api<MyType[]>("mycollection", { filter: { active: true }, limit: 20 })
|
||||
```
|
||||
|
||||
### `aggregate` for sub-queries
|
||||
|
||||
The server supports an `aggregate` parameter to compute reverse aggregates against another collection and store the result under `_aggregate`. This efficiently calculates counts, sums, existence, etc. without embedding the target documents.
|
||||
|
||||
```typescript
|
||||
const res = await api<MyEntry[]>("mycollection", {
|
||||
filter: { active: true },
|
||||
params: {
|
||||
// String syntax: "collection:foreignField:op:valueField:as"
|
||||
aggregate: "posts:categoryId:count",
|
||||
// JSON syntax for advanced use cases (custom source field, filtering)
|
||||
aggregate: JSON.stringify({
|
||||
collection: "comments",
|
||||
foreignField: "entryId",
|
||||
op: "count",
|
||||
filter: { approved: true },
|
||||
as: "approvedComments",
|
||||
}),
|
||||
},
|
||||
})
|
||||
// Result in res.data[0]._aggregate.postsCount and res.data[0]._aggregate.approvedComments
|
||||
```
|
||||
|
||||
Available operations: `count` (default), `exists`, `sum`, `avg`, `min`, `max`.
|
||||
|
||||
### Error handling
|
||||
|
||||
```typescript
|
||||
try {
|
||||
const result = await api<ContentEntry[]>("content", { filter: { path: "/missing" } })
|
||||
} catch (err) {
|
||||
// err has shape: { response: Response, data: { error: string } }
|
||||
const status = (err as any)?.response?.status // e.g. 404
|
||||
const message = (err as any)?.data?.error // e.g. "Not found"
|
||||
|
||||
// For user-visible errors:
|
||||
import { addToast } from "./lib/toast"
|
||||
addToast({ type: "error", message: "Seite nicht gefunden" })
|
||||
|
||||
// For debugging:
|
||||
console.error("[MyComponent] API error:", err)
|
||||
}
|
||||
```
|
||||
|
||||
### `aggregate` for sub-queries
|
||||
|
||||
The server supports an `aggregate` parameter to compute reverse aggregates against another collection and store the result under `_aggregate`. This efficiently calculates counts, sums, existence, etc. without embedding the target documents.
|
||||
|
||||
```typescript
|
||||
const res = await api<MyEntry[]>("mycollection", {
|
||||
filter: { active: true },
|
||||
params: {
|
||||
// String syntax: "collection:foreignField:op:valueField:as"
|
||||
aggregate: "posts:categoryId:count",
|
||||
// JSON syntax for advanced use cases (custom source field, filtering)
|
||||
aggregate: JSON.stringify({
|
||||
collection: "comments",
|
||||
foreignField: "entryId",
|
||||
op: "count",
|
||||
filter: { approved: true },
|
||||
as: "approvedComments",
|
||||
}),
|
||||
},
|
||||
})
|
||||
// Result in res.data[0]._aggregate.postsCount and res.data[0]._aggregate.approvedComments
|
||||
```
|
||||
|
||||
Available operations: `count` (default), `exists`, `sum`, `avg`, `min`, `max`.
|
||||
|
||||
### Error handling guidelines
|
||||
|
||||
| Scenario | Approach |
|
||||
| --------------------------------- | ------------------------------------------------- |
|
||||
| API error the user should see | `addToast({ type: "error", message })` |
|
||||
| API error that's silently handled | `console.error(...)` for dev logging |
|
||||
| Unexpected error in production | Sentry captures automatically (when enabled) |
|
||||
| Missing content / 404 | Set `notFound = true` → renders `NotFound.svelte` |
|
||||
| Network error / offline | Loading bar stays visible; user can retry |
|
||||
|
||||
### API request flow (client-side)
|
||||
|
||||
```
|
||||
Component calls api() / getCachedEntries()
|
||||
↓
|
||||
Deduplication check (skip if signal provided)
|
||||
↓
|
||||
incrementRequests() → LoadingBar appears
|
||||
↓
|
||||
__MOCK__? → mockApiRequest() (in-memory JSON filtering)
|
||||
↓ (else)
|
||||
apiRequest() from api/hooks/lib/ssr (shared with SSR bundle)
|
||||
↓
|
||||
fetch("${apiBaseURL}${endpoint}?filter=...&sort=...&limit=...")
|
||||
↓
|
||||
Parse response → check X-Build-Time header
|
||||
↓
|
||||
decrementRequests() → LoadingBar disappears
|
||||
↓
|
||||
Return { data, count, buildTime }
|
||||
```
|
||||
|
||||
### `_count` endpoint
|
||||
|
||||
Der tibi-server stellt einen dedizierten `_count`-Endpoint bereit, der **nur** `{"count": N}` zurückgibt – kein Data-Transfer:
|
||||
|
||||
```
|
||||
GET /api/{collection}/_count?filter={"active":true,"category":"<id>"}
|
||||
→ {"count": 8}
|
||||
```
|
||||
|
||||
Der Endpoint wird durch den BrowserSync-Proxy korrekt geroutet (`/api` → `/api/v1/_/{namespace}`).
|
||||
|
||||
**Frontend-Aufruf:**
|
||||
|
||||
```ts
|
||||
const res = await api<{ count: number }>("machines/_count", {
|
||||
filter: { active: true, category: catId },
|
||||
})
|
||||
// res.data.count === 8
|
||||
```
|
||||
|
||||
Das ist effizienter als `count=1&limit=1`, weil keine Collection-Objekte serialisiert/übertragen werden.
|
||||
|
||||
### `select` für schlanke Queries
|
||||
|
||||
Der tibi-server unterstützt einen `select`-Parameter als Komma-Liste der gewünschten Felder. Nicht gelistete Felder werden nicht übertragen:
|
||||
|
||||
```ts
|
||||
const res = await api<MachineEntry[]>("machines", {
|
||||
filter: { active: true, category: catId },
|
||||
sort: "sortOrder",
|
||||
limit: 20,
|
||||
params: {
|
||||
lookup: "images:medialib",
|
||||
select: "name,slug,tagline,priceFrom,weight,sku,images",
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Nicht aufgeführte Felder (z.B. `description`, `specs`) entfallen – spart Bandbreite bei Listen/Grids. `_lookup` und `id` werden automatisch ergänzt.
|
||||
|
||||
**Wichtig:** `select` muss als String im `params`-Objekt übergeben werden (der `apiRequest` hängt es als Query-Parameter an). Es wird direkt an den tibi-server durchgereicht.
|
||||
|
||||
---
|
||||
|
||||
## i18n system
|
||||
|
||||
### Architecture
|
||||
|
||||
- **svelte-i18n** for translation strings (`$_("key")`)
|
||||
- **URL-based language routing** (`/{lang}/...`)
|
||||
- **Lazy-loaded locale files** in `frontend/src/lib/i18n/locales/{lang}.json`
|
||||
- **Route translations** for localized URL slugs
|
||||
|
||||
### Adding a new language
|
||||
|
||||
1. Create locale file: `frontend/src/lib/i18n/locales/fr.json`
|
||||
2. Add to `SUPPORTED_LANGUAGES` in `frontend/src/lib/i18n.ts`:
|
||||
```typescript
|
||||
export const SUPPORTED_LANGUAGES = ["de", "en", "fr"] as const
|
||||
```
|
||||
3. Add label: `export const LANGUAGE_LABELS = { ..., fr: "Français" }`
|
||||
4. Add route translations for the new language in `ROUTE_TRANSLATIONS`.
|
||||
5. Register in `frontend/src/lib/i18n/index.ts` (lazy loader).
|
||||
6. Create content entries with `lang: "fr"` in the CMS.
|
||||
|
||||
### Translation usage
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { _ } from "./lib/i18n/index"
|
||||
</script>
|
||||
|
||||
<h1>{$_("hero.title")}</h1>
|
||||
<p>{$_("hero.subtitle", { values: { name: "World" } })}</p>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Never `spaNavigate()` in SSR** — always guard with `typeof window !== "undefined"`.
|
||||
- **Store subscriptions in modules** — if subscribing to stores outside components, remember to unsubscribe to prevent memory leaks.
|
||||
- **API PUT returns only changed fields** — don't expect a full object back from PUT requests.
|
||||
- **`_id` not `id` for filters** — API filters use MongoDB's `_id`, but response objects only have `id` as string via API.
|
||||
- **`$location` strips trailing slashes** — `/about/` becomes `/about` (except root `/`).
|
||||
- **Content cache is 1 hour** — `getCachedEntries` caches in memory for 1h. For admin previews, use `getDBEntries` (uncached).
|
||||
- **`$effect` alone is not SSR** — server-side rendering must trigger the same data path explicitly outside browser-only reactive effects.
|
||||
- **A rendered shell is not enough** — always verify that SSR HTML actually contains page-critical content and navigation.
|
||||
|
||||
## Cart persistence (localStorage)
|
||||
|
||||
For SSR-safe cart/inquiry persistence:
|
||||
|
||||
```ts
|
||||
let cartItems = $state<any[]>([])
|
||||
// Laden
|
||||
$effect(() => {
|
||||
try {
|
||||
if (typeof localStorage !== "undefined") {
|
||||
const saved = localStorage.getItem("cart_key")
|
||||
if (saved) cartItems = JSON.parse(saved)
|
||||
}
|
||||
} catch {}
|
||||
})
|
||||
// Speichern
|
||||
$effect(() => {
|
||||
try {
|
||||
if (typeof localStorage !== "undefined") {
|
||||
localStorage.setItem("cart_key", JSON.stringify(cartItems))
|
||||
}
|
||||
} catch {}
|
||||
})
|
||||
```
|
||||
|
||||
Immer mit `typeof localStorage !== "undefined"` für SSR-Sicherheit.
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
name: gitea-issue-attachments
|
||||
description: Upload files (screenshots, logs, etc.) to Gitea issues as attachments via the REST API. Use when attaching any file to a Gitea issue or comment.
|
||||
---
|
||||
|
||||
# Gitea Issue Attachments
|
||||
|
||||
Attach files to Gitea issues via the REST API:
|
||||
|
||||
1. Get the Gitea API token from the running MCP docker process:
|
||||
```bash
|
||||
GITEA_PID=$(ps aux | grep 'gitea-mcp-server' | grep -v grep | awk '{print $2}')
|
||||
GITEA_TOKEN=$(cat /proc/$GITEA_PID/environ | tr '\0' '\n' | grep GITEA_ACCESS_TOKEN | cut -d= -f2)
|
||||
```
|
||||
2. Upload the file as an issue attachment:
|
||||
```bash
|
||||
curl -s -X POST "https://gitbase.de/api/v1/repos/{owner}/{repo}/issues/{index}/assets" \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-F "attachment=@path/to/file"
|
||||
```
|
||||
This returns JSON with a `uuid` field.
|
||||
3. Reference the attachment in the issue or comment body:
|
||||
```markdown
|
||||

|
||||
```
|
||||
@@ -0,0 +1,175 @@
|
||||
---
|
||||
name: live-mongodb
|
||||
description: Connect to the live (production) MongoDB via chisel tunnel and perform read/write operations. Use this skill when you need to inspect or update live data directly in the production database.
|
||||
---
|
||||
|
||||
# Live MongoDB Access via Chisel Tunnel
|
||||
|
||||
## Overview
|
||||
|
||||
Die Produktions-MongoDB läuft auf dem Server aus `PRODUCTION_SERVER` in `.env`. Der Zugang erfolgt über einen **Chisel-Tunnel**, der den Remote-MongoDB-Port auf localhost mapped. Damit kann man dann entweder über `mongosh`, `mongodump`/`mongorestore`, oder den **MongoDB MCP Server** auf die Live-Daten zugreifen.
|
||||
|
||||
## Umgebungsvariablen (aus .env)
|
||||
|
||||
| Variable | Beschreibung |
|
||||
| ------------------------ | ----------------------------------------------- |
|
||||
| `PRODUCTION_SERVER` | Produktionsserver (z.B. `dock4.basehosts.de`) |
|
||||
| `PRODUCTION_TIBI_PREFIX` | DB-Prefix auf Produktion (z.B. `tibi`) |
|
||||
| `TIBI_NAMESPACE` | Projekt-Namespace |
|
||||
| **Live DB Name** | = `${PRODUCTION_TIBI_PREFIX}_${TIBI_NAMESPACE}` |
|
||||
| Chisel-Port (Remote) | `10987` — Chisel-Server auf dem Produktionshost |
|
||||
|
||||
## Schritt 1: Chisel-Tunnel starten
|
||||
|
||||
Das Chisel-Passwort muss vom User bereitgestellt werden. Tunnel starten:
|
||||
|
||||
```bash
|
||||
# Passwort vom User erfragen oder aus Umgebung nehmen
|
||||
read -s -p "Chisel-Passwort: " CHISEL_PASSWORD
|
||||
|
||||
# Tunnel starten (mappt remote mongo:27017 → localhost:27017)
|
||||
chisel client --auth "coder:${CHISEL_PASSWORD}" \
|
||||
http://${PRODUCTION_SERVER}:10987 \
|
||||
27017:mongo:27017 &
|
||||
|
||||
# Kurz warten, bis der Tunnel steht
|
||||
sleep 3
|
||||
```
|
||||
|
||||
**WICHTIG:** Der lokale Docker-Mongo-Container muss gestoppt sein oder auf einem anderen Port laufen, da der Tunnel Port 27017 lokal belegt. Falls der lokale Container läuft:
|
||||
|
||||
```bash
|
||||
# Lokales MongoDB stoppen (belegt sonst Port 27017)
|
||||
docker compose -f docker-compose-local.yml stop mongo
|
||||
```
|
||||
|
||||
Alternativ kann der Tunnel auf einen anderen lokalen Port gemappt werden:
|
||||
|
||||
```bash
|
||||
chisel client --auth "coder:${CHISEL_PASSWORD}" \
|
||||
http://${PRODUCTION_SERVER}:10987 \
|
||||
37017:mongo:27017 &
|
||||
# → erreichbar unter mongodb://localhost:37017/${PRODUCTION_TIBI_PREFIX}_${TIBI_NAMESPACE}
|
||||
```
|
||||
|
||||
## Schritt 2: Verbinden
|
||||
|
||||
### Option A: mongosh (interaktiv)
|
||||
|
||||
```bash
|
||||
mongosh "mongodb://localhost:27017/${PRODUCTION_TIBI_PREFIX}_${TIBI_NAMESPACE}"
|
||||
```
|
||||
|
||||
### Option B: MongoDB MCP Server (für Copilot)
|
||||
|
||||
Den MongoDB MCP über die Umgebungsvariable `MDB_MCP_CONNECTION_STRING` auf die Live-DB umleiten.
|
||||
|
||||
**Temporär für eine Session** – in `.vscode/mcp.json` eine zweite Server-Config eintragen:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"servers": {
|
||||
"mongodb-live": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "mongodb-mcp-server@latest"],
|
||||
"type": "stdio",
|
||||
"env": {
|
||||
"MDB_MCP_CONNECTION_STRING": "mongodb://localhost:27017/${PRODUCTION_TIBI_PREFIX}_${TIBI_NAMESPACE}",
|
||||
"MDB_MCP_READ_ONLY": "false",
|
||||
"MDB_MCP_TELEMETRY": "disabled",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
> **Achtung:** `MDB_MCP_READ_ONLY=false` erlaubt Schreiboperationen! Nach getaner Arbeit den Server wieder entfernen oder auf `true` setzen.
|
||||
|
||||
### Option C: Einmalige Kommandos via Terminal
|
||||
|
||||
```bash
|
||||
DB_NAME="${PRODUCTION_TIBI_PREFIX}_${TIBI_NAMESPACE}"
|
||||
|
||||
# Dokument suchen
|
||||
mongosh "mongodb://localhost:27017/$DB_NAME" \
|
||||
--eval 'db.content.findOne({path: "/"})'
|
||||
|
||||
# Feld updaten
|
||||
mongosh "mongodb://localhost:27017/$DB_NAME" \
|
||||
--eval 'db.content.updateOne({path: "/"}, {$set: {"title": "Neuer Titel"}})'
|
||||
```
|
||||
|
||||
## Schritt 3: Tunnel beenden
|
||||
|
||||
```bash
|
||||
killall chisel
|
||||
```
|
||||
|
||||
Falls der lokale Mongo-Container vorher gestoppt wurde, wieder starten:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose-local.yml start mongo
|
||||
```
|
||||
|
||||
## Sicherheitsregeln
|
||||
|
||||
1. **Immer zuerst lesen, dann schreiben.** Vor jedem Update das betroffene Dokument mit `find`/`findOne` inspizieren.
|
||||
2. **Backup vor Bulk-Updates.** Bei Massenänderungen vorher ein `mongodump` machen:
|
||||
```bash
|
||||
mongodump --uri="mongodb://localhost:27017" \
|
||||
--db=${PRODUCTION_TIBI_PREFIX}_${TIBI_NAMESPACE} \
|
||||
--collection=<collection> \
|
||||
--gzip --archive=backup-<collection>-$(date +%Y%m%d-%H%M%S).gz
|
||||
```
|
||||
3. **User muss Chisel-Passwort liefern.** Das Passwort niemals hardcoden oder in Dateien speichern.
|
||||
4. **Updates immer bestätigen lassen.** Vor jeder Schreiboperation dem User die geplante Query zeigen und explizit nach Bestätigung fragen.
|
||||
5. **Nach getaner Arbeit Tunnel schließen** und ggf. `mongodb-live` MCP-Server aus der Config entfernen.
|
||||
6. **Kein Drop/Delete von Collections** ohne explizite User-Anweisung.
|
||||
7. **SSR-Cache leeren nach Datenänderungen.** Wenn Daten in Collections geändert werden, die auf der Website gerendert werden (z.B. `content`, `navigation`), muss der SSR-Cache invalidiert werden, damit die Änderungen sichtbar werden. Dazu die `ssr`-Collection leeren:
|
||||
```bash
|
||||
mongosh "mongodb://localhost:27017/$DB_NAME" \
|
||||
--eval 'db.ssr.deleteMany({})'
|
||||
```
|
||||
Siehe auch den Skill `tibi-ssr-caching` für Details zur Cache-Invalidierung über die API.
|
||||
|
||||
## Wichtige Collections
|
||||
|
||||
| Collection | Beschreibung |
|
||||
| ------------ | ------------------------------- |
|
||||
| `content` | CMS-Inhaltsseiten (Pagebuilder) |
|
||||
| `navigation` | Navigationsstruktur |
|
||||
| `medialib` | Medien-Bibliothek |
|
||||
| `ssr` | SSR-Cache |
|
||||
|
||||
> Weitere Collections je nach Projekt — siehe `api/collections/` für die aktuelle Liste.
|
||||
|
||||
## Typische Anwendungsfälle
|
||||
|
||||
### Content-Eintrag inspizieren
|
||||
|
||||
```js
|
||||
db.content.findOne({ path: "/" })
|
||||
```
|
||||
|
||||
### Navigation aktualisieren
|
||||
|
||||
```js
|
||||
db.navigation.updateOne({ type: "header", language: "de" }, { $set: { "elements.0.name": "Neues Label" } })
|
||||
```
|
||||
|
||||
### Dokument-Struktur inspizieren
|
||||
|
||||
```js
|
||||
// Schema einer Collection anschauen
|
||||
db.content.findOne()
|
||||
|
||||
// Alle Felder eines Dokuments auflisten
|
||||
Object.keys(db.content.findOne())
|
||||
```
|
||||
|
||||
## Fehlerbehebung
|
||||
|
||||
- **"Connection refused" auf Port 27017:** Chisel-Tunnel läuft nicht oder lokaler Mongo blockiert den Port. Prüfen mit `ss -tlnp | grep 27017`.
|
||||
- **"Authentication failed":** Chisel-Passwort falsch. User erneut fragen.
|
||||
- **Langsame Queries:** Produktions-DB kann große Collections haben. Immer mit Filtern arbeiten, nie `find({})` ohne Limit.
|
||||
- **Rate Limiting:** Kein Thema bei direktem DB-Zugang (nur bei API-Calls relevant).
|
||||
@@ -0,0 +1,450 @@
|
||||
---
|
||||
name: media-seo-publishing
|
||||
description: Model media, SEO, and publishing workflows for website projects on this starter. Covers file fields, image validation/filtering, alt texts, social metadata, publication windows, and SSR/cache implications.
|
||||
---
|
||||
|
||||
# media-seo-publishing
|
||||
|
||||
## When to use this skill
|
||||
|
||||
Use this skill when:
|
||||
|
||||
- Designing media-heavy website content models
|
||||
- Adding image/file fields and image filters
|
||||
- Modeling SEO fields for pages or reusable content
|
||||
- Defining publication windows and how they interact with runtime and SSR
|
||||
- Building authoring workflows around images, metadata, and release control
|
||||
|
||||
## Medialib file serving
|
||||
|
||||
Images uploaded to medialib (via base64 or multipart) are stored with a relative `file.src` path such as `"file/example.jpg"`. In this starter, `MedialibImage` resolves that stored path together with the medialib entry ID into the project-local URL:
|
||||
|
||||
```
|
||||
/api/medialib/{id}/file/example.jpg
|
||||
```
|
||||
|
||||
Responsive image filters are then applied by the shared widget as query params:
|
||||
|
||||
```
|
||||
/api/medialib/{id}/file/example.jpg?filter=s-webp
|
||||
/api/medialib/{id}/file/example.jpg?filter=m-webp
|
||||
/api/medialib/{id}/file/example.jpg?filter=l-webp
|
||||
```
|
||||
|
||||
Those `/api/...` URLs are starter-local proxy URLs. For frontend rendering in Tibi website projects, the preferred approach is **not** to hand-build medialib URLs in each block or route component.
|
||||
|
||||
## Collection uploadPath
|
||||
|
||||
For Tibi file fields, the storage root is configured per collection via top-level `uploadPath`.
|
||||
|
||||
Important rules:
|
||||
|
||||
- `uploadPath` belongs on the collection itself, not on the individual `type: file` field
|
||||
- in this starter, collection YAML files live in `api/collections/`, while deploy syncs the repo-root `media/` directory
|
||||
- the current starter collections can omit `uploadPath` and rely on the tibi-server default derived from project config
|
||||
- if you explicitly override `uploadPath` in this starter, it should normally point to `../media/<collection-name>` so deploy and runtime stay aligned
|
||||
- do not write uploads into `api/media`; that path is not the persistent deploy target used by the project
|
||||
|
||||
Explicit override example:
|
||||
|
||||
```yaml
|
||||
name: medialib
|
||||
uploadPath: ../media/medialib
|
||||
|
||||
fields:
|
||||
- name: file
|
||||
type: file
|
||||
```
|
||||
|
||||
For hidden thumbnails stored on the `content` collection, the same rule applies when you choose an explicit override at collection level:
|
||||
|
||||
```yaml
|
||||
name: content
|
||||
uploadPath: ../media/content
|
||||
|
||||
fields:
|
||||
- name: _pagebuilderThumbnail
|
||||
type: file
|
||||
```
|
||||
|
||||
Tibi then stores uploads below:
|
||||
|
||||
```text
|
||||
{uploadPath}/{entryId}/{fieldName}/{filename}
|
||||
```
|
||||
|
||||
So an explicitly overridden medialib upload typically lands at `../media/medialib/{entryId}/file/{filename}`, while a content thumbnail lands at `../media/content/{entryId}/_pagebuilderThumbnail/{filename}`.
|
||||
|
||||
## Preferred frontend integration
|
||||
|
||||
Use a shared media widget such as `MedialibImage` as the frontend boundary for medialib-rendered images.
|
||||
|
||||
### MedialibImage with minimal entry (ID only)
|
||||
|
||||
When only a medialib ID is available (no resolved `_lookup` entry), pass a minimal entry structure together with the `id` prop:
|
||||
|
||||
```svelte
|
||||
<MedialibImage
|
||||
id={medialibId}
|
||||
entry={{ file: { src: "file/example.jpg", type: "image/jpeg" } }}
|
||||
class="w-full h-full object-cover"
|
||||
noPlaceholder
|
||||
/>
|
||||
```
|
||||
|
||||
`resolveFileSrc()` in `MedialibImage` kombiniert `entry.file.src` (zum Beispiel `"file/example.jpg"`) mit der `id` zur korrekten URL: `${apiBase}/medialib/{id}/{src}`. Filter werden im Widget als `?filter=...` angehängt. Wenn `_lookup`-Daten mit vollständigem Entry verfügbar sind, bevorzugt diese verwenden:
|
||||
|
||||
```svelte
|
||||
<MedialibImage entry={resolvedEntry} class="w-full h-full object-cover" noPlaceholder />
|
||||
```
|
||||
|
||||
The preferred flow is:
|
||||
|
||||
1. request medialib references with `lookup`
|
||||
2. pass the resolved `MedialibEntry` to the shared widget
|
||||
3. let that widget own URL resolution, filter choice, SSR markup, and admin/pagebuilder compatibility
|
||||
|
||||
Typical usage:
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import MedialibImage from "../widgets/MedialibImage.svelte"
|
||||
|
||||
let { block }: { block: ContentBlockEntry } = $props()
|
||||
</script>
|
||||
|
||||
{#if block._lookup?.image}
|
||||
<MedialibImage
|
||||
entry={block._lookup.image}
|
||||
class="w-full h-full object-cover"
|
||||
minWidth={900}
|
||||
lazy={true}
|
||||
/>
|
||||
{/if}
|
||||
```
|
||||
|
||||
For repeated collection data such as galleries, teaser lists, or detail-page image arrays, also request the lookup instead of rendering from raw ID strings:
|
||||
|
||||
```ts
|
||||
const entries = await getCachedEntries<CollectionName>("your-collection", {
|
||||
filter: { active: true },
|
||||
sort: "sortOrder",
|
||||
lookup: "imageField:medialib",
|
||||
})
|
||||
```
|
||||
|
||||
`getCachedEntries()` expects `lookup` as an option in the second arguments object.
|
||||
|
||||
Then consume the resolved entry from `_lookup`, for example `_lookup.imageField` or `_lookup.imageField?.[0]` depending on whether the schema stores one image or an array.
|
||||
|
||||
### URL resolution strategy (frontend)
|
||||
|
||||
At the shared widget boundary, resolve image data with this priority:
|
||||
|
||||
1. Prefer a resolved `MedialibEntry` from `_lookup`
|
||||
2. If `entry.file.src` is already absolute, use it directly
|
||||
3. Otherwise construct the file URL from `{apiBase}/medialib/{entryId}/{file.src}` inside the shared widget
|
||||
4. Only fall back to raw ID-to-URL construction in legacy code paths that cannot yet pass resolved entries
|
||||
|
||||
Do not duplicate medialib URL logic in every block. Keep it in one widget/helper layer so SSR, admin preview, and filter behavior stay consistent.
|
||||
|
||||
```ts
|
||||
function resolveFileSrc(src: string | undefined, entryId: string | undefined, apiBase: string): string | null {
|
||||
if (!src) return null
|
||||
if (/^(https?:)?\/\//.test(src) || src.startsWith("/")) return src
|
||||
if (!entryId) return null
|
||||
return `${apiBase.replace(/\/+$/, "")}/medialib/${entryId}/${src.replace(/^\/+/, "")}`
|
||||
}
|
||||
```
|
||||
|
||||
### Lookup parameter
|
||||
|
||||
Use the `lookup` API parameter to resolve medialib references automatically:
|
||||
|
||||
```
|
||||
/api/{collection}?lookup={fieldPath}:medialib
|
||||
```
|
||||
|
||||
The resolved data is available in the `_lookup` field of the returned object at the corresponding path.
|
||||
|
||||
Typical project patterns:
|
||||
|
||||
- pagebuilder blocks with nested image refs: `blocks.someImageField:medialib`
|
||||
- collection entries with a single image field: `image:medialib`
|
||||
- collection entries with image arrays: `images:medialib`
|
||||
- attachment arrays or document previews: `attachments:medialib`
|
||||
|
||||
Prefer adding the lookup at the data-loading boundary rather than rehydrating IDs later in the component tree.
|
||||
|
||||
## Filter sizing strategy
|
||||
|
||||
Do not hardcode one fixed filter per component unless the rendered width is truly fixed.
|
||||
|
||||
Preferred rules:
|
||||
|
||||
1. explicit `filter` prop wins when the caller already knows the right output size
|
||||
2. otherwise pass a meaningful `minWidth` for layout-stable contexts such as hero, card, or gallery slots
|
||||
3. let the shared widget derive the final filter from the measured width on the client
|
||||
4. keep the width-to-filter mapping centralized in the widget instead of repeating `xs/s/m/l/xl` logic in blocks
|
||||
|
||||
This keeps image payloads reasonable without forcing each block author to manually guess the correct filter.
|
||||
|
||||
Typical examples:
|
||||
|
||||
- hero/background image: `minWidth={1600}`
|
||||
- card image: `minWidth={640}`
|
||||
- product detail main gallery image: `minWidth={960}`
|
||||
- thumbnail image: `minWidth={240}`
|
||||
|
||||
The exact breakpoints can vary per project, but the sizing logic should remain centralized.
|
||||
|
||||
## SSR and no-JS behavior
|
||||
|
||||
For raster images, SSR cannot always know the final client width. The shared widget should therefore render deterministically:
|
||||
|
||||
1. render a normal `img` with a real `src` in SSR when a filter is explicit, `minWidth` is known, or admin rendering requires a fallback filter
|
||||
2. emit a `noscript` fallback for raster images so crawlers and JS-disabled clients still receive a concrete image URL
|
||||
3. avoid unstable per-block SSR hacks that guess widths differently from the client
|
||||
|
||||
This is especially important for image-bearing blocks, teasers, galleries, and detail views where HTML must remain stable between SSR, hydration, and no-JS rendering.
|
||||
|
||||
## Admin and pagebuilder compatibility
|
||||
|
||||
Medialib rendering must also work inside admin/pagebuilder preview contexts, not only on the public website.
|
||||
|
||||
Important rules:
|
||||
|
||||
- respect `apiBaseOverride` or the admin-provided project base when constructing medialib URLs
|
||||
- do not prepend frontend public paths blindly when the admin already passes an absolute file URL
|
||||
- consume hydrated `_lookup` data directly in preview/runtime code instead of trying to re-fetch references inside preview components
|
||||
- keep placeholder asset resolution admin-safe too, not just medialib file URLs
|
||||
|
||||
If a block preview loads collection data in the admin, the preview context must provide the project-specific API base and the frontend code must route requests through that base.
|
||||
|
||||
In practice, that means the media widget and any helper used by it should be aware of `apiBaseOverride` or an equivalent admin-provided API base so the same component works in:
|
||||
|
||||
- public frontend
|
||||
- SSR render
|
||||
- admin pagebuilder preview
|
||||
- collection-driven block previews
|
||||
|
||||
### Base64 file upload via API
|
||||
|
||||
Upload images to medialib via JSON API by including base64 data inline:
|
||||
|
||||
```json
|
||||
POST /api/medialib
|
||||
{
|
||||
"file": { "src": "data:image/jpeg;base64,..." },
|
||||
"title": "Image title",
|
||||
"alt": "Alt text"
|
||||
}
|
||||
```
|
||||
|
||||
The tibi-server saves the file below the collection's `uploadPath`, for example `../media/medialib/{entryId}/file/{filename}`, and creates the medialib entry.
|
||||
|
||||
## Goal
|
||||
|
||||
The goal is to make media, SEO, and publishing part of the actual solution design instead of leaving them as late add-ons.
|
||||
|
||||
For real website projects, these concerns affect:
|
||||
|
||||
- collection schema
|
||||
- admin ergonomics
|
||||
- frontend rendering
|
||||
- SSR/cache validity
|
||||
- editorial quality
|
||||
|
||||
## Source of truth
|
||||
|
||||
Use these sources when implementing or reviewing these areas:
|
||||
|
||||
- `tibi-server/docs/08-file-upload-images.md`
|
||||
- the project's medialib collection config
|
||||
- the relevant content or domain collection configs
|
||||
- `tibi-admin-nova/types/admin.d.ts`
|
||||
- `tibi-admin-nova/docs/collection-config.md`
|
||||
- the project's SSR/runtime config hooks
|
||||
- the project's frontend media widget/helper implementation
|
||||
|
||||
## Media modeling
|
||||
|
||||
Use file fields deliberately.
|
||||
|
||||
Typical choices:
|
||||
|
||||
- `file` for a single image or asset
|
||||
- `file[]` for galleries or multi-asset attachments
|
||||
- foreign references to a media collection when assets need their own lifecycle or reuse
|
||||
|
||||
Choose between inline file fields and dedicated media references based on reuse and editorial workflow, not just convenience.
|
||||
|
||||
## File validation rules
|
||||
|
||||
For serious website builds, do not leave file fields unconstrained.
|
||||
|
||||
Define validators where appropriate:
|
||||
|
||||
- accepted mime types
|
||||
- max file size
|
||||
- min/max image dimensions
|
||||
- whether mixed media is allowed
|
||||
|
||||
This should reflect actual content needs. Hero images, logos, documents, and gallery media often need different constraints.
|
||||
|
||||
## Image filters
|
||||
|
||||
If the project serves resized or transformed assets, define image filters intentionally.
|
||||
|
||||
Use filters for:
|
||||
|
||||
- thumbnails
|
||||
- card images
|
||||
- hero images
|
||||
- OpenGraph/social images when relevant
|
||||
|
||||
Do not leave every consuming component to invent its own ad hoc asset sizes.
|
||||
|
||||
## Alt texts and captions
|
||||
|
||||
Accessibility and SEO-relevant image metadata should be explicit in the model.
|
||||
|
||||
Recommended approach:
|
||||
|
||||
- store alt text explicitly
|
||||
- keep captions separate from alt text
|
||||
- use localized fields if the site is multilingual
|
||||
- optionally use AI assistance only as a suggestion flow
|
||||
|
||||
Do not treat filenames as acceptable alt text.
|
||||
|
||||
## SEO modeling
|
||||
|
||||
Page-like collections should usually model SEO explicitly.
|
||||
|
||||
Typical fields:
|
||||
|
||||
- `meta.title`
|
||||
- `meta.description`
|
||||
- social/share image
|
||||
- optional canonical information if required
|
||||
- optional index/follow controls for advanced projects
|
||||
|
||||
SEO fields should be easy to find in Nova, usually via sidebar groups or clearly named sections.
|
||||
|
||||
## Publishing model
|
||||
|
||||
If the site uses publication timing, define it intentionally.
|
||||
|
||||
Typical concerns:
|
||||
|
||||
- draft versus active state
|
||||
- publication window (`from` / `to`)
|
||||
- visibility of unpublished content in public reads
|
||||
- SSR cache validity for time-sensitive content
|
||||
|
||||
Publishing is not just a boolean. If publication windows exist, they must influence runtime and cache behavior.
|
||||
|
||||
## SSR implications
|
||||
|
||||
Media, SEO, and publishing affect SSR directly.
|
||||
|
||||
Examples:
|
||||
|
||||
- page meta tags must exist in SSR HTML when relevant
|
||||
- navigation or content with publication windows must invalidate cached HTML correctly
|
||||
- image-driven blocks must render stable URLs/markup in SSR
|
||||
|
||||
If publication timing can make cached HTML stale, the relevant collections must be accounted for in SSR publish-check logic.
|
||||
|
||||
## Admin ergonomics
|
||||
|
||||
Use current Nova features to make media/SEO workflows usable:
|
||||
|
||||
- sidebar groups for SEO/publication fields
|
||||
- `viewHint.media` for media-focused collections
|
||||
- previews for image-bearing entities
|
||||
- layout grouping so editors do not scroll through one long file/SEO form
|
||||
|
||||
Media and SEO fields are often technically present but operationally poor if the admin layout is ignored.
|
||||
|
||||
## Recommended modeling patterns
|
||||
|
||||
### Marketing page
|
||||
|
||||
Recommended shape:
|
||||
|
||||
- main content blocks
|
||||
- explicit SEO object or fields
|
||||
- hero/share image strategy
|
||||
- publication controls in sidebar
|
||||
|
||||
### Media library entry
|
||||
|
||||
Recommended shape:
|
||||
|
||||
- file field
|
||||
- title/name
|
||||
- alt text / caption
|
||||
- optional copyright/source
|
||||
- image-focused admin view
|
||||
|
||||
### Reusable teaser or card entity
|
||||
|
||||
Recommended shape:
|
||||
|
||||
- image reference or file
|
||||
- short label/title
|
||||
- teaser text
|
||||
- consistent image filter usage in frontend components
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- No file validators on public/editor-uploaded images
|
||||
- No explicit alt field
|
||||
- Mixing captions and alt text into one field
|
||||
- Hardcoding image sizes only in frontend CSS/components
|
||||
- Building raw `/api/medialib/{id}/{src}` URLs in every block or hardcoding `file/file` instead of using one shared widget
|
||||
- Repeating filter breakpoint logic in multiple components
|
||||
- Rendering public image URLs in frontend code that break in admin pagebuilder preview
|
||||
- Treating publication as frontend-only logic
|
||||
- Forgetting that publish windows can invalidate SSR HTML
|
||||
|
||||
## Publication verification matrix
|
||||
|
||||
If the project uses publication timing, do not verify only one happy-path entry.
|
||||
|
||||
Check a small matrix deliberately:
|
||||
|
||||
1. currently published entry
|
||||
2. future-scheduled entry
|
||||
3. expired entry
|
||||
4. token/admin visibility when editorial or operator access should still exist
|
||||
|
||||
This keeps publication modeling tied to the real public-read and SSR behavior instead of to optimistic field design.
|
||||
|
||||
## Verification checklist
|
||||
|
||||
After changing media/SEO/publishing behavior, verify all of these:
|
||||
|
||||
1. Upload validation matches the intended asset type.
|
||||
2. Image filters are named and used consistently.
|
||||
3. Shared image widgets receive resolved entries instead of rebuilding URLs ad hoc.
|
||||
4. Alt/caption/SEO fields are explicit and editor-friendly.
|
||||
5. Publication state affects public output correctly.
|
||||
6. SSR HTML still reflects the intended published state.
|
||||
7. Admin/pagebuilder preview resolves medialib images correctly.
|
||||
8. `yarn validate` stays clean.
|
||||
|
||||
## What an LLM should inspect first
|
||||
|
||||
When asked to work on media, SEO, or publishing on this starter, inspect in this order:
|
||||
|
||||
1. `tibi-server/docs/08-file-upload-images.md`
|
||||
2. the relevant collection YAML
|
||||
3. the shared media widget/helper layer used by the frontend
|
||||
4. admin layout and previews for those fields
|
||||
5. frontend components consuming the media/SEO data
|
||||
6. SSR publish-check and invalidation logic if timing matters
|
||||
|
||||
This prevents “just add an image field” changes that break runtime, editorial UX, or caching.
|
||||
@@ -0,0 +1,170 @@
|
||||
---
|
||||
name: mongodb-and-indexes
|
||||
description: Design MongoDB prerequisites and index strategy for tibi website projects. Covers replica-set requirements, collection index modeling, text/search index implications, index auto-management on reload, and operator checks for persistence, backups, and regeneration-sensitive features.
|
||||
---
|
||||
|
||||
# mongodb-and-indexes
|
||||
|
||||
## When to use this skill
|
||||
|
||||
Use this skill when:
|
||||
|
||||
- a project moves beyond simple demo data and needs deliberate MongoDB design
|
||||
- collections need unique, text, sparse, or compound indexes
|
||||
- search, audit, or larger datasets introduce operational database requirements
|
||||
- operators need clarity about replica-set, persistence, or backup assumptions
|
||||
|
||||
## Goal
|
||||
|
||||
Give later agents one place for MongoDB prerequisites and index strategy.
|
||||
|
||||
This skill exists because tibi projects do not just “use MongoDB”. They rely on config-driven indexes, reload-time index reconciliation, and environment choices that can help or break production behavior.
|
||||
|
||||
## Source of truth
|
||||
|
||||
Use these sources when implementing or reviewing MongoDB/index decisions:
|
||||
|
||||
- `tibi-server/docs/02-configuration.md`
|
||||
- `tibi-server/docs/04-collections.md`
|
||||
- `tibi-server/docs/12-deployment.md`
|
||||
- active project/server config
|
||||
|
||||
## MongoDB prerequisites
|
||||
|
||||
Current upstream deployment requirements include:
|
||||
|
||||
- MongoDB 4.4+
|
||||
- replica set for transactions
|
||||
|
||||
That means later agents should not promise production-like behavior while ignoring the actual database topology.
|
||||
|
||||
For this starter family, check explicitly:
|
||||
|
||||
- which MongoDB version is targeted
|
||||
- whether the environment runs as a replica set
|
||||
- where persistent data lives
|
||||
- how backup and restore are expected to work
|
||||
|
||||
## Index ownership model
|
||||
|
||||
Indexes can be defined in two places.
|
||||
|
||||
### Field-level indexes
|
||||
|
||||
Use for simple per-field indexes:
|
||||
|
||||
```yaml
|
||||
fields:
|
||||
- name: email
|
||||
type: string
|
||||
index:
|
||||
- single
|
||||
- unique
|
||||
```
|
||||
|
||||
Available field-level flags include:
|
||||
|
||||
- `single`
|
||||
- `unique`
|
||||
- `text`
|
||||
- `sparse`
|
||||
|
||||
### Collection-level indexes
|
||||
|
||||
Use for compound or more explicit index definitions:
|
||||
|
||||
```yaml
|
||||
indexes:
|
||||
- name: category_publish_date
|
||||
key:
|
||||
- category
|
||||
- -publishDate
|
||||
background: true
|
||||
- name: title_text
|
||||
key:
|
||||
- $text:title
|
||||
defaultLanguage: german
|
||||
```
|
||||
|
||||
Use collection-level indexes when:
|
||||
|
||||
- sort patterns span multiple fields
|
||||
- uniqueness depends on more than one field
|
||||
- you need text-index language control
|
||||
- the index needs a stable explicit name
|
||||
|
||||
## Important auto-management behavior
|
||||
|
||||
On project setup or reload:
|
||||
|
||||
1. configured indexes are created
|
||||
2. indexes present in MongoDB but no longer in config are dropped
|
||||
3. `_id_` is never dropped
|
||||
|
||||
This is a critical workflow fact.
|
||||
|
||||
Do not treat indexes as one-off manual DBA work while also expecting config reload to stay authoritative. In this stack, the YAML config owns the intended index state.
|
||||
|
||||
## Search and index interplay
|
||||
|
||||
Some search modes depend directly on index strategy:
|
||||
|
||||
- `mode: text` requires a text index
|
||||
- explicit text search can use field-level `index: [text]` or collection-level `$text:` indexes
|
||||
- regex fallback can become expensive without deliberate field/index choices
|
||||
- enrichment-based modes such as `ngram` and `vector` add `_search` data and may require regeneration workflows rather than classic MongoDB indexes alone
|
||||
|
||||
Keep classic indexes and search config aligned. Search should not be modeled in isolation from database cost and reload behavior.
|
||||
|
||||
## Query shape matters
|
||||
|
||||
Before adding indexes, inspect the real access patterns:
|
||||
|
||||
- which fields appear in permission filters
|
||||
- which fields appear in list sorting
|
||||
- which fields appear in frequent admin or public lookups
|
||||
- whether uniqueness is a real business rule or only a nice-to-have
|
||||
|
||||
Index design should follow concrete read and write patterns, not schema aesthetics.
|
||||
|
||||
## Operational decisions
|
||||
|
||||
At project-delivery level, document these choices explicitly:
|
||||
|
||||
- MongoDB version and topology
|
||||
- replica-set availability
|
||||
- persistence location for database and uploads
|
||||
- backup/restore responsibility
|
||||
- whether audit/search features introduce extra operational expectations
|
||||
|
||||
If these decisions are absent, later agents tend to over-focus on schema YAML while missing the operator-critical data layer.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- adding indexes without checking actual query or sort behavior
|
||||
- relying on manual indexes that the config will later drop
|
||||
- enabling text search without provisioning the needed text index
|
||||
- treating replica-set requirements as optional trivia
|
||||
- shipping production projects without a backup and restore story
|
||||
|
||||
## Verification checklist
|
||||
|
||||
After MongoDB/index-related changes, verify all of these:
|
||||
|
||||
1. the target environment satisfies MongoDB version and replica-set needs
|
||||
2. configured indexes match real query, sort, and uniqueness requirements
|
||||
3. project reload succeeds after index changes
|
||||
4. text/search features have the required index support
|
||||
5. persistence and backup assumptions are documented
|
||||
|
||||
## What an LLM should inspect first
|
||||
|
||||
When asked to design or review the data layer on this starter, inspect in this order:
|
||||
|
||||
1. `tibi-server/docs/12-deployment.md`
|
||||
2. `tibi-server/docs/02-configuration.md`
|
||||
3. `tibi-server/docs/04-collections.md`
|
||||
4. current collection YAML for indexes/search
|
||||
5. actual query, sort, audit, and search requirements
|
||||
|
||||
This keeps index design tied to real runtime behavior and not just to field definitions.
|
||||
@@ -0,0 +1,156 @@
|
||||
---
|
||||
name: monitoring-and-performance
|
||||
description: Configure and verify observability for tibi website projects. Covers OpenAPI exposure, Prometheus metrics, Sentry wiring, health/reachability checks, and the operator-facing validation that should exist before a project is considered production-ready.
|
||||
---
|
||||
|
||||
# monitoring-and-performance
|
||||
|
||||
## When to use this skill
|
||||
|
||||
Use this skill when:
|
||||
|
||||
- a project needs operator-facing visibility beyond “the page loads”
|
||||
- you need OpenAPI output for integrations or documentation
|
||||
- you need Prometheus/Grafana visibility
|
||||
- you need Sentry or similar error visibility in frontend or server flows
|
||||
- you want to define the minimum health and observability checks for deploys
|
||||
|
||||
## Goal
|
||||
|
||||
Give later agents a concrete workflow for deciding what should be observable, how it is exposed, and how to verify that exposure.
|
||||
|
||||
This skill is not about arbitrary performance tuning. It is about making the running system inspectable enough that operators and developers can see whether it is healthy.
|
||||
|
||||
## Source of truth
|
||||
|
||||
Use these sources when implementing or reviewing observability:
|
||||
|
||||
- `tibi-server/docs/13-openapi-metrics.md`
|
||||
- `tibi-server/docs/02-configuration.md`
|
||||
- `.agents/skills/deployment/SKILL.md`
|
||||
- `frontend/src/config.ts`
|
||||
- relevant deploy scripts and env/config files
|
||||
|
||||
## OpenAPI exposure
|
||||
|
||||
Tibi-server generates an OpenAPI spec per project:
|
||||
|
||||
```text
|
||||
GET /api/v1/{namespace}/openapi
|
||||
```
|
||||
|
||||
Use this when:
|
||||
|
||||
- a project exposes public API surfaces that need documentation
|
||||
- integrations or client generators benefit from a machine-readable contract
|
||||
|
||||
Collection-level OpenAPI customization lives in collection metadata via `meta.openapi`.
|
||||
|
||||
Use that metadata deliberately to:
|
||||
|
||||
- hide endpoints that should not appear in the spec
|
||||
- add summaries and descriptions
|
||||
- keep the public API contract readable
|
||||
|
||||
## Metrics exposure
|
||||
|
||||
Prometheus metrics are exposed at:
|
||||
|
||||
```text
|
||||
GET /metrics
|
||||
```
|
||||
|
||||
Key upstream metric documented today:
|
||||
|
||||
- `tibi_request_duration_seconds`
|
||||
|
||||
This is useful for:
|
||||
|
||||
- request latency visibility
|
||||
- collection-level timing comparisons
|
||||
- basic traffic and error observation in Grafana/Prometheus
|
||||
|
||||
Do not enable metrics-like operator expectations in a project and then forget to verify the endpoint actually works in the target environment.
|
||||
|
||||
## Sentry and error visibility
|
||||
|
||||
This stack can surface errors through Sentry-related configuration.
|
||||
|
||||
Relevant surfaces include:
|
||||
|
||||
- server-level `sentry` config in tibi-server
|
||||
- frontend runtime wiring in `frontend/src/config.ts`
|
||||
- deploy-time release/build metadata injection where the project uses it
|
||||
|
||||
Use Sentry deliberately:
|
||||
|
||||
- define DSN, environment, and release expectations
|
||||
- know whether tracing is wanted or only error capture
|
||||
- make sure deploy scripts and build metadata agree with the runtime setup
|
||||
|
||||
Do not leave a half-configured Sentry setup that looks present but produces unusable traces.
|
||||
|
||||
## Health and reachability checks
|
||||
|
||||
At minimum, operators should be able to verify:
|
||||
|
||||
- website URL responds
|
||||
- admin URL responds
|
||||
- API responds
|
||||
- OpenAPI and metrics endpoints respond when they are intended to be used
|
||||
|
||||
In this repo family, simple reachability probes are often the first useful health signal. For project delivery, these checks belong next to deploy and sign-off work, not only in ad-hoc troubleshooting.
|
||||
|
||||
## Recommended patterns
|
||||
|
||||
### Public API projects
|
||||
|
||||
Recommended shape:
|
||||
|
||||
- expose OpenAPI intentionally
|
||||
- add `meta.openapi` summaries for meaningful endpoints
|
||||
- verify the spec against the current collection model
|
||||
|
||||
### Operated production projects
|
||||
|
||||
Recommended shape:
|
||||
|
||||
- metrics endpoint reachable in the target environment
|
||||
- at least one documented Grafana/Prometheus use case for request timing
|
||||
- explicit decision whether Sentry is used or intentionally not used
|
||||
|
||||
### Basic website deployments
|
||||
|
||||
Recommended shape:
|
||||
|
||||
- website/admin/API reachability checks are part of deploy verification
|
||||
- observability is documented enough that later operators know what exists
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- treating observability as optional once the build passes
|
||||
- exposing OpenAPI or metrics accidentally without deciding who uses them
|
||||
- half-configured Sentry with no useful environment or release handling
|
||||
- relying on manual browser clicks as the only production health check
|
||||
|
||||
## Verification checklist
|
||||
|
||||
After observability-related work, verify all of these:
|
||||
|
||||
1. intended OpenAPI exposure works and reflects the current collection config
|
||||
2. intended metrics exposure works in the target environment
|
||||
3. Sentry/error visibility is either intentionally configured or intentionally absent
|
||||
4. deploy-time reachability checks cover website, admin, and API
|
||||
5. `yarn build`, `yarn build:server`, and `yarn validate` still pass when observability wiring touched frontend/server config
|
||||
|
||||
## What an LLM should inspect first
|
||||
|
||||
When asked to set up monitoring or observability on this starter, inspect in this order:
|
||||
|
||||
1. `tibi-server/docs/13-openapi-metrics.md`
|
||||
2. `tibi-server/docs/02-configuration.md`
|
||||
3. deploy scripts and env/config files
|
||||
4. `frontend/src/config.ts`
|
||||
5. whether the project truly needs OpenAPI, metrics, Sentry, or only reachability checks
|
||||
|
||||
This prevents over-documenting features that are not actually wired and under-documenting the ones that matter operationally.
|
||||
@@ -0,0 +1,150 @@
|
||||
---
|
||||
name: multi-tenancy-and-orgs
|
||||
description: Model org/team-aware tibi projects. Covers the org-team-user hierarchy, visibility vs working rights, permission resolution, project assignment, audit visibility, and how later agents should make an explicit single-tenant vs org/team decision.
|
||||
---
|
||||
|
||||
# multi-tenancy-and-orgs
|
||||
|
||||
## When to use this skill
|
||||
|
||||
Use this skill when:
|
||||
|
||||
- a project might need org/team-aware isolation or working rights
|
||||
- multiple organizations or departments share one tibi installation
|
||||
- project visibility and editing rights must be separated cleanly
|
||||
- LLM budget ownership or audit visibility must follow org boundaries
|
||||
|
||||
## Goal
|
||||
|
||||
Give later agents a concrete implementation workflow for enterprise-aware projects and a clear off-ramp for single-tenant projects.
|
||||
|
||||
The most important decision is not how to model orgs. It is whether the project should use org/team features at all.
|
||||
|
||||
## Source of truth
|
||||
|
||||
Use these sources when implementing or reviewing org/team-aware projects:
|
||||
|
||||
- `tibi-server/docs/18-orgs-teams.md`
|
||||
- `tibi-server/docs/05-authentication.md`
|
||||
- `tibi-server/docs/17-field-level-permissions.md`
|
||||
- `.agents/skills/permissions-and-editor-workflows/SKILL.md`
|
||||
- `.agents/skills/search-and-embeddings/SKILL.md` when LLM budget or shared AI/search budgets matter
|
||||
|
||||
## First decision: single-tenant or org/team-aware
|
||||
|
||||
Make this decision explicitly.
|
||||
|
||||
### Single-tenant projects
|
||||
|
||||
Recommended shape:
|
||||
|
||||
- do not model org/team support by default
|
||||
- document that visibility and permissions are handled without enterprise isolation
|
||||
- keep the project simpler unless there is a real multi-tenant requirement
|
||||
|
||||
### Org/team-aware projects
|
||||
|
||||
Use this branch only when you actually need:
|
||||
|
||||
- org-scoped project visibility
|
||||
- team-based working rights
|
||||
- cross-user governance inside shared organizations
|
||||
- org-level budget ownership or audit visibility constraints
|
||||
|
||||
## Core model
|
||||
|
||||
The upstream hierarchy is:
|
||||
|
||||
- org = visibility boundary
|
||||
- team = working-rights unit
|
||||
- user = member of orgs and teams
|
||||
|
||||
Key fields include:
|
||||
|
||||
- `project.orgId`
|
||||
- `project.teams[]`
|
||||
- `user.orgs[]`
|
||||
- `user.teams[]`
|
||||
- `user.primaryOrgId`
|
||||
|
||||
Do not flatten these concepts into a generic “role” discussion. Org visibility and team working rights are different concerns.
|
||||
|
||||
## Visibility vs working rights
|
||||
|
||||
Important rule:
|
||||
|
||||
- org membership controls which projects a user can see
|
||||
- team assignment controls which permission sets a user gets inside those visible projects
|
||||
|
||||
This is the main conceptual boundary later agents must keep intact.
|
||||
|
||||
If a user can see a project but cannot edit it, that can be correct. Visibility is not the same as edit access.
|
||||
|
||||
## Permission resolution
|
||||
|
||||
Team permissions map onto collection permission keys.
|
||||
|
||||
Example idea:
|
||||
|
||||
- team carries `permissions: ["editor"]`
|
||||
- collection defines an `editor` permission set
|
||||
- assigned team members inherit that working-rights set on the project
|
||||
|
||||
Also important:
|
||||
|
||||
- multiple team permissions merge as a union
|
||||
- admin tokens and system admins still sit above team-based resolution
|
||||
- token/public/user/team/custom permission order matters
|
||||
|
||||
## Project assignment rules
|
||||
|
||||
For org/team-aware projects, later agents must design all of these deliberately:
|
||||
|
||||
- which org owns the project
|
||||
- which teams are assigned to the project
|
||||
- who is allowed to manage those assignments
|
||||
- how custom collection permission keys map to teams
|
||||
|
||||
Half-modeled org/team setups create the worst of both worlds: extra complexity without trustworthy isolation.
|
||||
|
||||
## Audit and LLM implications
|
||||
|
||||
Enterprise modeling affects more than CRUD visibility.
|
||||
|
||||
Relevant side effects:
|
||||
|
||||
- audit visibility follows the team → project → collection access chain
|
||||
- non-admin users do not see system audit entries
|
||||
- LLM/org budget logic uses `user.primaryOrgId`
|
||||
|
||||
That means enterprise design can affect audit reviews and AI cost ownership even if the public website itself looks simple.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- enabling org/team concepts “just in case”
|
||||
- using teams as visibility boundaries instead of orgs
|
||||
- mixing custom collection permissions and team permissions without naming discipline
|
||||
- forgetting that users can belong to multiple teams
|
||||
- introducing org-aware billing or audit expectations without modeling `primaryOrgId`
|
||||
|
||||
## Verification checklist
|
||||
|
||||
After org/team-related changes, verify all of these:
|
||||
|
||||
1. the project explicitly states single-tenant or org/team-aware
|
||||
2. org ownership and team assignment are defined for enterprise projects
|
||||
3. collection permission keys map cleanly to team permissions
|
||||
4. representative visibility and edit-rights scenarios behave as designed
|
||||
5. audit and LLM budget implications are understood when those features are in scope
|
||||
|
||||
## What an LLM should inspect first
|
||||
|
||||
When asked whether a project needs enterprise features, inspect in this order:
|
||||
|
||||
1. the project's actual tenancy requirement
|
||||
2. `tibi-server/docs/18-orgs-teams.md`
|
||||
3. current permission model in collection configs
|
||||
4. whether audit visibility or LLM budgets depend on org ownership
|
||||
5. whether the simpler single-tenant branch is the right answer
|
||||
|
||||
This prevents unnecessary enterprise complexity in projects that only need normal editorial permissions.
|
||||
@@ -0,0 +1,219 @@
|
||||
---
|
||||
name: nova-ai-editor-features
|
||||
description: Use current AI and LLM capabilities in tibi-admin-nova and tibi-server responsibly. Covers media AI assist, LLM provider setup, token budgets, editor-facing AI workflows, and where AI should or should not be used in website projects.
|
||||
---
|
||||
|
||||
# nova-ai-editor-features
|
||||
|
||||
## When to use this skill
|
||||
|
||||
Use this skill when:
|
||||
|
||||
- A website project should use AI-assisted editor workflows
|
||||
- You want AI help for media metadata, alt texts, captions, or editorial helper flows
|
||||
- You need to design LLM-backed actions or admin features on top of tibi-server
|
||||
- You need to decide whether AI belongs in the admin, in actions, or nowhere
|
||||
|
||||
## Goal
|
||||
|
||||
The goal is to help an LLM build **useful and controllable** AI features for editors.
|
||||
|
||||
Use AI where it improves editorial throughput or content quality. Do not add AI just because it exists.
|
||||
|
||||
## Source of truth
|
||||
|
||||
Use these sources when implementing or reviewing AI-backed website features:
|
||||
|
||||
- `tibi-server/docs/09-llm-integration.md`
|
||||
- `tibi-admin-nova/types/admin.d.ts`
|
||||
- `tibi-admin-nova/docs/collection-config.md`
|
||||
- `api/config.yml`
|
||||
- the project's actual Nova runtime config when such a file exists
|
||||
|
||||
## Two AI surfaces in this stack
|
||||
|
||||
### 1. Nova media/editor assistance
|
||||
|
||||
Nova supports editor-facing AI assistance, especially around media workflows.
|
||||
|
||||
Typical pattern:
|
||||
|
||||
```yaml
|
||||
meta:
|
||||
viewHint:
|
||||
media:
|
||||
ai:
|
||||
targetField: alt
|
||||
prompt: Beschreibe das Bild kurz und sachlich für einen Alt-Text.
|
||||
image:
|
||||
maxWidth: 1280
|
||||
maxHeight: 1280
|
||||
quality: 0.82
|
||||
```
|
||||
|
||||
Use this when editors benefit from assisted metadata generation directly in the admin.
|
||||
|
||||
### 2. tibi-server LLM proxy and actions
|
||||
|
||||
tibi-server provides an LLM proxy with:
|
||||
|
||||
- provider configuration
|
||||
- model whitelisting
|
||||
- streaming support
|
||||
- user and org token budgets
|
||||
- usage logging
|
||||
|
||||
This is the right foundation when a project needs controlled backend LLM usage.
|
||||
|
||||
## Recommended AI use cases for websites
|
||||
|
||||
Good use cases:
|
||||
|
||||
- alt text suggestions for uploaded images
|
||||
- caption or summary suggestions for media-heavy content
|
||||
- internal editorial helper actions
|
||||
- controlled rewrite or classification helpers for structured content
|
||||
- AI support in specialized admin workflows where output is reviewed by humans
|
||||
|
||||
Weak or risky use cases:
|
||||
|
||||
- auto-publishing public text without review
|
||||
- replacing the content model with one giant AI prompt field
|
||||
- hiding important business logic inside opaque prompts
|
||||
- bypassing permissions or audit trails through AI shortcuts
|
||||
|
||||
## AI for media collections
|
||||
|
||||
For image-heavy collections, prefer AI as **assistive autofill**, not as a silent overwrite mechanism.
|
||||
|
||||
Use Nova media AI when:
|
||||
|
||||
- the editor already works inside a media-oriented screen
|
||||
- the target field is explicit
|
||||
- the generated text is reviewable
|
||||
- existing manual values are not overwritten automatically
|
||||
|
||||
Prefer explicit target fields such as:
|
||||
|
||||
- `alt`
|
||||
- `caption`
|
||||
- `localizedCaption.de`
|
||||
|
||||
## LLM provider architecture
|
||||
|
||||
When enabling server-side LLM usage, define:
|
||||
|
||||
- which providers are configured
|
||||
- which models are allowed
|
||||
- which model is default
|
||||
- max tokens per request
|
||||
- which users or orgs have budgets
|
||||
|
||||
Never assume arbitrary models are available. Model choice must stay inside the configured whitelist.
|
||||
|
||||
## Token budget design
|
||||
|
||||
Use budgets deliberately.
|
||||
|
||||
If a project adds editor-facing AI features, define:
|
||||
|
||||
- which users may use them
|
||||
- per-provider token budgets
|
||||
- org-level budgets if multiple editors share a pool
|
||||
- expected failure behavior when budgets are exhausted
|
||||
|
||||
An editor-facing AI workflow is incomplete if the quota/failure path is not planned.
|
||||
|
||||
## Where AI logic should live
|
||||
|
||||
Choose the surface intentionally:
|
||||
|
||||
- **Nova media AI** for direct editor assistance in image/media workflows
|
||||
- **Action endpoint** for reusable backend AI workflows with validation and auditing
|
||||
- **Collection config only** when Nova already provides the needed behavior declaratively
|
||||
|
||||
Do not push provider credentials or prompt orchestration into the browser.
|
||||
|
||||
## Prompting rules for serious projects
|
||||
|
||||
Prompts should be:
|
||||
|
||||
- narrow in purpose
|
||||
- reviewable by humans
|
||||
- tied to an explicit target field or action contract
|
||||
- stable enough that editors know what the feature does
|
||||
|
||||
Avoid vague prompts such as “improve this content” when the output target and editorial rules are unclear.
|
||||
|
||||
## AI + permissions + audit
|
||||
|
||||
AI features must still respect:
|
||||
|
||||
- field-level permissions
|
||||
- hidden/readonly fields
|
||||
- action permissions
|
||||
- org/user budget boundaries
|
||||
- logging/auditing expectations
|
||||
|
||||
Do not let AI become a side channel around your normal content governance.
|
||||
|
||||
## Recommended implementation patterns
|
||||
|
||||
### Media alt-text assist
|
||||
|
||||
Recommended shape:
|
||||
|
||||
- media-oriented collection or view
|
||||
- `viewHint.media.ai.targetField`
|
||||
- prompt focused on accessibility and factual image description
|
||||
- human review before publishing
|
||||
|
||||
### Editorial helper action
|
||||
|
||||
Recommended shape:
|
||||
|
||||
- authenticated action endpoint
|
||||
- input validation
|
||||
- provider/model chosen from allowed config
|
||||
- stable structured response for the admin/frontend
|
||||
- logging and budget-aware failure handling
|
||||
|
||||
### AI-backed enrichment workflow
|
||||
|
||||
Recommended shape:
|
||||
|
||||
- action reads current entry state
|
||||
- generates suggestion only
|
||||
- stores result in explicit reviewable fields or returns suggestion to editor
|
||||
- never silently mutates unrelated content
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- Enabling AI without provider/budget planning
|
||||
- Using AI for public content generation without editorial review
|
||||
- Letting AI write into fields that editors should not modify manually
|
||||
- Hiding core business logic inside prompts instead of code/config
|
||||
- Treating AI as a replacement for structured content modeling
|
||||
|
||||
## Verification checklist
|
||||
|
||||
After adding AI-backed editor features, verify all of these:
|
||||
|
||||
1. Provider and model configuration are valid.
|
||||
2. Token budgets and failure modes are defined.
|
||||
3. The AI target field or action contract is explicit.
|
||||
4. Editors can review the result before publication when appropriate.
|
||||
5. Permissions and audit expectations still hold.
|
||||
6. `yarn validate` stays clean.
|
||||
|
||||
## What an LLM should inspect first
|
||||
|
||||
When asked to add AI to a website project on this starter, inspect in this order:
|
||||
|
||||
1. `tibi-server/docs/09-llm-integration.md`
|
||||
2. current collection meta for media/admin workflows
|
||||
3. whether the use case fits Nova media AI, an action, or both
|
||||
4. user/org budget expectations
|
||||
5. the exact target field or response contract
|
||||
|
||||
This prevents random “AI features” that have no operational boundaries.
|
||||
@@ -0,0 +1,195 @@
|
||||
---
|
||||
name: nova-navigation-modeling
|
||||
description: Model navigations with current tibi-admin-nova navigation features. Covers recursive trees, declaredTrees, singleton navigation slots, preview design, language-specific trees, and how navigation modeling fits website information architecture.
|
||||
---
|
||||
|
||||
# nova-navigation-modeling
|
||||
|
||||
## When to use this skill
|
||||
|
||||
Use this skill when:
|
||||
|
||||
- Designing header, footer, service, or utility navigation for a website project
|
||||
- Modeling recursive navigation trees in Nova
|
||||
- Using `viewHint.navigation` with declared singleton trees
|
||||
- Refactoring a flat or editor-unfriendly navigation structure
|
||||
|
||||
## Goal
|
||||
|
||||
The goal is to model navigation as a first-class website structure with current Nova support, not as an afterthought or a plain array field.
|
||||
|
||||
On this stack, navigation influences:
|
||||
|
||||
- editor workflow
|
||||
- public rendering
|
||||
- SSR completeness
|
||||
- language structure
|
||||
- information architecture
|
||||
|
||||
## Source of truth
|
||||
|
||||
Use these sources when implementing or reviewing navigation modeling:
|
||||
|
||||
- `tibi-admin-nova/types/admin.d.ts`
|
||||
- `tibi-admin-nova/docs/collection-config.md`
|
||||
- `api/collections/navigation.yml`
|
||||
- `frontend/src/App.svelte`
|
||||
- the frontend navigation rendering surface
|
||||
|
||||
## Current Nova navigation capability
|
||||
|
||||
Nova supports navigation-aware collection rendering through `meta.viewHint.navigation`.
|
||||
|
||||
Important building blocks:
|
||||
|
||||
- `nodesField`
|
||||
- `declaredTrees`
|
||||
- singleton root identifiers
|
||||
- recursive tree editing for the configured nodes field
|
||||
|
||||
This is more than a plain `object[]` form. Use it when the project actually has structured navigation trees.
|
||||
|
||||
## Recommended mental model
|
||||
|
||||
Model navigation by tree purpose, not by arbitrary document naming.
|
||||
|
||||
Typical trees:
|
||||
|
||||
- main/header navigation
|
||||
- footer navigation
|
||||
- service/navigation variants
|
||||
- language-specific trees
|
||||
|
||||
Each tree should have a clear editorial purpose and runtime consumer.
|
||||
|
||||
## Declared trees and singleton slots
|
||||
|
||||
Use `declaredTrees` when the project has known required navigation trees.
|
||||
|
||||
This gives editors:
|
||||
|
||||
- visible expected navigation slots
|
||||
- stable entry points even when a tree is not created yet
|
||||
- clearer distinction between intended site structure and accidental extra entries
|
||||
|
||||
For website projects, this is usually better than asking editors to create free-form navigation documents manually.
|
||||
|
||||
## Language-specific navigation
|
||||
|
||||
If the website is multilingual, decide explicitly whether navigation is:
|
||||
|
||||
- shared across languages
|
||||
- separate per language
|
||||
- partially shared with localized labels
|
||||
|
||||
The starter's current navigation collection models separate declared trees per language and per type. That is a good default when localized slugs and labels differ.
|
||||
|
||||
## Node schema design
|
||||
|
||||
Each navigation node should represent an editorially meaningful choice.
|
||||
|
||||
Typical node fields:
|
||||
|
||||
- `name`
|
||||
- internal page reference
|
||||
- external toggle
|
||||
- external URL
|
||||
- hash/anchor
|
||||
- nested child nodes
|
||||
|
||||
Keep the schema focused. Do not overload navigation nodes with unrelated layout or content concerns unless the runtime genuinely needs them.
|
||||
|
||||
## Preview design
|
||||
|
||||
Navigation authoring depends heavily on preview quality.
|
||||
|
||||
Use previews so editors can quickly tell:
|
||||
|
||||
- whether a node points to an internal page or external URL
|
||||
- what label it displays
|
||||
- which tree they are editing
|
||||
|
||||
The current starter navigation config already demonstrates a strong pattern: previewing internal page lookup data and external URLs differently.
|
||||
|
||||
## Depth and constraints
|
||||
|
||||
Set `maxLevel` intentionally per tree.
|
||||
|
||||
Examples:
|
||||
|
||||
- header navigation may allow two levels
|
||||
- footer navigation may allow one level
|
||||
- service navigation may have different limits
|
||||
|
||||
Depth is an information-architecture decision, not only a UI detail.
|
||||
|
||||
## Navigation and runtime
|
||||
|
||||
Navigation modeling must match the frontend and SSR expectations.
|
||||
|
||||
Important checks:
|
||||
|
||||
- the frontend knows which tree to load
|
||||
- language/type keys are stable
|
||||
- SSR loads navigation as page-critical shell data when needed
|
||||
- internal page references remain readable in admin and resolvable in runtime
|
||||
|
||||
Do not design a navigation schema in admin that the frontend cannot consume cleanly.
|
||||
|
||||
## Recommended patterns
|
||||
|
||||
### Header/footer split
|
||||
|
||||
Recommended shape:
|
||||
|
||||
- separate tree purpose via singleton markers such as type/language
|
||||
- separate max depth per tree
|
||||
- stable declared trees for required site areas
|
||||
|
||||
### Internal/external mixed navigation
|
||||
|
||||
Recommended shape:
|
||||
|
||||
- explicit external toggle
|
||||
- page foreign key for internal links
|
||||
- external URL only when external is true
|
||||
- preview that makes the choice obvious
|
||||
|
||||
### Multilingual navigation
|
||||
|
||||
Recommended shape:
|
||||
|
||||
- declared trees per language
|
||||
- clear language field
|
||||
- editor-visible grouping of the trees
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- One generic navigation document with no stable tree identity
|
||||
- Weak previews that show only IDs or unclear node labels
|
||||
- No explicit distinction between internal and external targets
|
||||
- Unlimited nesting without an actual UX reason
|
||||
- Admin tree design that does not match frontend loading/runtime rules
|
||||
|
||||
## Verification checklist
|
||||
|
||||
After changing navigation modeling, verify all of these:
|
||||
|
||||
1. Editors can find every intended navigation tree quickly.
|
||||
2. Node previews make internal vs external links obvious.
|
||||
3. Allowed depth matches the site structure.
|
||||
4. Frontend loading still resolves the correct trees.
|
||||
5. SSR still includes required navigation shell data.
|
||||
6. `yarn validate` stays clean.
|
||||
|
||||
## What an LLM should inspect first
|
||||
|
||||
When asked to work on navigation in this starter, inspect in this order:
|
||||
|
||||
1. `api/collections/navigation.yml`
|
||||
2. `tibi-admin-nova/types/admin.d.ts` plus `tibi-admin-nova/docs/collection-config.md` for `viewHint.navigation`
|
||||
3. the frontend navigation loading/rendering path
|
||||
4. SSR assumptions around header/footer shell data
|
||||
5. the website's language and information-architecture requirements
|
||||
|
||||
This prevents navigation edits that are technically valid but editorially or runtime-wise incoherent.
|
||||
@@ -0,0 +1,581 @@
|
||||
---
|
||||
name: nova-pagebuilder-modeling
|
||||
description: Model editor-friendly block systems for tibi-admin-nova. Covers pagebuilder structure, block schemas, preview, drillDown, dependsOn, containerProps.layout, and the required alignment between admin config, frontend block registry, and SSR.
|
||||
---
|
||||
|
||||
# nova-pagebuilder-modeling
|
||||
|
||||
## When to use this skill
|
||||
|
||||
Use this skill when:
|
||||
|
||||
- Building a flexible pagebuilder for pages, landing pages, reusable sections, or site settings
|
||||
- Designing nested `object[]` schemas for blocks in Nova
|
||||
- Deciding how editors should create, scan, reorder, and edit blocks
|
||||
- Translating website requirements into maintainable block types
|
||||
- Refactoring a block system that is technically valid but editor-hostile
|
||||
|
||||
## Goal
|
||||
|
||||
The goal is not just to make blocks storable. The goal is to model a block system that is:
|
||||
|
||||
- understandable for editors
|
||||
- safe to extend over time
|
||||
- easy to preview in Nova
|
||||
- aligned with frontend rendering and SSR
|
||||
- structured enough that an LLM can add new blocks without inventing ad-hoc patterns
|
||||
|
||||
## Source of truth
|
||||
|
||||
Use these sources when designing or reviewing the schema:
|
||||
|
||||
- `tibi-admin-nova/types/admin.d.ts`
|
||||
- `tibi-admin-nova/docs/collection-config.md`
|
||||
- `api/collections/content.yml`
|
||||
- `frontend/src/blocks/`
|
||||
- `frontend/src/blocks/BlockRenderer.svelte`
|
||||
- `types/global.d.ts`
|
||||
|
||||
Do not model pagebuilder structures from memory when current Nova types are available.
|
||||
|
||||
## Core mental model
|
||||
|
||||
In this starter family, a pagebuilder is usually an `object[]` field where each array item represents one block. Each block needs three layers to stay coherent:
|
||||
|
||||
1. **Data model** in collection YAML
|
||||
2. **Render component** in `frontend/src/blocks/`
|
||||
3. **Type and registry alignment** in TypeScript and `BlockRenderer.svelte`
|
||||
|
||||
If one of these layers is missing, the system is incomplete.
|
||||
|
||||
## Design rules
|
||||
|
||||
### 1. Prefer a small block vocabulary with strong reuse
|
||||
|
||||
Do not create a new block type for every tiny content variation.
|
||||
|
||||
Prefer:
|
||||
|
||||
- `hero`
|
||||
- `richText`
|
||||
- `imageText`
|
||||
- `cta`
|
||||
- `featureGrid`
|
||||
- `faq`
|
||||
- `logos`
|
||||
- `testimonials`
|
||||
|
||||
Avoid block libraries that mirror every page one-to-one. That produces brittle schemas and weak editor UX.
|
||||
|
||||
### 2. Every block must be recognizable in lists
|
||||
|
||||
Editors should understand an entry without opening each block.
|
||||
|
||||
Use current Nova preview capabilities on block objects:
|
||||
|
||||
```yaml
|
||||
meta:
|
||||
preview:
|
||||
label: headline
|
||||
secondary: type
|
||||
badge: variant
|
||||
```
|
||||
|
||||
If a block has no single identifying field, use a preview `eval` that combines multiple fields.
|
||||
|
||||
### 3. Large blocks should open in drill-down editing
|
||||
|
||||
If a block contains many fields, nested objects, or repeated items, prefer drill-down editing instead of forcing everything into one long inline form.
|
||||
|
||||
Use `drillDown` when the inline view becomes noisy or error-prone.
|
||||
|
||||
### 4. Use `dependsOn` to keep block forms focused
|
||||
|
||||
Conditional fields are essential in block schemas.
|
||||
|
||||
Use `dependsOn` when:
|
||||
|
||||
- a field is only relevant for one `variant`
|
||||
- a CTA only appears when `showCta` is true
|
||||
- media settings depend on layout choice
|
||||
- a nested group only matters for one block subtype
|
||||
|
||||
Do not dump every optional field into the same visible form.
|
||||
|
||||
### 5. Use `containerProps.layout` to model editor flow
|
||||
|
||||
Block editing should reflect visual and editorial grouping, not raw storage order.
|
||||
|
||||
Use `containerProps.layout` to:
|
||||
|
||||
- put related fields side by side
|
||||
- separate content from appearance controls
|
||||
- reduce scroll depth
|
||||
- keep critical fields in the first viewport
|
||||
|
||||
### 6. Keep the block model SSR-safe
|
||||
|
||||
If a block is page-critical, it must render correctly in SSR too.
|
||||
|
||||
That means:
|
||||
|
||||
- the block data must come through the same content-loading path as the page
|
||||
- the Svelte block component must be importable by the SSR bundle
|
||||
- the renderer must not rely on browser-only APIs during initial render
|
||||
|
||||
### 7. Model for migrations, not just first delivery
|
||||
|
||||
Blocks evolve. Design schemas so fields can be added without breaking every existing entry.
|
||||
|
||||
Prefer additive changes and explicit defaults over brittle implicit assumptions.
|
||||
|
||||
## Recommended modeling workflow
|
||||
|
||||
### Step 1: Start from editorial jobs, not component names
|
||||
|
||||
Define what editors need to do:
|
||||
|
||||
- create a page hero
|
||||
- add structured intro content
|
||||
- place testimonials
|
||||
- create CTA sections
|
||||
- insert FAQs
|
||||
- reuse site-wide sections
|
||||
|
||||
Then derive block types from these jobs.
|
||||
|
||||
### Step 2: Decide which data belongs at page level and which belongs inside blocks
|
||||
|
||||
Keep page-level fields for concerns that apply to the whole page, such as:
|
||||
|
||||
- path
|
||||
- language
|
||||
- SEO
|
||||
- publication
|
||||
- translation linking
|
||||
|
||||
Keep block-level fields for modular content slices.
|
||||
|
||||
### Step 3: Define the block array schema
|
||||
|
||||
Typical pagebuilder field:
|
||||
|
||||
```yaml
|
||||
- name: blocks
|
||||
type: object[]
|
||||
meta:
|
||||
label: { de: "Blöcke", en: "Blocks" }
|
||||
widget: pagebuilder
|
||||
pagebuilder:
|
||||
blockTypeField: type
|
||||
preview:
|
||||
label: headline
|
||||
secondary: type
|
||||
badge: variant
|
||||
drillDown: true
|
||||
subFields:
|
||||
- name: type
|
||||
type: string
|
||||
meta:
|
||||
widget: select
|
||||
choices:
|
||||
- value: hero
|
||||
label: Hero
|
||||
- value: richText
|
||||
label: Rich text
|
||||
- value: featureGrid
|
||||
label: Feature grid
|
||||
- name: headline
|
||||
type: string
|
||||
- name: variant
|
||||
type: string
|
||||
meta:
|
||||
dependsOn:
|
||||
eval: "$parent.type === 'hero'"
|
||||
```
|
||||
|
||||
The exact shape can vary, but the pattern stays the same: block type first, then a previewable and conditionally focused schema.
|
||||
|
||||
### Step 3a: Build and wire the block registry
|
||||
|
||||
In current Tibi/Nova projects, the pagebuilder registry is typically not implicit. Nova loads it from the admin bundle via `meta.pagebuilder.blockRegistry.file`.
|
||||
|
||||
The concrete chain is:
|
||||
|
||||
1. define the registry in `frontend/src/admin.ts`
|
||||
2. export it as `blockRegistry`
|
||||
3. build the admin bundle with `yarn build`
|
||||
4. point the collection field or collection meta to the built module
|
||||
|
||||
The concrete file names vary by project, but the pattern is the same: registry code lives in the admin bundle and collection config points to the built admin asset.
|
||||
|
||||
Typical starter pattern:
|
||||
|
||||
```ts
|
||||
const blockRegistry = {
|
||||
hero: {
|
||||
label: "Hero",
|
||||
render(container, row, context) {
|
||||
return {
|
||||
update(nextRow, nextContext) {},
|
||||
destroy() {},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export { blockRegistry }
|
||||
```
|
||||
|
||||
And the collection wiring:
|
||||
|
||||
```yaml
|
||||
meta:
|
||||
widget: pagebuilder
|
||||
pagebuilder:
|
||||
blockTypeField: type
|
||||
blockRegistry:
|
||||
file: /_/assets/dist/admin.mjs?v=${ADMIN_ASSET_VERSION}
|
||||
```
|
||||
|
||||
Important constraints for this setup:
|
||||
|
||||
- the registry module must be part of the admin bundle, not a random standalone file outside the build pipeline
|
||||
- the exported registry keys must match the block type values stored in the collection
|
||||
- after registry changes, run `yarn build` so `frontend/dist/admin.mjs` is regenerated
|
||||
- if the registry file path in YAML and the built admin asset diverge, Nova can still render the schema but the pagebuilder preview/picker loses its real block definitions
|
||||
- in Nova pagebuilder preview, file fields are already normalized by the admin backend to absolute `http(s)://...` URLs when appropriate; preview code must not prepend `apiBase`, `projectBase`, or other frontend URL helpers when the value is already absolute
|
||||
- Nova may also pass preview rows with hydrated `_lookup` data for FK-like fields; the registry/block preview should consume that data directly instead of trying to re-fetch or manually hydrate references inside the admin preview
|
||||
- For medialib-based images, prefer the same shared frontend widget used on the public site rather than preview-only URL logic. The widget/helper must honor `apiBaseOverride` so filter URLs, placeholders, and medialib files keep working inside admin preview.
|
||||
- Nova's `render(container, row, context)` provides API path information. **`context.projectBase`** contains the full project-specific API base including namespace. `context.apiBase` may only contain the generic `/api/` root and is often not sufficient for project-scoped collection endpoints. Blocks that load collection data in preview should therefore use the project-specific base when one is available:
|
||||
|
||||
```ts
|
||||
import { apiBaseOverride } from "./lib/store"
|
||||
import { get } from "svelte/store"
|
||||
const prev = get(apiBaseOverride)
|
||||
if (context?.projectBase) apiBaseOverride.set(String(context.projectBase))
|
||||
// mount(BlockRenderer, ...)
|
||||
// in destroy(): apiBaseOverride.set(prev)
|
||||
```
|
||||
|
||||
When a pagebuilder block renders images from medialib, prefer this pattern:
|
||||
|
||||
1. request lookup-resolved medialib entries in the data load
|
||||
2. pass the resolved entry into the shared image widget
|
||||
3. let that widget decide filter sizing from explicit `filter` or `minWidth`
|
||||
4. rely on `apiBaseOverride` / `context.projectBase` for admin-safe URL resolution
|
||||
|
||||
Do not create a second, preview-only image rendering path that diverges from the public frontend. That usually causes broken placeholders, wrong filter URLs, or SSR/admin mismatches later.
|
||||
|
||||
Use collection-level `meta.pagebuilder.blockRegistry.file` when several pagebuilder fields share the same registry. Override at field level only when one field genuinely needs a different registry.
|
||||
|
||||
### Step 4: Map each block type to a frontend component
|
||||
|
||||
Every allowed `type` value in the schema must be handled in `BlockRenderer.svelte`.
|
||||
|
||||
Do not leave “temporary” admin-only block types without a renderer unless they are truly non-public and intentionally excluded.
|
||||
|
||||
## Frontend preparation requirements
|
||||
|
||||
For this starter, pagebuilder work is only half done when the collection schema exists. The frontend must be prepared explicitly so block-based rendering stays maintainable.
|
||||
|
||||
### 1. Keep one clear renderer boundary
|
||||
|
||||
`frontend/src/blocks/BlockRenderer.svelte` should remain the central registry that maps `block.type` to concrete Svelte components.
|
||||
|
||||
That means:
|
||||
|
||||
- every public block type in the schema gets one renderer branch
|
||||
- unknown block handling stays explicit
|
||||
- block selection logic stays centralized instead of being scattered across many unrelated files
|
||||
|
||||
Do not distribute block-type branching across the app shell, page components, and nested helpers at the same time.
|
||||
|
||||
### 2. Use a stable component contract
|
||||
|
||||
Each block component should receive the block object in a consistent way.
|
||||
|
||||
In this starter, the default contract is:
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
let { block }: { block: ContentBlockEntry } = $props()
|
||||
</script>
|
||||
```
|
||||
|
||||
This matters because the block system becomes much easier to extend when every component follows the same top-level prop contract.
|
||||
|
||||
If a block needs additional derived data, derive it inside the component or in a small helper, but do not invent a different top-level prop API for every block.
|
||||
|
||||
### 3. Keep `ContentBlockEntry` aligned with real frontend usage
|
||||
|
||||
The frontend preparation is incomplete until `types/global.d.ts` can express the fields the block components actually read.
|
||||
|
||||
Whenever a new block type or field is added, verify alignment between:
|
||||
|
||||
- collection YAML subfields
|
||||
- `ContentBlockEntry`
|
||||
- the block component implementation
|
||||
- `BlockRenderer.svelte`
|
||||
|
||||
If a component reads fields that are only implicit or typed as vague leftovers, the pagebuilder is not ready for reliable future extension.
|
||||
|
||||
### 4. Plan lookup data together with the block model
|
||||
|
||||
If blocks reference media or foreign entities, the frontend must be prepared to receive the resolved lookup data through the page-loading path.
|
||||
|
||||
For this starter, that usually means checking the lookup strings used when loading content in `App.svelte`.
|
||||
|
||||
For Nova admin previews, treat the incoming row differently from a raw frontend API payload:
|
||||
|
||||
- `_lookup` may already be hydrated by the admin preview pipeline
|
||||
- file/image values may already be absolute URLs
|
||||
|
||||
Do not add preview logic that blindly rewrites file URLs or assumes it still has to hydrate foreign references before rendering the admin pagebuilder preview.
|
||||
|
||||
Do not add a block that depends on:
|
||||
|
||||
- media lookups
|
||||
- referenced collections
|
||||
- nested foreign references
|
||||
|
||||
without also updating the content-loading layer so the renderer receives the required `_lookup` data.
|
||||
|
||||
### 5. Treat SSR compatibility as part of frontend preparation
|
||||
|
||||
A pagebuilder block is not frontend-ready if it only works after hydration.
|
||||
|
||||
Every public block should render safely during SSR:
|
||||
|
||||
- no unconditional `window`/`document` usage at module top level
|
||||
- browser-only behavior guarded inside `typeof window !== "undefined"`
|
||||
- meaningful initial markup without waiting for client-only effects
|
||||
|
||||
If a block absolutely requires browser APIs, keep the browser-only part small and ensure the surrounding block still renders a stable SSR shell.
|
||||
|
||||
### 6. Unknown block handling should help development without hiding errors
|
||||
|
||||
`BlockRenderer.svelte` should make unknown block types visible enough during development that schema/frontend drift is caught early.
|
||||
|
||||
For this starter, the current renderer already has a development-side unknown-block fallback. Keep a mechanism like that in place when the demo renderer is refactored.
|
||||
|
||||
Do not silently swallow unknown block types in a way that makes editor-created content disappear with no signal.
|
||||
|
||||
### 7. Keep block components presentation-focused
|
||||
|
||||
Pagebuilder block components should mostly render data, not own cross-page application logic.
|
||||
|
||||
Prefer:
|
||||
|
||||
- block-local formatting and small derived values
|
||||
- presentational composition
|
||||
- small helper components inside `frontend/src/blocks/`
|
||||
|
||||
Avoid pushing these concerns into block components unless there is a strong reason:
|
||||
|
||||
- route loading
|
||||
- global app state orchestration
|
||||
- unrelated API fetching
|
||||
- page-level navigation concerns
|
||||
|
||||
### 7a. Admin pagebuilder preview: CSS custom properties in shadow DOM
|
||||
|
||||
The Nova pagebuilder renders block previews in an isolated DOM context (shadow DOM or detached subtree). Tailwind 4's `@theme` directive generates CSS custom properties on `:root`, but these do **not** cascade into shadow DOM contexts.
|
||||
|
||||
**Consequence:** Block previews in the admin can have wrong colors (light text instead of dark, missing brand colors) because `var(--color-ink)` resolves to nothing.
|
||||
|
||||
**Fix:** Add a `:host` selector in the project's CSS file that redeclares the theme variables for the shadow DOM context. Also set a hardcoded `color` fallback on `[data-admin-preview]` since the Nova preview container has this attribute.
|
||||
|
||||
```css
|
||||
:host,
|
||||
[data-admin-preview] {
|
||||
--color-ink: #2c3e45;
|
||||
/* … all theme color variables … */
|
||||
font-family: "Inter Tight", system-ui, sans-serif;
|
||||
color: #2c3e45; /* hardcoded fallback, not var() */
|
||||
}
|
||||
```
|
||||
|
||||
Verify by checking admin pagebuilder block preview after any CSS theme changes.
|
||||
|
||||
### 7b. Admin pagebuilder preview: API calls from dynamic blocks
|
||||
|
||||
Blocks that load data via API (e.g. `CategoryGridBlock` using `getCachedEntries`) need the correct API base URL in the admin preview. The Nova pagebuilder's `render()` callback receives a `context` object with optional `apiBase` and `namespace`, but not all versions provide these.
|
||||
|
||||
The `admin.mjs` is loaded by the pagebuilder via dynamic `import()` — the URL is resolved relative to the admin page, NOT relative to the API. So `import.meta.url` contains the admin's page URL (e.g. `/_/assets/dist/admin.mjs`), not the tibi-server's API URL. Regex extraction from `import.meta.url` for the pattern `/api/_/{namespace}/` does NOT work for this reason.
|
||||
|
||||
**Reliable approach:** use multiple fallbacks in `admin.ts`, with the admin hostname pattern as the most robust:
|
||||
|
||||
1. `context.apiBase` (from Nova when available)
|
||||
2. `context.namespace` (from Nova)
|
||||
3. `import.meta.url` regex (works when admin serves admin.mjs through its own API proxy)
|
||||
4. **Hostname extraction**: admin URL is `{project}-tibiadmin.{domain}` → extract project name
|
||||
5. DOM scan: find any element with `src`/`href` containing `/api/_/{namespace}/`
|
||||
|
||||
```ts
|
||||
const prevApiBase = get(apiBaseOverride)
|
||||
let ns: string | null = null
|
||||
|
||||
if (context?.apiBase) {
|
||||
apiBaseOverride.set(String(context.apiBase))
|
||||
} else {
|
||||
if (context?.namespace) ns = String(context.namespace)
|
||||
if (!ns) {
|
||||
try {
|
||||
ns = ((import.meta as any).url || "").match(/\/api\/_\/([^/]+)\//)?.[1]
|
||||
} catch {}
|
||||
}
|
||||
// Most reliable: admin hostname is always {namespace}-tibiadmin.{domain}
|
||||
if (!ns && typeof window !== "undefined") {
|
||||
const h = window.location.hostname.match(/^(.+?)-tibiadmin\./)
|
||||
if (h) ns = h[1]
|
||||
}
|
||||
// Fallback: scan DOM for API references
|
||||
if (!ns && typeof document !== "undefined") {
|
||||
const el = document.querySelector('[src*="/api/_/"], [href*="/api/_/"]')
|
||||
if (el) {
|
||||
const a = el.getAttribute("src") || el.getAttribute("href") || ""
|
||||
ns = a.match(/\/api\/_\/([^/]+)\//)?.[1] || null
|
||||
}
|
||||
}
|
||||
if (ns) apiBaseOverride.set(`/api/_/${ns}/`)
|
||||
}
|
||||
```
|
||||
|
||||
Set the `apiBaseOverride` store BEFORE mounting the block component so API calls inside `$effect` use the correct base.
|
||||
|
||||
The Nova pagebuilder renders block previews in an isolated DOM context (shadow DOM or detached subtree). Tailwind 4's `@theme` directive generates CSS custom properties on `:root`, but these do **not** cascade into shadow DOM contexts.
|
||||
|
||||
**Consequence:** Block previews in the admin can have wrong colors (light text instead of dark, missing brand colors) because `var(--color-ink)` resolves to nothing.
|
||||
|
||||
**Fix:** Add a `:host` selector in the project's CSS file that redeclares the theme variables for the shadow DOM context. Also set a hardcoded `color` fallback on `[data-admin-preview]` since the Nova preview container has this attribute.
|
||||
|
||||
```css
|
||||
:host,
|
||||
[data-admin-preview] {
|
||||
--color-ink: #2c3e45;
|
||||
--color-ink-2: #3a4d56;
|
||||
/* … all theme color variables … */
|
||||
font-family: "Inter Tight", system-ui, sans-serif;
|
||||
color: #2c3e45; /* hardcoded fallback, not var() */
|
||||
}
|
||||
```
|
||||
|
||||
Verify by checking admin pagebuilder block preview after any CSS theme changes.
|
||||
|
||||
### 8. Prepare for styling consistency across blocks
|
||||
|
||||
A block system works better when blocks share a few stable layout conventions.
|
||||
|
||||
Examples:
|
||||
|
||||
- container width choices
|
||||
- vertical spacing conventions
|
||||
- anchor/id behavior
|
||||
- CTA shape and link handling
|
||||
- media aspect ratio conventions
|
||||
|
||||
Do not let every new block invent its own spacing, width, and link semantics from scratch unless the design system really requires it.
|
||||
|
||||
## When a block is actually ready in the frontend
|
||||
|
||||
A new pagebuilder block should only be considered integrated when all of these are true:
|
||||
|
||||
1. The schema contains the block type and required subfields.
|
||||
2. `ContentBlockEntry` expresses the fields used by the block.
|
||||
3. A dedicated Svelte block component exists in `frontend/src/blocks/`.
|
||||
4. `BlockRenderer.svelte` routes the block type to that component.
|
||||
5. Any required lookup data is loaded by the app content-loading path.
|
||||
6. The block renders acceptably in SSR and browser navigation.
|
||||
7. Unknown or stale block types remain debuggable.
|
||||
|
||||
### Step 5: Keep types aligned
|
||||
|
||||
Update project types when the block model changes.
|
||||
|
||||
In this starter family, block schemas usually affect:
|
||||
|
||||
- `types/global.d.ts`
|
||||
- Svelte component props
|
||||
- block renderer branching
|
||||
|
||||
If TypeScript cannot express the new block shape, the schema work is incomplete.
|
||||
|
||||
## Practical block design patterns
|
||||
|
||||
### Hero block
|
||||
|
||||
Use for top-of-page messaging. Keep the editor form short and obvious.
|
||||
|
||||
Typical fields:
|
||||
|
||||
- eyebrow
|
||||
- headline
|
||||
- subline
|
||||
- image
|
||||
- cta
|
||||
- variant
|
||||
|
||||
Use `dependsOn` for variant-specific media and CTA settings.
|
||||
|
||||
### Rich text block
|
||||
|
||||
Use for long-form body content. Avoid mixing it with too many presentational toggles.
|
||||
|
||||
Typical fields:
|
||||
|
||||
- headline
|
||||
- body
|
||||
- maxWidth
|
||||
|
||||
### Feature grid block
|
||||
|
||||
Use nested repeatable objects for feature items, but make the parent block previewable.
|
||||
|
||||
Typical fields:
|
||||
|
||||
- headline
|
||||
- items[]
|
||||
- columns
|
||||
- variant
|
||||
|
||||
For `items[]`, add its own preview so editors can scan the nested list.
|
||||
|
||||
### Reusable section reference
|
||||
|
||||
If the same content must appear on many pages, consider a dedicated collection plus foreign reference instead of copy-pasting large pagebuilder blocks.
|
||||
|
||||
Use foreign previews so editors understand the referenced entity before opening it.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- One block type per page template fragment with no reuse
|
||||
- Giant catch-all block with dozens of unrelated optional fields
|
||||
- No preview on nested objects
|
||||
- No drill-down for large objects
|
||||
- Using array order as the only meaning without labels or previews
|
||||
- Frontend blocks that exist without matching collection schema
|
||||
- Collection schema values that have no renderer
|
||||
|
||||
## Verification checklist
|
||||
|
||||
After adding or changing pagebuilder blocks, verify all of these:
|
||||
|
||||
1. Editors can identify blocks quickly in Nova.
|
||||
2. The block form hides irrelevant fields.
|
||||
3. Reordering works without losing meaning.
|
||||
4. `BlockRenderer.svelte` handles every public block type.
|
||||
5. SSR renders the affected page correctly.
|
||||
6. `yarn validate` stays clean.
|
||||
|
||||
## What an LLM should inspect first
|
||||
|
||||
When asked to extend a pagebuilder on this starter, inspect in this order:
|
||||
|
||||
1. `api/collections/content.yml`
|
||||
2. `frontend/src/blocks/BlockRenderer.svelte`
|
||||
3. existing files in `frontend/src/blocks/`
|
||||
4. `types/global.d.ts`
|
||||
5. `tibi-admin-nova/types/admin.d.ts`
|
||||
|
||||
This order prevents schema-only or frontend-only changes.
|
||||
@@ -0,0 +1,243 @@
|
||||
---
|
||||
name: permissions-and-editor-workflows
|
||||
description: Design safe editor workflows with current tibi-server permissions and Nova authoring patterns. Covers collection permissions, field-level readonly/hidden rules, roles, tokens, and how admin UX should reflect real editorial boundaries.
|
||||
---
|
||||
|
||||
# permissions-and-editor-workflows
|
||||
|
||||
## When to use this skill
|
||||
|
||||
Use this skill when:
|
||||
|
||||
- A project needs more than one editor/admin role
|
||||
- Collections or fields should be restricted by role or token
|
||||
- You need readonly/hidden field logic for real editorial workflows
|
||||
- You want Nova UX to reflect actual server-side permissions instead of pretending every field is editable
|
||||
|
||||
## Goal
|
||||
|
||||
The goal is to design permissions as part of the editorial workflow, not as a last-minute access check.
|
||||
|
||||
On this stack, permissions affect:
|
||||
|
||||
- API methods
|
||||
- field visibility
|
||||
- field editability
|
||||
- collection visibility
|
||||
- token-based integrations
|
||||
- admin usability
|
||||
|
||||
## Source of truth
|
||||
|
||||
Use these sources when implementing or reviewing permissions:
|
||||
|
||||
- `tibi-server/docs/17-field-level-permissions.md`
|
||||
- `tibi-server/docs/05-authentication.md`
|
||||
- relevant collection YAML files
|
||||
- `tibi-admin-nova/types/admin.d.ts`
|
||||
|
||||
## Permission layers
|
||||
|
||||
At minimum, reason about permissions on these levels:
|
||||
|
||||
- collection methods (`get`, `post`, `put`, `delete`)
|
||||
- field-level `readonlyFields`
|
||||
- field-level `hiddenFields`
|
||||
- field-definition overrides (`readonly`, `hidden`)
|
||||
- dynamic eval-based field rules
|
||||
- collection `meta.hide` for sidebar visibility
|
||||
|
||||
Do not flatten all of this into one vague notion of “editor access”.
|
||||
|
||||
**Custom role names:** Permission set keys in collection/action YAML are arbitrary strings. You can define any role name (e.g. `editor`, `reviewer`, `publisher`, `seo-manager`) and assign users with matching permissions. Combined with org/team membership (see `tibi-server/docs/18-orgs-teams.md`), this enables fine-grained editorial workflows beyond the built-in `public` and `user` roles.
|
||||
|
||||
### The 3-layer cascade model
|
||||
|
||||
Field-level permissions follow a strict 3-layer cascade:
|
||||
|
||||
1. **Collection-Level** (`collection.readonlyFields`, `collection.hiddenFields`): Base set applied to all permission sets.
|
||||
2. **PermissionSet-Level** (`permissions.<role>.readonlyFields`, `permissions.<role>.hiddenFields`): Adds to or removes from the collection-level set. Prefix a field with `-` to negate (e.g. `-createdBy` removes it from the effective set).
|
||||
3. **Field-Definition Override** (`field.readonly`, `field.hidden`): Absolute override — `true` forces the field into the set, `false` forces it out regardless of upper layers.
|
||||
|
||||
**Important:** Field-definition `readonly`/`hidden` also supports **eval expressions** (JS) for per-document dynamic evaluation. Eval rules are evaluated in a separate phase after the static cascade (Phase 1 = static cascade, Phase 2 = per-document eval). Admin role (role=0) bypasses all field-level restrictions.
|
||||
|
||||
See `tibi-server/docs/17-field-level-permissions.md` for the full reference with examples and eval expression context variables (`$`, `$this`, `$auth`, `$method`, `$project`, `$namespace`).
|
||||
|
||||
## Collection-level workflow design
|
||||
|
||||
Before implementing permissions, define who does what.
|
||||
|
||||
Typical roles/workflows:
|
||||
|
||||
- public readers
|
||||
- editors creating and updating content
|
||||
- reviewers or restricted staff
|
||||
- admins configuring structure and sensitive fields
|
||||
- token-based integrations
|
||||
|
||||
Then map those responsibilities to explicit permission sets.
|
||||
|
||||
## Field-level permissions
|
||||
|
||||
Current tibi-server field permissions are strong and should be used deliberately.
|
||||
|
||||
Important behavior:
|
||||
|
||||
- `readonlyFields`: writes fail with `400` if those fields are sent
|
||||
- `hiddenFields`: writes fail with `400`, reads strip the fields from responses
|
||||
- field-level `readonly` / `hidden` can override or dynamically extend behavior
|
||||
|
||||
This means field permissions are not mere UI hints. They are enforced server-side.
|
||||
|
||||
## Config delivery matters to the admin UX
|
||||
|
||||
Field permissions also affect what the client receives from the project config.
|
||||
|
||||
Important behavior for non-admin users:
|
||||
|
||||
- effective readonly information is exposed through `yourPermissions[collection].readonlyFields`
|
||||
- statically hidden field definitions are removed from `fields[]`
|
||||
- `hiddenFields` arrays are not delivered as-is to non-admin clients
|
||||
- eval-based field rules stay relevant because they depend on document context
|
||||
|
||||
Implication:
|
||||
|
||||
- Nova and other clients should reflect the real config/permission output instead of pretending every field is always present and editable
|
||||
|
||||
If later agents debug “missing” fields in the admin, check permission-shaped config delivery before assuming the admin UI is broken.
|
||||
|
||||
## Dynamic field rules
|
||||
|
||||
Use eval-based field rules when permissions depend on document state.
|
||||
|
||||
Typical examples:
|
||||
|
||||
- a field becomes readonly after approval
|
||||
- an internal note is hidden from non-admin roles
|
||||
- a billing field is editable only before status changes
|
||||
|
||||
Use these rules to model real editorial transitions, not to create confusing surprises.
|
||||
|
||||
For each eval-based rule, later agents should be able to name:
|
||||
|
||||
- one allowed write scenario
|
||||
- one denied write scenario
|
||||
- the document state that flips the rule
|
||||
|
||||
## Admin UX must reflect permission reality
|
||||
|
||||
If a field is hidden or readonly for a role, the Nova configuration and layout should support that reality.
|
||||
|
||||
Recommended patterns:
|
||||
|
||||
- keep critical restricted fields out of primary editorial flow
|
||||
- place admin-only or system-managed fields in sidebars or dedicated sections
|
||||
- avoid forms whose main content becomes unusable when half the fields are hidden by role
|
||||
- design previews so editors can still identify entries even when some internal fields are hidden
|
||||
|
||||
Server permissions are authoritative, but poor admin layout can still create a bad workflow.
|
||||
|
||||
## Permission matrix before YAML
|
||||
|
||||
Before writing or changing permission sets, write down a small matrix for the real actors.
|
||||
|
||||
Typical matrix columns:
|
||||
|
||||
- actor or role
|
||||
- collections they can read
|
||||
- collections they can write
|
||||
- fields hidden from them
|
||||
- fields readonly for them
|
||||
- machine/token access they need
|
||||
|
||||
Typical actors:
|
||||
|
||||
- public
|
||||
- editor
|
||||
- reviewer or publisher
|
||||
- admin
|
||||
- machine token or integration
|
||||
|
||||
This avoids permission YAML that is locally correct but globally incoherent.
|
||||
|
||||
## Tokens and integrations
|
||||
|
||||
Remember that token-based integrations can have their own permission sets.
|
||||
|
||||
Use this for:
|
||||
|
||||
- inbound integrations
|
||||
- service accounts
|
||||
- controlled automation
|
||||
- frontend-to-backend machine use cases when appropriate
|
||||
|
||||
Do not reuse broad admin permissions for integrations if a narrow token permission set is enough.
|
||||
|
||||
## Permission-driven architecture decisions
|
||||
|
||||
Permissions can change the correct data model.
|
||||
|
||||
Examples:
|
||||
|
||||
- if sensitive internal notes should never be visible to normal editors, consider whether they belong in the same collection or a separate one
|
||||
- if a public form creates internal records, the public action and the internal collection should have separate permission boundaries
|
||||
- if a workflow has approval stages, model status transitions and readonly behavior explicitly
|
||||
|
||||
## Recommended patterns
|
||||
|
||||
### Editorial content workflow
|
||||
|
||||
Recommended shape:
|
||||
|
||||
- editors can create and update content fields
|
||||
- publication/system fields may be restricted or conditionally readonly
|
||||
- admin-only technical fields are hidden or isolated in the UI
|
||||
|
||||
### Sensitive internal data
|
||||
|
||||
Recommended shape:
|
||||
|
||||
- hide internal-only fields from normal editors
|
||||
- prefer explicit server-side rules over relying on UI omission
|
||||
- ensure previews do not depend on hidden-only data
|
||||
|
||||
### Approval-style workflow
|
||||
|
||||
Recommended shape:
|
||||
|
||||
- status field controls editability of specific fields
|
||||
- post-approval fields become readonly via eval rules
|
||||
- admin or reviewer roles retain the intended override path
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- Treating permissions as frontend-only display logic
|
||||
- Leaving sensitive fields visible and merely asking editors not to touch them
|
||||
- Using one broad admin token for every integration
|
||||
- Designing forms that depend on fields many roles cannot access
|
||||
- Adding dynamic readonly/hidden logic without explaining the editorial workflow it represents
|
||||
|
||||
## Verification checklist
|
||||
|
||||
After changing permissions or editor workflows, verify all of these:
|
||||
|
||||
1. Collection methods match the intended role model.
|
||||
2. Hidden and readonly field behavior is correct on API reads/writes.
|
||||
3. Dynamic eval rules behave correctly for the intended document states.
|
||||
4. At least one representative allowed write and one denied write were checked for each important workflow state.
|
||||
5. Non-admin config delivery still makes sense for the admin UI and field layout.
|
||||
6. Nova forms remain usable for the non-admin roles that actually work there.
|
||||
7. Token/integration permissions are narrower than admin access when possible.
|
||||
8. `yarn validate` stays clean.
|
||||
|
||||
## What an LLM should inspect first
|
||||
|
||||
When asked to design permissions on this starter, inspect in this order:
|
||||
|
||||
1. the relevant collection YAML
|
||||
2. the intended human roles and machine integrations
|
||||
3. field-level readonly/hidden needs
|
||||
4. whether the current Nova layout still makes sense under those restrictions
|
||||
5. any workflow states that require dynamic eval rules
|
||||
|
||||
This prevents access rules that are technically correct but operationally unusable.
|
||||
@@ -0,0 +1,546 @@
|
||||
---
|
||||
name: playwright-testing
|
||||
description: Build, debug, and extend the current Playwright test setup for API, desktop E2E, mobile E2E, and visual checks. Use when changing tests, seeding deterministic content, or validating frontend/API behavior against the reverse-proxied CODING_URL.
|
||||
---
|
||||
|
||||
# playwright-testing
|
||||
|
||||
## When to use this skill
|
||||
|
||||
Use this skill when:
|
||||
|
||||
- Adding or updating Playwright API, E2E, mobile, or visual tests
|
||||
- Debugging failing tests in `tests/`
|
||||
- Extending deterministic seed data for frontend or API coverage
|
||||
- Verifying how `CODING_URL`, `ADMIN_TOKEN`, and collection permissions affect tests
|
||||
- Deciding where a frontend assertion belongs: API spec, E2E spec, or visual regression spec
|
||||
|
||||
---
|
||||
|
||||
## Current test architecture
|
||||
|
||||
This starter uses Playwright across four slices:
|
||||
|
||||
- `tests/api/` for API-level checks
|
||||
- `tests/e2e/` for desktop browser behavior
|
||||
- `tests/e2e-admin/` for committed admin smoke coverage
|
||||
- `tests/e2e-mobile/` for mobile behavior
|
||||
- `tests/e2e-visual/` for screenshot-based regression tests
|
||||
|
||||
The current baseline is deterministic and seed-driven, not demo-content-driven.
|
||||
|
||||
### Core files
|
||||
|
||||
| File | Responsibility |
|
||||
| ----------------------------------- | -------------------------------------------------------------------------------- |
|
||||
| `playwright.config.ts` | Playwright projects, `baseURL`, global setup/teardown, BrowserSync-safe defaults |
|
||||
| `tests/fixtures/test-constants.ts` | `ADMIN_TOKEN`, `API_BASE`, `TEST_BASE_URL`, seeded route constants |
|
||||
| `tests/global-setup.ts` | Verifies `TEST_BASE_URL`, probes `/api`, seeds deterministic content |
|
||||
| `tests/global-teardown.ts` | Cleans seeded content and disposes shared API contexts |
|
||||
| `tests/api/helpers/admin-api.ts` | Shared admin CRUD helper using the static `Token:` header |
|
||||
| `tests/api/helpers/seed-data.ts` | Seed definitions and seed cleanup for deterministic content pages |
|
||||
| `tests/fixtures/console-monitor.ts` | Fails browser-based tests on unexpected page, console, or request errors |
|
||||
| `tests/e2e/fixtures.ts` | Desktop browser fixtures and SPA helpers |
|
||||
| `tests/e2e-admin/fixtures.ts` | Admin login helpers and admin smoke fixture setup |
|
||||
| `tests/e2e-mobile/fixtures.ts` | Mobile browser fixtures and hamburger-menu helpers |
|
||||
|
||||
---
|
||||
|
||||
## Environment prerequisites
|
||||
|
||||
### Always use the configured `CODING_URL`
|
||||
|
||||
Playwright uses `TEST_BASE_URL` from `tests/fixtures/test-constants.ts`.
|
||||
|
||||
Resolution order:
|
||||
|
||||
1. `process.env.CODING_URL`
|
||||
2. `CODING_URL` from `.env`
|
||||
3. fallback `http://localhost:3000`
|
||||
|
||||
For this project, prefer the reverse-proxied `CODING_URL` from `.env` whenever it serves both:
|
||||
|
||||
- `/`
|
||||
- `/api/...`
|
||||
|
||||
If `/api/...` returns HTML instead of JSON, the seeded setup is not usable and `globalSetup` should fail fast.
|
||||
|
||||
### Admin host and default credentials
|
||||
|
||||
Admin browser tests use `TEST_ADMIN_BASE_URL` from `tests/fixtures/test-constants.ts`.
|
||||
|
||||
Resolution order:
|
||||
|
||||
1. `process.env.CODING_TIBIADMIN_URL`
|
||||
2. `CODING_TIBIADMIN_URL` from `.env`
|
||||
3. fallback `http://localhost:3000`
|
||||
|
||||
The current smoke setup assumes the default dev login unless overridden via env vars:
|
||||
|
||||
- `ADMIN_UI_USERNAME` default: `admin`
|
||||
- `ADMIN_UI_PASSWORD` default: `admin`
|
||||
|
||||
Keep this only for local/dev smoke coverage. Do not turn production credentials into committed test defaults.
|
||||
|
||||
### Static project token vs JWT user auth
|
||||
|
||||
This distinction matters for tests:
|
||||
|
||||
- `Token:` is the static project/admin token from `api/config.yml.env`
|
||||
- `X-Auth-Token` is a JWT user token from a login flow
|
||||
|
||||
Collection permissions under `user:` do **not** grant access for static `Token:` requests.
|
||||
If tests seed or mutate a collection through `ADMIN_TOKEN`, that collection must define explicit token permissions like:
|
||||
|
||||
```yaml
|
||||
permissions:
|
||||
"token:${ADMIN_TOKEN}":
|
||||
methods:
|
||||
get: true
|
||||
post: true
|
||||
put: true
|
||||
delete: true
|
||||
```
|
||||
|
||||
This is required for collections the seed helper writes to, such as `content`.
|
||||
|
||||
---
|
||||
|
||||
## BrowserSync navigation rule
|
||||
|
||||
BrowserSync keeps a WebSocket open permanently. Because of that:
|
||||
|
||||
- do **not** wait for `networkidle`
|
||||
- do **not** rely on `load`
|
||||
- use `domcontentloaded`
|
||||
|
||||
The shared fixtures already patch navigation helpers accordingly.
|
||||
When writing new tests, keep using the project fixtures rather than raw Playwright `test`.
|
||||
|
||||
## Console watcher
|
||||
|
||||
Browser-based fixtures attach `attachConsoleMonitor(page)` from `tests/fixtures/console-monitor.ts`.
|
||||
|
||||
This monitor records and fails tests on unexpected:
|
||||
|
||||
- `pageerror`
|
||||
- `console.error`
|
||||
- failed network requests except explicitly ignored infrastructure noise
|
||||
|
||||
The intent is to catch real frontend/runtime regressions even when visible assertions still pass.
|
||||
Do not silence app bugs by broadening ignored patterns unless the noise is clearly external infrastructure.
|
||||
|
||||
---
|
||||
|
||||
## Deterministic seed strategy
|
||||
|
||||
The current setup seeds content through the public collection API plus the static `Token:` header.
|
||||
|
||||
Use a hidden per-collection marker field as the default seed identity strategy.
|
||||
In this project the convention is `_testdata: true`.
|
||||
|
||||
### Seed lifecycle
|
||||
|
||||
1. `globalSetup` probes the configured base URL.
|
||||
2. `globalSetup` verifies `/api/content` returns JSON.
|
||||
3. `globalSetup` removes old seeded entries by their hidden test marker before recreating them.
|
||||
4. `globalSetup` creates deterministic seed entries.
|
||||
5. Tests run against those seeded routes.
|
||||
6. `globalTeardown` removes seeded entries again.
|
||||
|
||||
Setup cleanup and teardown cleanup are both required.
|
||||
The setup cleanup handles leftovers from aborted or previously failed runs.
|
||||
The teardown cleanup keeps the environment clean after successful runs.
|
||||
|
||||
### Hidden seed marker pattern
|
||||
|
||||
Prefer this pattern for every collection that may receive test-created data:
|
||||
|
||||
1. Add a hidden boolean field named `_testdata` as the last field in the collection schema.
|
||||
2. Set `_testdata: true` on every seeded entry.
|
||||
3. Let cleanup match `_testdata === true` first.
|
||||
4. Keep older identifiers such as fixed paths or translation keys only as migration fallbacks when existing seed data already used them.
|
||||
|
||||
This is more robust than relying on translation keys because not every collection has a natural grouping field.
|
||||
It also makes leftovers from aborted runs discoverable across heterogeneous collection shapes.
|
||||
|
||||
### Parallel worker rule
|
||||
|
||||
Seed creation and seed cleanup must remain run-scoped, not worker-scoped.
|
||||
|
||||
- perform seed cleanup and creation in `globalSetup`
|
||||
- perform final seed cleanup in `globalTeardown`
|
||||
- do not create or delete shared seeded data in per-test hooks or worker fixtures
|
||||
- keep seeded identifiers deterministic so many workers can read the same seeded dataset safely
|
||||
|
||||
This project runs with many workers.
|
||||
Parallel safety depends on one shared deterministic seed pass before the suite and one shared cleanup pass after the suite, not on each worker mutating shared fixtures independently.
|
||||
|
||||
### Current seeded routes
|
||||
|
||||
Defined in `tests/fixtures/test-constants.ts`:
|
||||
|
||||
- `SEEDED_TEST_CONTENT.home.path`
|
||||
- `SEEDED_TEST_CONTENT.contact.path`
|
||||
- `SEEDED_TEST_CONTENT.inactive.path`
|
||||
|
||||
These are backed by DE/EN content entries in `tests/api/helpers/seed-data.ts`.
|
||||
|
||||
### What the seed currently covers
|
||||
|
||||
- localized page routing
|
||||
- hero rendering
|
||||
- features rendering
|
||||
- richtext rendering
|
||||
- accordion rendering
|
||||
- contact-form rendering
|
||||
- inactive route -> 404 behavior
|
||||
|
||||
When adding new deterministic coverage, extend the seed data instead of asserting against editorial demo content.
|
||||
|
||||
---
|
||||
|
||||
## Which test type to use
|
||||
|
||||
## Checklist-facing minimum contract for derived projects
|
||||
|
||||
When this starter is used to build a real website project, the testing layer should usually cover these contracts explicitly:
|
||||
|
||||
1. deterministic seed setup for the data the suite depends on
|
||||
2. API smoke coverage for public reads and important write/action behavior
|
||||
3. desktop E2E coverage for core public journeys such as homepage, navigation, language switching, and 404 behavior
|
||||
4. admin smoke coverage for stable collection/admin contracts
|
||||
5. pagebuilder registry plus real preview rendering when the project uses block-based authoring
|
||||
6. SSR validation through direct endpoint checks, and committed tests where the SSR contract is central and stable enough
|
||||
|
||||
Do not treat the test suite as an optional polish step. It is one of the delivery contracts of the project.
|
||||
|
||||
### API tests
|
||||
|
||||
Use `tests/api/` when validating:
|
||||
|
||||
- collection filters
|
||||
- public vs token-backed API behavior
|
||||
- seeded content presence or absence
|
||||
- mutation semantics independent of DOM rendering
|
||||
|
||||
Keep them narrow and data-oriented.
|
||||
|
||||
### Desktop E2E tests
|
||||
|
||||
Use `tests/e2e/` when validating:
|
||||
|
||||
- route changes
|
||||
- language switching
|
||||
- SPA navigation behavior
|
||||
- block rendering in the real UI
|
||||
- keyboard/a11y interactions such as skip links
|
||||
|
||||
### Admin smoke tests
|
||||
|
||||
Use `tests/e2e-admin/` when validating stable admin contracts such as:
|
||||
|
||||
- admin login still works in dev
|
||||
- the project dashboard opens correctly
|
||||
- core collections are still reachable
|
||||
- critical collection views still render their configured labels/columns/actions
|
||||
- collection lists render meaningful previews instead of broken placeholders
|
||||
- important field widgets are configured and usable in entry forms
|
||||
- pagebuilder block choosers, block forms, and live previews load correctly
|
||||
|
||||
These tests should stay intentionally narrow. They are regression guards for admin configuration, not full editor journey automation.
|
||||
|
||||
### Mobile E2E tests
|
||||
|
||||
Use `tests/e2e-mobile/` when validating:
|
||||
|
||||
- hamburger menu behavior
|
||||
- responsive visibility
|
||||
- mobile-specific navigation or controls
|
||||
|
||||
### Visual regression tests
|
||||
|
||||
Use `tests/e2e-visual/` only when layout/styling stability matters and a semantic DOM assertion is not enough.
|
||||
|
||||
## SSR validation placement
|
||||
|
||||
Do not try to prove every SSR property only through browser navigation.
|
||||
|
||||
Use direct SSR endpoint checks when:
|
||||
|
||||
- validating route acceptance and canonicalization
|
||||
- validating SSR HTML content
|
||||
- validating cache-hit / cache-miss behavior
|
||||
- validating publication-window effects or cache invalidation after mutations
|
||||
|
||||
Use committed API/E2E tests when:
|
||||
|
||||
- the SSR-related behavior is stable enough to be a long-lived regression contract
|
||||
- the project depends heavily on SSR for page-critical content
|
||||
- a browser-level journey would otherwise hide SSR-specific regressions
|
||||
|
||||
Preferred rule:
|
||||
|
||||
- infrastructure-like SSR checks start as direct endpoint checks
|
||||
- promote them into committed tests when the behavior is important and deterministic enough
|
||||
|
||||
## Admin config coverage strategy
|
||||
|
||||
Use a hybrid approach:
|
||||
|
||||
- committed Playwright smoke tests for stable, repeatable admin contracts
|
||||
- one-shot MCP Playwright or VS Code browser checks for exploratory spot checks and ad-hoc audits
|
||||
|
||||
Committed tests should cover the admin paths that are expected to stay valid across everyday work, for example:
|
||||
|
||||
- login
|
||||
- opening the Nova project dashboard
|
||||
- visibility of the core collections
|
||||
- opening important collection views like `content`
|
||||
- checking that collection tables expose the intended columns, summaries, and preview thumbnails
|
||||
- checking that key widgets like selects, foreign/media pickers, sidebars, and pagebuilder controls actually render
|
||||
- checking that pagebuilder preview updates when block content changes
|
||||
|
||||
One-shot live browser checks are useful when:
|
||||
|
||||
- reviewing a newly added admin configuration once
|
||||
- probing a flaky or hard-to-stabilize UI area before deciding what deserves a real test
|
||||
- checking something highly visual or temporarily environment-specific
|
||||
|
||||
Do not rely on one-shot browser checks as the only safeguard for important admin paths. If a check matters repeatedly, promote it into `tests/e2e-admin/`.
|
||||
|
||||
---
|
||||
|
||||
## Current fixture conventions
|
||||
|
||||
### API
|
||||
|
||||
Use `tests/api/fixtures.ts`.
|
||||
|
||||
Current fixtures:
|
||||
|
||||
- `api`
|
||||
- `adminApi`
|
||||
|
||||
`adminApi` is backed by the static `Token:` header from `ADMIN_TOKEN`.
|
||||
|
||||
### Desktop E2E
|
||||
|
||||
Use `tests/e2e/fixtures.ts`.
|
||||
|
||||
Helpers include:
|
||||
|
||||
- `waitForSpaReady(page)`
|
||||
- `navigateToRoute(page, routePath)`
|
||||
- `clickSpaLink(page, selector)`
|
||||
- automatic console/page/request error monitoring via `attachConsoleMonitor(page)`
|
||||
|
||||
### Admin E2E
|
||||
|
||||
Use `tests/e2e-admin/fixtures.ts`.
|
||||
|
||||
Helpers include:
|
||||
|
||||
- `loginToAdmin(page)`
|
||||
- `openNovaProjectDashboard(page)`
|
||||
- automatic console/page/request error monitoring via `attachConsoleMonitor(page)`
|
||||
|
||||
### Mobile E2E
|
||||
|
||||
Use `tests/e2e-mobile/fixtures.ts`.
|
||||
|
||||
Helpers include:
|
||||
|
||||
- `waitForSpaReady(page)`
|
||||
- `navigateToRoute(page, routePath)`
|
||||
- `openHamburgerMenu(page)`
|
||||
- `closeHamburgerMenuViaEscape(page)`
|
||||
- automatic console/page/request error monitoring via `attachConsoleMonitor(page)`
|
||||
|
||||
### Visual E2E
|
||||
|
||||
Use `tests/e2e-visual/fixtures.ts`.
|
||||
|
||||
Helpers include:
|
||||
|
||||
- `waitForVisualReady(page)`
|
||||
- `prepareForScreenshot(page)`
|
||||
- `expectScreenshot(page, name, opts)`
|
||||
- automatic console/page/request error monitoring via `attachConsoleMonitor(page)`
|
||||
|
||||
Do not reintroduce the old starter `authedPage` / `testUser` assumptions unless the project really needs JWT-user coverage again.
|
||||
|
||||
---
|
||||
|
||||
## Writing stable selectors
|
||||
|
||||
Prefer selectors that reflect current rendered structure and locale:
|
||||
|
||||
- scope language links to the relevant container (`header`, `main`, footer)
|
||||
- avoid ambiguous `getByRole()` selectors when the same link text appears twice
|
||||
- use the actual locale strings from `frontend/src/lib/i18n/locales/*.json`
|
||||
- prefer stable block markers like `data-block="hero"`
|
||||
|
||||
Examples:
|
||||
|
||||
```ts
|
||||
const header = page.locator("header")
|
||||
await header.getByRole("link", { name: "en", exact: true }).click()
|
||||
|
||||
const homeLink = page.locator("main").getByRole("link", { name: "Zur Startseite" })
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Recommended commands
|
||||
|
||||
Run only the slice you changed.
|
||||
|
||||
```bash
|
||||
/usr/bin/node ./node_modules/playwright/cli.js test tests/api/health.spec.ts --project=api
|
||||
/usr/bin/node ./node_modules/playwright/cli.js test tests/e2e/home.spec.ts tests/e2e/demo.spec.ts --project=chromium
|
||||
/usr/bin/node ./node_modules/playwright/cli.js test tests/e2e-admin/smoke.spec.ts --project=admin
|
||||
/usr/bin/node ./node_modules/playwright/cli.js test tests/e2e-mobile/home.mobile.spec.ts --project=mobile-iphonese
|
||||
```
|
||||
|
||||
If the shell environment is broken, calling the Playwright CLI through `/usr/bin/node` is acceptable in this workspace.
|
||||
|
||||
---
|
||||
|
||||
## Common failure patterns
|
||||
|
||||
### `/api/...` returns HTML
|
||||
|
||||
Cause:
|
||||
|
||||
- wrong `CODING_URL`
|
||||
- fallback to BrowserSync without usable API proxy
|
||||
|
||||
Fix:
|
||||
|
||||
- verify the configured `CODING_URL` serves both `/` and `/api/...`
|
||||
|
||||
### `403 {"error":"empty token"}` on POST/PUT/DELETE
|
||||
|
||||
Cause:
|
||||
|
||||
- collection allows `user:` but not `"token:${ADMIN_TOKEN}":`
|
||||
|
||||
Fix:
|
||||
|
||||
- add explicit token permissions on that collection
|
||||
|
||||
### strict mode violations in role selectors
|
||||
|
||||
Cause:
|
||||
|
||||
- multiple matching links in header/footer/mobile menu
|
||||
|
||||
Fix:
|
||||
|
||||
- scope selectors to the intended container
|
||||
- use `exact: true` where needed
|
||||
|
||||
### 404 assertions fail by link text
|
||||
|
||||
Cause:
|
||||
|
||||
- locale-specific text mismatch
|
||||
|
||||
Fix:
|
||||
|
||||
- use the real translated string from the locale JSON files
|
||||
|
||||
---
|
||||
|
||||
## Change workflow
|
||||
|
||||
When extending or fixing tests:
|
||||
|
||||
1. Start from the failing spec or the exact behavior to cover.
|
||||
2. Check whether the needed content already exists in `seed-data.ts`.
|
||||
3. Extend seed data only if the behavior is not already representable.
|
||||
4. Run only the affected Playwright project/spec files.
|
||||
5. Fix selectors or seed shape before widening scope.
|
||||
|
||||
Keep the test basis deterministic. Do not fall back to existing editorial demo content just because it is already present in the database.
|
||||
|
||||
## Admin E2E: Boot abwarten
|
||||
|
||||
Admin-SPA lädt Chunks asynchron. Vor Interaktionen auf sichtbares Login-Formular warten:
|
||||
|
||||
```ts
|
||||
await page.goto("/login")
|
||||
await expect(page.getByLabel(/Benutzername|Username/i)).toBeVisible({ timeout: 20000 })
|
||||
```
|
||||
|
||||
Danach erst fill/click – das Formular erscheint erst wenn die App vollständig gebootet ist.
|
||||
|
||||
## MailDev E-Mail-Testing
|
||||
|
||||
MailDev läuft im Docker-Stack (SMTP Port 25, Web-API Port 1080). Die REST-API erlaubt E-Mails zu lesen und zu löschen:
|
||||
|
||||
```ts
|
||||
const MAILDEV = "https://{project}-maildev.code.testversion.online"
|
||||
|
||||
// Alle E-Mails abrufen
|
||||
const res = await request.get(`${MAILDEV}/email`)
|
||||
const emails = await res.json()
|
||||
|
||||
// Alle löschen (vor Test)
|
||||
await request.delete(`${MAILDEV}/email/all`)
|
||||
```
|
||||
|
||||
### Polling-Pattern für asynchrone E-Mails
|
||||
|
||||
Formular → Action-Hook sendet E-Mail via `context.smtp.sendMail()`. MailDev braucht Zeit zum Verarbeiten:
|
||||
|
||||
```ts
|
||||
// Nach Form-Submit auf E-Mails warten
|
||||
for (let i = 0; i < 15; i++) {
|
||||
await new Promise((r) => setTimeout(r, 1000))
|
||||
const res = await request.get(`${MAILDEV}/email`)
|
||||
if (res.ok()) {
|
||||
const emails = await res.json()
|
||||
if (emails.length >= 2) break // Kunde + Betreiber
|
||||
}
|
||||
}
|
||||
emails.find((e) => e.to.some((t) => t.address === "kunde@test.de"))
|
||||
```
|
||||
|
||||
Wichtig: Tests mit MailDev müssen sequentiell laufen (`--workers=1`), da parallele Tests sich gegenseitig die MailDev-Inbox überschreiben.
|
||||
|
||||
## Admin pagebuilder registry coverage
|
||||
|
||||
For starter-like projects, committed admin coverage should include both sides of the pagebuilder contract:
|
||||
|
||||
1. registry/chooser coverage on a new entry form
|
||||
2. actual preview rendering on an existing seeded entry
|
||||
|
||||
The second check is important because it catches failures that the chooser alone does not see:
|
||||
|
||||
- broken `meta.pagebuilder.blockRegistry.file` wiring
|
||||
- preview components that no longer mount through the shared block renderer
|
||||
- missing `_lookup` hydration for foreign media fields
|
||||
- image widgets that work on the public site but fail in admin preview because the API base or URL resolution is wrong
|
||||
|
||||
Preferred starter pattern:
|
||||
|
||||
- seed one deterministic medialib image through the collection API
|
||||
- seed one deterministic content entry that references that image in at least one pagebuilder block
|
||||
- open that entry in `tests/e2e-admin/pagebuilder.spec.ts`
|
||||
- assert both block text and `img[data-entry-id]` preview rendering
|
||||
|
||||
Keep this test generic. Do not tie it to customer-specific block sets unless the project has already diverged from the starter pattern.
|
||||
|
||||
## Delivery-checklist alignment
|
||||
|
||||
When using this skill together with `.agents/BUILD_CHECKLIST.md`, the testing phase should leave behind explicit evidence for:
|
||||
|
||||
- which specs were run
|
||||
- which seed data was extended or reused
|
||||
- whether admin smoke coverage exists for the configured collections
|
||||
- whether pagebuilder preview rendering is covered when pagebuilder is in scope
|
||||
- whether SSR was verified by direct endpoint checks, committed tests, or both
|
||||
|
||||
If that evidence only exists in chat history and not in the repo or task notes, the testing work is too fragile for later agents.
|
||||
@@ -0,0 +1,213 @@
|
||||
---
|
||||
name: realtime-and-live-workflows
|
||||
description: Use tibi-server SSE channels for live website and admin workflows. Covers channel design, subscription hooks, replay/TTL/buffer behavior, permission boundaries, and when realtime fits a website project.
|
||||
---
|
||||
|
||||
# realtime-and-live-workflows
|
||||
|
||||
## When to use this skill
|
||||
|
||||
Use this skill when:
|
||||
|
||||
- A website or admin feature needs live updates
|
||||
- You want SSE-based notifications, preview refreshes, status feeds, or dashboards
|
||||
- Hooks or jobs should push messages to connected clients
|
||||
- You need to decide whether realtime is actually appropriate for the feature
|
||||
|
||||
## Goal
|
||||
|
||||
The goal is to model realtime as a deliberate workflow, not as a random event stream.
|
||||
|
||||
On this stack, realtime means:
|
||||
|
||||
- SSE transport
|
||||
- in-memory per-project channels
|
||||
- server-side send from hooks/jobs
|
||||
- subscription endpoints implemented in hooks
|
||||
|
||||
## Source of truth
|
||||
|
||||
Use these sources when implementing or reviewing realtime behavior:
|
||||
|
||||
- `tibi-server/docs/07-realtime.md`
|
||||
- `tibi-server/docs/11-jobs.md`
|
||||
- the relevant hook files under `api/hooks/`
|
||||
|
||||
## Core architecture
|
||||
|
||||
tibi-server realtime is based on per-project in-memory pub/sub channels.
|
||||
|
||||
Important characteristics:
|
||||
|
||||
- channels are created on demand
|
||||
- channels are isolated per project
|
||||
- the transport is SSE, not WebSockets
|
||||
- messages are not durable across restarts
|
||||
- hooks subscribe, hooks or jobs send
|
||||
|
||||
This makes realtime useful for live UX, but not for durable messaging.
|
||||
|
||||
## Good use cases for website projects
|
||||
|
||||
Good fits:
|
||||
|
||||
- live status or progress streams
|
||||
- lightweight admin notifications
|
||||
- system messages pushed from jobs
|
||||
- preview or refresh signals after mutations
|
||||
- dashboards with current in-memory activity
|
||||
|
||||
Weak fits:
|
||||
|
||||
- business-critical guaranteed delivery
|
||||
- cross-instance distributed eventing
|
||||
- durable queue semantics
|
||||
- workflows that require replay beyond a bounded in-memory buffer
|
||||
|
||||
## Subscription design
|
||||
|
||||
Realtime subscriptions should be exposed intentionally through dedicated read hooks that hold the SSE connection open.
|
||||
|
||||
Design the endpoint around:
|
||||
|
||||
- who may subscribe
|
||||
- which channel names exist
|
||||
- what event shape clients receive
|
||||
- how replay and freshness should work
|
||||
|
||||
Do not expose a generic raw event hose unless the project truly needs that.
|
||||
|
||||
## Channel options that matter
|
||||
|
||||
When modeling realtime behavior, decide these explicitly:
|
||||
|
||||
- `bufferSize`
|
||||
- `onFull`
|
||||
- `messageTTL`
|
||||
- `lastN`
|
||||
- `maxAge`
|
||||
|
||||
These are product decisions, not low-level afterthoughts.
|
||||
|
||||
### Buffer size
|
||||
|
||||
Use a larger buffer only when reconnecting clients should receive some recent history. Do not overestimate it as persistence.
|
||||
|
||||
### On-full behavior
|
||||
|
||||
- `drop-oldest` favors receiving the newest state, even if some history is lost
|
||||
- `drop-newest` preserves older pending messages for the subscriber and skips the new one
|
||||
|
||||
For most live UI use cases, `drop-oldest` is the more natural choice.
|
||||
|
||||
### Replay and freshness
|
||||
|
||||
Use `lastN` or `maxAge` only when reconnecting clients genuinely benefit from recent context.
|
||||
|
||||
For notification-like channels, some replay can help.
|
||||
For pure live status indicators, it may be better to show only new events.
|
||||
|
||||
## Permission boundaries
|
||||
|
||||
Channels do not carry independent auth rules. Access is controlled by the hook/collection permission layer that exposes the SSE endpoint.
|
||||
|
||||
That means:
|
||||
|
||||
- secure the subscription endpoint, not just the client code
|
||||
- do not assume channel names themselves protect access
|
||||
- be explicit about who may connect and what data is safe to send
|
||||
|
||||
## Event design
|
||||
|
||||
Prefer small, explicit event shapes.
|
||||
|
||||
Good event payloads usually include:
|
||||
|
||||
- event `type`
|
||||
- relevant identifier
|
||||
- minimal status or message fields
|
||||
- timestamp when useful
|
||||
|
||||
Avoid pushing whole documents unless the live client truly needs them.
|
||||
|
||||
## Hooks vs. jobs
|
||||
|
||||
Use hooks to send events when changes happen immediately in response to requests.
|
||||
|
||||
Use jobs to send events when the trigger is scheduled or background-driven.
|
||||
|
||||
Typical patterns:
|
||||
|
||||
- hook sends `content-updated`
|
||||
- job sends `maintenance-warning`
|
||||
- hook sends `import-finished`
|
||||
- job sends `daily-report-ready`
|
||||
|
||||
## Operational limits
|
||||
|
||||
This realtime system is intentionally lightweight.
|
||||
|
||||
Important limits:
|
||||
|
||||
- messages are lost on server restart
|
||||
- no cross-server synchronization
|
||||
- no durable backlog
|
||||
- slow subscribers can miss messages due to ring-buffer behavior
|
||||
|
||||
If the feature cannot tolerate these limits, this realtime system is the wrong abstraction.
|
||||
|
||||
## Recommended modeling patterns
|
||||
|
||||
### Live admin notifications
|
||||
|
||||
Recommended shape:
|
||||
|
||||
- authenticated SSE endpoint
|
||||
- narrow event schema
|
||||
- optional short replay via `lastN`
|
||||
|
||||
### Preview refresh signal
|
||||
|
||||
Recommended shape:
|
||||
|
||||
- hook emits lightweight invalidation or refresh event
|
||||
- client decides whether to refetch
|
||||
- do not stream full content when a simple signal is enough
|
||||
|
||||
### Scheduled status feed
|
||||
|
||||
Recommended shape:
|
||||
|
||||
- job emits events to a system channel
|
||||
- UI listens and renders current status
|
||||
- TTL keeps stale messages from resurfacing after reconnect
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- Using realtime as a replacement for persistence
|
||||
- Publishing sensitive data because “the UI needs it quickly”
|
||||
- Creating one generic catch-all channel for unrelated features
|
||||
- Ignoring replay/TTL/buffer behavior and assuming delivery guarantees
|
||||
|
||||
## Verification checklist
|
||||
|
||||
After adding realtime behavior, verify all of these:
|
||||
|
||||
1. The subscription endpoint is permissioned correctly.
|
||||
2. The event shape is explicit and minimal.
|
||||
3. Replay/TTL/buffer settings match the intended UX.
|
||||
4. Disconnect/reconnect behavior is acceptable.
|
||||
5. The feature still behaves sensibly after a server restart.
|
||||
6. `yarn validate` stays clean.
|
||||
|
||||
## What an LLM should inspect first
|
||||
|
||||
When asked to add realtime to this starter, inspect in this order:
|
||||
|
||||
1. `tibi-server/docs/07-realtime.md`
|
||||
2. the hook that should expose or emit the events
|
||||
3. whether a job is also part of the workflow
|
||||
4. the permission boundary of the SSE endpoint
|
||||
5. the exact event contract the client needs
|
||||
|
||||
This prevents building live features with unclear delivery or security assumptions.
|
||||
@@ -0,0 +1,187 @@
|
||||
---
|
||||
name: scheduled-jobs-and-automation
|
||||
description: Build scheduled background workflows with tibi-server jobs. Covers cron design, job context, safe automation patterns, reporting/cleanup/sync use cases, and how jobs interact with hooks, audit, and realtime.
|
||||
---
|
||||
|
||||
# scheduled-jobs-and-automation
|
||||
|
||||
## When to use this skill
|
||||
|
||||
Use this skill when:
|
||||
|
||||
- A project needs scheduled cleanup, reporting, imports, syncs, reminders, or maintenance tasks
|
||||
- You want automation without an incoming HTTP request
|
||||
- Jobs should update data, send mail, call APIs, or emit realtime events
|
||||
- You need to decide whether logic belongs in a hook, an action, or a job
|
||||
|
||||
## Goal
|
||||
|
||||
The goal is to design jobs as reliable background workflows, not as miscellaneous scripts.
|
||||
|
||||
Jobs on this stack are:
|
||||
|
||||
- cron-triggered
|
||||
- goja-based JavaScript programs
|
||||
- independent of HTTP requests
|
||||
- able to use many of the same server-side packages as hooks
|
||||
|
||||
## Source of truth
|
||||
|
||||
Use these sources when implementing or reviewing jobs:
|
||||
|
||||
- `tibi-server/docs/11-jobs.md`
|
||||
- `tibi-server/docs/10-audit.md`
|
||||
- `tibi-server/docs/07-realtime.md`
|
||||
- `api/config.yml`
|
||||
|
||||
## Hook vs. action vs. job
|
||||
|
||||
Choose the right execution surface.
|
||||
|
||||
- **Hook**: request-coupled logic around CRUD or actions
|
||||
- **Action**: endpoint-style business workflow triggered by an explicit call
|
||||
- **Job**: scheduled background automation without an incoming request
|
||||
|
||||
Do not place scheduled logic into hooks just because the code already exists there.
|
||||
|
||||
## Good use cases
|
||||
|
||||
Strong job use cases:
|
||||
|
||||
- cleanup of old documents or temp data
|
||||
- periodic report generation
|
||||
- scheduled API synchronization
|
||||
- cache warming or maintenance tasks
|
||||
- reminder and digest emails
|
||||
- scheduled realtime announcements
|
||||
|
||||
Weak use cases:
|
||||
|
||||
- workflows that must run immediately on user action
|
||||
- logic that depends on live request/response objects
|
||||
- features that need interactive user feedback during execution
|
||||
|
||||
## Job configuration
|
||||
|
||||
Every job should define:
|
||||
|
||||
- cron schedule
|
||||
- file path
|
||||
- timeout when appropriate
|
||||
- optional metadata via `meta`
|
||||
|
||||
Treat cron frequency as a product and operations decision. Do not set aggressive schedules without a real need.
|
||||
|
||||
## Job context limits
|
||||
|
||||
Jobs have broad server-side access, but they are not request-driven.
|
||||
|
||||
Important consequences:
|
||||
|
||||
- no `request`
|
||||
- no `response`
|
||||
- no `user.*`
|
||||
- no `cookie.*`
|
||||
- no `channel.subscribe`
|
||||
- `channel.send` is available
|
||||
|
||||
If the logic depends on request context, it does not belong in a job.
|
||||
|
||||
## Safe automation patterns
|
||||
|
||||
Jobs should be:
|
||||
|
||||
- idempotent where possible
|
||||
- bounded in runtime
|
||||
- explicit in filters and update scope
|
||||
- observable through logs or downstream effects
|
||||
|
||||
Avoid “run and mutate everything” jobs without clear selection criteria.
|
||||
|
||||
## Interaction with audit and realtime
|
||||
|
||||
Jobs are not isolated from other system behavior.
|
||||
|
||||
- DB operations from jobs appear in audit with `source.type: "job"`
|
||||
- jobs can emit realtime events through `channel.send`
|
||||
|
||||
This makes jobs useful for background workflows that should still be visible operationally.
|
||||
|
||||
## Recommended job patterns
|
||||
|
||||
### Cleanup job
|
||||
|
||||
Recommended shape:
|
||||
|
||||
- explicit age threshold
|
||||
- narrow filter
|
||||
- bounded timeout
|
||||
- optional reporting of removed count
|
||||
|
||||
### Scheduled reporting
|
||||
|
||||
Recommended shape:
|
||||
|
||||
- aggregate counts or summaries
|
||||
- render a report template if needed
|
||||
- send mail or store result
|
||||
|
||||
### External sync
|
||||
|
||||
Recommended shape:
|
||||
|
||||
- pull from external API
|
||||
- normalize data
|
||||
- update local records idempotently
|
||||
- log enough context for troubleshooting
|
||||
|
||||
### Scheduled notifications
|
||||
|
||||
Recommended shape:
|
||||
|
||||
- compute upcoming or due events
|
||||
- send mail, action-like side effect, or realtime signal
|
||||
- avoid duplicate sends through clear state checks
|
||||
|
||||
## Operational concerns
|
||||
|
||||
When adding a job, decide:
|
||||
|
||||
- how often it runs
|
||||
- what timeout it needs
|
||||
- whether reruns are safe
|
||||
- how failure is detected
|
||||
- whether a manual rerun path exists
|
||||
|
||||
Jobs should not become invisible critical infrastructure.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- Using jobs for logic that belongs in request-time hooks or actions
|
||||
- Overly frequent cron schedules for expensive tasks
|
||||
- No timeout on potentially slow network-heavy jobs
|
||||
- Broad destructive updates without precise filters
|
||||
- Silent failures with no observable output or effect
|
||||
|
||||
## Verification checklist
|
||||
|
||||
After adding a job, verify all of these:
|
||||
|
||||
1. The cron schedule matches the real business need.
|
||||
2. The job logic does not rely on request-only APIs.
|
||||
3. Timeout and runtime expectations are reasonable.
|
||||
4. Repeated execution does not corrupt data.
|
||||
5. Any audit/realtime side effects are intentional.
|
||||
6. `yarn validate` stays clean.
|
||||
|
||||
## What an LLM should inspect first
|
||||
|
||||
When asked to automate something on this starter, inspect in this order:
|
||||
|
||||
1. `tibi-server/docs/11-jobs.md`
|
||||
2. whether the trigger is scheduled, request-driven, or manual
|
||||
3. whether the logic needs audit visibility or realtime side effects
|
||||
4. the project config area where the job will be declared
|
||||
5. the exact data mutation scope
|
||||
|
||||
This prevents turning cron tasks into unbounded background risk.
|
||||
@@ -0,0 +1,238 @@
|
||||
---
|
||||
name: search-and-embeddings
|
||||
description: Model search and semantic retrieval for tibi website projects. Covers embedding provider configuration, collection search modes, auto-regeneration, regenerate-search admin flows, and how later agents should decide between no search, classic search, ngram search, and vector search.
|
||||
---
|
||||
|
||||
# search-and-embeddings
|
||||
|
||||
## When to use this skill
|
||||
|
||||
Use this skill when:
|
||||
|
||||
- a project needs explicit search behavior beyond generic CRUD filtering
|
||||
- search should be typo-tolerant, weighted, or semantic
|
||||
- embedding providers must be configured
|
||||
- later agents need a clear yes/no decision for search instead of vague optionality
|
||||
|
||||
## Goal
|
||||
|
||||
Give later agents a practical workflow for deciding whether search is needed and, if yes, which search mode belongs to the project.
|
||||
|
||||
This skill is separate from editor AI features. Search and embeddings affect content retrieval, operational setup, and index/regeneration behavior, not just editor assistance.
|
||||
|
||||
## Source of truth
|
||||
|
||||
Use these sources when implementing or reviewing search behavior:
|
||||
|
||||
- `tibi-server/docs/02-configuration.md`
|
||||
- `tibi-server/docs/04-collections.md`
|
||||
- `tibi-server/docs/09-llm-integration.md`
|
||||
- `.agents/skills/nova-ai-editor-features/SKILL.md`
|
||||
- `.agents/skills/mongodb-and-indexes/SKILL.md`
|
||||
|
||||
## First decision: no search vs explicit search
|
||||
|
||||
Do not leave search in an implied state.
|
||||
|
||||
Make one explicit decision:
|
||||
|
||||
- no search in this project
|
||||
- classic keyword search only
|
||||
- fuzzy substring search (`ngram`)
|
||||
- semantic/vector search
|
||||
- hybrid search with deliberate ranking behavior
|
||||
|
||||
If the answer is “not used”, document that clearly so later agents do not accidentally wire providers or regress into half-configured search.
|
||||
|
||||
## Server-level provider setup
|
||||
|
||||
Embedding providers are configured server-side:
|
||||
|
||||
```yaml
|
||||
embedding:
|
||||
providers:
|
||||
- name: bge-m3
|
||||
type: native
|
||||
modelPath: /models/bge-m3
|
||||
dimensions: 1024
|
||||
- name: openai-embed
|
||||
type: openai
|
||||
model: text-embedding-3-small
|
||||
apiKey: ${EMBEDDING_OPENAI-EMBED_APIKEY}
|
||||
baseURL: https://api.openai.com/v1
|
||||
dimensions: 1536
|
||||
```
|
||||
|
||||
Important:
|
||||
|
||||
- collection search config references the provider by name
|
||||
- embedding secrets and model paths can come from environment variables
|
||||
- vector search is not only a collection concern; the server must actually provide the embedding backend
|
||||
|
||||
## Collection search modes
|
||||
|
||||
Tibi supports multiple search modes via collection `search:` config:
|
||||
|
||||
- `text`
|
||||
- `regex`
|
||||
- `eval`
|
||||
- `filter`
|
||||
- `ngram`
|
||||
- `vector`
|
||||
- `combined`
|
||||
|
||||
Use explicit search configs when search is a real product feature. Auto-fallback is useful, but it is not a substitute for a deliberate retrieval model.
|
||||
|
||||
## Choosing the right mode
|
||||
|
||||
### `text`
|
||||
|
||||
Use when:
|
||||
|
||||
- MongoDB text indexing is sufficient
|
||||
- exact field ownership of the text index is clear
|
||||
- keyword search is enough
|
||||
|
||||
Requires a MongoDB text index (`$text: $**` or specific).
|
||||
|
||||
### `regex`
|
||||
|
||||
Use when:
|
||||
|
||||
- the searchable fields are explicit
|
||||
- case-insensitive matching is enough
|
||||
- weighted field scoring is useful (via `regex.weights: { "meta.title": 10, path: 5 }`)
|
||||
|
||||
Good for smaller datasets or precise keyed fields. Very easy to configure without external dependencies. Example:
|
||||
|
||||
```yaml
|
||||
search:
|
||||
- name: default
|
||||
mode: regex
|
||||
fields: [title, "alt.de", description]
|
||||
```
|
||||
|
||||
### `filter` or `eval`
|
||||
|
||||
Use when:
|
||||
|
||||
- search logic depends on auth, project context, or business-specific filtering
|
||||
- plain keyword matching is not the full contract
|
||||
|
||||
Treat these as controlled power tools. The resulting filters are still sanitized against blocked operators.
|
||||
|
||||
### `ngram`
|
||||
|
||||
Use when:
|
||||
|
||||
- typo tolerance or substring matching is needed
|
||||
- users search codes, names, transliterated terms, or partial inputs
|
||||
|
||||
This is enrichment-based search. It stores generated `_search` data and benefits from clear regeneration expectations.
|
||||
_Note:_ Field weighting is not natively supported inside a single `ngram` mode, because all `fields` are concatenated into one large ngram index block per document.
|
||||
|
||||
### `vector`
|
||||
|
||||
Use when:
|
||||
|
||||
- semantic similarity matters more than literal keyword overlap
|
||||
- the project can support embedding-provider setup (e.g. `bge-m3` in `api/config.yml`)
|
||||
- search quality justifies added complexity
|
||||
|
||||
Vector mode requires a registered provider.
|
||||
|
||||
### `combined` (RRF)
|
||||
|
||||
Use when:
|
||||
|
||||
- Hybrid search is required (e.g. `vector` + `ngram` to catch typos and semantic meaning).
|
||||
- You need to simulate field-weighting for `vector` or `ngram` by breaking them up into multiple search blocks and fusing them with different weights.
|
||||
|
||||
`mode: combined` uses Reciprocal Rank Fusion (RRF). It delegates execution to other configured search blocks (which should be hidden in admin UI via `meta.hide: true`).
|
||||
|
||||
**Field-Weighting Workaround with combined:**
|
||||
Because `vector` and `ngram` concatenate all fields, you can weight highly important fields (like titles) higher than deep content fields by creating multiple ngram/vector blocks and boosting the important one in the `combined` weights:
|
||||
|
||||
```yaml
|
||||
search:
|
||||
- name: main_search
|
||||
mode: combined
|
||||
rrf:
|
||||
k: 60
|
||||
topK: 100
|
||||
weights:
|
||||
semantic: 1.5
|
||||
fuzzy_important: 2.0 # Boosts matches in title/headline
|
||||
fuzzy_content: 0.5 # Lowers weight for deep text matches
|
||||
meta:
|
||||
label: { de: "Suche", en: "Search" }
|
||||
|
||||
- name: fuzzy_important
|
||||
mode: ngram
|
||||
fields: [name, "meta.title", "blocks.headline"]
|
||||
autoRegenerate: true
|
||||
meta: { hide: true }
|
||||
|
||||
- name: fuzzy_content
|
||||
mode: ngram
|
||||
fields: ["blocks.text", "blocks.items.answer"]
|
||||
autoRegenerate: true
|
||||
meta: { hide: true }
|
||||
|
||||
- name: semantic
|
||||
mode: vector
|
||||
fields: [name, "meta.title", "blocks.text"]
|
||||
vector: { provider: bge-m3 }
|
||||
autoRegenerate: true
|
||||
```
|
||||
|
||||
## Auto-regeneration and admin flows
|
||||
|
||||
For `ngram` and `vector`, `autoRegenerate: true` can refresh stale enrichment data after config changes.
|
||||
|
||||
If regeneration is needed manually, the admin flow depends on project admin tokens with:
|
||||
|
||||
- `allowRegenerateSearch: true`
|
||||
|
||||
Treat regeneration as part of the search contract, not as an implementation footnote.
|
||||
|
||||
## Search and LLM are related but not identical
|
||||
|
||||
The LLM system and the embedding system are adjacent, but they are not the same thing.
|
||||
|
||||
- `llm.providers` drive chat/completion features
|
||||
- `embedding.providers` drive vector search enrichment
|
||||
- org/user budgets affect LLM usage workflows
|
||||
- search design still needs its own retrieval and operator decisions
|
||||
|
||||
Do not assume that enabling editor AI automatically defines a sound search architecture.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- leaving search unspecified and hoping auto-fallback is “good enough”
|
||||
- enabling vector search without a real provider/runtime plan
|
||||
- forgetting text indexes for `mode: text`
|
||||
- enabling enrichment modes without a regeneration story
|
||||
- mixing editor AI decisions with search decisions until neither is clear
|
||||
|
||||
## Verification checklist
|
||||
|
||||
After search-related changes, verify all of these:
|
||||
|
||||
1. the project has an explicit yes/no search decision
|
||||
2. server-side embedding providers exist when vector search is configured
|
||||
3. required text or search indexes exist
|
||||
4. `?q=` and `?qName=` behavior matches the intended search contract
|
||||
5. regeneration behavior is defined for enrichment-based modes
|
||||
|
||||
## What an LLM should inspect first
|
||||
|
||||
When asked to add or review search on this starter, inspect in this order:
|
||||
|
||||
1. `tibi-server/docs/04-collections.md`
|
||||
2. `tibi-server/docs/02-configuration.md`
|
||||
3. existing collection `search:` config
|
||||
4. whether the project needs keyword, fuzzy, semantic, or no search
|
||||
5. operator expectations for regeneration and provider secrets
|
||||
|
||||
This prevents over-engineered vector setups and under-specified search behavior.
|
||||
@@ -0,0 +1,262 @@
|
||||
---
|
||||
name: security-hardening-and-token-strategy
|
||||
description: Apply current tibi-server security practices to website projects. Covers token strategy, secret handling, rate limiting, bulk-permission safety, cookie settings, risky hook capabilities, and secure operator decisions for this stack.
|
||||
---
|
||||
|
||||
# security-hardening-and-token-strategy
|
||||
|
||||
## When to use this skill
|
||||
|
||||
Use this skill when:
|
||||
|
||||
- setting up or reviewing authentication and token usage on this stack
|
||||
- deciding how admin tokens, JWT auth, and token permissions should be used
|
||||
- hardening hooks, actions, and project config against current upstream security risks
|
||||
- reviewing bulk permissions, rate limiting, cookies, secrets, or risky server-side capabilities
|
||||
|
||||
## Goal
|
||||
|
||||
Keep projects on this starter aligned with the current tibi-server security model and with the security-sensitive operator decisions the stack exposes.
|
||||
|
||||
This is not a generic web-security primer. It is the practical security workflow for this repo family.
|
||||
|
||||
## Source of truth
|
||||
|
||||
Use these sources when implementing or reviewing security decisions:
|
||||
|
||||
- `tibi-server/docs/05-authentication.md`
|
||||
- `tibi-server/docs/14-security.md`
|
||||
- `tibi-server/docs/17-field-level-permissions.md`
|
||||
- `tibi-server/docs/02-configuration.md`
|
||||
- project config and collection/action permission YAML files
|
||||
|
||||
## Security review order
|
||||
|
||||
When asked to harden a project, inspect in this order:
|
||||
|
||||
1. secret sourcing in config/env
|
||||
2. token type and scope
|
||||
3. collection/action permissions
|
||||
4. bulk permission exposure
|
||||
5. field-level restrictions
|
||||
6. rate limiting and cookie settings
|
||||
7. risky hook capabilities such as outbound fetch or exec
|
||||
|
||||
This prevents “secure enough” changes that leave the real attack surface untouched.
|
||||
|
||||
## Authentication surfaces
|
||||
|
||||
This stack exposes multiple auth mechanisms. Do not mix them casually.
|
||||
|
||||
- JWT user auth
|
||||
- refresh-token cookie flow
|
||||
- admin tokens
|
||||
- token-based permission sets for narrower machine access
|
||||
|
||||
Recommended default:
|
||||
|
||||
- use JWT user auth for real users and editor/admin sessions
|
||||
- use refresh cookies for session continuation where appropriate
|
||||
- use admin tokens only for system/admin/ops flows that truly need them
|
||||
- use token permissions for narrow machine integrations
|
||||
|
||||
## Token header distinction
|
||||
|
||||
Use the right header for the right surface:
|
||||
|
||||
- system-level API such as project CRUD, admin reload, shutdown: `X-Admin-Token`
|
||||
- collection-level CRUD via static project token: `Token`
|
||||
- JWT-authenticated user flow: `X-Auth-Token`
|
||||
|
||||
Do not assume a working `Token` header implies system-level admin rights.
|
||||
|
||||
## Secret handling
|
||||
|
||||
Do not keep production secrets as committed literals if the deployment can source them from env or operator-managed secrets.
|
||||
|
||||
Review at minimum:
|
||||
|
||||
- JWT secrets
|
||||
- SMTP credentials
|
||||
- admin tokens
|
||||
- external API keys
|
||||
- LLM/embedding provider keys
|
||||
|
||||
If secrets are hardcoded in committed config, treat that as a structural problem, not as cleanup trivia.
|
||||
|
||||
## Bulk permission safety
|
||||
|
||||
Bulk operations are more dangerous than single-document mutations.
|
||||
|
||||
Important rule:
|
||||
|
||||
- boolean `post: true` / `put: true` / `delete: true` does not imply bulk access
|
||||
- bulk requires object-form permissions with `bulk: true`
|
||||
|
||||
Example:
|
||||
|
||||
```yaml
|
||||
permissions:
|
||||
user:
|
||||
methods:
|
||||
post:
|
||||
allow: true
|
||||
bulk: true
|
||||
```
|
||||
|
||||
Do not enable bulk access casually in website projects. Most editorial workflows do not need it.
|
||||
|
||||
## Field-level security
|
||||
|
||||
Security on this stack is not only collection-method based.
|
||||
|
||||
Review all of these layers together:
|
||||
|
||||
- collection methods
|
||||
- `readonlyFields`
|
||||
- `hiddenFields`
|
||||
- field-level `readonly` / `hidden`
|
||||
- eval-based field rules
|
||||
- collection visibility in the admin UI
|
||||
|
||||
If a field should not be editable or visible, enforce that on the server. Do not rely on frontend omission.
|
||||
|
||||
## Rate limiting and login hardening
|
||||
|
||||
Current upstream tibi-server supports login rate limiting with exponential backoff.
|
||||
|
||||
Review these config points:
|
||||
|
||||
- `ratelimit.enabled`
|
||||
- `ratelimit.loginInitialDelay`
|
||||
- `ratelimit.loginMaxDelay`
|
||||
- `ratelimit.loginResetAfter`
|
||||
|
||||
Security implication:
|
||||
|
||||
- a project may look fine in normal use while still being too soft against brute-force attempts if rate limiting is not configured as expected
|
||||
|
||||
For serious deployments, do not leave this unreviewed just because login works.
|
||||
|
||||
## Cookie and session hardening
|
||||
|
||||
Refresh-token flows should respect the target environment.
|
||||
|
||||
Review:
|
||||
|
||||
- `api.secureCookies`
|
||||
- HTTPS vs local HTTP expectations
|
||||
- whether debugging shortcuts are accidentally bleeding into production config
|
||||
|
||||
Do not weaken secure-cookie behavior globally just to make a dev shortcut work.
|
||||
|
||||
## Query-parameter token risk
|
||||
|
||||
Token-based permissions can be passed via query parameters in some cases, but this is a documented risk surface.
|
||||
|
||||
If query tokens are unavoidable:
|
||||
|
||||
- scope them narrowly
|
||||
- avoid logging full URLs with sensitive query strings
|
||||
- understand proxy, history, and referrer exposure
|
||||
|
||||
Prefer header-based transport whenever possible.
|
||||
|
||||
## Risky hook capabilities
|
||||
|
||||
Current tibi-server exposes powerful capabilities in hooks. Treat them as explicit design decisions, not utilities.
|
||||
|
||||
Particularly important:
|
||||
|
||||
- `context.http.fetch()` / `context.http.fetchStream()` can create SSRF risk
|
||||
- `context.exec.command()` can create command-execution risk
|
||||
- broad filesystem/network access in hooks should not be treated as harmless
|
||||
|
||||
If a feature can be implemented without shell execution or arbitrary internal fetches, prefer the safer path.
|
||||
|
||||
When such capabilities are used, document:
|
||||
|
||||
- why they are necessary
|
||||
- what the allowed target surface is
|
||||
- what the safer rejected alternatives were
|
||||
|
||||
## CORS configuration
|
||||
|
||||
CORS follows a hierarchy. Configure it deliberately instead of widening it reactively.
|
||||
|
||||
Levels:
|
||||
|
||||
- server-level `config.yml`
|
||||
- project-level `api/config.yml`
|
||||
- collection/action-level YAML overrides
|
||||
|
||||
For typical website projects on this starter, the default proxy setup often means no aggressive cross-origin opening is required. Add explicit CORS only when the real deployment needs external origins.
|
||||
|
||||
## Recommended implementation patterns
|
||||
|
||||
### Public form workflow
|
||||
|
||||
Recommended shape:
|
||||
|
||||
- public action with narrow methods
|
||||
- server-side validation
|
||||
- no admin token in the browser
|
||||
- separate internal persistence only when truly required
|
||||
|
||||
### Integration token
|
||||
|
||||
Recommended shape:
|
||||
|
||||
- dedicated narrow token permission set
|
||||
- minimal collection/action scope
|
||||
- header-based transport preferred
|
||||
|
||||
### Sensitive internal fields
|
||||
|
||||
Recommended shape:
|
||||
|
||||
- use hidden/readonly restrictions explicitly
|
||||
- keep admin UI aligned with those restrictions
|
||||
- do not let previews depend on hidden-only data
|
||||
|
||||
### Hook that calls external systems
|
||||
|
||||
Recommended shape:
|
||||
|
||||
- fixed or validated targets
|
||||
- no user-controlled arbitrary internal fetches
|
||||
- no shell execution unless unavoidable
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- hardcoded production secrets in committed config
|
||||
- broad admin tokens used for normal frontend or integration traffic
|
||||
- bulk permissions enabled without a concrete operator need
|
||||
- risky hook capabilities treated as harmless helpers
|
||||
- collection security solved in the UI instead of the server
|
||||
- production cookie or rate-limit settings weakened for convenience
|
||||
|
||||
## Verification checklist
|
||||
|
||||
After security-relevant changes, verify all of these:
|
||||
|
||||
1. secrets are sourced appropriately
|
||||
2. token type matches the intended actor and scope
|
||||
3. bulk permissions are not broader than necessary
|
||||
4. readonly/hidden behavior is correct on the API
|
||||
5. rate limiting and cookie settings match the environment
|
||||
6. risky hook capabilities are constrained by design
|
||||
7. `yarn validate` stays clean
|
||||
|
||||
## What an LLM should inspect first
|
||||
|
||||
When asked to harden or design secure access on this starter, inspect in this order:
|
||||
|
||||
1. `tibi-server/docs/05-authentication.md`
|
||||
2. `tibi-server/docs/14-security.md`
|
||||
3. `tibi-server/docs/17-field-level-permissions.md`
|
||||
4. the relevant collection/action permission sets
|
||||
5. secret sourcing in config/env
|
||||
6. whether hooks use risky capabilities like outbound fetch or exec
|
||||
|
||||
This prevents working implementations that quietly widen the attack surface.
|
||||
@@ -0,0 +1,376 @@
|
||||
---
|
||||
name: tibi-actions-and-forms
|
||||
description: Build endpoint-style website features with tibi-server actions. Covers when to use actions instead of collections, action hook flow, validation, permissions, CORS, mail/webhook patterns, and frontend integration for forms.
|
||||
---
|
||||
|
||||
# tibi-actions-and-forms
|
||||
|
||||
## When to use this skill
|
||||
|
||||
Use this skill when:
|
||||
|
||||
- Building contact forms, newsletter forms, quote requests, callbacks, or booking requests
|
||||
- Adding endpoint-like backend logic without CRUD storage
|
||||
- Replacing old collection hacks that only existed to accept POST requests
|
||||
- Designing frontend form submissions against tibi-server actions
|
||||
- Deciding whether a feature should be an action, a collection, or both
|
||||
|
||||
## Goal
|
||||
|
||||
The goal is to teach an LLM how to build website workflows that behave like real endpoints.
|
||||
|
||||
A form feature is complete only when all of these are coherent:
|
||||
|
||||
- action config
|
||||
- hook flow
|
||||
- validation
|
||||
- permissions and CORS
|
||||
- optional persistence or side effects
|
||||
- frontend submission and error handling
|
||||
|
||||
## Source of truth
|
||||
|
||||
Use these sources when implementing or reviewing action-based features:
|
||||
|
||||
- `tibi-server/docs/19-actions.md`
|
||||
- `tibi-server/docs/06-hooks.md`
|
||||
- `api/config.yml`
|
||||
- `api/hooks/`
|
||||
- `frontend/src/lib/api.ts`
|
||||
- `frontend/src/App.svelte` or the relevant frontend form surface
|
||||
|
||||
## Core decision: action or collection?
|
||||
|
||||
Use an **action** when the feature is primarily an endpoint or workflow.
|
||||
|
||||
Typical action cases:
|
||||
|
||||
- contact form
|
||||
- newsletter signup
|
||||
- quote request
|
||||
- callback request
|
||||
- webhook receiver
|
||||
- utility endpoint
|
||||
- AI-assisted helper endpoint
|
||||
|
||||
Use a **collection** when the feature is primarily stored content or stored records with CRUD semantics.
|
||||
|
||||
Typical collection cases:
|
||||
|
||||
- products
|
||||
- team members
|
||||
- events
|
||||
- testimonials
|
||||
- persisted inquiries that editors must browse/edit in admin
|
||||
|
||||
Use **action + collection** when you need a public workflow plus internal persistence.
|
||||
|
||||
Example:
|
||||
|
||||
- a contact form submits to an action
|
||||
- the action validates, sends mail, and optionally creates an `inquiries` entry for staff follow-up
|
||||
|
||||
Do not fake endpoint logic with empty collections unless there is a very specific reason.
|
||||
|
||||
## Routing model
|
||||
|
||||
Actions are exposed under:
|
||||
|
||||
```text
|
||||
POST /api/v1/_/:project/_actions/:action
|
||||
GET /api/v1/_/:project/_actions/:action
|
||||
```
|
||||
|
||||
The `_actions` prefix is part of the contract. Frontend form code should treat actions as explicit API endpoints, not as collection writes.
|
||||
|
||||
## Where actions are configured
|
||||
|
||||
Actions are declared in `api/config.yml` under `actions:` and typically point to files under:
|
||||
|
||||
- `api/actions/` for YAML configs
|
||||
- `api/hooks/<action-name>/` for hook files
|
||||
|
||||
Typical config shape:
|
||||
|
||||
```yaml
|
||||
actions:
|
||||
- !include actions/contact-form.yml
|
||||
```
|
||||
|
||||
```yaml
|
||||
name: contact-form
|
||||
|
||||
meta:
|
||||
label: { de: "Kontaktformular", en: "Contact Form" }
|
||||
|
||||
permissions:
|
||||
public:
|
||||
methods:
|
||||
post: true
|
||||
|
||||
hooks:
|
||||
post:
|
||||
bind:
|
||||
type: javascript
|
||||
file: hooks/contact-form/post_bind.js
|
||||
validate:
|
||||
type: javascript
|
||||
file: hooks/contact-form/post_validate.js
|
||||
handle:
|
||||
type: javascript
|
||||
file: hooks/contact-form/post_handle.js
|
||||
return:
|
||||
type: javascript
|
||||
file: hooks/contact-form/post_return.js
|
||||
```
|
||||
|
||||
## Action lifecycle
|
||||
|
||||
### POST flow
|
||||
|
||||
`bind` → `validate` → `handle` → `return`
|
||||
|
||||
Use the steps deliberately:
|
||||
|
||||
- `bind`: normalize the request body, derive helper data, prepare context
|
||||
- `validate`: enforce required fields, anti-spam checks, shape checks, consent checks
|
||||
- `handle`: execute the business logic
|
||||
- `return`: normalize the response payload for the frontend
|
||||
|
||||
### GET flow
|
||||
|
||||
`handle` → `return`
|
||||
|
||||
GET actions are useful for utility endpoints, signed links, status endpoints, or controlled data retrieval that is not a collection read.
|
||||
|
||||
## Recommended website patterns
|
||||
|
||||
### Contact form
|
||||
|
||||
Recommended behavior:
|
||||
|
||||
- public `POST` action
|
||||
- validate required fields, email format, consent, and anti-spam signal
|
||||
- send mail or queue message handling
|
||||
- optionally persist a normalized inquiry record
|
||||
- return a small stable payload for the frontend
|
||||
|
||||
### Newsletter signup
|
||||
|
||||
Recommended behavior:
|
||||
|
||||
- public `POST` action
|
||||
- validate email and consent
|
||||
- call external provider or create local opt-in entry
|
||||
- keep provider-specific logic in the action, not in the frontend
|
||||
|
||||
### Quote or booking request
|
||||
|
||||
Recommended behavior:
|
||||
|
||||
- public `POST` action
|
||||
- transform raw form data into a normalized structure in `bind`
|
||||
- validate business rules in `validate`
|
||||
- persist or forward the request in `handle`
|
||||
|
||||
### Webhook receiver
|
||||
|
||||
Recommended behavior:
|
||||
|
||||
- restricted `POST` action
|
||||
- verify secret/signature before any state change
|
||||
- keep webhook-specific logic isolated from public website forms
|
||||
|
||||
## Validation rules
|
||||
|
||||
Validation belongs server-side even when the frontend already validates.
|
||||
|
||||
Always validate:
|
||||
|
||||
- required fields
|
||||
- string lengths
|
||||
- email/phone formats when applicable
|
||||
- consent flags
|
||||
- expected enums or modes
|
||||
- anti-spam or rate-limit conditions
|
||||
|
||||
Do not trust the frontend form shape.
|
||||
|
||||
## Permissions and CORS
|
||||
|
||||
Actions use the same permission model as collections.
|
||||
|
||||
For public website forms:
|
||||
|
||||
- keep only the needed methods public
|
||||
- avoid opening `GET` unless the use case needs it
|
||||
- use action-level CORS only when the frontend origin truly differs from the project default
|
||||
|
||||
Public form access should be narrow, explicit, and auditable.
|
||||
|
||||
## Persistence strategy
|
||||
|
||||
Not every form submission belongs in a collection.
|
||||
|
||||
Choose persistence deliberately:
|
||||
|
||||
- no persistence: mail, webhook, or third-party API only
|
||||
- minimal persistence: store the normalized request for internal staff
|
||||
- full persistence: store and manage lifecycle in a dedicated collection
|
||||
|
||||
If editors must browse, triage, export, or annotate the data in Nova, add a dedicated collection instead of overloading the action itself.
|
||||
|
||||
## Frontend integration
|
||||
|
||||
Frontend forms should submit to actions through the normal API layer.
|
||||
|
||||
Keep the frontend responsible for:
|
||||
|
||||
- collecting input
|
||||
- disabled/loading state
|
||||
- optimistic or conservative UX
|
||||
- success and error messages
|
||||
|
||||
Keep the backend responsible for:
|
||||
|
||||
- real validation
|
||||
- side effects
|
||||
- persistence
|
||||
- normalization of response shape
|
||||
|
||||
## Hook step order: bind → validate → handle → return
|
||||
|
||||
Action hooks run in a **fixed step order** in tibi-server:
|
||||
|
||||
1. **bind** — runs first. `context.data` is NOT yet set (body not parsed).
|
||||
2. Body parsing — happens AFTER bind. JSON body is set to `context.data`.
|
||||
3. **validate** — `context.data` is available here for validation.
|
||||
4. **handle** — main business logic. `context.data` is available.
|
||||
5. **return** — final response shaping.
|
||||
|
||||
**Critical:** The bind hook runs BEFORE the HTTP body is parsed. Do NOT access `context.data` in bind — it will be undefined. Use `handle` or `validate` for data access.
|
||||
|
||||
```yaml
|
||||
# Correct: use handle step for data access
|
||||
hooks:
|
||||
post:
|
||||
handle:
|
||||
type: javascript
|
||||
file: hooks/actions/contact/handle.js
|
||||
```
|
||||
|
||||
Action URL pattern (through BrowserSync proxy): `/api/_actions/{name}` — NOT `/api/{name}`. The tibi-server registers actions under `/_actions/`.
|
||||
|
||||
```sh
|
||||
curl -X POST "https://project.code.testversion.online/api/_actions/contact"
|
||||
```
|
||||
|
||||
## Permissions for public actions
|
||||
|
||||
Actions need explicit public write permission for unauthenticated access:
|
||||
|
||||
```yaml
|
||||
- name: contact
|
||||
path: contact
|
||||
permissions:
|
||||
public:
|
||||
methods:
|
||||
post: true
|
||||
hooks:
|
||||
post:
|
||||
handle:
|
||||
type: javascript
|
||||
file: hooks/actions/contact/handle.js
|
||||
```
|
||||
|
||||
## Inline form validation (frontend)
|
||||
|
||||
Use `$state` variables for inline errors instead of `alert()`:
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
let startDate = $state("")
|
||||
let endDate = $state("")
|
||||
let dateError = $state("")
|
||||
|
||||
function handleSubmit() {
|
||||
if (!startDate) { dateError = "Bitte Mietbeginn wählen"; return }
|
||||
if (!endDate) { dateError = "Bitte Mietende wählen"; return }
|
||||
if (startDate > endDate) { dateError = "Mietende muss nach Mietbeginn liegen"; return }
|
||||
dateError = ""
|
||||
// submit logic
|
||||
}
|
||||
</script>
|
||||
|
||||
<form onsubmit={handleSubmit}>
|
||||
<input type="date" bind:value={startDate} />
|
||||
<input type="date" bind:value={endDate} />
|
||||
{#if dateError}
|
||||
<div class="text-red-600 bg-red-50 px-3 py-2 rounded-lg">{dateError}</div>
|
||||
{/if}
|
||||
<button type="submit">Absenden</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
Errors should appear directly below the relevant field group, not as a browser alert.
|
||||
|
||||
## Frontend form submission
|
||||
|
||||
Submit to the action endpoint using the correct path:
|
||||
|
||||
```ts
|
||||
fetch("/api/_actions/contact", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name, email, message, consent: true }),
|
||||
})
|
||||
```
|
||||
|
||||
## Response design
|
||||
|
||||
Return a small stable payload that the frontend can rely on.
|
||||
|
||||
Typical response examples:
|
||||
|
||||
```json
|
||||
{ "ok": true, "message": "Danke für Ihre Nachricht." }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "ok": true, "nextStep": "confirm-email" }
|
||||
```
|
||||
|
||||
Avoid leaking internal implementation details or raw provider responses to the frontend.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- Creating empty fake collections just to receive POST requests
|
||||
- Moving validation only into the browser
|
||||
- Sending third-party API credentials from the frontend
|
||||
- Returning unstable error shapes
|
||||
- Mixing public forms and internal admin workflows into one hook without boundaries
|
||||
- Persisting everything by default without a real editorial or operational need
|
||||
|
||||
## Verification checklist
|
||||
|
||||
After adding an action-based workflow, verify all of these:
|
||||
|
||||
1. The action is declared in `api/config.yml`.
|
||||
2. The hook chain exists and has the intended steps.
|
||||
3. Invalid submissions fail with useful status and message.
|
||||
4. Valid submissions trigger the intended side effects.
|
||||
5. Public permissions are no broader than necessary.
|
||||
6. The frontend handles success and failure predictably.
|
||||
7. `yarn validate` stays clean.
|
||||
|
||||
## What an LLM should inspect first
|
||||
|
||||
When asked to add a website form or endpoint on this starter, inspect in this order:
|
||||
|
||||
1. `tibi-server/docs/19-actions.md`
|
||||
2. `api/config.yml`
|
||||
3. related hooks in `api/hooks/`
|
||||
4. the frontend form/API surface
|
||||
5. whether persistence belongs in a collection too
|
||||
|
||||
This prevents the common mistake of starting with a fake collection when the feature is really an action.
|
||||
@@ -0,0 +1,369 @@
|
||||
---
|
||||
name: tibi-hook-authoring
|
||||
description: Write and debug server-side hooks for tibi-server (goja Go JS runtime). Covers IIFE structure, HookResponse/HookException types, context.filter Go-object quirk, single-item vs list retrieval, and MongoDB filter patterns. Use when creating or modifying files in api/hooks/.
|
||||
---
|
||||
|
||||
# tibi-hook-authoring
|
||||
|
||||
Use this skill for **current tibi-server hook architecture**, not just simple CRUD filters. A real website project on this starter typically needs hooks for public filtering, SSR invalidation, action endpoints, validation, and editor safety.
|
||||
|
||||
## Source of truth
|
||||
|
||||
Use these sources when implementing or reviewing hooks:
|
||||
|
||||
- `tibi-server/docs/06-hooks.md`
|
||||
- `tibi-server/docs/19-actions.md`
|
||||
- `tibi-server/internal/models/eval_context.go`
|
||||
- `tibi-server/internal/hook/context_*.go`
|
||||
- `api/hooks/config.js`
|
||||
- `api/hooks/filter_public.js`
|
||||
- `api/hooks/clear_cache.js`
|
||||
- `.agents/skills/tibi-ssr-caching/SKILL.md`
|
||||
|
||||
When hook examples and prose ever disagree about how helpers are exposed, trust the current implementation in `eval_context.go` plus the `context_*.go` registrations.
|
||||
|
||||
## First routing decision: collection hook or action
|
||||
|
||||
Before writing hook code, decide whether the workflow belongs to CRUD data or to an endpoint.
|
||||
|
||||
Use collection hooks when:
|
||||
|
||||
- the workflow is about reads or writes on a real collection
|
||||
- publication filtering belongs to collection reads
|
||||
- cache invalidation belongs to collection mutations
|
||||
|
||||
Use actions when:
|
||||
|
||||
- the workflow is endpoint-style business logic
|
||||
- there is no durable CRUD collection behind it
|
||||
- validation and side effects matter more than storage
|
||||
|
||||
Typical action use cases:
|
||||
|
||||
- contact forms
|
||||
- newsletter signups
|
||||
- quote or order requests
|
||||
- webhook receivers
|
||||
- helper endpoints
|
||||
|
||||
Do not implement fake empty collections just to gain a hook surface.
|
||||
|
||||
## Action hook context.data quirk
|
||||
|
||||
In **action hooks**, the body is NOT parsed before the `bind` step runs. `context.data` is only available starting from the `validate` step. The order is:
|
||||
|
||||
1. `bind` — runs, but `context.data` is undefined (body not yet parsed)
|
||||
2. Body parsing — server parses JSON body into `context.data`
|
||||
3. `validate` — `context.data` is now available
|
||||
4. `handle` — `context.data` available, this is where main logic goes
|
||||
5. `return` — final response
|
||||
|
||||
**Never access `context.data` in a bind hook** — it will be empty. Use `handle` for data access.
|
||||
|
||||
For action config, always use the `handle` step:
|
||||
|
||||
```yaml
|
||||
hooks:
|
||||
post:
|
||||
handle:
|
||||
type: javascript
|
||||
file: hooks/actions/contact/handle.js
|
||||
```
|
||||
|
||||
Also note: `context.data` can be an array or object depending on the request. Always guard:
|
||||
|
||||
```js
|
||||
const data = (Array.isArray(context.data) ? {} : context.data) || {}
|
||||
```
|
||||
|
||||
## Hook file structure
|
||||
|
||||
Wrap every hook in an IIFE:
|
||||
|
||||
```js
|
||||
;(function () {
|
||||
/** @type {HookResponse} */
|
||||
const response = { status: 200 }
|
||||
|
||||
// ... hook logic ...
|
||||
|
||||
return response
|
||||
})()
|
||||
```
|
||||
|
||||
Always return a `HookResponse` or throw a `HookException`.
|
||||
|
||||
For many hooks, throwing is the normal control flow, especially in SSR hooks where HTML/status are returned via a thrown object.
|
||||
|
||||
## Type safety
|
||||
|
||||
- Use inline JSDoc type casting: `/** @type {TypeName} */ (value)`.
|
||||
- Reference typed collection entries from `types/global.d.ts`.
|
||||
- Avoid `@ts-ignore`; use proper casting instead.
|
||||
- Use `const` and `let` instead of `var` — the goja runtime supports them.
|
||||
|
||||
## Hook API exposure model
|
||||
|
||||
In hook JavaScript, the server injects one top-level object: `context`.
|
||||
|
||||
That means runtime helpers and registered packages are accessed through `context`, for example:
|
||||
|
||||
- `context.request()`
|
||||
- `context.db.find()`
|
||||
- `context.http.fetch()`
|
||||
- `context.smtp.sendMail()`
|
||||
- `context.debug.dump()`
|
||||
- `context.exec.command()`
|
||||
|
||||
Do not silently rewrite these to bare `request()`, `db.find()`, or `http.fetch()` when editing docs or examples for hook code.
|
||||
|
||||
## context.filter — Go object quirk
|
||||
|
||||
`context.filter` is a Go object, not a regular JS object. Even when empty, it is **truthy**.
|
||||
|
||||
Always check with `Object.keys()`:
|
||||
|
||||
```js
|
||||
const requestedFilter =
|
||||
context.filter &&
|
||||
typeof context.filter === "object" &&
|
||||
!Array.isArray(context.filter) &&
|
||||
Object.keys(context.filter).length > 0
|
||||
? context.filter
|
||||
: null
|
||||
```
|
||||
|
||||
**Never** use `context.filter || null` — it is always truthy and produces an empty filter inside `$and`, which crashes the Go server.
|
||||
|
||||
## Single-item vs. list retrieval
|
||||
|
||||
For `GET /:collection/:id`, the Go server sets `_id` automatically from the URL parameter.
|
||||
|
||||
GET read hooks should **not** set their own `_id` filter for `req.param("id")`. Only add authorization filters (e.g. `{ userId: userId }`).
|
||||
|
||||
|
||||
## Interne DB-Lookups in Hooks (Read & Write)
|
||||
|
||||
Innerhalb von goja-Hooks hast du über die `context.db`-API Vollzugriff auf die lokale MongoDB. Dies ist essenziell für komplexe Prüfungen (z. B. "Gehört der angemeldete User wirklich zur ID im Foreign-Key des Objekts?").
|
||||
|
||||
**Wichtige Konzepte für DB-Calls in Hooks:**
|
||||
1. **Keine Automatik-Lookups (`_lookup`) in Hook-Queries:** Der Go-Befehl `context.db.find` liefert nur die flachen Datenbank-Dokumente als Array. Die in der REST-API verfügbare `lookup`-Automatik für Foreign-Keys wird in den internen Backend-Hooks *nicht* angewendet. Du musst die verknüpften Collections ggf. manuell nachladen.
|
||||
2. **Immer Arrays:** `context.db.find` gibt **immer** ein Array zurück, auch wenn du `limit: 1` setzt.
|
||||
3. **Rechte ignorierend:** Die `context.db.*`-Methoden umgehen alle `permissions` der YAML-Rollen. Du lädst als System-Benutzer!
|
||||
|
||||
**Beispiel: Datensatz validieren / verknüpftes Element prüfen**
|
||||
|
||||
```javascript
|
||||
// hooks/my_action/post.before
|
||||
(function() {
|
||||
var userId = context.auth().id;
|
||||
var submittedRefId = context.data.refId;
|
||||
|
||||
// 1. Manuell nachladen
|
||||
var targetList = context.db.find("target_collection", {
|
||||
filter: { id: submittedRefId },
|
||||
limit: 1 // Begrenzen für Performance
|
||||
});
|
||||
|
||||
if (targetList.length === 0) {
|
||||
throw { status: 404, json: { error: "Ziel nicht gefunden" } };
|
||||
}
|
||||
|
||||
var target = targetList[0];
|
||||
|
||||
// 2. Custom Security Check
|
||||
if (target.ownerId !== userId) {
|
||||
throw { status: 403, json: { error: "Keine Berechtigung für dieses Ziel" } };
|
||||
}
|
||||
|
||||
// ... Hook fortsetzen
|
||||
})();
|
||||
```
|
||||
|
||||
**Verfügbare DB-Methoden in `context.db`:**
|
||||
* `context.db.find(collection, { filter: {}, selector: {}, sort: [], limit: 10 })`
|
||||
* `context.db.count(collection, { filter: {} })`
|
||||
* `context.db.create(collection, { field: "value" })`
|
||||
* `context.db.update(collection, "id_string", { field: "new_value" })` (bzw. mit Mongo-Operatoren `$set`, `$inc`, etc.)
|
||||
* `context.db.delete(collection, "id_string")`
|
||||
|
||||
## Current hook surfaces that matter for website projects
|
||||
|
||||
- Collection CRUD hooks under `get`, `post`, `put`, `delete`
|
||||
- Bulk hooks for optimized bulk operations
|
||||
- `audit.return` hooks for stripping sensitive data from audit output
|
||||
- `actions:` hook chains for endpoint-like behavior without a backing CRUD collection
|
||||
|
||||
For website builds on this starter, do not force everything into collections. Contact forms, newsletter signups, webhook receivers, import jobs, calculators, or other endpoint-style logic often belong into `actions:` instead.
|
||||
|
||||
## HookResponse fields (GET hooks)
|
||||
|
||||
| Field | Purpose |
|
||||
| ------------- | ------------------------------------------------------------------- |
|
||||
| `filter` | MongoDB filter (list retrieval, or restrict single-item) |
|
||||
| `selector` | MongoDB projection (`{ field: 0 }` exclude, `{ field: 1 }` include) |
|
||||
| `offset` | Pagination offset |
|
||||
| `limit` | Pagination limit |
|
||||
| `sort` | Sort specification |
|
||||
| `pipelineMod` | Function to manipulate the aggregation pipeline |
|
||||
|
||||
## context.data for write hooks
|
||||
|
||||
- `context.data` can be an array for bulk operations — always guard with `!Array.isArray(context.data)`.
|
||||
- For POST hooks, `context.data.id` may contain the new entry ID.
|
||||
- For PUT, `req.param("id")` gives the entry ID.
|
||||
|
||||
## Bulk and optimized paths
|
||||
|
||||
- tibi-server supports optimized bulk paths.
|
||||
- In bulk scenarios, `bind` still runs once at the start.
|
||||
- Per-document validation/update/delete hooks may be skipped depending on the chosen bulk path.
|
||||
|
||||
If a website feature depends on per-entry logic, do not assume a bulk update behaves exactly like N single updates. Check whether a dedicated bulk hook exists or whether the optimized path changes the behavior you rely on.
|
||||
|
||||
## Action hooks
|
||||
|
||||
Actions are first-class endpoints and should be part of the skill set for complete website builds.
|
||||
|
||||
Typical action steps:
|
||||
|
||||
- `post.bind`
|
||||
- `post.validate`
|
||||
- `post.handle`
|
||||
- `post.return`
|
||||
- `get.handle`
|
||||
- `get.return`
|
||||
|
||||
Use actions when the website needs business logic without a CRUD collection.
|
||||
|
||||
Typical website use cases:
|
||||
|
||||
- contact forms
|
||||
- newsletter signups
|
||||
- quote/order requests
|
||||
- webhook receivers
|
||||
- utility endpoints
|
||||
- AI-assisted helper endpoints
|
||||
|
||||
## Practical hook patterns for this starter family
|
||||
|
||||
- public read filtering for `active`/publication state
|
||||
- SSR cache invalidation after writes
|
||||
- route-level SSR validation
|
||||
- mutation safeguards for readonly/system-managed fields
|
||||
- custom form/action validation
|
||||
- audit-output sanitizing for sensitive fields
|
||||
|
||||
## Public filter and publication contract
|
||||
|
||||
For website projects, `filter_public.js` and `publishedFilter` are not optional examples. They are part of the public-delivery contract.
|
||||
|
||||
Later agents should validate all of these deliberately:
|
||||
|
||||
- anonymous public reads see only the intended active/published records
|
||||
- token-backed or admin-backed reads can still reach records needed for cleanup or operator workflows
|
||||
- collections that feed navigation or pages do not silently disappear because `active: true` was forgotten
|
||||
|
||||
If the public site depends on a collection, a broken public filter is a delivery bug, not only a hook bug.
|
||||
|
||||
## Mutation-side SSR invalidation
|
||||
|
||||
If a mutation can change rendered HTML, the invalidation belongs in hooks.
|
||||
|
||||
Typical SSR-critical mutation domains:
|
||||
|
||||
- content
|
||||
- navigation
|
||||
- medialib or page-critical referenced media
|
||||
- publication-relevant fields
|
||||
|
||||
For these collections, later agents should verify:
|
||||
|
||||
- which mutation steps call cache-clearing behavior
|
||||
- whether post/put/delete are all covered when needed
|
||||
- whether a representative mutation actually changes the next SSR response
|
||||
|
||||
## Public filter: token bypass for testdata cleanup
|
||||
|
||||
`filter_public.js` applies `publishedFilter` (active=true + publication window) to all unauthenticated GET requests. This works well for public traffic but causes a problem: **Playwright test cleanup can't see inactive `_testdata` entries** because `context.user.auth()` returns false even when a static `Token:` header is present. The filter runs, inactive entries are hidden from the API response, and the cleanup never deletes them. Over multiple test runs, stale entries accumulate in MongoDB.
|
||||
|
||||
**Fix:** check for any auth header in `filter_public.js` and skip the filter when present:
|
||||
|
||||
```js
|
||||
const req = context.request()
|
||||
const hasToken =
|
||||
req.header &&
|
||||
(req.header("Token") || req.header("X-Admin-Token") || req.header("X-Auth-Token") || req.header("Authorization"))
|
||||
|
||||
if (!context.user.auth() && !hasToken) {
|
||||
// apply publishedFilter
|
||||
}
|
||||
```
|
||||
|
||||
This way:
|
||||
|
||||
- **Anonymous requests** → public filter applies (only active entries visible)
|
||||
- **Requests with Token header** → no filter → all entries visible → cleanup works
|
||||
|
||||
The same fix applies to any collection that uses a public filter hook and also receives testdata writes from Playwright.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- Do not assume browser/Node APIs in hooks. The runtime is goja-based server-side JS.
|
||||
- Do not treat actions as fake collections unless there is a good reason.
|
||||
- Do not assume bulk hooks run per document.
|
||||
- Do not build SSR/cache logic into frontend code when the invalidation belongs in hooks.
|
||||
|
||||
## Verification checklist
|
||||
|
||||
After hook-related changes, verify all of these:
|
||||
|
||||
1. the hook is attached to the right lifecycle step
|
||||
2. actions are used for endpoint workflows instead of fake collections
|
||||
3. anonymous vs token-backed reads behave correctly where public filtering exists
|
||||
4. representative valid and invalid action submissions behave as designed
|
||||
5. representative SSR-critical mutations invalidate or preserve cache as intended
|
||||
6. bulk behavior is understood when the workflow depends on per-document logic
|
||||
|
||||
## Sending emails from hooks (`context.smtp.sendMail()`)
|
||||
|
||||
The `sendMail()` function is registered on `context.smtp` (NOT as a global). Always call via:
|
||||
|
||||
```js
|
||||
context.smtp.sendMail({
|
||||
to: "recipient@example.com", // string or string[]
|
||||
cc: "cc@example.com", // optional
|
||||
bcc: "bcc@example.com", // optional
|
||||
from: "sender@example.com", // required
|
||||
fromName: "Sender Name", // optional
|
||||
replyTo: "reply@example.com", // optional
|
||||
subject: "Subject line",
|
||||
plain: "Plain text version",
|
||||
html: "<h1>HTML version</h1>", // optional
|
||||
})
|
||||
```
|
||||
|
||||
The SMTP host is configured via:
|
||||
|
||||
- Server-level `config.yml`: `mail.host`
|
||||
- Environment variable: `MAIL_HOST` (e.g. `maildev:1025`)
|
||||
|
||||
MailDev (dev SMTP server) runs in the Docker stack at `maildev:25` (SMTP) with a web UI at `:1080`.
|
||||
|
||||
## publishedFilter: `active: true` erforderlich
|
||||
|
||||
Der `publishedFilter` in `api/hooks/config.js` filtert nach `active: true`:
|
||||
|
||||
```js
|
||||
const publishedFilter = {
|
||||
active: true,
|
||||
$or: [ ... ]
|
||||
}
|
||||
```
|
||||
|
||||
Einträge OHNE `active`-Feld werden bei öffentlichen API-Calls UNSICHTBAR. Das betrifft besonders:
|
||||
|
||||
- **Navigationseinträge** – werden via `getCachedEntries("navigation", ...)` geladen. Fehlt `active: true`, bleibt `navItems` leer.
|
||||
- **Manuell via API/MongoDB angelegte Einträge** – das `active`-Feld muss explizit gesetzt werden.
|
||||
|
||||
Der `filter_public.js`-Hook überspringt den Filter nur wenn ein Token-Header gesetzt ist. Bei öffentlichen API-Calls (z.B. aus dem SPA ohne Token) greift der Filter immer. Daher: alle Einträge in allen Collections müssen `active: true` haben, sonst sind sie auf der Website nicht sichtbar.
|
||||
@@ -0,0 +1,347 @@
|
||||
---
|
||||
name: tibi-project-setup
|
||||
description: Set up a new tibi project from the tibi-svelte-starter template. Covers placeholder replacement, env/config setup, Docker startup, optional shared-server registration, and build verification. Use when creating a new project or onboarding into this template.
|
||||
---
|
||||
|
||||
# tibi-project-setup
|
||||
|
||||
## When to use this skill
|
||||
|
||||
Use this skill when:
|
||||
|
||||
- creating a new project from `tibi-svelte-starter`
|
||||
- onboarding into a freshly cloned project where starter placeholders are still present
|
||||
- fixing a project that was renamed but never fully registered/configured in the current tibi stack
|
||||
|
||||
Goal: end with a project that is not only renamed, but actually reachable as a working website, admin, and API project in the current Docker/tibi-server setup.
|
||||
|
||||
## Source of truth
|
||||
|
||||
Use these sources when bootstrapping or auditing setup:
|
||||
|
||||
- `.agents/BUILD_CHECKLIST.md` phase 0
|
||||
- `AGENTS.md`
|
||||
- `README.md`
|
||||
- `Makefile`
|
||||
- `docker-compose-local.yml`
|
||||
- `.env`
|
||||
- `api/config.yml`
|
||||
- `api/config.yml.env`
|
||||
- `api/hooks/config-client.js`
|
||||
- `.gitea/workflows/deploy.yml`
|
||||
- `scripts/ci-deploy.sh`
|
||||
- `scripts/ci-staging.sh`
|
||||
- tibi-server server-level config requirements from `tibi-server/docs/02-configuration.md` when the project does not run on the starter's local Docker stack
|
||||
|
||||
## Core setup rule
|
||||
|
||||
Do not stop after placeholder replacement.
|
||||
|
||||
A project is only set up when all of these are true:
|
||||
|
||||
- placeholders and visible starter identity leftovers are gone
|
||||
- env and token values are present
|
||||
- Docker stack comes up
|
||||
- the intended operator path is explicit: local starter Docker stack or shared/external tibi-server stack
|
||||
- website, admin, and API respond on the expected project URLs
|
||||
- if the current stack requires server-level config and project registration, that operator flow is completed
|
||||
- `yarn build`, `yarn build:server`, and `yarn validate` pass
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `git`, `yarn`, `make`, `docker compose`, `curl`
|
||||
- current Code-Server / Docker environment for `*.code.testversion.online`
|
||||
- reverse proxy/Traefik managed by the host environment
|
||||
|
||||
## Step 1 — Clone and prepare remotes
|
||||
|
||||
Skip if the project is already cloned.
|
||||
|
||||
```sh
|
||||
git clone https://gitbase.de/cms/tibi-svelte-starter.git my-project
|
||||
cd my-project
|
||||
git remote rename origin template
|
||||
git remote add origin https://gitbase.de/<org>/<repo>.git
|
||||
```
|
||||
|
||||
## Step 2 — Replace starter placeholders and identity surfaces
|
||||
|
||||
Replace placeholders in all required files:
|
||||
|
||||
- `.env`
|
||||
- `api/config.yml`
|
||||
- `frontend/.htaccess` when the deployment path uses the shipped Apache rewrite/proxy file
|
||||
- `api/hooks/config-client.js`
|
||||
- `package.json`
|
||||
- `README.md` or other visible starter naming surfaces when the repo is already project-facing
|
||||
- any other file that still contains starter markers
|
||||
|
||||
Minimum placeholders to replace:
|
||||
|
||||
- `__PROJECT_NAME__`
|
||||
- `__TIBI_NAMESPACE__`
|
||||
- `__ORG__`
|
||||
- `__PROJECT__`
|
||||
|
||||
Verify with:
|
||||
|
||||
```sh
|
||||
rg '__[A-Z0-9_]+__' . --glob '*.{yml,js,env,htaccess,json,md,ts,svelte}'
|
||||
```
|
||||
|
||||
If anything remains, the setup is not complete.
|
||||
|
||||
## Step 3 — Fill project env, token, and metadata files
|
||||
|
||||
Set the current project URLs in `.env`:
|
||||
|
||||
- `LIVE_URL`
|
||||
- `CODING_URL`
|
||||
- `STAGING_URL`
|
||||
- `CODING_TIBIADMIN_URL`
|
||||
- `CODING_TIBISERVER_URL` only when the current environment exposes a dedicated raw tibi-server host
|
||||
|
||||
Generate `api/config.yml.env` values:
|
||||
|
||||
```sh
|
||||
token=$(openssl rand -hex 20)
|
||||
cat > api/config.yml.env <<EOF
|
||||
ADMIN_TOKEN=$token
|
||||
ADMIN_ASSET_VERSION=$(node -e "process.stdout.write(require('crypto').randomBytes(6).toString('hex'))")-dirty-$(date +%s)
|
||||
EOF
|
||||
```
|
||||
|
||||
Important:
|
||||
|
||||
- `ADMIN_TOKEN` is used for collection-level writes through the header name declared by the collection permission key; in this starter that is typically `Token` via `token:${ADMIN_TOKEN}`
|
||||
- the current deploy scripts also use the same secret as a bearer token on the project-local reload endpoint
|
||||
- `ADMIN_ASSET_VERSION` is required so Nova picks up the current admin bundle
|
||||
- `PROJECT_NAME`, `TIBI_NAMESPACE`, `PRODUCTION_PATH`, and `STAGING_PATH` should be project-specific before the first deploy
|
||||
- `package.json` should no longer advertise the starter repository or default package name once the project is bootstrapped
|
||||
|
||||
## Step 4 — Install and start the Docker stack
|
||||
|
||||
Use the Docker targets from the project. Do not try to start the frontend with local dev servers.
|
||||
|
||||
```sh
|
||||
yarn install
|
||||
make docker-up
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `make docker-up` already depends on `init`; do not duplicate bootstrap steps unless debugging Make targets directly
|
||||
- for foreground operation use `make docker-start`
|
||||
|
||||
## Step 5 — Choose the active bootstrap path
|
||||
|
||||
### Path A — Local starter Docker stack
|
||||
|
||||
This repo's default local path is the Docker stack in `docker-compose-local.yml` started via `make docker-up`.
|
||||
|
||||
Important characteristics:
|
||||
|
||||
- the project is mounted into `tibiserver` as `/data`
|
||||
- `DB_DIAL`, `DB_PREFIX`, `MAIL_HOST`, and security overrides are injected via container environment
|
||||
- the project is served from the repo's own `api/config.yml`
|
||||
- no extra root `config.yml` or `/api/v1/project` registration step is required for basic local startup
|
||||
|
||||
Use this path unless the operator environment clearly tells you otherwise.
|
||||
|
||||
### Path B — Shared or external tibi-server stack
|
||||
|
||||
Only use this path when the project is not started through the local starter compose stack and the operator environment requires explicit server-level config or project registration.
|
||||
|
||||
In that case, confirm all of these with the operator first:
|
||||
|
||||
- where the server-level `config.yml` lives
|
||||
- which admin token is valid for raw system-level APIs
|
||||
- which base URL exposes `/api/v1/project`
|
||||
- how the project path is mounted into the shared tibi-server instance
|
||||
|
||||
Do not invent Path B steps in the local starter Docker stack just because upstream tibi-server docs mention them.
|
||||
|
||||
## Step 6 — Optional server-level config and project registration for Path B
|
||||
|
||||
Shared or external tibi-server setups may require a server-level `config.yml` outside the project config. That file defines database connection, JWT secret, and admin tokens used for project CRUD and reload.
|
||||
|
||||
Create a root-level `config.yml` such as:
|
||||
|
||||
```yaml
|
||||
db:
|
||||
dial: mongodb://mongo
|
||||
prefix: tibi
|
||||
|
||||
api:
|
||||
port: 8080
|
||||
jwtSecret: <random-secret>
|
||||
adminTokens:
|
||||
- token: "<ADMIN_TOKEN>"
|
||||
label: "admin"
|
||||
permissions:
|
||||
- project
|
||||
- project.reload
|
||||
- user
|
||||
- namespace.<PROJECT_NAME>
|
||||
- server.shutdown
|
||||
|
||||
mail:
|
||||
host: localhost:25
|
||||
|
||||
security:
|
||||
allowAbsolutePaths: false
|
||||
allowUpperPaths: true
|
||||
```
|
||||
|
||||
Then copy it into the tibi-server container and restart that container if the current environment requires this manual step.
|
||||
|
||||
## Step 7 — Verify website, admin, and API reachability
|
||||
|
||||
Run the project-local checks after startup:
|
||||
|
||||
```sh
|
||||
curl -I "$CODING_URL"
|
||||
curl -I "$CODING_TIBIADMIN_URL"
|
||||
curl -I "$CODING_URL/api/content?limit=1"
|
||||
```
|
||||
|
||||
If the current environment also exposes a raw tibi-server host, add:
|
||||
|
||||
```sh
|
||||
curl -I "$CODING_TIBISERVER_URL/api/v1/version"
|
||||
```
|
||||
|
||||
If `/api/...` returns HTML instead of JSON, the reverse-proxy/setup path is still wrong.
|
||||
|
||||
## Step 8 — Optional project registration for Path B
|
||||
|
||||
Projects are not assumed to exist just because files are present on disk. Register and reload them explicitly when the current stack requires project registration.
|
||||
|
||||
```sh
|
||||
curl -s -X POST "$CODING_TIBISERVER_URL/api/v1/project" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Admin-Token: $ADMIN_TOKEN" \
|
||||
-d '{
|
||||
"name": "<PROJECT_NAME>",
|
||||
"description": "...",
|
||||
"configFile": "/data/api/config.yml",
|
||||
"enabled": true
|
||||
}'
|
||||
```
|
||||
|
||||
Reload after creation or config changes:
|
||||
|
||||
```sh
|
||||
curl -s -X POST "$CODING_TIBISERVER_URL/api/v1/_/<PROJECT_NAME>/_/admin/reload" \
|
||||
-H "X-Admin-Token: $ADMIN_TOKEN"
|
||||
```
|
||||
|
||||
### Token header distinction
|
||||
|
||||
- raw system-level API such as project CRUD or direct admin reload: `X-Admin-Token`
|
||||
- collection-level CRUD such as content/navigation writes: use the header name from the collection permission key, typically `Token` in this starter via `token:${ADMIN_TOKEN}`
|
||||
- JWT-authenticated user requests: `X-Auth-Token`
|
||||
|
||||
The current starter deploy scripts are a separate case: they call the reverse-proxied reload endpoint on `LIVE_URL` or `STAGING_URL` with `Authorization: Bearer ${ADMIN_TOKEN}`.
|
||||
|
||||
Do not mix these headers casually. A working collection token does not imply project-admin access.
|
||||
|
||||
## Step 9 — Build and validate
|
||||
|
||||
```sh
|
||||
yarn build
|
||||
yarn build:server
|
||||
yarn validate
|
||||
```
|
||||
|
||||
The project is not considered bootstrapped until all three succeed.
|
||||
|
||||
## Step 10 — Optional immediate follow-up work
|
||||
|
||||
Depending on the project state, continue with:
|
||||
|
||||
- seed or create initial content/navigation entries
|
||||
- remove demo content and demo assets
|
||||
- update project imagery/icons
|
||||
- run the first targeted Playwright smoke checks
|
||||
|
||||
## Recommended verification sequence
|
||||
|
||||
Use this exact order when debugging a broken setup:
|
||||
|
||||
1. placeholder scan
|
||||
2. env/token/metadata presence
|
||||
3. Docker stack or target operator stack up
|
||||
4. choose Path A or Path B explicitly
|
||||
5. if Path B: server-level config and project registration/reload succeed
|
||||
6. website/admin/API reachability
|
||||
7. build/SSR build/validate
|
||||
|
||||
This prevents wasting time in frontend code when the real issue is project registration or server-level config.
|
||||
|
||||
## Common failure modes
|
||||
|
||||
### Placeholders still present
|
||||
|
||||
Symptom:
|
||||
|
||||
- URLs or namespace stay wrong even though the project name was changed manually
|
||||
|
||||
Fix:
|
||||
|
||||
- rerun the placeholder scan and replace every remaining marker
|
||||
|
||||
### Website works but API probes return HTML
|
||||
|
||||
Symptom:
|
||||
|
||||
- `curl "$CODING_URL/api/content?limit=1"` returns HTML
|
||||
|
||||
Fix:
|
||||
|
||||
- verify reverse-proxy routing and the configured API/admin URLs
|
||||
|
||||
### Files exist but the project is invisible to tibi-server
|
||||
|
||||
Symptom:
|
||||
|
||||
- project does not show in admin or reload endpoint fails
|
||||
|
||||
Fix:
|
||||
|
||||
- this is a Path B problem; verify the shared-stack server-level config and project registration flow instead of changing the local starter stack
|
||||
|
||||
### Admin bundle changes do not appear
|
||||
|
||||
Symptom:
|
||||
|
||||
- Nova still loads stale admin assets
|
||||
|
||||
Fix:
|
||||
|
||||
- regenerate or bump `ADMIN_ASSET_VERSION`
|
||||
|
||||
### Build passes locally but operational setup is still broken
|
||||
|
||||
Symptom:
|
||||
|
||||
- files compile, but website/admin/API are not all reachable
|
||||
|
||||
Fix:
|
||||
|
||||
- return to the reachability and registration checks instead of continuing with feature work
|
||||
|
||||
## What an LLM should inspect first
|
||||
|
||||
When asked to bootstrap or audit a starter-derived project, inspect in this order:
|
||||
|
||||
1. `README.md`
|
||||
2. `.env`
|
||||
3. `api/config.yml`
|
||||
4. `api/config.yml.env`
|
||||
5. `api/hooks/config-client.js`
|
||||
6. `docker-compose-local.yml` and `Makefile`
|
||||
7. whether the current stack is Path A (local starter Docker) or Path B (shared/external tibi-server)
|
||||
8. whether website, admin, and API URLs all respond
|
||||
|
||||
This avoids the common mistake of treating setup as a naming exercise instead of a full stack-registration task.
|
||||
@@ -0,0 +1,307 @@
|
||||
---
|
||||
name: tibi-ssr-caching
|
||||
description: Implement and debug server-side rendering with goja (Go JS runtime) and dependency-based HTML cache invalidation for tibi-server. Use when working on SSR hooks, cache clearing, or the server-side Svelte rendering pipeline.
|
||||
---
|
||||
|
||||
# tibi-ssr-caching
|
||||
|
||||
This skill should teach the **SSR architecture and implementation pattern** used in this repo family, not just describe one demo content setup. The important question is: **how is SSR built here, where is responsibility split, and which parts must be adapted per project?**
|
||||
|
||||
## SSR request flow
|
||||
|
||||
1. `ssr/get_read.js` receives a page request and calls `lib/ssr-server.js`.
|
||||
2. `ssr/get_read.js` loads `lib/app.server.js` and calls `app.default.render({ url })`.
|
||||
3. `frontend/src/ssr.ts` only initializes i18n and delegates rendering to `svelte/server`.
|
||||
4. `frontend/src/App.svelte` owns the actual data loading for both browser and SSR.
|
||||
5. During SSR, the app calls its normal page-loading path directly inside a `typeof window === "undefined"` guard.
|
||||
6. During browser navigation, the same page-loading path is triggered from `$effect`.
|
||||
7. API calls made during SSR are tracked as dependency strings (`col:id` or `col:*`) and cached in `window.__SSR_CACHE__`.
|
||||
8. The rendered HTML + dependency list are stored in the `ssr` collection.
|
||||
|
||||
## Responsibility split
|
||||
|
||||
- `frontend/src/ssr.ts` should stay minimal.
|
||||
- `frontend/src/ssr.ts` is responsible for SSR bootstrapping only: locale setup, SSR-safe render wrapper, and calling `render(App, { props: { url } })`.
|
||||
- The app component should own data loading.
|
||||
- Hooks under `api/hooks/ssr/` should own caching, cache lookup, and cache persistence.
|
||||
- `api/hooks/lib/ssr.js` should own the shared API helper that works in both browser and SSR.
|
||||
|
||||
If these responsibilities get mixed together, SSR usually becomes harder to reason about and harder for an LLM to modify safely.
|
||||
|
||||
## Building the SSR bundle
|
||||
|
||||
```bash
|
||||
yarn build:server
|
||||
```
|
||||
|
||||
- Output: `api/hooks/lib/app.server.js`
|
||||
- The project no longer uses Babel for SSR.
|
||||
- The goja-compatible transform happens in `esbuild.config.server.js` via `supported`:
|
||||
- `async-await: false`
|
||||
- `async-generator: false`
|
||||
- `dynamic-import: false`
|
||||
- The SSR build writes directly to `api/hooks/lib/app.server.js`.
|
||||
- Remove splitting-related frontend options (`outdir`, `splitting`, `entryNames`, `chunkNames`, `outExtension`) from the server build, otherwise esbuild will fail with `outfile`/`outdir` conflicts.
|
||||
|
||||
## Core design rule
|
||||
|
||||
- Prefer **one shared data-loading path** for browser and SSR.
|
||||
- The browser should trigger it reactively.
|
||||
- SSR should call that same path explicitly before rendering completes.
|
||||
- Avoid maintaining a separate SSR-only content-loading implementation unless there is no viable alternative.
|
||||
|
||||
In this repo family, the practical pattern is:
|
||||
|
||||
- browser: `$effect(() => loadContent(...))`
|
||||
- SSR: call the same `loadContent(...)` once inside a server guard
|
||||
|
||||
The main trap is assuming `$effect` alone is enough for SSR. It is not.
|
||||
|
||||
## Dependency-based cache invalidation
|
||||
|
||||
When content changes, `clear_cache.js` only invalidates SSR entries that depend on the changed collection/entry:
|
||||
|
||||
```js
|
||||
// Each SSR cache entry stores dependency strings:
|
||||
{
|
||||
path: "/de/ueber-uns",
|
||||
content: "...",
|
||||
dependencies: ["content:abc123", "navigation:*", "medialib:*"]
|
||||
}
|
||||
```
|
||||
|
||||
- `col:id` means a detail dependency.
|
||||
- `col:*` means a list dependency.
|
||||
- `clear_cache.js` must handle `DELETE` robustly, because `context.data.id` and route params may be missing. Fallback to the last path segment if needed.
|
||||
- `utils.clearSSRCache()` must clear:
|
||||
- `col:*` on `POST`
|
||||
- `col:id` OR `col:*` on `PUT`/`DELETE`
|
||||
- everything on manual clear (`POST /ssr?clear=1` with no collection context)
|
||||
|
||||
## Limit: 1 ensures precise dependencies
|
||||
|
||||
By default, an API query for a collection (like `/api/v1/_/content?filter=...`) sets a list dependency `collection:*`. This means _any_ change to ANY entry in that collection will clear the SSR cache for this page.
|
||||
|
||||
If you are querying a single document (like a page or article based on its path or slug), you should ALWAYS append `limit: 1` to your API call (or pass `{ filter: {...}, limit: 1 }` to `getDBEntries`).
|
||||
|
||||
When `api/hooks/lib/ssr-server.js` intercepts a request with `limit === 1` and exactly one result is returned, it will register a precise `collection:id` dependency instead of a wildcard `collection:*`. This optimizes the cache drastically, because edits to _other_ pages won't invalidate this page.
|
||||
|
||||
### Automatic dependency tracking via `lookup` and `aggregate`
|
||||
|
||||
When options like `lookup` (e.g. `lookup: "image:medialib"`) or `aggregate` (e.g. `aggregate: "comments:contentId:count"`) are provided to an API call, `ssr-server.js` automatically parses these values and adds wildcard cache dependencies (`medialib:*` or `comments:*`) to the page. This guarantees that if a referenced image or child comment changes, the parent's SSR HTML is correctly flushed.
|
||||
|
||||
## How SSR data loading is supposed to work
|
||||
|
||||
- Keep `frontend/src/ssr.ts` thin. It should set up locale state and call `render(App, { props: { url } })`.
|
||||
- Do not move application-specific prefetch logic into `ssr.ts` unless absolutely necessary.
|
||||
- The app itself should own the page-loading behavior.
|
||||
- In projects using this starter architecture, the correct pattern is:
|
||||
- browser: `$effect(() => loadContent(...))`
|
||||
- SSR: call the same `loadContent(...)` once inside `typeof window === "undefined"`
|
||||
- This keeps SSR and client navigation on one shared code path.
|
||||
- `loadContent(...)` must load **all data required for a fully rendered page**. In this repo that includes both navigation and page content. SSR is incomplete if only the main content entry is loaded.
|
||||
- Because goja runs the transformed async path synchronously enough for this setup, the direct SSR call works. The problem was the reactive `$effect`, not the shared async loader itself.
|
||||
|
||||
## What is project-specific vs. architecture-specific
|
||||
|
||||
Architecture-specific rules:
|
||||
|
||||
- SSR entry goes through `api/hooks/ssr/get_read.js`
|
||||
- HTML caching lives in the `ssr` collection
|
||||
- SSR API calls are tracked through `context.ssrRequest`
|
||||
- Client hydration reuses `window.__SSR_CACHE__`
|
||||
- The app owns its own data-loading logic
|
||||
|
||||
Project-specific rules that an LLM must inspect before changing SSR:
|
||||
|
||||
- which collections contribute to rendered pages
|
||||
- which routes should SSR vs. skip SSR
|
||||
- whether URLs are language-prefixed
|
||||
- whether DB paths are stored with or without language prefix
|
||||
- which lookups are required to make a page fully render
|
||||
- which collections need publication-aware invalidation
|
||||
- whether there are canonical/alias paths
|
||||
|
||||
Do not hardcode demo assumptions into the skill. Instead, use the architecture rules above and inspect the current project's route model, collections, and page-loading code.
|
||||
|
||||
## SSR route validation
|
||||
|
||||
Route validation in `config.js` controls which paths get SSR treatment. Return:
|
||||
|
||||
- `1` to render the requested path as-is
|
||||
- a string to rewrite to the canonical cache path
|
||||
- `-1` for not found
|
||||
|
||||
For projects following this setup, route validation must understand the public URL shape used by the frontend router:
|
||||
|
||||
- `/` and `/{lang}` are valid SSR roots.
|
||||
- Public content URLs are language-prefixed (`/de/...`, `/en/...`).
|
||||
- Content entries in the DB are stored **without** the language prefix in `content.path`.
|
||||
- `ssrValidatePath()` therefore needs to:
|
||||
- extract the language prefix from the URL
|
||||
- strip it before querying `content.path`
|
||||
- include `{ lang }` in the content query
|
||||
- support `alternativePaths.path`
|
||||
- return a canonical language-prefixed URL when the request matched via an alternative path
|
||||
|
||||
If this mapping is wrong, SSR may appear to work for root pages while returning 404 or empty content for real CMS pages.
|
||||
|
||||
## Publication-aware SSR caching
|
||||
|
||||
- `config.js` exports `publishedFilter` and `ssrPublishCheckCollections`.
|
||||
- `ssrPublishCheckCollections` should include every collection whose publication window can make cached HTML stale.
|
||||
- In this starter, `content` is currently included.
|
||||
- `ssr-server.js` uses `publication.from` / `publication.to` to compute `context.ssrCacheValidUntil`.
|
||||
- `get_read.js` must reject expired cache entries and delete them before rendering anew.
|
||||
|
||||
## Hydration cache behavior
|
||||
|
||||
- `api/hooks/lib/ssr.js` uses the same API helper for browser and SSR.
|
||||
- On the server, `apiRequest(...)` delegates to `context.ssrRequest(...)`.
|
||||
- On the client, `window.__SSR_CACHE__` is checked first for GET requests.
|
||||
- This means SSR is not just HTML prerendering; it also primes client-side data access.
|
||||
- If HTML renders but `window.__SSR_CACHE__` is missing, the SSR pipeline is incomplete.
|
||||
|
||||
## SSR 404 signaling
|
||||
|
||||
When a page is not found during SSR, the framework returns the 404 page but with HTTP status **200** unless a 404 signal is set. The SSR hook (`get_read.js`) checks `context.is404` after rendering:
|
||||
|
||||
```js
|
||||
// get_read.js, after app.default.render()
|
||||
if (context.is404) {
|
||||
status = 404
|
||||
}
|
||||
```
|
||||
|
||||
The signal is set from `NotFound.svelte` — when this component is rendered during SSR, it sets the flag directly. This keeps the 404 logic in the component that owns it:
|
||||
|
||||
```ts
|
||||
// NotFound.svelte — top-level script, runs during render:
|
||||
if (typeof window === "undefined") {
|
||||
// @ts-ignore - context is the goja global in SSR runtime
|
||||
context.is404 = true
|
||||
}
|
||||
```
|
||||
|
||||
**Why this works:**
|
||||
|
||||
- The `tibi-types` package declares `var context: HookContext` as a global (available because goja provides it during SSR).
|
||||
- During SSR, `loadContent()` runs synchronously (goja transforms `async`/`await`).
|
||||
- By the time `render(App)` returns in `ssr.ts`, `context.is404` is already `true`.
|
||||
- `get_read.js` reads it, returns HTTP 404, and the rendered 404 page HTML is sent with the correct status.
|
||||
- Caching is automatically skipped for 404 responses.
|
||||
|
||||
**Verification:** Test with a non-existent URL:
|
||||
|
||||
```bash
|
||||
curl -w "\nHTTP Status: %{http_code}\n" "http://tibiserver:8080/api/v1/_/<namespace>/ssr?url=/de/nicht-existierend"
|
||||
# Expected: HTTP 404, body contains the 404 page HTML
|
||||
```
|
||||
|
||||
## What an LLM should inspect first when changing SSR
|
||||
|
||||
1. `api/hooks/ssr/get_read.js` to understand cache lookup, route validation, and template injection.
|
||||
2. `api/hooks/lib/ssr-server.js` to understand dependency tracking and SSR-side API behavior.
|
||||
3. `frontend/src/ssr.ts` to confirm how the SSR render wrapper is bootstrapped.
|
||||
4. The top-level app/page-loading surface (for example `frontend/src/App.svelte`) to see where data is actually loaded.
|
||||
5. `api/hooks/config.js` to understand route validation, canonicalization, and publication-aware collections.
|
||||
6. `api/hooks/clear_cache.js` plus `api/hooks/lib/utils.js` to understand invalidation behavior.
|
||||
|
||||
This order helps an LLM separate infrastructure problems from app-loading problems.
|
||||
|
||||
## How to verify SSR correctly
|
||||
|
||||
- Do not rely only on the BrowserSync/frontend proxy when debugging SSR.
|
||||
- Test the SSR API endpoint directly, for example:
|
||||
|
||||
```bash
|
||||
curl "http://tibiserver:8080/api/v1/_/<namespace>/ssr?url=/de/ueber-uns"
|
||||
```
|
||||
|
||||
- Verify all of the following:
|
||||
- HTTP status is correct
|
||||
- expected page content is present in the HTML
|
||||
- all page-critical content is present in the HTML
|
||||
- navigation labels are present in the HTML when navigation is part of the app shell
|
||||
- `window.__SSR_CACHE__` exists
|
||||
- no `error:` comment was injected into the template
|
||||
- second request returns `X-SSR-Cache: true`
|
||||
- `POST /ssr?clear=1` removes cache entries and the next request is a miss again
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Do not document Babel anymore**: the current SSR build is esbuild-only.
|
||||
- **goja does not parse every modern syntax feature**: dynamic import must be downlevelled in the server build.
|
||||
- **Do not leave frontend build options on the server build**: `splitting`/`outdir` inherited from the frontend config will break `build:server`.
|
||||
- **No browser globals**: `window`, `document`, `localStorage` etc. don't exist in goja. Guard with `typeof window !== "undefined"`.
|
||||
- **`$effect` does not solve SSR loading**: server-side content must be loaded outside browser-only reactive effects.
|
||||
- **SSR can look healthy while content is missing**: a 200 response plus app shell is not enough; always verify actual DB content in the HTML.
|
||||
- **Navigation is part of SSR**: if header/footer are missing, the SSR setup is still incomplete even when the page body renders.
|
||||
- **SSR cache can go stale**: Always ensure `clear_cache.js` covers every collection that affects rendered output.
|
||||
- **Do not overfit the skill to demo content**: the skill should explain the architecture and where to inspect project-specific route/content rules, not freeze one content model as universal.
|
||||
|
||||
## SSR data loading pattern
|
||||
|
||||
In Svelte 5, SSR data loading works via **top-level `loadData()` calls** (NOT inside `$effect`):
|
||||
|
||||
```typescript
|
||||
// ✅ Richtig: Top-Level-Aufruf für SSR + Browser
|
||||
loadData()
|
||||
|
||||
async function loadData() {
|
||||
const data = await getCachedEntries(...)
|
||||
state = data
|
||||
}
|
||||
|
||||
// ❌ Falsch: $effect wird für SSR nicht rechtzeitig abgearbeitet
|
||||
$effect(() => { loadData() })
|
||||
```
|
||||
|
||||
**Warum das funktioniert:**
|
||||
|
||||
- `loadData()` läuft während der Component-Initialisierung (vor Template-Auswertung)
|
||||
- `getCachedEntries` → `apiRequest` → SSR-Pfad → `context.ssrRequest()` → blockierender HTTP-Fetch in goja
|
||||
- goja's `await` auf einem bereits aufgelösten Promise läuft synchron weiter (kein Microtask-Hickhack)
|
||||
- State-Änderungen sind vor der Template-Auswertung sichtbar
|
||||
|
||||
**Browser-Reaktivität:** Wenn Props sich ändern (z.B. Navigation zu anderer Kategorie), wird die Component via `{#if}`/`{#key}` neu erstellt → `loadData()` läuft erneut.
|
||||
|
||||
## SSR-Cache in der Entwicklung
|
||||
|
||||
Der SSR-Cache ist das häufigste Debugging-Hindernis. Der Proxy in `esbuild.config.js` MUSS `&noCache=1` an den SSR-Request anhängen:
|
||||
|
||||
```javascript
|
||||
// esbuild.config.js – SSR-Proxy
|
||||
pathRewrite: function (path, req) {
|
||||
return "/ssr?url=" + encodeURIComponent(path) + "&noCache=1"
|
||||
}
|
||||
```
|
||||
|
||||
Ohne `noCache` wird die erste SSR-Antwort gecached und bei Code-Änderungen nicht invalidiert. Der Entwickler sieht immer den alten Stand. **Immer zuerst den Cache-Bypass prüfen, bevor SSR-Fehler gesucht werden.**
|
||||
|
||||
**Erkennungsmerkmale für veralteten SSR-Cache:**
|
||||
|
||||
- `X-SSR-Cache: true` im Response-Header
|
||||
- `<!--COMMENT--><!--SSR.ERROR-->` im HTML
|
||||
- `__SSR_CACHE__` enthält nicht die erwarteten Daten
|
||||
- Neustart von tibi-server nötig nach `app.server.js`-Änderungen (`docker restart <tibiserver>`)
|
||||
|
||||
## Build-Arbeitsschritte bei SSR-Änderungen
|
||||
|
||||
Nach jeder Änderung an Svelte-Komponenten oder `api.ts` ist folgendes nötig:
|
||||
|
||||
```bash
|
||||
# 1. Frontend-Bundle bauen
|
||||
yarn build
|
||||
|
||||
# 2. SSR-Bundle bauen (app.server.js)
|
||||
yarn build:server
|
||||
|
||||
# 3. tibi-server neustarten (lädt neues app.server.js)
|
||||
docker restart <tibiserver>
|
||||
|
||||
# 4. Frontend neustarten (für Entwicklungs-Proxy)
|
||||
make docker-restart-frontend
|
||||
```
|
||||
|
||||
**Wichtig:** `yarn build:server` allein reicht nicht – der tibi-server cached das Modul im Speicher und lädt es nur beim Start neu.
|
||||
@@ -0,0 +1,222 @@
|
||||
---
|
||||
name: troubleshooting-and-debugging
|
||||
description: Diagnose common tibi website-project failures. Covers config loading, auth and permission mistakes, hook/goja errors, upload and CORS issues, and a practical debugging order so later agents do not thrash across unrelated layers.
|
||||
---
|
||||
|
||||
# troubleshooting-and-debugging
|
||||
|
||||
## When to use this skill
|
||||
|
||||
Use this skill when:
|
||||
|
||||
- a tibi project is failing in a way that is not obviously tied to one file
|
||||
- hooks, config, permissions, uploads, or routing behave unexpectedly
|
||||
- a project worked before and now fails after configuration or integration changes
|
||||
- you need a practical debugging order instead of ad-hoc guessing
|
||||
|
||||
## Goal
|
||||
|
||||
Help later agents debug the stack systematically.
|
||||
|
||||
The main value of this skill is not “more tips”. It is the order of operations: isolate the failing layer first, then inspect the right tool surface for that layer.
|
||||
|
||||
## Source of truth
|
||||
|
||||
Use these sources when debugging:
|
||||
|
||||
- `tibi-server/docs/15-troubleshooting.md`
|
||||
- `tibi-server/docs/06-hooks.md`
|
||||
- `tibi-server/docs/05-authentication.md`
|
||||
- `tibi-server/docs/17-field-level-permissions.md`
|
||||
- `tibi-server/internal/models/eval_context.go` when hook API exposure is in doubt
|
||||
- `tibi-server/internal/hook/context_*.go` when helper registration is in doubt
|
||||
- `.agents/skills/tibi-hook-authoring/SKILL.md`
|
||||
- `.agents/skills/security-hardening-and-token-strategy/SKILL.md`
|
||||
- `.agents/skills/tibi-ssr-caching/SKILL.md`
|
||||
|
||||
## Debugging order
|
||||
|
||||
Start in this order unless a more specific failure anchor already exists:
|
||||
|
||||
1. reachability and environment
|
||||
2. auth/token/permission layer
|
||||
3. config loading and YAML shape
|
||||
4. hook/goja behavior
|
||||
5. uploads/media pathing
|
||||
6. SSR/routing/publication behavior
|
||||
7. CORS or browser-integration issues
|
||||
|
||||
This order prevents chasing frontend symptoms when the real issue is project registration, token scope, or a broken config reload.
|
||||
|
||||
## Layer 1 — Reachability and environment
|
||||
|
||||
Check first:
|
||||
|
||||
- website URL responds
|
||||
- admin URL responds
|
||||
- API responds with JSON where expected
|
||||
- Docker services are actually up
|
||||
|
||||
If `/api/...` returns HTML, do not debug application logic yet. Fix the environment/proxy path first.
|
||||
|
||||
## Layer 2 — Auth and permission mistakes
|
||||
|
||||
Common patterns:
|
||||
|
||||
- `401 Unauthorized` → missing/invalid JWT or wrong auth surface
|
||||
- `403 Forbidden` → collection/action permissions wrong, user not assigned correctly, or token permission mismatch
|
||||
- static `Token` works for one surface but not another → wrong header type for the requested operation
|
||||
|
||||
Check:
|
||||
|
||||
- `X-Auth-Token` vs `Token` vs `X-Admin-Token`
|
||||
- collection/action permissions
|
||||
- project/user assignment
|
||||
- field-level readonly/hidden behavior if writes fail unexpectedly
|
||||
|
||||
Important current note:
|
||||
|
||||
- current upstream troubleshooting and auth docs still describe MD5-managed passwords; do not debug login failures by assuming the active username/password flow has already moved to bcrypt
|
||||
|
||||
## Layer 3 — Config and reload problems
|
||||
|
||||
Common patterns:
|
||||
|
||||
- project not loading after config change
|
||||
- env vars not resolving
|
||||
- CORS behaving differently than expected
|
||||
|
||||
Check:
|
||||
|
||||
- YAML syntax
|
||||
- whether the correct config file is being loaded
|
||||
- whether project reload actually ran
|
||||
- whether env placeholders use `${VAR_NAME}` format
|
||||
|
||||
When the problem smells like “the server ignores my config change”, verify reload and active config path before editing more files.
|
||||
|
||||
## Layer 4 — Hook and goja errors
|
||||
|
||||
Common patterns:
|
||||
|
||||
- `require is not defined`
|
||||
- `async`/`await` assumptions in hooks
|
||||
- wrong context object assumptions
|
||||
- timeouts or infinite loops
|
||||
|
||||
Remember:
|
||||
|
||||
- hooks run in goja, not Node.js
|
||||
- no `require()` or npm runtime
|
||||
- no normal async/await model
|
||||
- hook behavior often depends on the exact lifecycle step
|
||||
- tibi hook surfaces are accessed through `context`, including request and registered packages such as `context.request()`, `context.db.find()`, `context.http.fetch()`, `context.smtp.sendMail()`, `context.debug.dump()`, or `context.exec.command()`
|
||||
|
||||
Use the hook skill for step-specific quirks such as `context.data` timing or `context.filter` behavior.
|
||||
|
||||
## Layer 5 — Upload and media failures
|
||||
|
||||
Common patterns:
|
||||
|
||||
- files not being saved
|
||||
- media URLs render incorrectly
|
||||
- image filters return 404
|
||||
|
||||
Check:
|
||||
|
||||
- collection `uploadPath`
|
||||
- file field type
|
||||
- base64/data-URI or multipart expectations
|
||||
- filter names in collection config
|
||||
- whether frontend/admin preview code expects `_lookup` or raw IDs
|
||||
|
||||
## Layer 6 — SSR, routing, and publication failures
|
||||
|
||||
Common patterns:
|
||||
|
||||
- route works in browser but not in SSR
|
||||
- SSR returns empty page or wrong status
|
||||
- unpublished or inactive entries disappear unexpectedly
|
||||
|
||||
Check:
|
||||
|
||||
- route model and language-prefix handling
|
||||
- `ssrValidatePath()`
|
||||
- `publishedFilter`
|
||||
- lookup usage for page-critical relations
|
||||
- cache invalidation after mutations
|
||||
|
||||
Do not debug SSR only through browser navigation. Use the SSR endpoint directly when the failure is SSR-shaped.
|
||||
|
||||
## Layer 7 — Browser integration and CORS
|
||||
|
||||
Common patterns:
|
||||
|
||||
- browser form fails while direct API call works
|
||||
- preflight fails
|
||||
- credentials or auth headers do not cross origins correctly
|
||||
|
||||
Check:
|
||||
|
||||
- which layer owns CORS config
|
||||
- `allowOrigins`, `allowMethods`, `allowHeaders`, `allowCredentials`
|
||||
- whether the real deployment even needs cross-origin calls
|
||||
|
||||
## Useful debugging tools
|
||||
|
||||
### Hook-side debug helpers
|
||||
|
||||
```js
|
||||
context.debug.dump(context.data, "payload")
|
||||
context.debug.dump(context.request().header("Authorization"), "auth-header")
|
||||
```
|
||||
|
||||
### Request inspection
|
||||
|
||||
```js
|
||||
const req = context.request()
|
||||
context.debug.dump({
|
||||
method: req.method,
|
||||
path: req.path,
|
||||
url: req.url,
|
||||
host: req.host,
|
||||
clientIp: req.clientIp,
|
||||
})
|
||||
```
|
||||
|
||||
### Database inspection from hooks
|
||||
|
||||
```js
|
||||
const rows = context.db.find("content", { limit: 5 })
|
||||
context.debug.dump(rows, "sample-content")
|
||||
```
|
||||
|
||||
### Direct endpoint debugging
|
||||
|
||||
Prefer targeted `curl` probes for:
|
||||
|
||||
- API JSON responses
|
||||
- SSR endpoint behavior
|
||||
- auth header behavior
|
||||
- audit endpoint behavior when relevant
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- jumping between frontend, hooks, and config without isolating the failing layer
|
||||
- assuming a browser symptom proves a frontend bug
|
||||
- trying to fix permissions only in the UI
|
||||
- debugging SSR without the SSR endpoint
|
||||
- relying on outdated assumptions from old stack behavior
|
||||
|
||||
## What an LLM should inspect first
|
||||
|
||||
When asked to debug a tibi project with unclear failure ownership, inspect in this order:
|
||||
|
||||
1. current failing command or URL
|
||||
2. environment reachability
|
||||
3. auth/permission boundary
|
||||
4. config/reload state
|
||||
5. hook/runtime layer
|
||||
6. SSR/publication layer if the failure is page-related
|
||||
|
||||
This keeps debugging local and falsifiable instead of turning into broad repo wandering.
|
||||
@@ -0,0 +1,309 @@
|
||||
---
|
||||
name: website-solution-architecture
|
||||
description: Translate website requirements into a complete tibi-svelte-starter solution. Covers solution decomposition across collections, pagebuilder, navigation, SSR, actions, permissions, media, admin UX, and validation.
|
||||
---
|
||||
|
||||
# website-solution-architecture
|
||||
|
||||
## When to use this skill
|
||||
|
||||
Use this skill when:
|
||||
|
||||
- The user wants a complete website built on this starter
|
||||
- Requirements exist, but the data model and project structure do not yet
|
||||
- A feature request spans frontend, admin, hooks, SSR, and content modeling together
|
||||
- An LLM needs to decide what belongs in collections, blocks, actions, settings, or frontend code
|
||||
|
||||
## Goal
|
||||
|
||||
The goal is to teach an LLM how to convert requirements into a coherent website solution on this stack.
|
||||
|
||||
That means choosing the right shape for:
|
||||
|
||||
- content model
|
||||
- admin authoring UX
|
||||
- frontend rendering
|
||||
- SSR behavior
|
||||
- actions and workflows
|
||||
- media handling
|
||||
- permissions
|
||||
- verification
|
||||
|
||||
This skill is about architecture decisions, not isolated file edits.
|
||||
|
||||
## Core principle
|
||||
|
||||
Do not start by adding components. Start by modeling the system.
|
||||
|
||||
On this starter, a complete website usually spans these layers:
|
||||
|
||||
1. collections and actions in `api/`
|
||||
2. shared types in `types/`
|
||||
3. app shell, routing, and rendering in `frontend/src/`
|
||||
4. SSR validation and caching in `api/hooks/`
|
||||
5. admin ergonomics in collection meta/field config
|
||||
6. tests or direct validation steps
|
||||
|
||||
If the work starts at the UI layer without a content/admin model, the solution usually drifts.
|
||||
|
||||
## Canonical architecture areas
|
||||
|
||||
### 1. Content model
|
||||
|
||||
Decide early which collections exist and why.
|
||||
|
||||
Typical website collections:
|
||||
|
||||
- `content` for pages
|
||||
- `navigation` for header/footer/site menus
|
||||
- `medialib` for media assets
|
||||
- site settings or global content singleton
|
||||
- optional domain collections such as team, jobs, events, products, references
|
||||
|
||||
Do not create collections just because the frontend has a section. Create them when the data needs its own lifecycle, relations, searchability, or editorial ownership.
|
||||
|
||||
### 2. Pagebuilder model
|
||||
|
||||
Decide whether pages are best modeled as:
|
||||
|
||||
- page entries with `blocks[]`
|
||||
- references to reusable sections
|
||||
- a mix of page-local blocks and reusable records
|
||||
|
||||
Use pagebuilder structures when editors need flexible composition. Use separate collections when content is reused across multiple pages or needs its own workflows.
|
||||
|
||||
### 3. Navigation model
|
||||
|
||||
Navigation is not an afterthought. It is often page-critical runtime and SSR data.
|
||||
|
||||
Design:
|
||||
|
||||
- header navigation
|
||||
- footer navigation
|
||||
- utility navigation if needed
|
||||
- relation to localized paths
|
||||
|
||||
If navigation is part of the shell, it must be loaded and rendered coherently in browser and SSR.
|
||||
|
||||
### 4. Route model
|
||||
|
||||
This starter uses content-driven routing, not file-based routing.
|
||||
|
||||
Architectural decisions must account for:
|
||||
|
||||
- language-prefixed public URLs
|
||||
- DB paths stored without language prefix
|
||||
- route translations
|
||||
- canonical paths and alias paths
|
||||
- SSR route validation in `api/hooks/config.js`
|
||||
|
||||
If the route model is unclear, the frontend and SSR will diverge.
|
||||
|
||||
### 5. Admin authoring model
|
||||
|
||||
Every serious website on this stack needs an editor-friendly Nova model.
|
||||
|
||||
Use:
|
||||
|
||||
- `preview`
|
||||
- `sidebar`
|
||||
- `containerProps.layout`
|
||||
- `dependsOn`
|
||||
- `drillDown`
|
||||
- `pagebuilder`
|
||||
- `subNavigation`
|
||||
- `singleton`
|
||||
- foreign previews
|
||||
|
||||
**I18n field config:** When modeling multilingual content, decide early whether to use:
|
||||
- **Field-level i18n** — object fields whose subField names match language codes (`de`, `en`, etc.) are auto-detected and rendered with language tabs in Nova. Configured via `api.meta.i18n` in `config.yml` or per-collection `meta.i18n`.
|
||||
- **Entry-level i18n** — each entry represents one language, linked by a shared `translationGroup` UUID. The `I18nEntryConfig` type (from `tibi-admin-nova/types/admin.d.ts`) defines `languageField`, `groupField`, and `copyFields`/`clearFields` for translation cloning behavior.
|
||||
|
||||
See `tibi-admin-nova/types/admin.d.ts` interfaces `I18nFieldConfig` and `I18nEntryConfig` for the full API.
|
||||
|
||||
Do not treat admin config as optional polish. It is part of the solution architecture.
|
||||
|
||||
### 6. Actions and workflows
|
||||
|
||||
Decide whether non-page features belong in collections, actions, or both.
|
||||
|
||||
Typical action-based website workflows:
|
||||
|
||||
- contact form
|
||||
- newsletter signup
|
||||
- booking or quote request
|
||||
- webhook receiver
|
||||
- AI helper endpoint
|
||||
|
||||
Do not force endpoint logic into fake CRUD collections.
|
||||
|
||||
### 7. SSR and caching
|
||||
|
||||
If the project uses SSR, the architecture must define:
|
||||
|
||||
- which routes are SSR-valid
|
||||
- which collections influence rendered HTML
|
||||
- how invalidation happens
|
||||
- which page-critical data must be loaded during SSR
|
||||
|
||||
On this starter, SSR is architecture, not a plugin. It must be considered while modeling routing, navigation, page content, and mutation hooks.
|
||||
|
||||
### 8. Media and SEO
|
||||
|
||||
Most website projects need explicit decisions for:
|
||||
|
||||
- media library usage
|
||||
- image fields and filters
|
||||
- alt texts and captions
|
||||
- SEO metadata per page
|
||||
- social/share metadata where required
|
||||
- publication windows
|
||||
|
||||
These decisions often belong partly in content collections and partly in admin ergonomics.
|
||||
|
||||
### 9. Permissions and editorial safety
|
||||
|
||||
Before implementing, decide:
|
||||
|
||||
- who may edit which collection
|
||||
- which fields are readonly or hidden
|
||||
- which collections are public or internal
|
||||
- whether actions are public, authenticated, or internal only
|
||||
|
||||
Permissions are part of the architecture, not only a final hardening step.
|
||||
|
||||
## Recommended planning flow
|
||||
|
||||
### Step 1: Extract the website capabilities
|
||||
|
||||
Turn the brief into concrete capability buckets:
|
||||
|
||||
- page types
|
||||
- reusable sections
|
||||
- navigation
|
||||
- forms/workflows
|
||||
- media/SEO
|
||||
- localization
|
||||
- editor roles
|
||||
- SSR/publication needs
|
||||
|
||||
### Step 2: Map capabilities to runtime surfaces
|
||||
|
||||
For each capability, decide whether it belongs in:
|
||||
|
||||
- collection schema
|
||||
- action endpoint
|
||||
- pagebuilder block
|
||||
- site settings singleton
|
||||
- frontend-only presentation
|
||||
- hook-based server logic
|
||||
|
||||
### Step 3: Shape the editor workflows
|
||||
|
||||
Before building components, decide how editors will:
|
||||
|
||||
- create pages
|
||||
- compose blocks
|
||||
- edit navigation
|
||||
- manage reusable entities
|
||||
- preview references
|
||||
- find the right entries in Nova
|
||||
|
||||
If this step is skipped, the content model often becomes technically correct but operationally poor.
|
||||
|
||||
### Step 4: Define the frontend boundaries
|
||||
|
||||
Clarify:
|
||||
|
||||
- app shell responsibilities
|
||||
- which parts are pure presentation blocks
|
||||
- where data loading lives
|
||||
- how route parameters map to content queries
|
||||
- which features must be SSR-safe
|
||||
|
||||
### Step 5: Define server responsibilities
|
||||
|
||||
Clarify:
|
||||
|
||||
- route validation
|
||||
- public read filtering
|
||||
- cache invalidation
|
||||
- action validation and side effects
|
||||
- publication behavior
|
||||
|
||||
### Step 6: Define verification before implementation expands
|
||||
|
||||
At minimum, define how to verify:
|
||||
|
||||
- pages load in the browser
|
||||
- pages render in SSR when applicable
|
||||
- admin authoring is usable
|
||||
- actions/forms behave correctly
|
||||
- build and validate stay clean
|
||||
|
||||
## Typical solution patterns
|
||||
|
||||
### Marketing website
|
||||
|
||||
Typical shape:
|
||||
|
||||
- `content` collection with pagebuilder blocks
|
||||
- `navigation` collection
|
||||
- global site settings singleton
|
||||
- SSR enabled for public pages
|
||||
- one or more public actions for forms
|
||||
|
||||
### Content-heavy editorial website
|
||||
|
||||
Typical shape:
|
||||
|
||||
- `content` plus additional domain collections
|
||||
- stronger use of relations and reusable entities
|
||||
- richer preview/search ergonomics in Nova
|
||||
- publication-aware SSR invalidation
|
||||
|
||||
### Product or service website with lead generation
|
||||
|
||||
Typical shape:
|
||||
|
||||
- structured domain collections for offers/services
|
||||
- pagebuilder pages for marketing presentation
|
||||
- public actions for inquiry flows
|
||||
- staff-facing inquiry persistence when follow-up is needed
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- Starting from Svelte components before defining collections and flows
|
||||
- Treating admin ergonomics as a later cleanup step
|
||||
- Mixing page data, workflow data, and settings without clear boundaries
|
||||
- Creating one-off block types for every page variation
|
||||
- Using collections where actions are the better model
|
||||
- Forgetting SSR implications while changing route or content shape
|
||||
- Leaving types, renderer, and collection schema out of sync
|
||||
|
||||
## Architecture checklist
|
||||
|
||||
Before calling a website solution on this starter coherent, verify that all of these are answered:
|
||||
|
||||
1. Which collections exist and why?
|
||||
2. Which content is page-local versus reusable?
|
||||
3. How are routes, language prefixes, and canonical paths modeled?
|
||||
4. Which authoring workflows exist in Nova?
|
||||
5. Which non-CRUD workflows require actions?
|
||||
6. Which data is page-critical for SSR?
|
||||
7. Which permissions protect content and workflows?
|
||||
8. How will success be validated technically and functionally?
|
||||
|
||||
## What an LLM should inspect first
|
||||
|
||||
When asked to build a complete website on this starter, inspect in this order:
|
||||
|
||||
1. `api/collections/content.yml`
|
||||
2. `api/collections/navigation.yml`
|
||||
3. `frontend/src/App.svelte`
|
||||
4. `frontend/src/blocks/BlockRenderer.svelte`
|
||||
5. `types/global.d.ts`
|
||||
6. `api/hooks/config.js`
|
||||
7. existing actions/hooks if the project already has workflows
|
||||
|
||||
This order exposes the actual project architecture before the LLM starts generating new code.
|
||||
@@ -0,0 +1 @@
|
||||
code:$apr1$AeePIAei$E9E6E6jtFFtwmtGhIEG.Y/
|
||||
@@ -0,0 +1,2 @@
|
||||
code:$apr1$AeePIAei$E9E6E6jtFFtwmtGhIEG.Y/
|
||||
web:$apr1$/zc/TBtD$ZGr3RqPiULYMD0kJUup5E0
|
||||
-191
@@ -1,191 +0,0 @@
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: default
|
||||
|
||||
workspace:
|
||||
path: /drone/workdir
|
||||
|
||||
steps:
|
||||
- name: load dependencies
|
||||
image: node
|
||||
pull: if-not-exists
|
||||
environment:
|
||||
FORCE_COLOR: "true"
|
||||
volumes:
|
||||
- name: cache
|
||||
path: /cache
|
||||
commands:
|
||||
- mkdir -p /cache/node_modules
|
||||
- mkdir -p /cache/user-cache
|
||||
- ln -s /cache/node_modules ./node_modules
|
||||
- ln -s /cache/user-cache ~/.cache
|
||||
- echo cache=/cache/npm-cache >> .npmrc
|
||||
- "echo 'enableGlobalCache: false' >> .yarnrc"
|
||||
- 'echo ''cacheFolder: "/cache/yarn-cache"'' >> .yarnrc'
|
||||
- 'echo ''yarn-offline-mirror "/cache/npm-packages-offline-cache"'' >> .yarnrc'
|
||||
- "echo 'yarn-offline-mirror-pruning: true' >> .yarnrc"
|
||||
- cat .yarnrc
|
||||
- yarn install --verbose --frozen-lockfile
|
||||
|
||||
- name: mongo
|
||||
image: mongo
|
||||
pull: if-not-exists
|
||||
detach: true
|
||||
|
||||
- name: maildev
|
||||
image: node
|
||||
pull: if-not-exists
|
||||
volumes:
|
||||
- name: cache
|
||||
path: /cache
|
||||
commands:
|
||||
- yarn run maildev --web 80 --smtp 25 -v --hide-extensions=STARTTLS
|
||||
detach: true
|
||||
|
||||
- name: liveserver
|
||||
image: node
|
||||
pull: if-not-exists
|
||||
volumes:
|
||||
- name: cache
|
||||
path: /cache
|
||||
commands:
|
||||
- yarn run -- live-server --no-browser --port=80 --ignore='*' --entry-file=spa.html --no-css-inject --proxy=/api:http://tibi-server:8080/api/v1/_/__NAMESPACE__ dist
|
||||
detach: true
|
||||
|
||||
- name: tibi-server
|
||||
image: registry.webmakers.de/tibi/tibi-server
|
||||
pull: never
|
||||
environment:
|
||||
DB_DIAL: mongodb://mongo
|
||||
API_PORT: 8080
|
||||
MAIL_HOST: maildev:25
|
||||
detach: true
|
||||
|
||||
- name: cypress run
|
||||
image: cypress/base
|
||||
pull: if-not-exists
|
||||
volumes:
|
||||
- name: cache
|
||||
path: /cache
|
||||
environment:
|
||||
FORCE_COLOR: "true"
|
||||
CYPRESS_BASE_URL: http://liveserver
|
||||
CYPRESS_CI: "true"
|
||||
CYPRESS_mongodbUri: mongodb://mongo
|
||||
CYPRESS_tibiApiUrl: http://tibi-server:8080/api/v1
|
||||
CYPRESS_projectApiConfig: /drone/workdir/api/config.yml
|
||||
commands:
|
||||
- ln -s /cache/user-cache ~/.cache
|
||||
- yarn build:instanbul
|
||||
- yarn cy:run
|
||||
- yarn run nyc report --exclude-after-remap false
|
||||
|
||||
- name: modify master config
|
||||
image: bash
|
||||
pull: if-not-exists
|
||||
commands:
|
||||
- bash scripts/modify-config.sh master __MASTER_URL__
|
||||
when:
|
||||
branch: [master]
|
||||
|
||||
- name: modify dev config
|
||||
image: bash
|
||||
pull: if-not-exists
|
||||
commands:
|
||||
- bash scripts/modify-config.sh dev __DEV_URL__
|
||||
when:
|
||||
branch: [dev]
|
||||
|
||||
- name: build
|
||||
image: node
|
||||
pull: if-not-exists
|
||||
volumes:
|
||||
- name: cache
|
||||
path: /cache
|
||||
commands:
|
||||
- yarn build
|
||||
|
||||
- name: build ssr
|
||||
image: node
|
||||
pull: if-not-exists
|
||||
volumes:
|
||||
- name: cache
|
||||
path: /cache
|
||||
commands:
|
||||
- yarn build:server
|
||||
|
||||
- name: build legacy
|
||||
image: node
|
||||
pull: if-not-exists
|
||||
volumes:
|
||||
- name: cache
|
||||
path: /cache
|
||||
commands:
|
||||
- yarn build:legacy
|
||||
|
||||
- name: modify html
|
||||
image: bash
|
||||
pull: if-not-exists
|
||||
commands:
|
||||
- bash scripts/preload-meta.sh public/spa.html
|
||||
- bash scripts/preload-meta.sh public/spa.html > dist/spa.html
|
||||
- export stamp=`date +%s`
|
||||
- echo $$stamp
|
||||
- sed -i s/__TIMESTAMP__/$$stamp/g dist/spa.html
|
||||
- sed -i s/__TIMESTAMP__/$$stamp/g dist/serviceworker.js
|
||||
- cat dist/serviceworker.js
|
||||
- cp dist/spa.html api/templates/spa.html
|
||||
- cat dist/spa.html
|
||||
|
||||
- name: deploy master
|
||||
image: instrumentisto/rsync-ssh
|
||||
pull: if-not-exists
|
||||
environment:
|
||||
RSYNC_USER: USER_PROJECT_master
|
||||
RSYNC_PASS:
|
||||
from_secret: rsync_master
|
||||
commands:
|
||||
- apk add --no-cache sshpass
|
||||
- scripts/deploy.sh ftp1.webmakers.de $${RSYNC_USER} $${RSYNC_PASS}
|
||||
when:
|
||||
branch: [master]
|
||||
event: [push]
|
||||
|
||||
- name: deploy dev
|
||||
image: instrumentisto/rsync-ssh
|
||||
pull: if-not-exists
|
||||
environment:
|
||||
RSYNC_USER: USER_PROJECT_dev
|
||||
RSYNC_PASS:
|
||||
from_secret: rsync_dev
|
||||
commands:
|
||||
- apk add --no-cache sshpass
|
||||
- scripts/deploy.sh ftp1.webmakers.de $${RSYNC_USER} $${RSYNC_PASS}
|
||||
when:
|
||||
branch: [dev]
|
||||
event: [push]
|
||||
|
||||
- name: prepare notify
|
||||
image: cypress/base
|
||||
pull: if-not-exists
|
||||
commands:
|
||||
- find cypress -type f -wholename "cypress/videos/*" -or -wholename "cypress/screenshots/*" | tar -cvf cypress-media.tar -T -
|
||||
when:
|
||||
status:
|
||||
- failure
|
||||
|
||||
- name: notify
|
||||
image: drillster/drone-email
|
||||
pull: if-not-exists
|
||||
settings:
|
||||
from: noreply@ci.gitbase.de
|
||||
host: smtp.basehosts.de
|
||||
attachment: cypress-media.tar
|
||||
when:
|
||||
status:
|
||||
- failure
|
||||
|
||||
volumes:
|
||||
- name: cache
|
||||
host:
|
||||
path: /tmp/cache/drone/${DRONE_REPO}
|
||||
@@ -0,0 +1,27 @@
|
||||
PROJECT_NAME=__PROJECT_NAME__
|
||||
TIBI_PREFIX=tibi
|
||||
TIBI_NAMESPACE=__TIBI_NAMESPACE__
|
||||
CODER_UID=100
|
||||
CODER_GID=101
|
||||
|
||||
SENTRY_URL=https://sentry.basehosts.de
|
||||
SENTRY_ORG=webmakers
|
||||
SENTRY_PROJECT=
|
||||
|
||||
RSYNC_HOST=ftp1.webmakers.de
|
||||
RSYNC_PORT=22223
|
||||
|
||||
PRODUCTION_SERVER=dock4.basehosts.de
|
||||
PRODUCTION_TIBI_PREFIX=tibi
|
||||
PRODUCTION_PATH=/webroots2/customers/_CUSTOMER_ID_/____
|
||||
|
||||
STAGING_PATH=/staging/__ORG__/__PROJECT__/dev
|
||||
|
||||
LIVE_URL=https://www
|
||||
STAGING_URL=https://dev-__PROJECT_NAME__.staging.testversion.online
|
||||
CODING_URL=https://__PROJECT_NAME__.code.testversion.online
|
||||
CODING_TIBIADMIN_URL=https://__PROJECT_NAME__-tibiadmin.code.testversion.online
|
||||
|
||||
|
||||
#START_SCRIPT=:ssr
|
||||
#MOCK=1
|
||||
@@ -0,0 +1,70 @@
|
||||
name: deploy to production
|
||||
|
||||
on: "push"
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: deploy
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: gitbase.de/actions/ubuntu:latest
|
||||
volumes:
|
||||
- /data:/data
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
lfs: true
|
||||
submodules: true
|
||||
|
||||
- run: |
|
||||
git fetch --force --tags
|
||||
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: install dependencies
|
||||
run: |
|
||||
npm install -g yarn
|
||||
yarn install
|
||||
|
||||
- name: validate
|
||||
run: |
|
||||
yarn validate
|
||||
|
||||
- name: modify config
|
||||
run: ./scripts/ci-modify-config.sh
|
||||
|
||||
- name: build
|
||||
env:
|
||||
FORCE_COLOR: "true"
|
||||
run: |
|
||||
yarn build
|
||||
|
||||
- name: build ssr
|
||||
env:
|
||||
FORCE_COLOR: "true"
|
||||
run: |
|
||||
yarn build:server
|
||||
|
||||
- name: upload sourcemaps to sentry
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
run: ./scripts/ci-upload-sourcemaps.sh
|
||||
|
||||
- name: staging
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
env:
|
||||
API_BASEDIR: /data/${{ github.repository }}/${{ github.ref_name }}
|
||||
COMPOSE_PROJECT_NAME: ${{ github.repository }}-${{ github.ref_name }}
|
||||
run: ./scripts/ci-staging.sh
|
||||
|
||||
- name: deploy
|
||||
if: github.ref == 'refs/heads/master'
|
||||
env:
|
||||
RSYNC_USER: ${{ github.repository }}
|
||||
RSYNC_PASS: ${{ github.token }}
|
||||
BRANCH: ${{ github.ref_name }}
|
||||
run: ./scripts/ci-deploy.sh
|
||||
+23
-16
@@ -1,17 +1,24 @@
|
||||
_temp/
|
||||
node_modules/
|
||||
dist/
|
||||
build/
|
||||
build_ssr/
|
||||
stat/
|
||||
api/hooks/lib/app.server*
|
||||
api/hooks/lib/buildInfo.js
|
||||
frontend/src/lib/buildInfo.ts
|
||||
node_modules
|
||||
media
|
||||
tmp
|
||||
_temp
|
||||
frontend/dist
|
||||
yarn-error.log
|
||||
/media/
|
||||
/test.js
|
||||
/api/templates/spa.html
|
||||
/api/hooks/lib/app.server*
|
||||
cypress/_old
|
||||
cypress/videos
|
||||
cypress/screenshots
|
||||
.~lock.*
|
||||
coverage/
|
||||
.nyc_output/
|
||||
test-results/
|
||||
playwright-report/
|
||||
playwright/.cache/
|
||||
visual-review/
|
||||
video-tours/output/
|
||||
.playwright-mcp/
|
||||
.agents/STARTER_ALIGNMENT_*.md
|
||||
|
||||
.yarn/*
|
||||
!.yarn/cache
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
"check-parameters"
|
||||
],
|
||||
"no-var-keyword": true,
|
||||
"svelteSortOrder": "scripts-markup-styles",
|
||||
"svelteSortOrder": "scripts-options-markup-styles",
|
||||
"svelteStrictMode": true,
|
||||
"svelteBracketNewLine": true,
|
||||
"svelteAllowShorthand": true,
|
||||
|
||||
Vendored
+15
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+32
-27
@@ -1,34 +1,39 @@
|
||||
{
|
||||
"eslint.alwaysShowStatus": true,
|
||||
"tslint.autoFixOnSave": true,
|
||||
"editor.tabCompletion": "on",
|
||||
"diffEditor.codeLens": true,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnPaste": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"[markdown]": {
|
||||
"editor.wordWrap": "on",
|
||||
"editor.defaultFormatter": "vscode.markdown-language-features"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"liveServer.settings.root": "/dist",
|
||||
"liveServer.settings.file": "spa.html",
|
||||
"liveServer.settings.port": 5502,
|
||||
"liveServer.settings.proxy": {
|
||||
"enable": true,
|
||||
"baseUri": "/api",
|
||||
"proxyUri": "http://127.0.0.1:8080/api/v1/_/__NAMESPACE__"
|
||||
},
|
||||
"extensions.ignoreRecommendations": true,
|
||||
"files.autoSave": "off",
|
||||
"typescript.tsc.autoDetect": "off",
|
||||
"npm.autoDetect": "off",
|
||||
"debug.allowBreakpointsEverywhere": true,
|
||||
"html.autoClosingTags": false,
|
||||
"yaml.schemas": {
|
||||
"node_modules/tibi-types/schemas/api-config/config.json": "api/config.y*ml",
|
||||
"node_modules/tibi-types/schemas/api-config/collection.json": "api/collections/*.y*ml",
|
||||
"node_modules/tibi-types/schemas/api-config/field.json": "api/collections/fields/*.y*ml"
|
||||
"./../../cms/tibi-types/schemas/config/project.schema.json": "api/config.y*ml",
|
||||
"./../../cms/tibi-types/schemas/config/collection.schema.json": "api/collections/*.y*ml",
|
||||
"./../../cms/tibi-types/schemas/config/field.schema.json": "api/collections/fields/*.y*ml",
|
||||
"./../../cms/tibi-types/schemas/config/field-list.schema.json": "api/collections/fieldLists/*.y*ml",
|
||||
"./../../cms/tibi-types/schemas/config/job.schema.json": "api/jobs/*.y*ml",
|
||||
"./../../cms/tibi-types/schemas/config/asset.schema.json": "api/assets/*.y*ml"
|
||||
},
|
||||
"yaml.customTags": ["!include scalar"]
|
||||
"yaml.customTags": ["!include scalar"],
|
||||
"filewatcher.commands": [
|
||||
{
|
||||
"match": "/api/.*(\\.ya?ml|js|env)$",
|
||||
"isAsync": false,
|
||||
"cmd": "cd ${currentWorkspace} && scripts/reload-local-tibi.sh",
|
||||
"event": "onFileChange"
|
||||
}
|
||||
],
|
||||
"i18n-ally.localesPaths": ["frontend/src/lib/i18n/locales"],
|
||||
"i18n-ally.sourceLanguage": "de",
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"i18n-ally.enabledFrameworks": ["svelte"],
|
||||
"i18n-ally.displayLanguage": "de",
|
||||
"[svelte]": {
|
||||
"editor.defaultFormatter": "svelte.svelte-vscode"
|
||||
},
|
||||
"files.associations": {
|
||||
"css": "tailwindcss"
|
||||
},
|
||||
"css.validate": true,
|
||||
"css.lint.unknownAtRules": "ignore",
|
||||
"playwright.reuseBrowser": false,
|
||||
"playwright.showTrace": true
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
LFS
Vendored
BIN
Binary file not shown.
LFS
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
LFS
Vendored
BIN
Binary file not shown.
LFS
Vendored
BIN
Binary file not shown.
LFS
Vendored
BIN
Binary file not shown.
LFS
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
LFS
Vendored
BIN
Binary file not shown.
LFS
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
LFS
Vendored
BIN
Binary file not shown.
LFS
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
LFS
Vendored
BIN
Binary file not shown.
LFS
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
LFS
Vendored
BIN
Binary file not shown.
LFS
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
LFS
Vendored
BIN
Binary file not shown.
LFS
Vendored
BIN
Binary file not shown.
LFS
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
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