feat: enhance medialib image handling and add asset URL resolution

- Implemented `resolveApiAssetUrl` function to normalize asset URLs based on API base.
- Updated `MedialibImage` component to utilize new asset URL resolution and added support for alt text and class properties.
- Enhanced image loading behavior with improved width measurement and focal point handling.
- Added placeholder image handling and improved accessibility with alt text.
- Introduced new test script for auditing broken links in skill documentation.
- Expanded seeded test content to include medialib entries and updated related tests for pagebuilder previews.
- Improved global setup and teardown logging for clarity on seeded content management.
This commit is contained in:
2026-05-17 00:52:41 +00:00
parent 958b45272d
commit 4020ad62c5
44 changed files with 4276 additions and 867 deletions
+607 -206
View File
@@ -1,248 +1,649 @@
# Build Checklist — Autonomous Website Project
> Navigate this checklist **in order** when building a complete website project from the `tibi-svelte-starter`. Each phase produces concrete artifacts. Do not skip phases — earlier decisions constrain later ones.
> 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.
---
## Phase 0: Project Bootstrap
## How to use this checklist
**Skills:** `tibi-project-setup`
For every phase, complete all five parts:
- [ ] Replace all starter placeholders in ALL files:
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: __TIBI_NAMESPACE__`
- `frontend/.htaccess`: both `__TIBI_NAMESPACE__` entries
- `api/hooks/config-client.js`: `__PROJECT__` (not `__PROJECT_NAME__`)
- [ ] Configure `.env` URLs: `CODING_URL`, `STAGING_URL`, `CODING_TIBIADMIN_URL`
- [ ] Update `package.json` metadata (name, repository)
- [ ] Run `grep -n '__[A-Z0-9_]\+__' . --include='*.{yml,js,env,htaccess,json}'` to verify no placeholder remains
- [ ] Generate secure `ADMIN_TOKEN` in `api/config.yml.env`
- [ ] Verify `ADMIN_ASSET_VERSION` exists in `api/config.yml.env` (re-generate if missing: `echo "ADMIN_ASSET_VERSION=$(node -e "process.stdout.write(require('crypto').randomBytes(6).toString('hex'))")-dirty-${Date.now()}" >> api/config.yml.env`)
- [ ] `yarn install` — must succeed
- [ ] `make docker-up` — verify all containers "Up"
- [ ] Verify URLs respond: website, tibiadmin, tibiserver API
- [ ] `yarn build && yarn build:server && yarn validate` — 0 errors, 0 warnings
- `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.
## Phase 1: Solution Architecture
**Validation commands:**
**Skills:** `website-solution-architecture`, `security-hardening-and-token-strategy`
```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
```
- [ ] Document the website's content model:
- Which page types exist?
- Which data is page-local vs. reusable?
- Which domain collections exist (team, products, events, etc.)?
- Is content multilingual? Entry-level or field-level i18n?
- [ ] Document the navigation model:
- Header, footer, utility navigation?
- Language-specific or shared?
- Max nesting levels per tree?
**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-prefixed URLs (`/{lang}/{path}`)?
- Content `path` stored without language prefix?
- Route translations needed?
- [ ] Document forms/workflows:
- Contact form, newsletter, booking, etc.?
- Action endpoints or collections?
- Persistence needed (inquiries collection)?
- 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 are SSR-valid?
- Which collections are page-critical (content, navigation)?
- Publication windows needed?
- [ ] Document permissions:
- Who can CRUD which collections?
- Field-level readonly/hidden for editors?
- Token-based integrations?
- 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
## Phase 2: Collection & Admin Model
**Validation commands:**
**Skills:** `content-authoring`, `admin-ui-config`, `nova-pagebuilder-modeling`, `nova-navigation-modeling`, `media-seo-publishing`
```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"
```
- [ ] Create/modify collection YAML files in `api/collections/`:
- `content.yml` with pagebuilder blocks
- `navigation.yml` with `viewHint.navigation`
- `medialib.yml` with media library
- Domain collections (team, products, events, etc.)
- `ssr.yml` (SSR cache — keep as-is)
- [ ] Include all collections in `api/config.yml`
- [ ] Configure admin ergonomics for EVERY collection:
- `meta.preview` for row/breadcrumb/FK display
- `meta.viewHint` (table, cards, media, navigation)
- `sidebar` groups for publication/SEO/settings
- `containerProps.layout` for multi-column forms
- `drillDown` for complex `object[]` arrays
- `dependsOn` for conditional fields
- `pagebuilder` + `blockRegistry` for block-based collections
- `singleton` for single-document config collections
- `choices`, `foreign`, `widget` overrides as needed
- [ ] Configure field validators:
- `required`, `maxLength`, `min`, `max` where appropriate
- `accept` MIME types for file fields
- `image` dimension constraints for image fields
- `format: "email" | "url" | "slug"` for string fields
- [ ] Verify in Nova admin:
- Collection sidebar labels, icons, groups
- List views show meaningful previews
- Entry forms are usable (not one long scrolling column)
- Sidebar groups and sections are coherent
- Foreign references show readable previews
- Pagebuilder blocks are selectable and editable
**Exit criteria:**
## Phase 3: TypeScript Types
- [ ] 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.
**Skills:** `content-authoring` (types section)
## Phase 2 — Collection model and Nova admin ergonomics
- [ ] Create/modify `types/global.d.ts`:
- `ContentBlockEntry` fields matching collection YAML subFields
- Domain collection entry types (e.g. `TeamEntry`, `ProductEntry`)
- `Ssr` type for SSR config interface
- [ ] Wire collection types into `EntryTypeSwitch` in `frontend/src/lib/api.ts`
- [ ] `yarn validate` — 0 errors, 0 warnings
**Required skills:** `content-authoring`, `admin-ui-config`, `nova-pagebuilder-modeling`, `nova-navigation-modeling`, `media-seo-publishing`
## Phase 4: Frontend Components
**Required project artifacts:**
**Skills:** `frontend-architecture`, `nova-pagebuilder-modeling`
- `api/config.yml`
- `api/collections/content.yml`
- `api/collections/navigation.yml`
- `api/collections/medialib.yml`
- domain collection YAML files
- [ ] Create block components in `frontend/src/blocks/`:
- Each block type gets a Svelte 5 component
- Accept `block: ContentBlockEntry` as props
- SSR-safe (guard browser APIs with `typeof window !== "undefined"`)
- [ ] Register blocks in `frontend/src/blocks/BlockRenderer.svelte`:
- Add `import` and `{:else if}` case for each type
- [ ] Register pagebuilder blocks in admin bundle:
- Add each block to `blockRegistry` in `frontend/src/admin.ts`
- `yarn build` to regenerate `frontend/dist/admin.mjs`
- [ ] Verify frontend rendering:
- `yarn build` succeeds
- Page loads in browser
- All block types render
- Navigation and media references work
- Language switching works
**Implementation checks:**
## Phase 5: SSR Setup
- [ ] 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.
**Skills:** `tibi-ssr-caching`
**Validation commands:**
- [ ] Update `api/hooks/config.js`:
- `ssrValidatePath()` validates all public routes
- `publishedFilter` matches the publication model
- `ssrPublishCheckCollections` includes all time-sensitive collections
- [ ] Verify SSR endpoint directly:
```bash
curl "http://tibiserver:8080/api/v1/_/<namespace>/ssr?url=/de/..."
```
- HTTP status correct
- Page content present in HTML
- Navigation labels present in HTML
- `window.__SSR_CACHE__` present
- Second request returns `X-SSR-Cache: true`
- `yarn build:server` succeeds
```bash
yarn validate
```
## Phase 6: Backend Hooks & Actions
Admin validation should also cover:
**Skills:** `tibi-hook-authoring`, `tibi-actions-and-forms`, `scheduled-jobs-and-automation`, `realtime-and-live-workflows`
- 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
- [ ] Create/update public read hooks (`api/hooks/<collection>/get_read.js`):
- Filter inactive/unpublished entries for public users
- Use `filter_public.js` pattern
- [ ] Create/update cache invalidation hooks (`api/hooks/clear_cache.js`):
- Clear SSR cache on content/navigation/medialib changes
- Handle `POST`, `PUT`, `DELETE` for each collection that affects SSR
- [ ] Create action endpoints in `api/actions/`:
- Contact form, newsletter, etc.
- Hook chain: bind → validate → handle → return
- Permissions per action (public vs. authenticated)
- Wire into `api/config.yml` under `actions:`
- [ ] Register all hooks in collection/action YAML files
- [ ] Verify hooks work:
- Public API returns only active entries
- Actions respond correctly to valid/invalid submissions
- Cache clears on content mutation
**Exit criteria:**
## Phase 7: Permissions & Security
- [ ] 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.
**Skills:** `permissions-and-editor-workflows`, `security-hardening-and-token-strategy`
## Phase 3 — Type contracts and API typing
- [ ] Configure collection permissions:
- `public` read methods where appropriate
- `user` write methods for editors
- `"token:${ADMIN_TOKEN}"` for seed/test access
- [ ] Configure field-level permissions:
- `readonlyFields` at collection or permissionSet level
- `hiddenFields` for sensitive internal data
- `readonly`/`hidden` with eval for dynamic rules
- [ ] Secure sensitive config:
- Production `ADMIN_TOKEN` uses a real random value
- Hook `http.fetch` and `exec.command` used with caution
- [ ] Verify: non-admin users see only permitted fields/collections
- [ ] Verify CORS if external origins access the API
**Required skills:** `content-authoring`
## Phase 8: Media & SEO
**Required project artifacts:**
**Skills:** `media-seo-publishing`, `nova-ai-editor-features` (optional)
- `types/global.d.ts`
- `frontend/src/lib/api.ts`
- [ ] Configure `api/collections/medialib.yml`:
- File field with `widget: image`
- Alt text, caption fields
- Image filters for thumbnails, cards, heroes
- [ ] Add SEO fields to content/page collections:
- `meta.title`, `meta.description`
- Social share image reference
- Sidebar placement for SEO fields
- [ ] Configure publication model:
- `active` boolean
- `publication.from` / `publication.to` if time-based
- SSR-aware cache invalidation for publication changes
- [ ] Verify: SSR HTML includes SEO meta tags
**Implementation checks:**
## Phase 9: Testing
- [ ] 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.
**Skills:** `playwright-testing`
**Validation commands:**
- [ ] Extend seed data in `tests/api/helpers/seed-data.ts`:
- Seeded pages for all public routes
- Seeded navigation entries
- Hidden `_testdata: true` marker for seed identity
- [ ] Write API tests for collections:
- Public reads return expected data
- Auth-required writes are enforced
- Actions respond correctly
- [ ] Write E2E tests for critical user journeys:
- Homepage loads
- Language switching works
- SPA navigation works
- Each block type renders
- 404 for non-existent pages
- [ ] Write admin smoke tests if admin workflows are stable:
- Login works
- Core collections are reachable
- [ ] Run affected tests:
```bash
npx playwright test tests/api/health.spec.ts --project=api
npx playwright test tests/e2e/home.spec.ts --project=chromium
```
```bash
yarn validate
```
## Phase 10: Video Tours (optional)
**Exit criteria:**
- [ ] Update/create tour files in `video-tours/tours/`:
- Key user flows for documentation/training
- Desktop and mobile variants
- [ ] Run tours and verify output:
```bash
yarn tour && yarn tour:mobile
```
- [ ] The current project types describe the collection and block model accurately.
- [ ] Validation passes cleanly.
## Phase 11: Final Verification
## Phase 4 — Frontend blocks, routing, and admin registry
**Skills:** `tibi-project-setup` (build steps), `playwright-testing` (test suite)
**Required skills:** `frontend-architecture`, `nova-pagebuilder-modeling`
- [ ] `yarn build` — success
- [ ] `yarn build:server` — success
- [ ] `yarn validate` — 0 errors, 0 warnings
- [ ] `rg '__[A-Z0-9_]\+__' . --include='*.{yml,js,env,htaccess,json,ts,svelte}'` — no placeholder remains
- [ ] All Playwright tests pass (or known failures documented)
- [ ] Public site loads at website URL
- [ ] Nova admin loads at admin URL
- [ ] Pages are creatable and editable in admin
- [ ] SSR renders real page content
- [ ] Forms/actions work (if applicable)
- [ ] `config.yml.env` has production-ready `ADMIN_TOKEN`
**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.
+100 -8
View File
@@ -1,6 +1,6 @@
---
name: admin-ui-config
description: Configure the admin UI for collections — meta labels, preview/viewHint, field widgets, inputProps, sidebar layout, choices, foreign references, and image handling. Use when setting up or customizing collection admin views.
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
@@ -24,6 +24,46 @@ Treat this skill as Nova-first. Use current Nova concepts such as `preview`, `si
---
## 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.
@@ -84,6 +124,42 @@ meta:
- `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
@@ -247,6 +323,8 @@ Use `foreign.id: id` for the outward FK field identity. Only Mongo-style filters
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
@@ -466,13 +544,9 @@ 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
}) {
function createContentBlockDefinition(presentation: { label: string; icon: string; color: string }) {
return {
css: [previewCssUrl], // CSS files to inject into Shadow DOM
css: [previewCssUrl], // CSS files to inject into Shadow DOM
label: presentation.label,
icon: presentation.icon,
color: presentation.color,
@@ -517,6 +591,7 @@ 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.
@@ -665,7 +740,7 @@ indexes:
- name: price_sort
key: [price]
- name: category_active
key: [category, -active] # -prefix for descending
key: [category, -active] # -prefix for descending
- name: slug_unique
key: [slug]
unique: true
@@ -682,9 +757,26 @@ search:
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.
@@ -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.
+124 -9
View File
@@ -1,6 +1,6 @@
---
name: content-authoring
description: Add new pages, content blocks, and collections to a tibi project. Covers the content-based routing model, block registration in BlockRenderer, collection YAML authoring, and TypeScript type definitions. Use when creating new pages, block types, or collections.
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
@@ -29,6 +29,20 @@ This project does **NOT** use file-based routing (no SvelteKit router). Instead:
**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
@@ -148,7 +162,30 @@ import MyNewBlock from "./MyNewBlock.svelte"
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: Extend TypeScript types (if new fields are needed)
### 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`:
@@ -161,7 +198,9 @@ interface ContentBlockEntry {
}
```
### Step 4: Extend collection YAML (if new fields need admin editing)
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`:
@@ -192,11 +231,13 @@ Use current Nova patterns when extending block schemas:
- `dependsOn` for block-type-specific fields
- collection- or field-level `meta.pagebuilder` for registry/default viewport settings
### Step 5: Update mock data (if using MOCK=1)
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 6: Verify
### Step 7: Verify
```sh
yarn validate # TypeScript check — must be warning-free
@@ -209,6 +250,12 @@ 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 |
@@ -309,17 +356,21 @@ interface MyCollectionEntry {
If you need typed helpers, extend the `EntryTypeSwitch` in `frontend/src/lib/api.ts`:
```typescript
type CollectionNameT = "medialib" | "content" | "mycollection" | string
type CollectionNameT = "medialib" | "content" | "navigation" | "mycollection" | string
type EntryTypeSwitch<T extends string> = T extends "medialib"
? MedialibEntry
: T extends "content"
? ContentEntry
: T extends "mycollection"
? MyCollectionEntry
: Record<string, unknown>
: 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:
@@ -374,8 +425,28 @@ For collections intended for rich editorial usage, also verify in Nova:
- 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.
---
## 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.
@@ -385,3 +456,47 @@ For collections intended for rich editorial usage, also verify in Nova:
- **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 als 8. Argument an `getCachedEntries` übergeben:
```ts
const products = await getCachedEntries<"machines">(
"machines",
{ active: true, category: catId },
"sortOrder",
undefined,
undefined,
undefined,
undefined,
"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.
+192
View File
@@ -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 |
@@ -343,6 +343,48 @@ 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
@@ -389,3 +431,30 @@ Return { data, count, buildTime }
- **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.
+264 -11
View File
@@ -15,6 +15,239 @@ Use this skill when:
- 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",
{ active: true },
"sortOrder",
undefined,
undefined,
undefined,
undefined,
"imageField:medialib"
)
```
`getCachedEntries()` expects `lookup` as the 8th argument and as a string, not as part of the `params` 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.
@@ -32,11 +265,12 @@ For real website projects, these concerns affect:
Use these sources when implementing or reviewing these areas:
- `tibi-server/docs/08-file-upload-images.md`
- `api/collections/medialib.yml`
- `api/collections/content.yml`
- 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`
- `api/hooks/config.js`
- `api/hooks/lib/ssr-server.js`
- the project's SSR/runtime config hooks
- the project's frontend media widget/helper implementation
## Media modeling
@@ -175,19 +409,37 @@ Recommended shape:
- 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. Alt/caption/SEO fields are explicit and editor-friendly.
4. Publication state affects public output correctly.
5. SSR HTML still reflects the intended published state.
6. `yarn validate` stays clean.
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
@@ -195,8 +447,9 @@ When asked to work on media, SEO, or publishing on this starter, inspect in this
1. `tibi-server/docs/08-file-upload-images.md`
2. the relevant collection YAML
3. admin layout and previews for those fields
4. frontend components consuming the media/SEO data
5. SSR publish-check and invalidation logic if timing matters
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.
+170
View File
@@ -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.
@@ -25,8 +25,8 @@ Use AI where it improves editorial throughput or content quality. Do not add AI
Use these sources when implementing or reviewing AI-backed website features:
- `tibi-server/docs/09-llm-integration.md`
- `tibi-admin-nova/docs/collection-config.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
@@ -30,6 +30,7 @@ On this stack, navigation influences:
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`
@@ -186,7 +187,7 @@ After changing navigation modeling, verify all of these:
When asked to work on navigation in this starter, inspect in this order:
1. `api/collections/navigation.yml`
2. `tibi-admin-nova/docs/collection-config.md` section for `viewHint.navigation`
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
@@ -198,7 +198,7 @@ The exact shape can vary, but the pattern stays the same: block type first, then
### Step 3a: Build and wire the block registry
For this starter, the pagebuilder registry is not implicit. Nova loads it from the admin bundle via `meta.pagebuilder.blockRegistry.file`.
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:
@@ -207,7 +207,7 @@ The concrete chain is:
3. build the admin bundle with `yarn build`
4. point the collection field or collection meta to the built module
The current starter already does this in `frontend/src/admin.ts` and `api/collections/content.yml`.
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:
@@ -238,7 +238,7 @@ meta:
file: /_/assets/dist/admin.mjs?v=${ADMIN_ASSET_VERSION}
```
Important constraints for this starter:
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
@@ -246,6 +246,26 @@ Important constraints for this starter:
- 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.
@@ -358,6 +378,91 @@ Avoid pushing these concerns into block components unless there is a strong reas
- 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.
@@ -89,6 +89,23 @@ Important 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.
@@ -101,6 +118,12 @@ Typical examples:
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.
@@ -114,6 +137,29 @@ Recommended patterns:
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.
@@ -178,9 +224,11 @@ 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. Nova forms remain usable for the non-admin roles that actually work there.
5. Token/integration permissions are narrower than admin access when possible.
6. `yarn validate` stays clean.
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
+115
View File
@@ -202,6 +202,19 @@ When adding new deterministic coverage, extend the seed data instead of assertin
## 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:
@@ -249,6 +262,28 @@ Use `tests/e2e-mobile/` when validating:
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:
@@ -429,3 +464,83 @@ When extending or fixing tests:
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,191 @@
---
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`
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 text index.
### `regex`
Use when:
- the searchable fields are explicit
- case-insensitive matching is enough
- weighted field scoring is useful
Good for smaller datasets or precise keyed fields.
### `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.
### `vector`
Use when:
- semantic similarity matters more than literal keyword overlap
- the project can support embedding-provider setup and operator cost expectations
- search quality justifies added complexity
Vector mode can use:
- `fields`
- custom `eval` transformation
- `documentPrefix`
- `queryPrefix`
- `overflow: truncate|chunk`
- `rrf` tuning for hybrid scoring
## 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.
@@ -1,6 +1,6 @@
---
name: security-hardening-and-token-strategy
description: Apply current tibi-server security practices to website projects. Covers secret handling, token strategies, bulk-permission safety, cookie settings, SSRF/exec risks in hooks, and secure operator decisions for this stack.
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
@@ -9,150 +9,199 @@ description: Apply current tibi-server security practices to website projects. C
Use this skill when:
- Setting up or reviewing authentication and token use on this stack
- Deciding how admin tokens, JWT auth, and token permissions should be used
- Hardening hooks, actions, and project config against obvious security mistakes
- Reviewing bulk permissions, secrets, cookie settings, or risky server-side capabilities
- 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
The goal is to keep projects on this starter aligned with the current tibi-server security model and known risk areas.
Keep projects on this starter aligned with the current tibi-server security model and with the security-sensitive operator decisions the stack exposes.
This skill is not a generic web security guide. It is about the concrete operator and implementation choices this 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-related decisions:
Use these sources when implementing or reviewing security decisions:
- `tibi-server/docs/05-authentication.md`
- `tibi-server/docs/14-security.md`
- relevant collection/action permissions in `api/`
- project environment/config files
- `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:
This stack exposes multiple auth mechanisms. Do not mix them casually.
- JWT user auth
- refresh token cookie flow
- refresh-token cookie flow
- admin tokens
- token-based permissions for API-style access
- token-based permission sets for narrower machine access
Do not mix them casually. Each one has a different operational purpose.
Recommended default:
## Recommended token strategy
- 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
Use:
## Token header distinction
- **JWT user auth** for real users in admin or authenticated workflows
- **refresh cookies** for session continuation where appropriate
- **admin tokens** only for server/admin/ops scenarios that truly need that level of access
- **token permissions** for narrow integration access or machine clients
Use the right header for the right surface:
Avoid using broad admin tokens where a narrow project or collection-level token permission is enough.
- 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`
## Secrets handling
Do not assume a working `Token` header implies system-level admin rights.
Do not keep production secrets as plain literals in committed config.
## Secret handling
Prefer environment-variable substitution for:
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
- LLM API keys
- external API tokens
- admin token values
- admin tokens
- external API keys
- LLM/embedding provider keys
If a project ships real secrets in config, treat that as a structural problem, not a cosmetic cleanup.
If secrets are hardcoded in committed config, treat that as a structural problem, not as cleanup trivia.
## Bulk permission safety
Bulk mutations are explicitly more dangerous than single-document mutations.
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`
Do not enable bulk operations casually in website projects. Most editor workflows do not need them.
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
For refresh-token flows, ensure the deployment matches secure cookie expectations.
Refresh-token flows should respect the target environment.
Important considerations:
Review:
- secure cookies should stay enabled in HTTPS environments
- local non-HTTPS development may need explicit relaxation
- do not debug production cookie issues by weakening production defaults globally
- `api.secureCookies`
- HTTPS vs local HTTP expectations
- whether debugging shortcuts are accidentally bleeding into production config
## Hook risk surfaces
Do not weaken secure-cookie behavior globally just to make a dev shortcut work.
Current tibi-server exposes powerful server-side capabilities. Some of them require explicit restraint.
## 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:
- `http.fetch` / `http.fetchStream` can create SSRF risk
- `exec.command` can create command-execution risk
- `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.
## Query-parameter token risk
When such capabilities are used, document:
Token permissions may be passed through query parameters for specific cases, but this is a documented risk surface.
Prefer header-based token transport when possible.
If query tokens are unavoidable:
- avoid logging full URLs with sensitive query strings
- understand proxy/referrer/history exposure
- scope the token as narrowly as possible
## Permission boundaries
Security on this stack is layered.
Think in terms of:
- project visibility
- collection method permissions
- field-level restrictions
- token scope
- public vs authenticated action access
Do not rely on frontend hiding or convention where server-side permissions should be explicit.
- why they are necessary
- what the allowed target surface is
- what the safer rejected alternatives were
## CORS configuration
CORS follows a 3-level hierarchy. Configure it in `api/config.yml` under `cors:` for project-wide settings, or in individual collection/action YAML for per-endpoint overrides:
CORS follows a hierarchy. Configure it deliberately instead of widening it reactively.
| Level | Configuration location | Scope |
|-------|----------------------|-------|
| Server | tibi-server `config.yml` | Global default |
| Project | `api/config.yml``cors:` | Per project |
| Collection/Action | Collection or action YAML → `cors:` | Per endpoint |
Levels:
Each level can `merge: true` (append to parent) or `merge: false` (replace entirely).
- server-level `config.yml`
- project-level `api/config.yml`
- collection/action-level YAML overrides
For a project that serves a browser-based SPA to end users on its own domain and serves API/tibiadmin on separate subdomains, the default (no explicit CORS config) is usually correct since the SPA makes same-origin API calls via the BrowserSync/production reverse proxy. Add explicit CORS only when:
- the API is called from external origins (e.g. third-party integrations)
- the admin UI is served on a different origin than the API
- an action endpoint needs to support cross-origin form submissions
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.
See `tibi-server/docs/02-configuration.md` (section "CORS Configuration Hierarchy") for details.
## Recommended implementation patterns
## Secure implementation patterns
### Public form endpoint
### Public form workflow
Recommended shape:
- public action with narrow allowed methods
- public action with narrow methods
- server-side validation
- no broad admin tokens in the browser
- no unnecessary collection write permissions exposed publicly
- no admin token in the browser
- separate internal persistence only when truly required
### Integration token
@@ -162,32 +211,42 @@ Recommended shape:
- minimal collection/action scope
- header-based transport preferred
### Hook that calls external services
### Sensitive internal fields
Recommended shape:
- fixed or validated destination URLs
- no arbitrary user-controlled internal target fetching
- minimal capability needed for the feature
- 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
- Using admin tokens for routine frontend or integration traffic
- Enabling bulk write permissions without a strong operational reason
- Treating hook `http.fetch` and `exec.command` as risk-free utilities
- Solving access control in the UI instead of on the server
- 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. Public endpoints expose only the required methods.
5. Risky hook capabilities are constrained by design.
6. `yarn validate` stays clean.
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
@@ -195,8 +254,9 @@ When asked to harden or design secure access on this starter, inspect in this or
1. `tibi-server/docs/05-authentication.md`
2. `tibi-server/docs/14-security.md`
3. the relevant collection/action permission sets
4. secret sourcing in config/env
5. whether hooks use risky capabilities like outbound fetch or exec
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.
This prevents working implementations that quietly widen the attack surface.
@@ -238,6 +238,94 @@ Keep the backend responsible for:
- 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.
+191
View File
@@ -7,6 +7,75 @@ description: Write and debug server-side hooks for tibi-server (goja Go JS runti
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:
@@ -33,6 +102,21 @@ For many hooks, throwing is the normal control flow, especially in SSR hooks whe
- 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**.
@@ -124,9 +208,116 @@ Typical website use cases:
- 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.
+274 -192
View File
@@ -1,6 +1,6 @@
---
name: tibi-project-setup
description: Set up a new tibi project from the tibi-svelte-starter template. Covers cloning, placeholder replacement, environment config, Docker startup, mock mode, demo cleanup, and build verification. Use when creating a new project or onboarding into this template.
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
@@ -9,257 +9,339 @@ description: Set up a new tibi project from the tibi-svelte-starter template. Co
Use this skill when:
- Creating a new project from the `tibi-svelte-starter` template
- Onboarding into a freshly cloned starter project where placeholders haven't been replaced yet
- The user asks to "set up", "initialize", or "bootstrap" a new tibi project
- 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: a new website project should end up as a **fully working tibi-server + tibi-admin-nova project**, not just a renamed starter clone.
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
- Code-Server environment at `*.code.testversion.online`
- `git`, `yarn`, `make`, `docker compose` available
- Traefik reverse proxy running on the host (manages `*.code.testversion.online` subdomains automatically via Docker labels)
- `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 set up remotes
## Step 1 — Clone and prepare remotes
Skip this step if already inside a cloned project.
Skip if the project is already cloned.
```sh
# In the workspace directory (e.g. /WM_Dev/src/gitbase.de/cms/)
git clone https://gitbase.de/cms/tibi-svelte-starter.git my-project
cd my-project
git remote rename origin template
# Create a new remote repo (e.g. on gitbase.de) and add as origin:
# git remote add origin https://gitbase.de/org/my-project.git
git remote add origin https://gitbase.de/<org>/<repo>.git
```
**Verify:** `git remote -v` shows `template` pointing to the starter and optionally `origin` pointing to the new repo.
## Step 2 — Replace starter placeholders and identity surfaces
## Step 2 — Replace placeholders and starter values
Replace placeholders in all required files:
Replace the starter placeholders and starter-derived values in the correct 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
| Placeholder | Files | Format | Example |
| -------------------- | ---------------------------------------------- | --------------------------------------------------------- | ------------ |
| `__PROJECT_NAME__` | `.env` | kebab-case (used for URLs, Docker containers, subdomains) | `my-project` |
| `__TIBI_NAMESPACE__` | `.env`, `api/config.yml`, `frontend/.htaccess` | kebab-case, same value as `PROJECT_NAME` | `my-project` |
Minimum placeholders to replace:
- `__PROJECT_NAME__`
- `__TIBI_NAMESPACE__`
- `__ORG__`
- `__PROJECT__`
Verify with:
```sh
PROJECT=my-project # kebab-case
NAMESPACE=my-project # same kebab-case value as PROJECT
sed -i "s/__PROJECT_NAME__/$PROJECT/g" .env
sed -i "s/__TIBI_NAMESPACE__/$NAMESPACE/g" .env api/config.yml frontend/.htaccess
rg '__[A-Z0-9_]+__' . --glob '*.{yml,js,env,htaccess,json,md,ts,svelte}'
```
Also update the starter-derived values that are not placeholder tokens anymore, especially `STAGING_PATH`, `STAGING_URL`, `CODING_URL`, `CODING_TIBIADMIN_URL`, `api/hooks/config-client.js`, and starter metadata in `package.json`.
If anything remains, the setup is not complete.
**Important:** The file `api/hooks/config-client.js` contains a **separate** placeholder `__PROJECT__` (not `__PROJECT_NAME__`):
## 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
# api/hooks/config-client.js has: const originURL = "https://__PROJECT__.code.testversion.online"
sed -i "s/__PROJECT__/$PROJECT/g" api/hooks/config-client.js
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
```
**Verify all placeholders:**
Important:
```sh
grep -n '__PROJECT_NAME__\|__TIBI_NAMESPACE__\|__PROJECT__\|__ORG__' .env api/config.yml frontend/.htaccess api/hooks/config-client.js
# Expected: no output (all placeholders replaced)
```
- `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
**Result in `.env`:**
## Step 4 — Install and start the Docker stack
```dotenv
PROJECT_NAME=my-project
TIBI_NAMESPACE=my-project
CODING_URL=https://my-project.code.testversion.online
STAGING_URL=https://dev-my-project.staging.testversion.online
```
### Common mistakes
- **Using different values for `PROJECT` and `NAMESPACE`**: In this starter, `TIBI_NAMESPACE` must match `PROJECT_NAME` and use the same kebab-case value.
- **Forgetting `frontend/.htaccess`**: Contains the namespace for API rewrite rules. If missed, API calls from the frontend will fail silently.
- **Forgetting `api/config.yml`**: First line is `namespace: __TIBI_NAMESPACE__`. If not replaced, tibi-server won't start correctly.
## Step 3 — Page title
The page title is set dynamically via `<svelte:head>` in `frontend/src/App.svelte`. The demo app uses the constant `SITE_NAME` for this. In a new project, `App.svelte` is typically rewritten completely — just make sure `<svelte:head>` with a `<title>` is present. SSR automatically injects it via the `<!--HEAD-->` placeholder in `spa.html`.
Also verify that SSR still renders meaningful page content and not just the shell after the rewrite.
## Step 4 — Admin token and config.yml.env
**How config.yml.env works:** The file `api/config.yml.env` is **not** a standard `.env` file. It is an env-file that the tibi-server reads from the same directory as `config.yml`. The server resolves `${ADMIN_TOKEN}` and `${ADMIN_ASSET_VERSION}` variables in the YAML config from this file. This is separate from the project-root `.env` which serves Docker Compose and the Makefile.
`api/config.yml.env` ships with a default `ADMIN_TOKEN`. For production projects, generate a secure one:
```sh
token=$(openssl rand -hex 20) && sed -i "s/^ADMIN_TOKEN=.*/ADMIN_TOKEN=$token/" api/config.yml.env
```
This updates only `ADMIN_TOKEN` and keeps the other env keys in the file intact.
**Verify:** `cat api/config.yml.env` shows a 40-character hex token while preserving entries such as `ADMIN_ASSET_VERSION`.
**Note:** The `ADMIN_ASSET_VERSION` in the same file is used for cache-busting the admin bundle (`frontend/dist/admin.mjs`). It is auto-generated on build — but if missing, the admin bundle may not load correctly.
## Step 5 — Install, upgrade, and start
Use the Docker targets from the project. Do not try to start the frontend with local dev servers.
```sh
yarn install
make docker-up # Start stack in background
# or
make docker-start # Start stack in foreground (CTRL-C to stop)
make docker-up
```
Do not blindly run a full dependency upgrade as part of project bootstrap unless the task explicitly includes dependency maintenance. First get the starter running as-is, then upgrade intentionally and validate.
Notes:
**Important:** After changing `.env` or `api/config.yml.env`, you must run `make docker-down && make docker-up` for the changes to take effect. Docker Compose reads `.env` at startup time; tibi-server reads `config.yml.env` at reload. A simple `make docker-restart-frontend` is not sufficient for environment variable changes.
- `make docker-up` already depends on `init`; do not duplicate bootstrap steps unless debugging Make targets directly
- for foreground operation use `make docker-start`
**Verify containers are running:**
## 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
make docker-ps # All containers should be "Up"
make docker-logs # Check for errors
curl -I "$CODING_URL"
curl -I "$CODING_TIBIADMIN_URL"
curl -I "$CODING_URL/api/content?limit=1"
```
## Step 6 — Verify URLs
Traefik picks up Docker labels automatically — no manual config needed. After `make docker-up`, these URLs become available:
| Service | URL |
| --------------------- | ------------------------------------------------------------ |
| Website (BrowserSync) | `https://{PROJECT_NAME}.code.testversion.online/` |
| Tibi Admin | `https://{PROJECT_NAME}-tibiadmin.code.testversion.online/` |
| Tibi Server API | `https://{PROJECT_NAME}-tibiserver.code.testversion.online/` |
| Maildev | `https://{PROJECT_NAME}-maildev.code.testversion.online/` |
The subdomains are registered via the Docker label `online.testversion.code.subdomain=${PROJECT_NAME}`. Traefik watches Docker events and creates routes dynamically.
**Verify:** `curl -sI https://{PROJECT_NAME}.code.testversion.online/ | head -1` returns `HTTP/2 200`.
## Step 7 — Mock mode (optional)
For frontend development without a running tibi-server backend:
If the current environment also exposes a raw tibi-server host, add:
```sh
# Set in .env:
MOCK=1
# Then full restart (env change requires docker-down first):
make docker-down && make docker-up
curl -I "$CODING_TIBISERVER_URL/api/v1/version"
```
**When to use mock mode:** Early UI prototyping, frontend-only work, CI environments without a database.
If `/api/...` returns HTML instead of JSON, the reverse-proxy/setup path is still wrong.
## Step 8 — Remove demo content
## Step 8 — Optional project registration for Path B
For a real project, remove or replace the demo files:
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.
| File/Folder | Content |
| ---------------------------------- | ------------------------------------------------------ |
| `frontend/src/blocks/` | Demo block components (HeroBlock, RichtextBlock, etc.) |
| `frontend/mocking/content.json` | Demo mock data for content |
| `frontend/mocking/navigation.json` | Demo mock data for navigation |
| `api/collections/content.yml` | Content collection config |
| `api/collections/navigation.yml` | Navigation collection config |
| `tests/e2e/` | Demo E2E tests |
| `video-tours/tours/` | Demo video tour |
```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
}'
```
Then adapt `frontend/src/App.svelte` (header, footer, content loading) to your own data model.
Reload after creation or config changes:
But do not delete starter structures blindly. For a serious project build-out, first decide which parts remain useful foundations:
```sh
curl -s -X POST "$CODING_TIBISERVER_URL/api/v1/_/<PROJECT_NAME>/_/admin/reload" \
-H "X-Admin-Token: $ADMIN_TOKEN"
```
- SSR pipeline
- i18n route model
- pagebuilder-based content collection
- navigation collection
- media library and image handling
- tests / tours as scaffolding
### Token header distinction
The goal is not "delete the demo". The goal is "reshape the starter into a project architecture that editors can use productively".
- 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`
**Decision guide:**
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}`.
- **Keep demo content** if you want to use it as a reference while building your own components.
- **Delete immediately** if you're starting with a completely custom design and the demo files would only cause confusion.
Do not mix these headers casually. A working collection token does not imply project-admin access.
## Step 9 — Build and validate
```sh
yarn build # Frontend bundle for modern browsers
yarn build:server # SSR bundle (for tibi-server goja hooks)
yarn validate # TypeScript + Svelte checks (must show 0 errors and 0 warnings)
```
**These commands must succeed before the project is considered set up.**
## Step 10 — Shape the project for real editor workflows
For a complete website on this starter, setup is not done when Docker runs. It is done when the project has a coherent content/admin model.
Inspect and adapt at least these areas:
- `api/collections/content.yml`: page types, block schema, SEO, i18n, pagebuilder config
- `api/collections/navigation.yml`: header/footer structure and editor UX
- `frontend/src/blocks/`: real block set for the website, not just demo showcase blocks
- `frontend/src/blocks/BlockRenderer.svelte`: final block registry
- `types/global.d.ts`: actual project data model
- `frontend/src/App.svelte`: final shell, content-loading, SSR-safe behavior
For Nova specifically, use current capabilities where they improve the website build process:
- `preview` for readable row, breadcrumb, and foreign-key display
- `sidebar` groups for publication/SEO/settings
- `containerProps.layout` for usable forms
- `dependsOn` for block-specific fields
- `drillDown` for complex arrays
- `pagebuilder` for heterogeneous page content
- `subNavigation`, `singleton`, `viewHint`, and foreign previews where appropriate
For tibi-server specifically, decide early whether the site also needs:
- `actions:` for forms, newsletter, calculators, imports, or webhooks
- publication-aware SSR invalidation
- field-level permissions
- AI/LLM integration for admin/editor workflows
## SSR debugging: manual project reload
After changing collection YAML files, hook code, or `config.js` (SSR route validation), you may need to trigger a project reload for tibi-server to pick up the changes. Hook files auto-reload, but structural changes (new collections, config changes) require explicit reload:
```bash
curl -X POST "$CODING_URL/api/v1/_/$TIBI_NAMESPACE/_/admin/reload" \
-H "Token: $ADMIN_TOKEN"
```
The starter's `api/config.yml` has `allowReload: true` for the admin token by default. Response `{"message": "ok"}` confirms the reload.
Use this when:
- A new collection was added to `api/config.yml` but the API doesn't see it
- Hook files changed but the server hasn't picked them up
- SSR route validation (`api/hooks/config.js`) was updated and the old behavior persists
## Step 11 — Functional verification for a real website project
After the first project shaping pass, verify more than just TypeScript:
```sh
yarn build
yarn build:server
yarn validate
```
Then also verify:
The project is not considered bootstrapped until all three succeed.
- the public site loads via the website URL
- the Nova admin loads via the admin URL
- pages can be created and edited in admin
- pagebuilder blocks are usable in admin
- SSR renders real page content, not only the shell
- navigation and media references render correctly
- forms/actions work if the project uses them
## Step 10 — Optional immediate follow-up work
If the goal is "LLM can build a complete website automatically", the project setup skill must lead to a fully functional content/admin/runtime stack, not merely a placeholder replacement.
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.
+64
View File
@@ -226,3 +226,67 @@ curl "http://tibiserver:8080/api/v1/_/<namespace>/ssr?url=/de/ueber-uns"
- **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.
+2
View File
@@ -13,6 +13,8 @@ playwright/.cache/
visual-review/
video-tours/output/
.playwright-mcp/
.agents/STARTER_ALIGNMENT_*.md
.yarn/*
!.yarn/cache
!.yarn/patches
+106 -23
View File
@@ -2,6 +2,27 @@
Tibi CMS starter template — Svelte 5 SPA with esbuild, SSR via goja, and Playwright tests.
## Skill auto-improvement
Skills in `.agents/skills/` are **automatically updated** when the agent discovers patterns, gotchas, or workflows that are:
- Generic enough to apply across projects (not project-specific workarounds)
- Missing from the current skill documentation
- Important enough to prevent the same mistake in future projects
If you see an agent updating a skill file, it is capturing reusable knowledge. Review skill diffs to stay informed about evolving conventions.
## Token header distinction
- **System-level API** (raw tibi-server project CRUD, raw admin reload, server ops): use `X-Admin-Token` header
- **Collection-level API** (CRUD on content, navigation, etc.): use the header name defined by the collection permission key. In this starter that is typically `Token` via permission entries such as `token:${ADMIN_TOKEN}`.
- **User-level API** (JWT-authenticated requests): use `X-Auth-Token` header
## Medialib image URL pattern
In this starter, medialib URLs are derived from the entry ID plus the stored relative `file.src` value. If `file.src` is `file/example.jpg`, the project-local URL is `/api/medialib/{entryId}/file/example.jpg`.
Shared widgets such as `MedialibImage` append responsive filters as query params, for example `/api/medialib/{entryId}/file/example.jpg?filter=l-webp`.
These `/api/...` URLs are starter-local proxy URLs. Do not hardcode `file/file`.
## Project overview
- **Frontend**: Svelte 5 SPA in `frontend/src/`, esbuild, Tailwind CSS 4.
@@ -16,6 +37,23 @@ Tibi CMS starter template — Svelte 5 SPA with esbuild, SSR via goja, and Playw
- `yarn` is for standalone tasks: `build`, `build:server`, `validate`.
- Bootstrap details (placeholder replacement, docker setup) → `tibi-project-setup` skill.
- Build checklist for full website projects → `.agents/BUILD_CHECKLIST.md`.
- Starter maintenance current state → `.agents/STARTER_ALIGNMENT_STATUS.md`.
- Starter maintenance and upstream/reference alignment workflow → `.agents/STARTER_ALIGNMENT_PLAN.md`.
## Project delivery workflow
When building a new website project from this starter, work through `.agents/BUILD_CHECKLIST.md` **phase by phase**. Do not skip phases, even when individual checklist items are already satisfied by the starter defaults.
For each phase:
1. Load the required skills listed in that phase.
2. Create or update the required project artifacts.
3. Work through every `[ ]` implementation check.
4. Run the listed validation commands. A phase is not done until all validations pass.
5. Mark each `[ ]` as `[x]` only when the concrete check is satisfied and its validation command passes cleanly.
6. Confirm all exit criteria before moving to the next phase.
The checklist covers the full delivery lifecycle: bootstrap → architecture → collections → types → frontend → SSR → hooks/actions → permissions → audit → media/SEO → optional branches → ops/deploy → tests → final verification.
## Quick reference
@@ -39,9 +77,10 @@ Tibi CMS starter template — Svelte 5 SPA with esbuild, SSR via goja, and Playw
## API access
- `Token` header with `ADMIN_TOKEN` from `api/config.yml.env`.
- `Token` header with `ADMIN_TOKEN` from `api/config.yml.env` when the collection permission key is `token:${ADMIN_TOKEN}`.
- Collection `user:` permissions apply to JWT auth (`X-Auth-Token`), not static `Token:`.
- For write access via `ADMIN_TOKEN`, add `"token:${ADMIN_TOKEN}": { methods: { post: true, put: true } }` to the collection permissions.
- The current deploy scripts call the reverse-proxied reload endpoint on `LIVE_URL` or `STAGING_URL` with `Authorization: Bearer ${ADMIN_TOKEN}`. Treat that as a separate project-local proxy surface from raw tibi-server project CRUD APIs.
## Required secrets
@@ -54,26 +93,6 @@ Tibi CMS starter template — Svelte 5 SPA with esbuild, SSR via goja, and Playw
Generated dev defaults in `api/config.yml.env` may be committed. Production values are overwritten in CI/CD via secrets.
## Reference repositories (sibling repos)
| Repo | Path | Key file |
|------|------|----------|
| **tibi-types** | `../../cms/tibi-types` | `index.d.ts` (context types), `schemas/config/collection.schema.json` |
| **tibi-server** | `../../cms/tibi-server` | `docs/` (19 files: collections, hooks, auth, actions, SSE, jobs, etc.) |
| **tibi-admin-nova** | `../../cms/tibi-admin-nova` | `types/admin.d.ts` (all admin config types) |
### When to consult which repo
- **Collection YAML** → `tibi-server/docs/04-collections.md` + `tibi-types/schemas/config/collection.schema.json`
- **Hooks** → `tibi-server/docs/06-hooks.md` + `tibi-types/index.d.ts`
- **Admin UI config** → `tibi-admin-nova/types/admin.d.ts` + `tibi-admin-nova/api/collections/` (examples)
- **Permissions** → `tibi-server/docs/05-authentication.md`
- **Actions/forms** → `tibi-server/docs/19-actions.md`
- **Realtime/SSE** → `tibi-server/docs/07-realtime.md`
- **Images/uploads** → `tibi-server/docs/08-file-upload-images.md`
- **LLM integration** → `tibi-server/docs/09-llm-integration.md`
- **Field-level permissions** → `tibi-server/docs/17-field-level-permissions.md`
## TypeScript
```json
@@ -85,6 +104,53 @@ Generated dev defaults in `api/config.yml.env` may be committed. Production valu
- **Zero warnings policy**: always run `yarn validate` after changes; fix all warnings.
- Svelte 5 with Runes: `$props()`, `$state()`, `$derived()`, `$effect()`.
## Svelte 5 patterns
- **Prop destructuring**: Destructuring `block` from `$props()` into local variables triggers `state_referenced_locally` warnings. The values capture initial state and lose reactivity. Either access through the parent object (`block.headline`) or use `$derived` for each value.
- **`$derived.by()`**: Use for multi-statement derived values: `let filtered = $derived.by(() => { if (x) return a; return b; })`
- **`class:` directive**: Invalid with `/` in class names like `class:text-white/85`. Use ternary in `class` attribute instead: `class={condition ? 'text-white/85' : ''}`.
## Reference repositories (sibling repos)
| Repo | Path | Key file |
|------|------|----------|
| **tibi-types** | `../tibi-types` | `index.d.ts` (context types), `schemas/config/collection.schema.json` |
| **tibi-server** | `../tibi-server` | `docs/` (20 files: collections, hooks, auth, actions, SSE, jobs, etc.) |
| **tibi-admin-nova** | `../tibi-admin-nova` | `types/admin.d.ts` (all admin config types) |
### When to consult which repo
- **Collection YAML** → `tibi-server/docs/04-collections.md` + `tibi-types/schemas/config/collection.schema.json`
- **Hooks** → `tibi-server/docs/06-hooks.md` + `tibi-types/index.d.ts`
- **Admin UI config** → `tibi-admin-nova/types/admin.d.ts` + `tibi-admin-nova/docs/collection-config.md`
- **Permissions** → `tibi-server/docs/05-authentication.md`
- **Actions/forms** → `tibi-server/docs/19-actions.md`
- **Realtime/SSE** → `tibi-server/docs/07-realtime.md`
- **Images/uploads** → `tibi-server/docs/08-file-upload-images.md`
- **LLM integration** → `tibi-server/docs/09-llm-integration.md`
- **Field-level permissions** → `tibi-server/docs/17-field-level-permissions.md`
## API lookup für aufgelöste Referenzen
Beim Laden von Collections können Fremdschlüssel via `lookup`-Parameter automatisch aufgelöst werden:
```ts
// entries mit aufgelösten medialib-Bildern laden
const entries = await getCachedEntries<"content">(
"content",
{ active: true },
"sort",
undefined,
undefined,
undefined,
undefined,
"blocks.heroImage.image:medialib"
)
// Ergebnis: entry._lookup enthält die aufgelösten Referenzen
```
Der `lookup`-Parameter muss als 8. Argument an `getCachedEntries` übergeben werden. Ohne lookup bleiben Referenzfelder reine ID-Werte ohne `_lookup`.
## Tailwind CSS 4
Canonical v4 syntax — no v3 legacy classes:
@@ -92,7 +158,7 @@ Canonical v4 syntax — no v3 legacy classes:
| v3 | v4 |
|----|----|
| `bg-gradient-to-*` | `bg-linear-to-*` |
| `aspect-[4/3]` | `aspect-4/3` |
| `aspect-[4/3]` | `aspect-[4/3]` (v4 hat nur `aspect-square`/`aspect-video` als Built-in-Utilities; benutzerdefinierte Ratios bleiben arbitrary) |
| `!bg-brand-600` | `bg-brand-600!` |
| `hover:!bg-brand-700` | `hover:bg-brand-700!` |
@@ -112,7 +178,7 @@ Use these tables as the lookup index. Each skill is a deep-dive for its domain.
| Skill | When |
|-------|------|
| `content-authoring` | Add pages, block types, collections |
| `content-authoring` | Add pages, block types, collections, and required type/registry wiring |
| `nova-pagebuilder-modeling` | Block schemas, preview, drillDown, dependsOn |
| `nova-navigation-modeling` | Header/footer trees, viewHint.navigation, declaredTrees |
| `admin-ui-config` | Field widgets, sidebar, layout, choices, foreign refs |
@@ -128,6 +194,7 @@ Use these tables as the lookup index. Each skill is a deep-dive for its domain.
| `tibi-actions-and-forms` | Contact forms, newsletter, webhooks, action endpoints |
| `scheduled-jobs-and-automation` | Cron tasks, cleanup, reports, sync |
| `realtime-and-live-workflows` | SSE channels, live notifications, preview refresh |
| `audit-and-compliance` | Audit logging, retention, audit.return filtering, source semantics |
### 4. Frontend and delivery
@@ -136,3 +203,19 @@ Use these tables as the lookup index. Each skill is a deep-dive for its domain.
| `frontend-architecture` | SPA router, stores, Svelte 5 patterns, API layer |
| `tibi-ssr-caching` | SSR rendering, cache invalidation, 404 signaling |
| `playwright-testing` | Deterministic seed data, API/E2E/admin/visual tests |
| `deployment` | Production deployment setup, Basispanel subdomains, CI, rsync, staging |
### 5. Operations and diagnostics
| Skill | When |
|-------|------|
| `mongodb-and-indexes` | Mongo prerequisites, replica set, indexes, persistence, backup assumptions |
| `monitoring-and-performance` | OpenAPI, metrics, Sentry, reachability and observability checks |
| `troubleshooting-and-debugging` | Common stack failures, hook/runtime debugging, config/auth diagnosis |
### 6. AI, search, and enterprise
| Skill | When |
|-------|------|
| `search-and-embeddings` | Search modes, embedding providers, regeneration, semantic retrieval |
| `multi-tenancy-and-orgs` | Org/team isolation, enterprise permissions, visibility vs working rights |
+1 -1
View File
@@ -10,7 +10,7 @@ Starter Kit für SPAs(s) `;)` mit Svelte und TibiCMS inkl. SSR
[ ] Repository geklont und Remotes konfiguriert
[ ] __PROJECT_NAME__ in .env ersetzt (kebab-case)
[ ] __TIBI_NAMESPACE__ in .env ersetzt (snake_case)
[ ] __TIBI_NAMESPACE__ in api/config.yml und frontend/.htaccess ersetzt
[ ] __TIBI_NAMESPACE__ in api/config.yml und, falls Apache-Rewrite/Proxy genutzt wird, in frontend/.htaccess ersetzt
[ ] Keine verbleibenden __*__-Platzhalter (mit grep prüfen)
[ ] App.svelte hat <svelte:head> mit <title>
[ ] ADMIN_TOKEN in api/config.yml.env gesetzt
+1
View File
@@ -3,6 +3,7 @@
########################################################################
name: content
uploadPath: ../media/content
meta:
label: { de: "Inhalte", en: "Content" }
muiIcon: article
+7
View File
@@ -3,6 +3,7 @@
########################################################################
name: medialib
uploadPath: ../media/medialib
meta:
label: { de: "Mediathek", en: "Media Library" }
muiIcon: image_multiple
@@ -50,6 +51,12 @@ permissions:
post: true
put: true
delete: true
"token:${ADMIN_TOKEN}":
methods:
get: true
post: true
put: true
delete: true
imageFilter:
xs-webp:
+10
View File
@@ -2,6 +2,14 @@
Hook files run inside the tibi-server Go runtime (goja).
For the full workflow, prefer the `tibi-hook-authoring` skill.
## Related skills
- `security-hardening-and-token-strategy` when hooks touch secrets, external requests, auth, or risky capabilities.
- `tibi-actions-and-forms` when the behavior is endpoint-like and should be modeled as an action instead of collection CRUD.
- `troubleshooting-and-debugging` when goja/runtime failures span config, auth, hook execution, or reverse-proxy behavior.
## Conventions
- Wrap hook files in an IIFE: `;(function () { ... })()`.
@@ -9,6 +17,7 @@ Hook files run inside the tibi-server Go runtime (goja).
- Use inline type casting with `/** @type {TypeName} */ (value)` and typed collection entries from `types/global.d.ts`.
- Avoid `@ts-ignore`; use proper casting instead.
- Use `const` and `let` instead of `var`. The tibi-server runtime supports modern JS declarations.
- Public-read or mutation hooks that affect rendered content must be reviewed together with `filter_public.js`, `config.js`, and cache invalidation behavior.
## context.filter — Go object quirk
@@ -45,3 +54,4 @@ instead, only add authorization filters (e.g. `{ userId: userId }`).
- When creating or modifying collections/hooks: extend or create corresponding API tests in `tests/api/`.
- After hook changes, run only affected API tests: `npx playwright test tests/api/filename.spec.ts`.
- When tests fail, clarify whether the hook or the test needs adjustment — coordinate with the user.
- If hook changes affect public rendering, add or rerun the narrowest SSR/public-read validation instead of relying only on CRUD-oriented API tests.
+16
View File
@@ -2,6 +2,14 @@
Server-side rendering via goja (Go JS runtime) with HTML caching.
For the full workflow, prefer the `tibi-ssr-caching` skill.
## Related skills
- `frontend-architecture` when SSR changes interact with route loading, i18n, or `App.svelte` data flow.
- `media-seo-publishing` when SSR output includes image URLs, SEO metadata, or publication timing.
- `tibi-hook-authoring` when SSR changes depend on neighboring hook behavior such as public filtering or cache invalidation.
## Request flow
1. `get_read.js` receives the request and calls `lib/ssr-server.js`.
@@ -11,6 +19,8 @@ Server-side rendering via goja (Go JS runtime) with HTML caching.
5. During SSR, `App.svelte` calls the same `loadContent(...)` path directly inside `typeof window === "undefined"`.
6. Rendered HTML is stored in the `ssr` collection together with dependency tracking strings.
Keep browser and SSR content loading on the same application path whenever possible. Do not fork a second data-loading model for SSR unless the current architecture explicitly requires it.
## Build
- SSR bundle is built via `yarn build:server` and outputs to `lib/app.server.js`.
@@ -29,6 +39,7 @@ Server-side rendering via goja (Go JS runtime) with HTML caching.
- SSR route validation is active in `config.js`.
- Public page URLs are language-prefixed (`/de/...`, `/en/...`), while `content.path` in the DB is stored without that prefix.
- `ssrValidatePath()` must strip the language prefix before querying content and return a canonical language-prefixed URL when needed.
- If route validation changes, inspect `api/hooks/config.js`, `filter_public.js`, and the frontend route-loading path together instead of patching only one side.
## 404 signaling
@@ -37,3 +48,8 @@ The SSR hook (`get_read.js`) checks `context.is404` after rendering to determine
`NotFound.svelte` sets `context.is404 = true` during SSR. When the component renders (only when the page is not found), its top-level script sets the flag. The goja runtime provides `context` as a global during SSR, so it's available from the compiled frontend code.
When adding a 404 page or changing the not-found logic, ensure `context.is404` is still set during SSR.
## Related validation
- After SSR changes, run `yarn build:server` plus the narrowest reachable SSR/public-read check.
- If caching or publication behavior changed, also rerun the relevant mutation-side invalidation check instead of relying on HTML diffing alone.
+1 -1
View File
@@ -230,7 +230,7 @@ if (process.argv[2] == "start") {
changeOrigin: true,
logLevel: "debug",
pathRewrite: function (path, req) {
return "/ssr?url=" + encodeURIComponent(path)
return "/ssr?url=" + encodeURIComponent(path) + "&noCache=1"
},
})
)
+10 -1
View File
@@ -10,6 +10,13 @@ Svelte 5 SPA bundled with esbuild and styled with Tailwind CSS 4.
- `src/css/` — global styles and Tailwind imports.
- `src/routes/` — page-level components (e.g. `NotFound.svelte` for the 404 page). Not file-based routing — these are manually imported by `App.svelte`.
## Related skills
- `frontend-architecture` for routing, stores, API behavior, and i18n flow.
- `content-authoring` when frontend changes are driven by new collections, blocks, lookup paths, or `types/global.d.ts` changes.
- `nova-pagebuilder-modeling` when block components, `BlockRenderer.svelte`, and admin preview contracts must stay aligned.
- `media-seo-publishing` when rendering medialib or file fields; prefer `src/widgets/MedialibImage.svelte` as the shared image boundary.
## Routing
This project uses a **custom SPA router** (NOT SvelteKit, NOT file-based routing). Pages are CMS content entries loaded dynamically by URL path.
@@ -27,10 +34,12 @@ This project uses a **custom SPA router** (NOT SvelteKit, NOT file-based routing
- Keep code and comments in English.
- SSR safety: guard browser-only code with `typeof window !== "undefined"`.
- API behavior: PUT responses return only changed fields; filter by id uses `_id`; API requests reject non-2xx with `{ response, data }` and error payload in `error.data.error`.
- Treat public rendering and admin-preview rendering as the same block contract whenever possible.
- If blocks or widgets render foreign media or entities, make the required `lookup` paths explicit instead of assuming `_lookup` data is always present.
## Tailwind CSS
- Always use canonical Tailwind utility classes instead of arbitrary values when a standard equivalent exists (e.g. `h-16.5` not `h-[66px]`, `min-h-3` not `min-h-[12px]`).
- Always use canonical Tailwind utility classes instead of arbitrary values when a standard equivalent exists.
- Only use arbitrary values (`[...]`) when no standard utility covers the needed value.
## i18n
+7 -4
View File
@@ -165,7 +165,8 @@
undefined,
undefined,
undefined,
{ lookup: NAVIGATION_CONTENT_LOOKUP }
undefined,
NAVIGATION_CONTENT_LOOKUP
),
getCachedEntries<"navigation">(
"navigation",
@@ -174,13 +175,14 @@
undefined,
undefined,
undefined,
{ lookup: NAVIGATION_CONTENT_LOOKUP }
undefined,
NAVIGATION_CONTENT_LOOKUP
),
])
headerNav = headerEntries[0] || null
footerNav = footerEntries[0] || null
// Load content for current path
// Load content for current path. Limit 1 so SSR tracks content:<id> instead of content:*.
const contentEntries = await getCachedEntries<"content">(
"content",
{
@@ -189,10 +191,11 @@
active: true,
},
"sort",
1,
undefined,
undefined,
undefined,
{ lookup: CONTENT_MEDIA_LOOKUP }
CONTENT_MEDIA_LOOKUP
)
if (contentEntries.length > 0) {
+12 -1
View File
@@ -1,4 +1,5 @@
import { mount, unmount, type Component, type SvelteComponent } from "svelte"
import { apiBaseOverride } from "./lib/store"
import BlockRenderer from "./blocks/BlockRenderer.svelte"
const previewCssUrl = new URL("./index.css", import.meta.url).toString()
@@ -86,6 +87,11 @@ function createContentBlockDefinition(presentation: BlockPresentation): BlockDef
position: "relative",
},
render(container, row, context) {
const previewApiBase = context?.projectBase || context?.apiBase
if (previewApiBase) {
apiBaseOverride.set(String(previewApiBase))
}
const target = document.createElement("div")
target.dataset.adminPreview = "true"
container.appendChild(target)
@@ -99,7 +105,12 @@ function createContentBlockDefinition(presentation: BlockPresentation): BlockDef
})
return {
update(nextRow) {
update(nextRow, nextContext) {
const nextPreviewApiBase = nextContext?.projectBase || nextContext?.apiBase
if (nextPreviewApiBase) {
apiBaseOverride.set(String(nextPreviewApiBase))
}
unmount(mountedComponent)
target.innerHTML = ""
mountedComponent = mount(BlockRenderer as Component<any>, {
+17 -10
View File
@@ -137,13 +137,15 @@ const CACHE_TTL = 1000 * 60 * 60 // 1 hour
// Generic collection helpers
// ---------------------------------------------------------------------------
type CollectionNameT = "medialib" | "content" | string
type CollectionNameT = "medialib" | "content" | "navigation" | string
type EntryTypeSwitch<T extends string> = T extends "medialib"
? MedialibEntry
: T extends "content"
? ContentEntry
: Record<string, unknown>
: T extends "navigation"
? NavigationEntry
: Record<string, unknown>
export async function getDBEntries<T extends CollectionNameT>(
collectionName: T,
@@ -152,7 +154,8 @@ export async function getDBEntries<T extends CollectionNameT>(
limit?: number,
offset?: number,
projection?: string,
params?: Record<string, string>
params?: Record<string, string>,
lookup?: string
): Promise<EntryTypeSwitch<T>[]> {
const c = await api<EntryTypeSwitch<T>[]>(collectionName, {
filter,
@@ -161,6 +164,7 @@ export async function getDBEntries<T extends CollectionNameT>(
offset,
projection,
params,
lookup,
})
return c.data
}
@@ -172,13 +176,14 @@ export async function getCachedEntries<T extends CollectionNameT>(
limit?: number,
offset?: number,
projection?: string,
params?: Record<string, string>
params?: Record<string, string>,
lookup?: string
): Promise<EntryTypeSwitch<T>[]> {
const filterStr = obj2str({ collectionName, filter, sort, limit, offset, projection, params })
const filterStr = obj2str({ collectionName, filter, sort, limit, offset, projection, params, lookup })
if (cache[filterStr] && cache[filterStr].expire >= Date.now()) {
return cache[filterStr].data as EntryTypeSwitch<T>[]
}
const entries = await getDBEntries<T>(collectionName, filter, sort, limit, offset, projection, params)
const entries = await getDBEntries<T>(collectionName, filter, sort, limit, offset, projection, params, lookup)
cache[filterStr] = { expire: Date.now() + CACHE_TTL, data: entries }
return entries
}
@@ -187,18 +192,20 @@ export async function getDBEntry<T extends CollectionNameT>(
collectionName: T,
filter: MongoFilter,
projection?: string,
params?: Record<string, string>
params?: Record<string, string>,
lookup?: string
): Promise<EntryTypeSwitch<T> | undefined> {
return (await getDBEntries<T>(collectionName, filter, "_id", 1, undefined, projection, params))?.[0]
return (await getDBEntries<T>(collectionName, filter, "_id", 1, undefined, projection, params, lookup))?.[0]
}
export async function getCachedEntry<T extends CollectionNameT>(
collectionName: T,
filter: MongoFilter,
projection?: string,
params?: Record<string, string>
params?: Record<string, string>,
lookup?: string
): Promise<EntryTypeSwitch<T> | undefined> {
return (await getCachedEntries<T>(collectionName, filter, "_id", 1, undefined, projection, params))?.[0]
return (await getCachedEntries<T>(collectionName, filter, "_id", 1, undefined, projection, params, lookup))?.[0]
}
export async function postDBEntry<T extends CollectionNameT>(
+29
View File
@@ -1,3 +1,32 @@
import { get } from "svelte/store"
import { apiBaseURL } from "../config"
import { apiBaseOverride } from "./store"
function isAbsoluteUrl(url: string | undefined): boolean {
return !!url && (/^(?:https?:)?\/\//.test(url) || url.startsWith("data:") || url.startsWith("blob:"))
}
function normalizeBase(base: string | null | undefined): string | null {
return base ? base.replace(/\/+$/, "") + "/" : null
}
export function resolveApiAssetUrl(
url: string | null | undefined,
apiBase: string | null | undefined = get(apiBaseOverride) || apiBaseURL
): string | null | undefined {
if (!url || isAbsoluteUrl(url) || !url.startsWith("/assets/")) {
return url
}
const normalizedApiBase = normalizeBase(apiBase)
if (!normalizedApiBase) {
return url
}
return normalizedApiBase + "_/assets/" + url.replace(/^\/+/, "")
}
export function debounce<T extends (...args: never[]) => void>(
func: T,
wait: number
+125 -42
View File
@@ -1,30 +1,36 @@
<script lang="ts">
import { apiBaseURL } from "../config"
import { currentLanguage, DEFAULT_LANGUAGE } from "../lib/i18n"
import { apiBaseOverride } from "../lib/store"
import { resolveApiAssetUrl } from "../lib/utils"
interface Props {
id: string
id?: string
entry?: MedialibEntry | null
filter?: string | null
noPlaceholder?: boolean
alt?: string
caption?: string
showCaption?: boolean
minWidth?: number
widthMultiplier?: number
lazy?: boolean
class?: string
style?: string
}
let {
id,
id = "",
entry = null,
filter = null,
noPlaceholder = false,
alt = "",
caption = "",
showCaption = false,
minWidth = 0,
widthMultiplier = 1,
lazy = false,
class: className = "",
style = "",
}: Props = $props()
@@ -32,15 +38,38 @@
let currentFilter = $state<string>("l-webp")
const effectiveId = $derived(entry?.id || entry?._id || id || "")
const fileSrc = $derived(resolveFileSrc(entry?.file?.src, entry?.id || entry?._id || effectiveId))
const placeholderSrc = $derived(
resolveApiAssetUrl("/assets/img/placeholder-image.svg") || "/assets/img/placeholder-image.svg"
)
// Sync explicit filter prop reactively
$effect(() => {
if (filter) currentFilter = filter
if (filter) {
currentFilter = filter
return
}
if (minWidth) {
currentFilter = getFilterForWidth(minWidth * (widthMultiplier || 1))
}
})
function getAutoFilter(imgWidth: number): string {
const width = minWidth ? Math.max(imgWidth, minWidth) : imgWidth
const effectiveWidth = width * (widthMultiplier || 1)
function getMeasuredWidth(node: HTMLImageElement): number {
const pictureElement = node.closest("picture") as HTMLElement | null
const figureElement = node.closest("figure") as HTMLElement | null
const widthCandidates = [
node.getBoundingClientRect().width,
node.width,
node.parentElement?.getBoundingClientRect().width || 0,
pictureElement?.getBoundingClientRect().width || 0,
figureElement?.getBoundingClientRect().width || 0,
]
const measuredWidth = Math.max(...widthCandidates)
const width = minWidth ? Math.max(measuredWidth, minWidth) : measuredWidth
return width * (widthMultiplier || 1)
}
function getFilterForWidth(imgWidth: number): string {
const effectiveWidth = imgWidth
if (effectiveWidth <= 90) return "xs-webp"
if (effectiveWidth <= 300) return "s-webp"
@@ -50,6 +79,31 @@
return "xxl-webp"
}
function clampFocalPoint(value?: number): number | null {
if (typeof value !== "number" || Number.isNaN(value)) {
return null
}
return Math.min(1, Math.max(0, value))
}
function getFocalPoint(entry: MedialibEntry | null): { x?: number; y?: number } | null {
const fileWithFocalPoint = entry?.file as { focalPoint?: { x?: number; y?: number } } | undefined
return fileWithFocalPoint?.focalPoint || null
}
function getObjectPosition(entry: MedialibEntry | null): string | null {
const focalPoint = getFocalPoint(entry)
const x = clampFocalPoint(focalPoint?.x)
const y = clampFocalPoint(focalPoint?.y)
if (x === null || y === null) {
return "50% 50%"
}
return `${x * 100}% ${y * 100}%`
}
function isRasterImage(entry: MedialibEntry | null): boolean {
if (entry?.file?.type?.match(/^image\/(png|jpe?g|webp)/)) return true
// Fallback: check file extension when MIME type is missing (e.g. public projection)
@@ -59,33 +113,56 @@
function resolveFileSrc(src: string | undefined, entryId: string | undefined): string | null {
if (!src) return null
if (/^(https?:)?\/\//.test(src) || src.startsWith("/")) return src
if (
/^(?:https?:)?\/\//.test(src) ||
src.startsWith("/") ||
src.startsWith("data:") ||
src.startsWith("blob:")
) {
return src
}
if (!entryId) return null
const normalizedApiBase = apiBaseURL.replace(/\/+$/, "")
const normalizedApiBase = ($apiBaseOverride || apiBaseURL).replace(/\/+$/, "")
return `${normalizedApiBase}/medialib/${entryId}/${src.replace(/^\/+/, "")}`
}
// ResizeObserver: only when no explicit filter and raster image
$effect(() => {
const el = imgEl
if (!el || filter || !entry || !isRasterImage(entry)) return
if (typeof ResizeObserver === "undefined") return
let maxObservedWidth = 0
let deferredUpdateFrame: number | null = null
const observer = new ResizeObserver(() => {
const newWidth = el.clientWidth
if (newWidth <= maxObservedWidth) return // only scale up
maxObservedWidth = newWidth
currentFilter = getAutoFilter(newWidth)
const updateFilter = () => {
const width = getMeasuredWidth(el)
if (width <= maxObservedWidth) return
maxObservedWidth = width
currentFilter = getFilterForWidth(width)
}
const observer = new ResizeObserver(updateFilter)
const observedElements = [el, el.parentElement, el.closest("picture"), el.closest("figure")].filter(
(element, index, all) => element && all.indexOf(element) === index
) as Element[]
observedElements.forEach((element) => observer.observe(element))
updateFilter()
deferredUpdateFrame = requestAnimationFrame(() => {
deferredUpdateFrame = null
updateFilter()
})
observer.observe(el)
return () => observer.disconnect()
return () => {
observer.disconnect()
if (deferredUpdateFrame !== null) {
cancelAnimationFrame(deferredUpdateFrame)
}
}
})
function getSrc(src: string | null, entry: MedialibEntry | null): string {
if (!src) return "/assets/img/placeholder-image.svg"
if (!src) return placeholderSrc
if (!isRasterImage(entry)) return src
if (filter) return src + `?filter=${filter}`
return src + `?filter=${currentFilter}`
@@ -97,41 +174,47 @@
return value[lang] || value[DEFAULT_LANGUAGE] || Object.values(value).find((entry) => !!entry) || ""
}
function getAltText(entry: MedialibEntry | null): string {
return alt || resolveLocalizedText(entry?.alt, $currentLanguage)
}
</script>
{#if effectiveId}
{#if entry && fileSrc}
{#if showCaption && caption}
<figure>
<picture>
<img
bind:this={imgEl}
src={getSrc(fileSrc, entry)}
alt={resolveLocalizedText(entry.alt, $currentLanguage)}
data-entry-id={id}
loading={lazy ? "lazy" : undefined}
{style}
/>
</picture>
<figcaption class="mt-2 text-sm text-gray-500 text-center italic">
{@html caption}
</figcaption>
</figure>
{:else}
{#if entry && fileSrc}
{#if showCaption && caption}
<figure>
<picture>
<img
bind:this={imgEl}
src={getSrc(fileSrc, entry)}
alt={resolveLocalizedText(entry.alt, $currentLanguage)}
data-entry-id={id}
alt={getAltText(entry)}
data-entry-id={effectiveId}
class={className}
style:object-position={getObjectPosition(entry)}
loading={lazy ? "lazy" : undefined}
{style}
/>
</picture>
{/if}
{:else if !noPlaceholder}
<figcaption class="mt-2 text-sm text-gray-500 text-center italic">
{@html caption}
</figcaption>
</figure>
{:else}
<picture>
<img src="/assets/img/placeholder-image.svg" alt="not found" data-entry-id={effectiveId} />
<img
bind:this={imgEl}
src={getSrc(fileSrc, entry)}
alt={getAltText(entry)}
data-entry-id={effectiveId}
class={className}
style:object-position={getObjectPosition(entry)}
loading={lazy ? "lazy" : undefined}
{style}
/>
</picture>
{/if}
{:else if !noPlaceholder && (effectiveId || entry)}
<picture>
<img src={placeholderSrc} alt="not found" data-entry-id={effectiveId} class={className} />
</picture>
{/if}
+9
View File
@@ -4,6 +4,12 @@ Playwright tests for E2E, API, mobile, and visual regression. Config in `playwri
For the full current workflow, prefer the `playwright-testing` skill.
## Related skills
- `content-authoring` when test failures may be caused by collection, block-registry, or typing drift.
- `media-seo-publishing` when the failing surface includes medialib/image rendering or shared widget behavior.
- `tibi-hook-authoring` or `tibi-actions-and-forms` when API behavior under test is defined by hooks or actions instead of plain CRUD.
## Running tests
- All tests: `yarn test`
@@ -44,6 +50,7 @@ BrowserSync keeps a WebSocket open permanently, preventing `networkidle` and `lo
- `e2e/fixtures.ts` — Shared desktop helpers (`waitForSpaReady`, `navigateToRoute`, `clickSpaLink`) with BrowserSync-safe navigation defaults.
- `e2e-admin/fixtures.ts` — Shared admin helpers (`loginToAdmin`, `openNovaProjectDashboard`) for committed admin smoke coverage.
- `e2e-admin/content-config.spec.ts` — Checks that collection config is actually reflected in Nova: sensible list columns/previews, usable widgets, and working pagebuilder preview.
- `e2e-admin/pagebuilder.spec.ts` — Checks that the configured pagebuilder block registry renders seeded preview blocks, including an image-backed block via the shared media widget.
- `e2e-visual/fixtures.ts` — Visual test helpers (`waitForVisualReady`, `hideDynamicContent`, `prepareForScreenshot`, `expectScreenshot`, `getDynamicMasks`).
- `e2e-mobile/fixtures.ts` — Mobile helpers (`openHamburgerMenu`, `isMobileViewport`, `isTabletViewport`, `isBelowLg`).
- `api/fixtures.ts` — API fixtures (`api`, `adminApi`).
@@ -51,6 +58,8 @@ BrowserSync keeps a WebSocket open permanently, preventing `networkidle` and `lo
- `fixtures/test-constants.ts` — Central constants (`ADMIN_TOKEN`, `API_BASE`, `TEST_BASE_URL`, `SEEDED_TEST_CONTENT`).
- Use committed admin smoke tests for stable admin contracts. Use one-shot MCP/browser checks only as exploratory supplements, not as the sole regression guard for important admin paths.
- Use admin tests specifically to catch broken collection configuration: empty/bad list previews, missing widgets, broken dependsOn behavior, or non-rendering pagebuilder previews.
- Prefer at least one seeded admin preview test that covers a real image-backed pagebuilder block. That catches broken block registries, missing `_lookup` hydration, and admin-incompatible media URL handling early.
- Keep test fixtures aligned with the real shared frontend contract; do not add test-only media URL rules or duplicate block-rendering assumptions that diverge from runtime code.
- `api/helpers/test-user.ts` is legacy starter scaffolding and should only be reused if the project really needs JWT-user coverage again.
## Visual regression
+28 -1
View File
@@ -1,5 +1,32 @@
import { APIRequestContext, request } from "@playwright/test"
declare const process: {
env: Record<string, string | undefined>
cwd(): string
}
declare function require(name: string): any
function detectMaildevUrlFromProject(): string | null {
const projectName =
process.env.PROJECT_NAME ||
(() => {
try {
const fs = require("fs")
const path = require("path")
const envPath = path.resolve(process.cwd(), ".env")
if (fs.existsSync(envPath)) {
const match = fs.readFileSync(envPath, "utf8").match(/PROJECT_NAME=(.+)/)
return match ? match[1].trim() : null
}
} catch {}
return null
})()
return projectName ? `https://${projectName}-maildev.code.testversion.online` : null
}
/**
* MailDev API helper for testing email flows.
*
@@ -8,7 +35,7 @@ import { APIRequestContext, request } from "@playwright/test"
* - MAILDEV_USER: Basic auth username (default: code)
* - MAILDEV_PASS: Basic auth password
*/
const MAILDEV_URL = process.env.MAILDEV_URL || "http://localhost:1080"
const MAILDEV_URL = process.env.MAILDEV_URL || detectMaildevUrlFromProject() || "http://localhost:1080"
const MAILDEV_USER = process.env.MAILDEV_USER || "code"
const MAILDEV_PASS = process.env.MAILDEV_PASS || ""
+349 -247
View File
@@ -7,6 +7,7 @@ type ContentEntry = {
_testdata?: boolean
translationKey?: string
path?: string
title?: string
[key: string]: unknown
}
@@ -19,6 +20,8 @@ function getEntryId(entry: ContentEntry): string | undefined {
const SEEDED_TRANSLATION_KEYS = new Set<string>(Object.values(SEEDED_TEST_CONTENT).map((entry) => entry.translationKey))
const SEEDED_PATHS = new Set<string>(Object.values(SEEDED_TEST_CONTENT).map((entry) => entry.path))
const SEEDED_MEDIA_DATA_URI =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIHWP4////fwAJ+wP9KobjigAAAABJRU5ErkJggg=="
function isSeededContentEntry(entry: ContentEntry): boolean {
if (entry._testdata === true) {
@@ -36,257 +39,353 @@ function isSeededContentEntry(entry: ContentEntry): boolean {
return false
}
const SEEDED_CONTENT_ENTRIES = [
function isSeededMedialibEntry(entry: ContentEntry): boolean {
return entry._testdata === true
}
const SEEDED_MEDIALIB_ENTRIES = [
{
_testdata: true,
active: true,
type: "page",
lang: "de",
translationKey: SEEDED_TEST_CONTENT.home.translationKey,
name: "Playwright Startseite",
path: SEEDED_TEST_CONTENT.home.path,
teaserText: "Deterministisch erzeugte Testseite fuer API- und E2E-Tests.",
meta: {
title: "Playwright Startseite",
description: "Seeded Startseite fuer stabile Playwright-Tests.",
keywords: ["playwright", "seed", "e2e"],
title: "Playwright Seed Image",
alt: {
de: "Playwright Seed Bild",
en: "Playwright seed image",
},
blocks: [
{
type: "hero",
headline: "Playwright Seed Startseite",
headlineH1: true,
tagline: "Deterministische Testdaten",
subline: "Diese Seite wird vor dem Testlauf frisch ueber die Admin-API angelegt.",
containerWidth: "wide",
callToAction: {
buttonText: "Zum Kontakt",
buttonLink: `/de${SEEDED_TEST_CONTENT.contact.path}`,
},
},
{
type: "features",
anchorId: "seed-features",
headline: "Stabile Grundlage fuer Frontend-Tests",
tagline: "Seed",
padding: { top: "md", bottom: "md" },
featureBoxes: [
{
icon: "lightning",
title: "Frisch angelegt",
text: "Die Inhalte werden in globalSetup erstellt statt aus Demo-Daten uebernommen.",
},
{
icon: "database",
title: "API-nah",
text: "Die Seed-Daten kommen ueber dieselben Collection-Endpunkte wie das CMS.",
},
{
icon: "globe",
title: "Mehrsprachig",
text: "DE und EN teilen sich denselben translationKey fuer Routing-Checks.",
},
],
},
{
type: "richtext",
anchorId: "seed-richtext",
headline: "Mehr Kontext",
tagline: "API + UI",
padding: { top: "md", bottom: "md" },
imagePosition: "none",
text: "<p>Dieser Richtext-Block prueft, dass formatierter HTML-Inhalt im SPA gerendert wird.</p>",
},
{
type: "accordion",
anchorId: "seed-faq",
headline: "Hauefige Fragen",
tagline: "Verhalten",
padding: { top: "md", bottom: "md" },
accordionItems: [
{
question: "Warum Seed-Daten?",
answer: "<p>Damit Tests nicht blind auf bestehende Inhalte oder Demo-Routen vertrauen.</p>",
open: true,
},
{
question: "Was wird geprueft?",
answer: "<p>API-Antworten, Routing, Sprachwechsel und Block-Rendering.</p>",
open: false,
},
],
},
],
},
{
_testdata: true,
active: true,
type: "page",
lang: "en",
translationKey: SEEDED_TEST_CONTENT.home.translationKey,
name: "Playwright Home",
path: SEEDED_TEST_CONTENT.home.path,
teaserText: "Deterministically seeded page for API and E2E coverage.",
meta: {
title: "Playwright Home",
description: "Seeded home page for stable Playwright tests.",
keywords: ["playwright", "seed", "home"],
description: "Deterministisches Medialib-Bild fuer Admin- und Preview-Tests.",
tags: ["playwright", "seed", "preview"],
file: {
src: SEEDED_MEDIA_DATA_URI,
},
blocks: [
{
type: "hero",
headline: "Playwright Seed Home",
headlineH1: true,
tagline: "Deterministic fixtures",
subline: "This page is recreated before every test run through the admin API.",
containerWidth: "wide",
callToAction: {
buttonText: "Go to contact",
buttonLink: `/en${SEEDED_TEST_CONTENT.contact.path}`,
},
},
{
type: "features",
anchorId: "seed-features",
headline: "Stable frontend coverage",
tagline: "Seed",
padding: { top: "md", bottom: "md" },
featureBoxes: [
{
icon: "lightning",
title: "Freshly created",
text: "The content is created during globalSetup instead of relying on demo data.",
},
{
icon: "database",
title: "API-backed",
text: "The seed uses the same collection endpoints as the CMS itself.",
},
{
icon: "globe",
title: "Localized",
text: "DE and EN share the same translationKey for route switching checks.",
},
],
},
{
type: "richtext",
anchorId: "seed-richtext",
headline: "More context",
tagline: "API + UI",
padding: { top: "md", bottom: "md" },
imagePosition: "none",
text: "<p>This richtext block proves that formatted HTML content renders in the SPA.</p>",
},
{
type: "accordion",
anchorId: "seed-faq",
headline: "Common questions",
tagline: "Behavior",
padding: { top: "md", bottom: "md" },
accordionItems: [
{
question: "Why seeded data?",
answer: "<p>So the tests do not depend on existing demo pages or editorial content.</p>",
open: true,
},
{
question: "What is covered?",
answer: "<p>API responses, routing, locale switching and block rendering.</p>",
open: false,
},
],
},
],
},
{
_testdata: true,
active: true,
type: "page",
lang: "de",
translationKey: SEEDED_TEST_CONTENT.contact.translationKey,
name: "Playwright Kontakt",
path: SEEDED_TEST_CONTENT.contact.path,
teaserText: "Seeded Kontaktseite fuer Formular- und Routing-Tests.",
meta: {
title: "Playwright Kontakt",
description: "Seeded Kontaktseite fuer Playwright.",
keywords: ["playwright", "kontakt"],
},
blocks: [
{
type: "hero",
headline: "Kontakt fuer Testlauf",
headlineH1: true,
tagline: "Seed",
subline: "Diese Seite prueft das aktuelle ContactForm-Rendering.",
},
{
type: "contact-form",
anchorId: "kontaktformular",
headline: "Schreibe uns",
padding: { top: "md", bottom: "md" },
},
],
},
{
_testdata: true,
active: true,
type: "page",
lang: "en",
translationKey: SEEDED_TEST_CONTENT.contact.translationKey,
name: "Playwright Contact",
path: SEEDED_TEST_CONTENT.contact.path,
teaserText: "Seeded contact page for form and routing tests.",
meta: {
title: "Playwright Contact",
description: "Seeded contact page for Playwright.",
keywords: ["playwright", "contact"],
},
blocks: [
{
type: "hero",
headline: "Contact for the test run",
headlineH1: true,
tagline: "Seed",
subline: "This page verifies the current contact form rendering.",
},
{
type: "contact-form",
anchorId: "contact-form",
headline: "Write to us",
padding: { top: "md", bottom: "md" },
},
],
},
{
_testdata: true,
active: false,
type: "page",
lang: "de",
translationKey: SEEDED_TEST_CONTENT.inactive.translationKey,
name: "Playwright Inaktiv",
path: SEEDED_TEST_CONTENT.inactive.path,
teaserText: "Nicht aktive Seed-Seite fuer 404-Checks.",
meta: {
title: "Playwright Inaktiv",
description: "Nicht aktive Seed-Seite fuer Routing-Tests.",
keywords: ["playwright", "inactive"],
},
blocks: [
{
type: "richtext",
anchorId: "inactive",
headline: "Sollte nicht sichtbar sein",
tagline: "Seed",
padding: { top: "md", bottom: "md" },
imagePosition: "none",
text: "<p>Diese Seite ist absichtlich inaktiv und darf im Frontend nicht erscheinen.</p>",
},
],
},
] as const
function getSeededContentEntries(previewImageId: string) {
return [
{
_testdata: true,
active: true,
type: "page",
lang: "de",
translationKey: SEEDED_TEST_CONTENT.home.translationKey,
name: "Playwright Startseite",
path: SEEDED_TEST_CONTENT.home.path,
teaserText: "Deterministisch erzeugte Testseite fuer API- und E2E-Tests.",
meta: {
title: "Playwright Startseite",
description: "Seeded Startseite fuer stabile Playwright-Tests.",
keywords: ["playwright", "seed", "e2e"],
},
blocks: [
{
type: "hero",
headline: "Playwright Seed Startseite",
headlineH1: true,
tagline: "Deterministische Testdaten",
subline: "Diese Seite wird vor dem Testlauf frisch ueber die Admin-API angelegt.",
containerWidth: "wide",
callToAction: {
buttonText: "Zum Kontakt",
buttonLink: `/de${SEEDED_TEST_CONTENT.contact.path}`,
},
},
{
type: "features",
anchorId: "seed-features",
headline: "Stabile Grundlage fuer Frontend-Tests",
tagline: "Seed",
padding: { top: "md", bottom: "md" },
featureBoxes: [
{
icon: "lightning",
title: "Frisch angelegt",
text: "Die Inhalte werden in globalSetup erstellt statt aus Demo-Daten uebernommen.",
},
{
icon: "database",
title: "API-nah",
text: "Die Seed-Daten kommen ueber dieselben Collection-Endpunkte wie das CMS.",
},
{
icon: "globe",
title: "Mehrsprachig",
text: "DE und EN teilen sich denselben translationKey fuer Routing-Checks.",
},
],
},
{
type: "richtext",
anchorId: "seed-richtext",
headline: "Mehr Kontext",
tagline: "API + UI",
padding: { top: "md", bottom: "md" },
imagePosition: "none",
text: "<p>Dieser Richtext-Block prueft, dass formatierter HTML-Inhalt im SPA gerendert wird.</p>",
},
{
type: "accordion",
anchorId: "seed-faq",
headline: "Hauefige Fragen",
tagline: "Verhalten",
padding: { top: "md", bottom: "md" },
accordionItems: [
{
question: "Warum Seed-Daten?",
answer: "<p>Damit Tests nicht blind auf bestehende Inhalte oder Demo-Routen vertrauen.</p>",
open: true,
},
{
question: "Was wird geprueft?",
answer: "<p>API-Antworten, Routing, Sprachwechsel und Block-Rendering.</p>",
open: false,
},
],
},
],
},
{
_testdata: true,
active: true,
type: "page",
lang: "en",
translationKey: SEEDED_TEST_CONTENT.home.translationKey,
name: "Playwright Home",
path: SEEDED_TEST_CONTENT.home.path,
teaserText: "Deterministically seeded page for API and E2E coverage.",
meta: {
title: "Playwright Home",
description: "Seeded home page for stable Playwright tests.",
keywords: ["playwright", "seed", "home"],
},
blocks: [
{
type: "hero",
headline: "Playwright Seed Home",
headlineH1: true,
tagline: "Deterministic fixtures",
subline: "This page is recreated before every test run through the admin API.",
containerWidth: "wide",
callToAction: {
buttonText: "Go to contact",
buttonLink: `/en${SEEDED_TEST_CONTENT.contact.path}`,
},
},
{
type: "features",
anchorId: "seed-features",
headline: "Stable frontend coverage",
tagline: "Seed",
padding: { top: "md", bottom: "md" },
featureBoxes: [
{
icon: "lightning",
title: "Freshly created",
text: "The content is created during globalSetup instead of relying on demo data.",
},
{
icon: "database",
title: "API-backed",
text: "The seed uses the same collection endpoints as the CMS itself.",
},
{
icon: "globe",
title: "Localized",
text: "DE and EN share the same translationKey for route switching checks.",
},
],
},
{
type: "richtext",
anchorId: "seed-richtext",
headline: "More context",
tagline: "API + UI",
padding: { top: "md", bottom: "md" },
imagePosition: "none",
text: "<p>This richtext block proves that formatted HTML content renders in the SPA.</p>",
},
{
type: "accordion",
anchorId: "seed-faq",
headline: "Common questions",
tagline: "Behavior",
padding: { top: "md", bottom: "md" },
accordionItems: [
{
question: "Why seeded data?",
answer: "<p>So the tests do not depend on existing demo pages or editorial content.</p>",
open: true,
},
{
question: "What is covered?",
answer: "<p>API responses, routing, locale switching and block rendering.</p>",
open: false,
},
],
},
],
},
{
_testdata: true,
active: true,
type: "page",
lang: "de",
translationKey: SEEDED_TEST_CONTENT.pagebuilderPreview.translationKey,
name: "Playwright Pagebuilder Preview",
path: SEEDED_TEST_CONTENT.pagebuilderPreview.path,
teaserText: "Seeded Admin-Vorschauseite fuer Pagebuilder-Registry und Bildrendering.",
meta: {
title: "Playwright Pagebuilder Preview",
description: "Seeded Seite fuer Pagebuilder- und Medialib-Vorschau-Tests.",
keywords: ["playwright", "pagebuilder", "preview"],
},
blocks: [
{
type: "hero",
headline: "Playwright Registry Hero",
headlineH1: true,
tagline: "Admin Preview",
subline: "Dieses Hero-Preview prueft den Block-Registry-Pfad inklusive Bild.",
containerWidth: "wide",
heroImage: {
image: previewImageId,
},
},
{
type: "richtext",
anchorId: "pagebuilder-preview-richtext",
headline: "Richtext mit Bild",
tagline: "Shared Media Widget",
padding: { top: "md", bottom: "md" },
imagePosition: "right",
image: previewImageId,
text: "<p>Dieser Block prueft, dass ein image-gestuetzter Preview-Block im Admin ueber dieselbe Registry und dasselbe Bild-Widget rendert.</p>",
},
],
},
{
_testdata: true,
active: true,
type: "page",
lang: "de",
translationKey: SEEDED_TEST_CONTENT.contact.translationKey,
name: "Playwright Kontakt",
path: SEEDED_TEST_CONTENT.contact.path,
teaserText: "Seeded Kontaktseite fuer Formular- und Routing-Tests.",
meta: {
title: "Playwright Kontakt",
description: "Seeded Kontaktseite fuer Playwright.",
keywords: ["playwright", "kontakt"],
},
blocks: [
{
type: "hero",
headline: "Kontakt fuer Testlauf",
headlineH1: true,
tagline: "Seed",
subline: "Diese Seite prueft das aktuelle ContactForm-Rendering.",
},
{
type: "contact-form",
anchorId: "kontaktformular",
headline: "Schreibe uns",
padding: { top: "md", bottom: "md" },
},
],
},
{
_testdata: true,
active: true,
type: "page",
lang: "en",
translationKey: SEEDED_TEST_CONTENT.contact.translationKey,
name: "Playwright Contact",
path: SEEDED_TEST_CONTENT.contact.path,
teaserText: "Seeded contact page for form and routing tests.",
meta: {
title: "Playwright Contact",
description: "Seeded contact page for Playwright.",
keywords: ["playwright", "contact"],
},
blocks: [
{
type: "hero",
headline: "Contact for the test run",
headlineH1: true,
tagline: "Seed",
subline: "This page verifies the current contact form rendering.",
},
{
type: "contact-form",
anchorId: "contact-form",
headline: "Write to us",
padding: { top: "md", bottom: "md" },
},
],
},
{
_testdata: true,
active: false,
type: "page",
lang: "de",
translationKey: SEEDED_TEST_CONTENT.inactive.translationKey,
name: "Playwright Inaktiv",
path: SEEDED_TEST_CONTENT.inactive.path,
teaserText: "Nicht aktive Seed-Seite fuer 404-Checks.",
meta: {
title: "Playwright Inaktiv",
description: "Nicht aktive Seed-Seite fuer Routing-Tests.",
keywords: ["playwright", "inactive"],
},
blocks: [
{
type: "richtext",
anchorId: "inactive",
headline: "Sollte nicht sichtbar sein",
tagline: "Seed",
padding: { top: "md", bottom: "md" },
imagePosition: "none",
text: "<p>Diese Seite ist absichtlich inaktiv und darf im Frontend nicht erscheinen.</p>",
},
],
},
] as const
}
async function cleanupSeededMedialibEntries(baseURL: string): Promise<number> {
const medialibEntries = await listCollectionEntries<ContentEntry>(baseURL, "medialib")
const seededEntries = medialibEntries.filter((entry) => isSeededMedialibEntry(entry))
let deleted = 0
for (const entry of seededEntries) {
const entryId = getEntryId(entry)
if (!entryId) continue
const ok = await deleteCollectionEntry(baseURL, "medialib", entryId)
if (ok) deleted++
}
return deleted
}
async function seedMedialibEntries(baseURL: string): Promise<{ created: number; previewImageId: string }> {
let previewImageId = ""
let created = 0
for (const entry of SEEDED_MEDIALIB_ENTRIES) {
const createdEntry = await createCollectionEntry<ContentEntry>(baseURL, "medialib", {
...(entry as Record<string, unknown>),
})
const entryId = getEntryId(createdEntry)
if (!entryId) {
throw new Error("Seeded medialib entry was created without an id")
}
previewImageId = entryId
created++
}
return { created, previewImageId }
}
export async function cleanupSeededTestContent(baseURL: string): Promise<number> {
const contentEntries = await listCollectionEntries<ContentEntry>(baseURL, "content")
// Cleanup runs before every seed pass so leftovers from aborted test runs
@@ -302,12 +401,15 @@ export async function cleanupSeededTestContent(baseURL: string): Promise<number>
if (ok) deleted++
}
return deleted
const deletedMediaEntries = await cleanupSeededMedialibEntries(baseURL)
return deleted + deletedMediaEntries
}
export async function seedTestContent(baseURL: string): Promise<number> {
let created = 0
for (const entry of SEEDED_CONTENT_ENTRIES) {
const mediaSeed = await seedMedialibEntries(baseURL)
let created = mediaSeed.created
for (const entry of getSeededContentEntries(mediaSeed.previewImageId)) {
await createCollectionEntry(baseURL, "content", entry as unknown as Record<string, unknown>)
created++
}
+14
View File
@@ -21,6 +21,9 @@ export const test = base.extend({
export async function loginToAdmin(page: Page): Promise<void> {
await page.goto(`${TEST_ADMIN_BASE_URL}/login`)
// Admin bootet als SPA; das Formular ist oft erst nach Chunk-Ladevorgang interaktiv.
await expect(page.getByLabel(/Benutzername|Username/i)).toBeVisible({ timeout: 20000 })
await page.getByLabel(/Benutzername|Username/i).fill(ADMIN_UI_CREDENTIALS.username)
await page.getByLabel(/Passwort|Password/i).fill(ADMIN_UI_CREDENTIALS.password)
await page.getByRole("button", { name: /Anmelden|Sign in|Login/i }).click()
@@ -55,6 +58,17 @@ export async function openNewContentEntry(page: Page): Promise<void> {
await openNewCollectionEntry(page, /Inhalte/, "content", "Inhalte")
}
export async function openContentEntry(page: Page, rowText: string | RegExp): Promise<void> {
await openContentCollection(page)
const entryRow = page.getByRole("row").filter({ hasText: rowText }).first()
await expect(entryRow).toBeVisible()
await entryRow.click()
await expect(page).toHaveURL(/\/collections\/content\/entries\/[^/?]+/)
await expect(page.locator("main")).toBeVisible()
}
export async function openNavigationCollection(page: Page): Promise<void> {
await openCollection(page, /Navigation/, "navigation", "Navigation")
}
+21
View File
@@ -0,0 +1,21 @@
import { test, expect, openContentEntry } from "./fixtures"
test.describe("Admin pagebuilder registry and preview rendering", () => {
test("renders seeded image-backed blocks through the configured pagebuilder registry", async ({ page }) => {
await openContentEntry(page, /Playwright Pagebuilder Preview/)
const previewRoot = page.locator("[data-admin-preview]")
const heroPreview = previewRoot.locator("[data-block='hero']").first()
const richtextPreview = previewRoot.locator("[data-block='richtext']").first()
await expect(previewRoot.first()).toBeVisible()
await expect(heroPreview.getByRole("heading", { name: "Playwright Registry Hero" })).toBeVisible()
await expect(richtextPreview).toContainText("Richtext mit Bild")
await expect(richtextPreview).toContainText("dass ein image-gestuetzter Preview-Block")
const previewImages = previewRoot.locator("img[data-entry-id]")
await expect(previewImages.first()).toBeVisible()
await expect(previewImages.first()).toHaveAttribute("src", /\/medialib\/[^/]+\/file\/[^?]+(?:\?filter=[^"']+)?/)
await expect(previewImages).toHaveCount(2)
})
})
+4
View File
@@ -57,6 +57,10 @@ export const SEEDED_TEST_CONTENT = {
translationKey: "pw-e2e-home",
path: "/playwright-e2e-home",
},
pagebuilderPreview: {
translationKey: "pw-e2e-pagebuilder-preview",
path: "/playwright-e2e-pagebuilder-preview",
},
contact: {
translationKey: "pw-e2e-contact",
path: "/playwright-e2e-contact",
+1 -1
View File
@@ -34,7 +34,7 @@ async function globalSetup() {
const result = await ensureSeededTestContent(baseURL)
console.log(
`Playwright setup: seeded deterministic content (deleted ${result.deleted}, created ${result.created})`
`Playwright setup: seeded deterministic test data (deleted ${result.deleted}, created ${result.created})`
)
} finally {
await ctx.dispose()
+1 -1
View File
@@ -28,7 +28,7 @@ async function globalTeardown() {
const result = await cleanupAllTestData(baseURL)
if (deletedSeedEntries > 0 || result.users > 0) {
console.log(
`Playwright teardown: deleted ${deletedSeedEntries} seeded content entries and ${result.users} test users`
`Playwright teardown: deleted ${deletedSeedEntries} seeded content/media entries and ${result.users} test users`
)
}
} catch (err) {