✨ 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:
+614
-213
@@ -1,248 +1,649 @@
|
|||||||
# Build Checklist — Autonomous Website Project
|
# 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__`
|
- `.env`: `__PROJECT_NAME__`, `__TIBI_NAMESPACE__`, `__ORG__`, `__PROJECT__`
|
||||||
- `api/config.yml`: `namespace: __TIBI_NAMESPACE__`
|
- `api/config.yml`: `namespace`
|
||||||
- `frontend/.htaccess`: both `__TIBI_NAMESPACE__` entries
|
- `frontend/.htaccess`: namespace placeholders when Apache rewrite/proxy is actually used
|
||||||
- `api/hooks/config-client.js`: `__PROJECT__` (not `__PROJECT_NAME__`)
|
- `api/hooks/config-client.js`: project placeholder
|
||||||
- [ ] Configure `.env` URLs: `CODING_URL`, `STAGING_URL`, `CODING_TIBIADMIN_URL`
|
- [ ] Replace or remove visible starter identity leftovers in `README.md`, `package.json`, and other project-facing metadata.
|
||||||
- [ ] Update `package.json` metadata (name, repository)
|
- [ ] Set all required project identity and URL values in `.env`:
|
||||||
- [ ] Run `grep -n '__[A-Z0-9_]\+__' . --include='*.{yml,js,env,htaccess,json}'` to verify no placeholder remains
|
- `PROJECT_NAME`
|
||||||
- [ ] Generate secure `ADMIN_TOKEN` in `api/config.yml.env`
|
- `TIBI_NAMESPACE`
|
||||||
- [ ] 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`)
|
- `LIVE_URL`
|
||||||
- [ ] `yarn install` — must succeed
|
- `CODING_URL`
|
||||||
- [ ] `make docker-up` — verify all containers "Up"
|
- `STAGING_URL`
|
||||||
- [ ] Verify URLs respond: website, tibiadmin, tibiserver API
|
- `CODING_TIBIADMIN_URL`
|
||||||
- [ ] `yarn build && yarn build:server && yarn validate` — 0 errors, 0 warnings
|
- `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`
|
|
||||||
|
|
||||||
- [ ] 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?
|
|
||||||
- [ ] 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)?
|
|
||||||
- [ ] 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?
|
|
||||||
|
|
||||||
## Phase 2: Collection & Admin Model
|
|
||||||
|
|
||||||
**Skills:** `content-authoring`, `admin-ui-config`, `nova-pagebuilder-modeling`, `nova-navigation-modeling`, `media-seo-publishing`
|
|
||||||
|
|
||||||
- [ ] 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
|
|
||||||
|
|
||||||
## Phase 3: TypeScript Types
|
|
||||||
|
|
||||||
**Skills:** `content-authoring` (types section)
|
|
||||||
|
|
||||||
- [ ] 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
|
|
||||||
|
|
||||||
## Phase 4: Frontend Components
|
|
||||||
|
|
||||||
**Skills:** `frontend-architecture`, `nova-pagebuilder-modeling`
|
|
||||||
|
|
||||||
- [ ] 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
|
|
||||||
|
|
||||||
## Phase 5: SSR Setup
|
|
||||||
|
|
||||||
**Skills:** `tibi-ssr-caching`
|
|
||||||
|
|
||||||
- [ ] 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
|
```bash
|
||||||
curl "http://tibiserver:8080/api/v1/_/<namespace>/ssr?url=/de/..."
|
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
|
||||||
```
|
```
|
||||||
- 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
|
|
||||||
|
|
||||||
## Phase 6: Backend Hooks & Actions
|
**Exit criteria:**
|
||||||
|
|
||||||
**Skills:** `tibi-hook-authoring`, `tibi-actions-and-forms`, `scheduled-jobs-and-automation`, `realtime-and-live-workflows`
|
- [ ] 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.
|
||||||
|
|
||||||
- [ ] Create/update public read hooks (`api/hooks/<collection>/get_read.js`):
|
## Phase 1 — Solution architecture and delivery decisions
|
||||||
- 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
|
|
||||||
|
|
||||||
## Phase 7: Permissions & Security
|
**Required skills:** `website-solution-architecture`, `security-hardening-and-token-strategy`
|
||||||
|
|
||||||
**Skills:** `permissions-and-editor-workflows`, `security-hardening-and-token-strategy`
|
**Required project artifacts:**
|
||||||
|
|
||||||
- [ ] Configure collection permissions:
|
- `docs/solution-architecture.md` or `plans/solution-architecture.md`
|
||||||
- `public` read methods where appropriate
|
- optional companion notes such as `docs/permissions.md` or `docs/content-model.md` when the project is large enough to need them
|
||||||
- `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
|
|
||||||
|
|
||||||
## Phase 8: Media & SEO
|
**Implementation checks:**
|
||||||
|
|
||||||
**Skills:** `media-seo-publishing`, `nova-ai-editor-features` (optional)
|
- [ ] Create one primary architecture document with explicit sections for:
|
||||||
|
- context and scope
|
||||||
|
- route and i18n model
|
||||||
|
- collections and ownership
|
||||||
|
- navigation and pagebuilder
|
||||||
|
- forms, actions, jobs, and realtime
|
||||||
|
- SSR, publication, and cache behavior
|
||||||
|
- permissions and integrations
|
||||||
|
- decision matrix
|
||||||
|
- [ ] Document the content model:
|
||||||
|
- page types
|
||||||
|
- reusable entities
|
||||||
|
- singleton/config collections
|
||||||
|
- page-local vs reusable data
|
||||||
|
- [ ] Document the route model:
|
||||||
|
- language prefix strategy
|
||||||
|
- `content.path` expectations
|
||||||
|
- route translations
|
||||||
|
- alias/canonical handling
|
||||||
|
- [ ] Document the i18n strategy explicitly:
|
||||||
|
- single-language or multilingual
|
||||||
|
- field-level i18n or entry-level i18n
|
||||||
|
- default language
|
||||||
|
- supported languages
|
||||||
|
- localized slug/path strategy
|
||||||
|
- [ ] Document forms and workflows:
|
||||||
|
- which features are CRUD collections
|
||||||
|
- which features are actions
|
||||||
|
- whether jobs or realtime are required
|
||||||
|
- whether persistence is required
|
||||||
|
- [ ] Document SSR requirements:
|
||||||
|
- which routes must SSR
|
||||||
|
- which collections are page-critical
|
||||||
|
- whether publication windows exist
|
||||||
|
- [ ] Document permissions strategy:
|
||||||
|
- human roles
|
||||||
|
- token-based integrations
|
||||||
|
- hidden/readonly needs
|
||||||
|
- [ ] Make an explicit decision whether the project is single-tenant or needs org/team support.
|
||||||
|
- [ ] Make an explicit decision whether AI/editor assistance, classic search, or embeddings/search are in scope.
|
||||||
|
- [ ] Record a searchable yes/no decision matrix for at least:
|
||||||
|
- single-language vs multilingual
|
||||||
|
- field-level vs entry-level i18n
|
||||||
|
- single-tenant vs org/team
|
||||||
|
- AI/editor assistance
|
||||||
|
- classic search or embeddings
|
||||||
|
- actions/jobs/realtime usage
|
||||||
|
- publication scheduling
|
||||||
|
|
||||||
- [ ] Configure `api/collections/medialib.yml`:
|
**Validation commands:**
|
||||||
- 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
|
|
||||||
|
|
||||||
## Phase 9: Testing
|
```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"
|
||||||
|
```
|
||||||
|
|
||||||
**Skills:** `playwright-testing`
|
**Exit criteria:**
|
||||||
|
|
||||||
|
- [ ] A later agent can open the architecture document and answer the route, content, SSR, permissions, and i18n questions without guessing.
|
||||||
|
- [ ] The architecture document contains explicit recorded choices for single-language vs multilingual and field-level vs entry-level i18n.
|
||||||
|
- [ ] The project has an explicit yes/no decision recorded for org/team support, AI/editor assistance, and search/embeddings scope.
|
||||||
|
|
||||||
|
## Phase 2 — Collection model and Nova admin ergonomics
|
||||||
|
|
||||||
|
**Required skills:** `content-authoring`, `admin-ui-config`, `nova-pagebuilder-modeling`, `nova-navigation-modeling`, `media-seo-publishing`
|
||||||
|
|
||||||
|
**Required project artifacts:**
|
||||||
|
|
||||||
|
- `api/config.yml`
|
||||||
|
- `api/collections/content.yml`
|
||||||
|
- `api/collections/navigation.yml`
|
||||||
|
- `api/collections/medialib.yml`
|
||||||
|
- domain collection YAML files
|
||||||
|
|
||||||
|
**Implementation checks:**
|
||||||
|
|
||||||
|
- [ ] Create or update all required collections in `api/collections/`.
|
||||||
|
- [ ] Include every collection in `api/config.yml`.
|
||||||
|
- [ ] Configure meaningful `meta.preview` for each collection.
|
||||||
|
- [ ] Configure the right `meta.viewHint` for each collection.
|
||||||
|
- [ ] Configure usable forms with layout, sidebar groups, drillDown, dependsOn, and widget overrides where needed.
|
||||||
|
- [ ] Use `pagebuilder` plus `blockRegistry` for block-driven collections.
|
||||||
|
- [ ] Use readable foreign-key previews instead of raw IDs.
|
||||||
|
- [ ] Configure field validators, file acceptance, and image constraints.
|
||||||
|
- [ ] If the project benefits from grouped collection navigation or project-level admin i18n, configure those contracts deliberately instead of leaving them implicit.
|
||||||
|
- [ ] If a collection is effectively single-document config, use `singleton` deliberately.
|
||||||
|
|
||||||
|
**Validation commands:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn validate
|
||||||
|
```
|
||||||
|
|
||||||
|
Admin validation should also cover:
|
||||||
|
|
||||||
|
- collection sidebar labels and icons
|
||||||
|
- list previews and columns
|
||||||
|
- entry-form usability
|
||||||
|
- foreign-reference readability
|
||||||
|
- pagebuilder block chooser availability
|
||||||
|
- pagebuilder preview rendering for at least one representative block when pagebuilder is used
|
||||||
|
|
||||||
|
**Exit criteria:**
|
||||||
|
|
||||||
|
- [ ] Every collection has a clear admin presentation model.
|
||||||
|
- [ ] Editors can identify and edit entries without raw-ID workflows.
|
||||||
|
- [ ] Pagebuilder-driven collections have a complete block registry and editable block forms.
|
||||||
|
|
||||||
|
## Phase 3 — Type contracts and API typing
|
||||||
|
|
||||||
|
**Required skills:** `content-authoring`
|
||||||
|
|
||||||
|
**Required project artifacts:**
|
||||||
|
|
||||||
|
- `types/global.d.ts`
|
||||||
|
- `frontend/src/lib/api.ts`
|
||||||
|
|
||||||
|
**Implementation checks:**
|
||||||
|
|
||||||
|
- [ ] Model all block and domain-entry types in `types/global.d.ts`.
|
||||||
|
- [ ] Add or update `EntryTypeSwitch` coverage in `frontend/src/lib/api.ts`.
|
||||||
|
- [ ] When a block or collection participates in public rendering or admin preview, update all affected types in the same change.
|
||||||
|
- [ ] Keep API and block types aligned with the collection YAML definitions.
|
||||||
|
- [ ] Avoid type drift between the CMS config and the frontend assumptions.
|
||||||
|
|
||||||
|
**Validation commands:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn validate
|
||||||
|
```
|
||||||
|
|
||||||
|
**Exit criteria:**
|
||||||
|
|
||||||
|
- [ ] The current project types describe the collection and block model accurately.
|
||||||
|
- [ ] Validation passes cleanly.
|
||||||
|
|
||||||
|
## Phase 4 — Frontend blocks, routing, and admin registry
|
||||||
|
|
||||||
|
**Required skills:** `frontend-architecture`, `nova-pagebuilder-modeling`
|
||||||
|
|
||||||
|
**Required project artifacts:**
|
||||||
|
|
||||||
|
- `frontend/src/blocks/*`
|
||||||
|
- `frontend/src/blocks/BlockRenderer.svelte`
|
||||||
|
- `frontend/src/admin.ts`
|
||||||
|
- routing/i18n files under `frontend/src/lib/`
|
||||||
|
|
||||||
|
**Implementation checks:**
|
||||||
|
|
||||||
|
- [ ] Implement every required block component.
|
||||||
|
- [ ] Register every block in `BlockRenderer.svelte`.
|
||||||
|
- [ ] Register every pagebuilder block in `frontend/src/admin.ts`.
|
||||||
|
- [ ] Keep blocks SSR-safe.
|
||||||
|
- [ ] Ensure the route layer, i18n layer, and content lookup logic agree on the public URL model.
|
||||||
|
- [ ] If blocks or pages render foreign media/references, confirm lookup expectations explicitly instead of assuming resolved data is present.
|
||||||
|
- [ ] Treat public rendering and admin-preview rendering as the same contract whenever possible.
|
||||||
|
|
||||||
|
**Validation commands:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn build
|
||||||
|
yarn validate
|
||||||
|
```
|
||||||
|
|
||||||
|
**Exit criteria:**
|
||||||
|
|
||||||
|
- [ ] All configured block types render in the site and in the admin registry.
|
||||||
|
- [ ] Navigation, i18n, and media references behave correctly in the browser.
|
||||||
|
|
||||||
|
## Phase 5 — SSR, publication model, and cache invalidation
|
||||||
|
|
||||||
|
**Required skills:** `tibi-ssr-caching`, `tibi-hook-authoring`
|
||||||
|
|
||||||
|
**Required project artifacts:**
|
||||||
|
|
||||||
|
- `api/hooks/config.js`
|
||||||
|
- `api/hooks/clear_cache.js`
|
||||||
|
- SSR-related hook files under `api/hooks/ssr/`
|
||||||
|
|
||||||
|
**Implementation checks:**
|
||||||
|
|
||||||
|
- [ ] Update `ssrValidatePath()` for the real public route model.
|
||||||
|
- [ ] Ensure `publishedFilter` matches the actual publication model.
|
||||||
|
- [ ] Ensure `ssrPublishCheckCollections` covers all time-sensitive collections.
|
||||||
|
- [ ] Confirm page-critical collections are loaded in an SSR-safe way.
|
||||||
|
- [ ] Confirm mutations to content, navigation, media, and publication-critical data invalidate SSR as intended.
|
||||||
|
- [ ] Verify at least one representative mutation path against the SSR response instead of only checking static SSR HTML.
|
||||||
|
- [ ] If lookups are needed for page-critical references, ensure SSR data loading uses them deliberately.
|
||||||
|
|
||||||
|
**Validation commands:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn build:server
|
||||||
|
curl "$CODING_TIBISERVER_URL/api/v1/_/<namespace>/ssr?url=/de/..."
|
||||||
|
curl -I "$CODING_TIBISERVER_URL/api/v1/_/<namespace>/ssr?url=/de/..."
|
||||||
|
```
|
||||||
|
|
||||||
|
Check all of these on the SSR response:
|
||||||
|
|
||||||
|
- HTTP status is correct.
|
||||||
|
- HTML contains the expected page content.
|
||||||
|
- HTML contains navigation labels.
|
||||||
|
- `window.__SSR_CACHE__` is present.
|
||||||
|
- a repeated request can return an SSR cache hit where expected.
|
||||||
|
- after a representative mutation, the next SSR response reflects the change or expected invalidation behavior.
|
||||||
|
|
||||||
|
**Exit criteria:**
|
||||||
|
|
||||||
|
- [ ] SSR returns correct HTML for valid routes.
|
||||||
|
- [ ] Publication and invalidation behavior is verified, not assumed.
|
||||||
|
|
||||||
|
## Phase 6 — Hooks, actions, jobs, and realtime
|
||||||
|
|
||||||
|
**Required skills:** `tibi-hook-authoring`, `tibi-actions-and-forms`, `scheduled-jobs-and-automation`, `realtime-and-live-workflows`
|
||||||
|
|
||||||
|
**Required project artifacts:**
|
||||||
|
|
||||||
|
- collection hook files in `api/hooks/`
|
||||||
|
- action files in `api/actions/` when applicable
|
||||||
|
- job config/hooks when applicable
|
||||||
|
|
||||||
|
**Implementation checks:**
|
||||||
|
|
||||||
|
- [ ] Implement public read hooks where public filtering differs from raw CRUD reads.
|
||||||
|
- [ ] Implement cache invalidation hooks for page-critical mutations.
|
||||||
|
- [ ] Use actions for endpoint-like workflows instead of fake CRUD collections.
|
||||||
|
- [ ] For each endpoint-style workflow, record why it is an action and not a collection.
|
||||||
|
- [ ] Respect current action-hook behavior, especially `context.data` timing.
|
||||||
|
- [ ] If jobs or realtime are used, document why they belong in the project and what they affect.
|
||||||
|
- [ ] Register every hook/action/job in the project config.
|
||||||
|
|
||||||
|
**Validation commands:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn validate
|
||||||
|
```
|
||||||
|
|
||||||
|
Add targeted API or Playwright checks for:
|
||||||
|
|
||||||
|
- anonymous vs token-backed public filtering
|
||||||
|
- valid vs invalid action submissions
|
||||||
|
- cache-clear side effects
|
||||||
|
- job/realtime behavior when those features exist
|
||||||
|
|
||||||
|
**Exit criteria:**
|
||||||
|
|
||||||
|
- [ ] Backend behavior matches the modeled workflows.
|
||||||
|
- [ ] Endpoint-style features are implemented as actions when appropriate.
|
||||||
|
|
||||||
|
## Phase 7 — Permissions and security hardening
|
||||||
|
|
||||||
|
**Required skills:** `permissions-and-editor-workflows`, `security-hardening-and-token-strategy`
|
||||||
|
|
||||||
|
**Required project artifacts:**
|
||||||
|
|
||||||
|
- collection/action permission sections in YAML
|
||||||
|
- security-relevant config in server/project config
|
||||||
|
- optional `docs/permissions.md` for larger projects
|
||||||
|
|
||||||
|
**Implementation checks:**
|
||||||
|
|
||||||
|
- [ ] Configure collection permissions for `public`, `user`, and any custom roles.
|
||||||
|
- [ ] Add explicit token permission sets where machine access is required.
|
||||||
|
- [ ] Use bulk permissions only when there is a real operational need.
|
||||||
|
- [ ] Write down representative permission actors and workflow states before finalizing the YAML.
|
||||||
|
- [ ] Configure `readonlyFields`, `hiddenFields`, and any field-level overrides deliberately.
|
||||||
|
- [ ] Use eval-based field rules where editorial state transitions require them.
|
||||||
|
- [ ] Review CORS for real cross-origin requirements instead of weakening it by default.
|
||||||
|
- [ ] Review login rate limiting and secure-cookie expectations for the target environment.
|
||||||
|
- [ ] Review risky hook capabilities such as outbound fetch or command execution and document why they are necessary when used.
|
||||||
|
- [ ] Ensure production secrets come from proper sources rather than committed literals.
|
||||||
|
|
||||||
|
**Validation commands:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn validate
|
||||||
|
```
|
||||||
|
|
||||||
|
Add targeted API checks for:
|
||||||
|
|
||||||
|
- public vs authenticated vs token-backed access
|
||||||
|
- readonly/hidden enforcement on read and write
|
||||||
|
- eval-based permission behavior for representative entry states
|
||||||
|
- at least one allowed and one denied write for each important workflow state
|
||||||
|
|
||||||
|
**Exit criteria:**
|
||||||
|
|
||||||
|
- [ ] Permissions reflect the real editor/integration model.
|
||||||
|
- [ ] Security-sensitive config and risky capabilities were reviewed explicitly.
|
||||||
|
|
||||||
|
## Phase 8 — Audit and compliance readiness
|
||||||
|
|
||||||
|
**Required skills:** `audit-and-compliance`, `tibi-hook-authoring`
|
||||||
|
|
||||||
|
**Required project artifacts:**
|
||||||
|
|
||||||
|
- audit config in the active tibi-server config
|
||||||
|
- collection-level audit settings when relevant
|
||||||
|
- `audit.return` hooks where sensitive data must be stripped
|
||||||
|
|
||||||
|
**Implementation checks:**
|
||||||
|
|
||||||
|
- [ ] Decide whether audit logging is required for the project.
|
||||||
|
- [ ] If enabled, configure server-level audit settings deliberately.
|
||||||
|
- [ ] If sensitive fields can land in snapshots, add `audit.return` hooks.
|
||||||
|
- [ ] If hooks/jobs/actions mutate important collections, account for the resulting audit source semantics.
|
||||||
|
- [ ] If audit is required in production, confirm retention/TTL expectations with operations.
|
||||||
|
|
||||||
|
**Validation commands:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -H "X-Auth-Token: <jwt-token>" "$CODING_TIBISERVER_URL/api/v1/audit?limit=5"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Exit criteria:**
|
||||||
|
|
||||||
|
- [ ] The project has an explicit audit decision: enabled with rules, or deliberately not used.
|
||||||
|
- [ ] Sensitive audit exposure has been considered, not ignored.
|
||||||
|
|
||||||
|
## Phase 9 — Media, SEO, and publication
|
||||||
|
|
||||||
|
**Required skills:** `media-seo-publishing`, `nova-ai-editor-features` when AI-assisted media workflows are in scope
|
||||||
|
|
||||||
|
**Required project artifacts:**
|
||||||
|
|
||||||
|
- `api/collections/medialib.yml`
|
||||||
|
- SEO/publication fields in content or domain collections
|
||||||
|
- any image-filter configuration used by the frontend or admin
|
||||||
|
|
||||||
|
**Implementation checks:**
|
||||||
|
|
||||||
|
- [ ] Configure medialib fields, filters, alt/caption handling, and admin widgets.
|
||||||
|
- [ ] Treat the shared media widget/helper boundary as canonical for public, SSR, and admin-preview image rendering.
|
||||||
|
- [ ] Add SEO fields with sensible admin placement.
|
||||||
|
- [ ] Configure social/share image handling where needed.
|
||||||
|
- [ ] Configure the publication model explicitly.
|
||||||
|
- [ ] If publication windows exist, define representative current, future, and expired states.
|
||||||
|
- [ ] Ensure the chosen publication model matches `publishedFilter`, SSR logic, and editor workflows.
|
||||||
|
|
||||||
|
**Validation commands:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn validate
|
||||||
|
curl "$CODING_TIBISERVER_URL/api/v1/_/<namespace>/ssr?url=/de/..."
|
||||||
|
```
|
||||||
|
|
||||||
|
Check for:
|
||||||
|
|
||||||
|
- expected media URLs
|
||||||
|
- expected SEO/meta output in SSR HTML
|
||||||
|
- expected social/share metadata when used
|
||||||
|
- expected publication visibility behavior
|
||||||
|
- representative current/future/expired publication states when timing is used
|
||||||
|
|
||||||
|
**Exit criteria:**
|
||||||
|
|
||||||
|
- [ ] Media and SEO behavior is verified from schema through SSR/public output.
|
||||||
|
- [ ] Publication state is enforced consistently across schema, public reads, and SSR output.
|
||||||
|
|
||||||
|
## Phase 10 — Optional AI, search, and enterprise branches
|
||||||
|
|
||||||
|
**Required skills:** `nova-ai-editor-features`, `search-and-embeddings`, `multi-tenancy-and-orgs`
|
||||||
|
|
||||||
|
**Required project artifacts:**
|
||||||
|
|
||||||
|
- explicit architecture note even when the answer is “not used”
|
||||||
|
- AI/action config when enabled
|
||||||
|
- org/team config or rollout notes when enabled
|
||||||
|
- embedding/search config when enabled
|
||||||
|
|
||||||
|
**Implementation checks:**
|
||||||
|
|
||||||
|
- [ ] Record whether editor AI is enabled.
|
||||||
|
- [ ] Record whether embeddings/search are enabled.
|
||||||
|
- [ ] Record whether org/team support is enabled.
|
||||||
|
- [ ] Record whether the project is explicitly single-tenant or org/team-aware.
|
||||||
|
- [ ] If AI is enabled, define provider, model, budget, target fields or action contracts, and failure behavior.
|
||||||
|
- [ ] If org/team support is enabled, define org visibility, team working rights, project assignment rules, and permission ownership.
|
||||||
|
- [ ] If search/embeddings are enabled, define provider setup, search mode, index/search contracts, regeneration expectations, and operator ownership.
|
||||||
|
|
||||||
|
**Validation commands:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn validate
|
||||||
|
```
|
||||||
|
|
||||||
|
Add feature-specific checks only if the feature is enabled.
|
||||||
|
|
||||||
|
Feature-specific checks can include:
|
||||||
|
|
||||||
|
- `curl "$CODING_TIBISERVER_URL/api/v1/_/<namespace>/<collection>?q=...&qName=..."`
|
||||||
|
- representative org/team visibility or permission checks with the intended auth model
|
||||||
|
|
||||||
|
**Exit criteria:**
|
||||||
|
|
||||||
|
- [ ] The project has an explicit yes/no decision for AI, search/embeddings, and enterprise org/team support.
|
||||||
|
- [ ] The project has an explicit single-tenant vs org/team-aware decision.
|
||||||
|
- [ ] Enabled optional branches have concrete contracts and not just ideas.
|
||||||
|
|
||||||
|
## Phase 11 — Operations, deployment, and observability
|
||||||
|
|
||||||
|
**Required skills:** `deployment`, `monitoring-and-performance`, `mongodb-and-indexes`, `security-hardening-and-token-strategy`
|
||||||
|
|
||||||
|
**Required project artifacts:**
|
||||||
|
|
||||||
|
- deployment workflow files
|
||||||
|
- deploy scripts
|
||||||
|
- environment and secret configuration
|
||||||
|
- optional monitoring/operations notes
|
||||||
|
|
||||||
|
**Implementation checks:**
|
||||||
|
|
||||||
|
- [ ] Configure staging and production deployment paths and URLs.
|
||||||
|
- [ ] Configure CI or other deployment automation.
|
||||||
|
- [ ] Confirm admin reload and SSR cache clear behavior on deploy.
|
||||||
|
- [ ] If Sentry or other observability tooling is used, wire it deliberately.
|
||||||
|
- [ ] If external operators need OpenAPI or metrics, confirm the requirement and the exposure model.
|
||||||
|
- [ ] Confirm MongoDB version, replica-set, persistence, and backup assumptions for the target environment.
|
||||||
|
- [ ] Confirm backup/media persistence assumptions and collection upload paths.
|
||||||
|
|
||||||
|
**Validation commands:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn build
|
||||||
|
yarn build:server
|
||||||
|
```
|
||||||
|
|
||||||
|
Operator checks when applicable:
|
||||||
|
|
||||||
|
- staging deploy works
|
||||||
|
- production deploy flow is documented
|
||||||
|
- admin reload works
|
||||||
|
- SSR cache clear works
|
||||||
|
- health endpoints and metrics/OpenAPI exposure behave as expected
|
||||||
|
|
||||||
|
**Exit criteria:**
|
||||||
|
|
||||||
|
- [ ] The project has a concrete deploy path, not just local Docker success.
|
||||||
|
- [ ] Operational dependencies and visibility expectations are documented.
|
||||||
|
|
||||||
|
## Phase 12 — Testing and regression coverage
|
||||||
|
|
||||||
|
**Required skills:** `playwright-testing`, `tibi-ssr-caching` when SSR-specific checks are needed
|
||||||
|
|
||||||
|
**Required project artifacts:**
|
||||||
|
|
||||||
|
- seeded data helpers in `tests/api/helpers/`
|
||||||
|
- Playwright specs in the appropriate test slice
|
||||||
|
|
||||||
|
**Implementation checks:**
|
||||||
|
|
||||||
|
- [ ] Use deterministic seed data for content and any other collections needed by tests.
|
||||||
|
- [ ] Keep seed identity explicit, preferably with hidden `_testdata` markers.
|
||||||
|
- [ ] Record which seed data was reused or extended for the committed test slice.
|
||||||
|
- [ ] Add API tests for critical collection and action behavior.
|
||||||
|
- [ ] Add desktop E2E tests for core public journeys.
|
||||||
|
- [ ] Add admin smoke tests for stable admin contracts.
|
||||||
|
- [ ] For pagebuilder-driven projects, include committed admin coverage for block chooser/registry behavior and at least one real preview rendering path.
|
||||||
|
- [ ] If SSR is critical, include SSR-specific verification through targeted checks or dedicated tests.
|
||||||
|
- [ ] Record whether SSR proof comes from direct endpoint checks, committed tests, or both.
|
||||||
|
|
||||||
|
**Validation commands:**
|
||||||
|
|
||||||
- [ ] 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
|
```bash
|
||||||
npx playwright test tests/api/health.spec.ts --project=api
|
npx playwright test tests/api/health.spec.ts --project=api
|
||||||
npx playwright test tests/e2e/home.spec.ts --project=chromium
|
npx playwright test tests/e2e/home.spec.ts --project=chromium
|
||||||
```
|
```
|
||||||
|
|
||||||
## Phase 10: Video Tours (optional)
|
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:**
|
||||||
|
|
||||||
- [ ] Update/create tour files in `video-tours/tours/`:
|
|
||||||
- Key user flows for documentation/training
|
|
||||||
- Desktop and mobile variants
|
|
||||||
- [ ] Run tours and verify output:
|
|
||||||
```bash
|
```bash
|
||||||
yarn tour && yarn tour:mobile
|
yarn tour
|
||||||
|
yarn tour:mobile
|
||||||
```
|
```
|
||||||
|
|
||||||
## Phase 11: Final Verification
|
**Exit criteria:**
|
||||||
|
|
||||||
**Skills:** `tibi-project-setup` (build steps), `playwright-testing` (test suite)
|
- [ ] Tours run successfully when the project uses them.
|
||||||
|
|
||||||
- [ ] `yarn build` — success
|
## Phase 14 — Final verification and production readiness
|
||||||
- [ ] `yarn build:server` — success
|
|
||||||
- [ ] `yarn validate` — 0 errors, 0 warnings
|
**Required skills:** `tibi-project-setup`, `playwright-testing`
|
||||||
- [ ] `rg '__[A-Z0-9_]\+__' . --include='*.{yml,js,env,htaccess,json,ts,svelte}'` — no placeholder remains
|
|
||||||
- [ ] All Playwright tests pass (or known failures documented)
|
**Required project artifacts:**
|
||||||
- [ ] Public site loads at website URL
|
|
||||||
- [ ] Nova admin loads at admin URL
|
- all previously generated project artifacts
|
||||||
- [ ] Pages are creatable and editable in admin
|
- any remaining project-polish assets
|
||||||
- [ ] SSR renders real page content
|
|
||||||
- [ ] Forms/actions work (if applicable)
|
**Implementation checks:**
|
||||||
- [ ] `config.yml.env` has production-ready `ADMIN_TOKEN`
|
|
||||||
|
- [ ] 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.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: admin-ui-config
|
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
|
# 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
|
## Collection meta configuration
|
||||||
|
|
||||||
The `meta` key in a collection YAML controls how the collection appears in the admin sidebar and collection/list UI.
|
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.
|
- `preview.select` can reduce lookup work for preview table columns.
|
||||||
- `meta.subNavigation` defines filtered entry tabs in the sidebar.
|
- `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
|
## 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
|
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
|
## Layout: position, sections, sidebar
|
||||||
@@ -466,11 +544,7 @@ import BlockRenderer from "./blocks/BlockRenderer.svelte"
|
|||||||
// Creates a block definition that renders the same Svelte component
|
// Creates a block definition that renders the same Svelte component
|
||||||
// used in the public frontend. The block is mounted inside Shadow DOM
|
// used in the public frontend. The block is mounted inside Shadow DOM
|
||||||
// for style isolation.
|
// for style isolation.
|
||||||
function createContentBlockDefinition(presentation: {
|
function createContentBlockDefinition(presentation: { label: string; icon: string; color: string }) {
|
||||||
label: string
|
|
||||||
icon: string
|
|
||||||
color: string
|
|
||||||
}) {
|
|
||||||
return {
|
return {
|
||||||
css: [previewCssUrl], // CSS files to inject into Shadow DOM
|
css: [previewCssUrl], // CSS files to inject into Shadow DOM
|
||||||
label: presentation.label,
|
label: presentation.label,
|
||||||
@@ -517,6 +591,7 @@ export { blockRegistry }
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Key points:**
|
**Key points:**
|
||||||
|
|
||||||
- Each registry entry wraps the Svelte `BlockRenderer` to render the block in the admin preview.
|
- 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`).
|
- 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.
|
- Preview data may contain hydrated `_lookup.<fieldPath>` foreign key data and absolute file URLs — do not prepend `apiBase` or attempt re-fetching.
|
||||||
@@ -682,9 +757,26 @@ search:
|
|||||||
|
|
||||||
See `tibi-server/docs/04-collections.md` (sections on indexes and search config) for full reference.
|
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
|
## Common pitfalls
|
||||||
|
|
||||||
- **`meta.label` supports both strings and i18n objects** — Use i18n objects only when the collection or field label must be localized.
|
- **`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.
|
- **`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.
|
- **`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.
|
- **`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.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: content-authoring
|
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
|
# 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.
|
**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
|
## 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.
|
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`:
|
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`:
|
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
|
- `dependsOn` for block-type-specific fields
|
||||||
- collection- or field-level `meta.pagebuilder` for registry/default viewport settings
|
- 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`.
|
Add a block with your new type to `frontend/mocking/content.json`.
|
||||||
|
|
||||||
### Step 6: Verify
|
### Step 7: Verify
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
yarn validate # TypeScript check — must be warning-free
|
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
|
# 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
|
### Existing block types for reference
|
||||||
|
|
||||||
| Type | Component | Purpose |
|
| Type | Component | Purpose |
|
||||||
@@ -309,17 +356,21 @@ interface MyCollectionEntry {
|
|||||||
If you need typed helpers, extend the `EntryTypeSwitch` in `frontend/src/lib/api.ts`:
|
If you need typed helpers, extend the `EntryTypeSwitch` in `frontend/src/lib/api.ts`:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
type CollectionNameT = "medialib" | "content" | "mycollection" | string
|
type CollectionNameT = "medialib" | "content" | "navigation" | "mycollection" | string
|
||||||
|
|
||||||
type EntryTypeSwitch<T extends string> = T extends "medialib"
|
type EntryTypeSwitch<T extends string> = T extends "medialib"
|
||||||
? MedialibEntry
|
? MedialibEntry
|
||||||
: T extends "content"
|
: T extends "content"
|
||||||
? ContentEntry
|
? ContentEntry
|
||||||
|
: T extends "navigation"
|
||||||
|
? NavigationEntry
|
||||||
: T extends "mycollection"
|
: T extends "mycollection"
|
||||||
? MyCollectionEntry
|
? MyCollectionEntry
|
||||||
: Record<string, unknown>
|
: 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)
|
### Step 5: Add hooks (optional)
|
||||||
|
|
||||||
Common hook patterns:
|
Common hook patterns:
|
||||||
@@ -374,8 +425,28 @@ For collections intended for rich editorial usage, also verify in Nova:
|
|||||||
- foreign-key displays use meaningful previews
|
- foreign-key displays use meaningful previews
|
||||||
- pagebuilder fields render previews and screenshots correctly
|
- 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
|
## Common pitfalls
|
||||||
|
|
||||||
- **Path format**: Content paths do NOT include the language prefix. The path `/ueber-uns` becomes `/{lang}/ueber-uns` via the i18n layer.
|
- **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`.
|
- **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 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.
|
- **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.
|
||||||
|
|||||||
@@ -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 }
|
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
|
## 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).
|
- **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.
|
- **`$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.
|
- **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.
|
||||||
|
|||||||
@@ -15,6 +15,239 @@ Use this skill when:
|
|||||||
- Defining publication windows and how they interact with runtime and SSR
|
- Defining publication windows and how they interact with runtime and SSR
|
||||||
- Building authoring workflows around images, metadata, and release control
|
- 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
|
## Goal
|
||||||
|
|
||||||
The goal is to make media, SEO, and publishing part of the actual solution design instead of leaving them as late add-ons.
|
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:
|
Use these sources when implementing or reviewing these areas:
|
||||||
|
|
||||||
- `tibi-server/docs/08-file-upload-images.md`
|
- `tibi-server/docs/08-file-upload-images.md`
|
||||||
- `api/collections/medialib.yml`
|
- the project's medialib collection config
|
||||||
- `api/collections/content.yml`
|
- the relevant content or domain collection configs
|
||||||
|
- `tibi-admin-nova/types/admin.d.ts`
|
||||||
- `tibi-admin-nova/docs/collection-config.md`
|
- `tibi-admin-nova/docs/collection-config.md`
|
||||||
- `api/hooks/config.js`
|
- the project's SSR/runtime config hooks
|
||||||
- `api/hooks/lib/ssr-server.js`
|
- the project's frontend media widget/helper implementation
|
||||||
|
|
||||||
## Media modeling
|
## Media modeling
|
||||||
|
|
||||||
@@ -175,19 +409,37 @@ Recommended shape:
|
|||||||
- No explicit alt field
|
- No explicit alt field
|
||||||
- Mixing captions and alt text into one field
|
- Mixing captions and alt text into one field
|
||||||
- Hardcoding image sizes only in frontend CSS/components
|
- 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
|
- Treating publication as frontend-only logic
|
||||||
- Forgetting that publish windows can invalidate SSR HTML
|
- 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
|
## Verification checklist
|
||||||
|
|
||||||
After changing media/SEO/publishing behavior, verify all of these:
|
After changing media/SEO/publishing behavior, verify all of these:
|
||||||
|
|
||||||
1. Upload validation matches the intended asset type.
|
1. Upload validation matches the intended asset type.
|
||||||
2. Image filters are named and used consistently.
|
2. Image filters are named and used consistently.
|
||||||
3. Alt/caption/SEO fields are explicit and editor-friendly.
|
3. Shared image widgets receive resolved entries instead of rebuilding URLs ad hoc.
|
||||||
4. Publication state affects public output correctly.
|
4. Alt/caption/SEO fields are explicit and editor-friendly.
|
||||||
5. SSR HTML still reflects the intended published state.
|
5. Publication state affects public output correctly.
|
||||||
6. `yarn validate` stays clean.
|
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
|
## 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`
|
1. `tibi-server/docs/08-file-upload-images.md`
|
||||||
2. the relevant collection YAML
|
2. the relevant collection YAML
|
||||||
3. admin layout and previews for those fields
|
3. the shared media widget/helper layer used by the frontend
|
||||||
4. frontend components consuming the media/SEO data
|
4. admin layout and previews for those fields
|
||||||
5. SSR publish-check and invalidation logic if timing matters
|
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.
|
This prevents “just add an image field” changes that break runtime, editorial UX, or caching.
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
---
|
||||||
|
name: mongodb-and-indexes
|
||||||
|
description: Design MongoDB prerequisites and index strategy for tibi website projects. Covers replica-set requirements, collection index modeling, text/search index implications, index auto-management on reload, and operator checks for persistence, backups, and regeneration-sensitive features.
|
||||||
|
---
|
||||||
|
|
||||||
|
# mongodb-and-indexes
|
||||||
|
|
||||||
|
## When to use this skill
|
||||||
|
|
||||||
|
Use this skill when:
|
||||||
|
|
||||||
|
- a project moves beyond simple demo data and needs deliberate MongoDB design
|
||||||
|
- collections need unique, text, sparse, or compound indexes
|
||||||
|
- search, audit, or larger datasets introduce operational database requirements
|
||||||
|
- operators need clarity about replica-set, persistence, or backup assumptions
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Give later agents one place for MongoDB prerequisites and index strategy.
|
||||||
|
|
||||||
|
This skill exists because tibi projects do not just “use MongoDB”. They rely on config-driven indexes, reload-time index reconciliation, and environment choices that can help or break production behavior.
|
||||||
|
|
||||||
|
## Source of truth
|
||||||
|
|
||||||
|
Use these sources when implementing or reviewing MongoDB/index decisions:
|
||||||
|
|
||||||
|
- `tibi-server/docs/02-configuration.md`
|
||||||
|
- `tibi-server/docs/04-collections.md`
|
||||||
|
- `tibi-server/docs/12-deployment.md`
|
||||||
|
- active project/server config
|
||||||
|
|
||||||
|
## MongoDB prerequisites
|
||||||
|
|
||||||
|
Current upstream deployment requirements include:
|
||||||
|
|
||||||
|
- MongoDB 4.4+
|
||||||
|
- replica set for transactions
|
||||||
|
|
||||||
|
That means later agents should not promise production-like behavior while ignoring the actual database topology.
|
||||||
|
|
||||||
|
For this starter family, check explicitly:
|
||||||
|
|
||||||
|
- which MongoDB version is targeted
|
||||||
|
- whether the environment runs as a replica set
|
||||||
|
- where persistent data lives
|
||||||
|
- how backup and restore are expected to work
|
||||||
|
|
||||||
|
## Index ownership model
|
||||||
|
|
||||||
|
Indexes can be defined in two places.
|
||||||
|
|
||||||
|
### Field-level indexes
|
||||||
|
|
||||||
|
Use for simple per-field indexes:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
fields:
|
||||||
|
- name: email
|
||||||
|
type: string
|
||||||
|
index:
|
||||||
|
- single
|
||||||
|
- unique
|
||||||
|
```
|
||||||
|
|
||||||
|
Available field-level flags include:
|
||||||
|
|
||||||
|
- `single`
|
||||||
|
- `unique`
|
||||||
|
- `text`
|
||||||
|
- `sparse`
|
||||||
|
|
||||||
|
### Collection-level indexes
|
||||||
|
|
||||||
|
Use for compound or more explicit index definitions:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
indexes:
|
||||||
|
- name: category_publish_date
|
||||||
|
key:
|
||||||
|
- category
|
||||||
|
- -publishDate
|
||||||
|
background: true
|
||||||
|
- name: title_text
|
||||||
|
key:
|
||||||
|
- $text:title
|
||||||
|
defaultLanguage: german
|
||||||
|
```
|
||||||
|
|
||||||
|
Use collection-level indexes when:
|
||||||
|
|
||||||
|
- sort patterns span multiple fields
|
||||||
|
- uniqueness depends on more than one field
|
||||||
|
- you need text-index language control
|
||||||
|
- the index needs a stable explicit name
|
||||||
|
|
||||||
|
## Important auto-management behavior
|
||||||
|
|
||||||
|
On project setup or reload:
|
||||||
|
|
||||||
|
1. configured indexes are created
|
||||||
|
2. indexes present in MongoDB but no longer in config are dropped
|
||||||
|
3. `_id_` is never dropped
|
||||||
|
|
||||||
|
This is a critical workflow fact.
|
||||||
|
|
||||||
|
Do not treat indexes as one-off manual DBA work while also expecting config reload to stay authoritative. In this stack, the YAML config owns the intended index state.
|
||||||
|
|
||||||
|
## Search and index interplay
|
||||||
|
|
||||||
|
Some search modes depend directly on index strategy:
|
||||||
|
|
||||||
|
- `mode: text` requires a text index
|
||||||
|
- explicit text search can use field-level `index: [text]` or collection-level `$text:` indexes
|
||||||
|
- regex fallback can become expensive without deliberate field/index choices
|
||||||
|
- enrichment-based modes such as `ngram` and `vector` add `_search` data and may require regeneration workflows rather than classic MongoDB indexes alone
|
||||||
|
|
||||||
|
Keep classic indexes and search config aligned. Search should not be modeled in isolation from database cost and reload behavior.
|
||||||
|
|
||||||
|
## Query shape matters
|
||||||
|
|
||||||
|
Before adding indexes, inspect the real access patterns:
|
||||||
|
|
||||||
|
- which fields appear in permission filters
|
||||||
|
- which fields appear in list sorting
|
||||||
|
- which fields appear in frequent admin or public lookups
|
||||||
|
- whether uniqueness is a real business rule or only a nice-to-have
|
||||||
|
|
||||||
|
Index design should follow concrete read and write patterns, not schema aesthetics.
|
||||||
|
|
||||||
|
## Operational decisions
|
||||||
|
|
||||||
|
At project-delivery level, document these choices explicitly:
|
||||||
|
|
||||||
|
- MongoDB version and topology
|
||||||
|
- replica-set availability
|
||||||
|
- persistence location for database and uploads
|
||||||
|
- backup/restore responsibility
|
||||||
|
- whether audit/search features introduce extra operational expectations
|
||||||
|
|
||||||
|
If these decisions are absent, later agents tend to over-focus on schema YAML while missing the operator-critical data layer.
|
||||||
|
|
||||||
|
## Anti-patterns
|
||||||
|
|
||||||
|
- adding indexes without checking actual query or sort behavior
|
||||||
|
- relying on manual indexes that the config will later drop
|
||||||
|
- enabling text search without provisioning the needed text index
|
||||||
|
- treating replica-set requirements as optional trivia
|
||||||
|
- shipping production projects without a backup and restore story
|
||||||
|
|
||||||
|
## Verification checklist
|
||||||
|
|
||||||
|
After MongoDB/index-related changes, verify all of these:
|
||||||
|
|
||||||
|
1. the target environment satisfies MongoDB version and replica-set needs
|
||||||
|
2. configured indexes match real query, sort, and uniqueness requirements
|
||||||
|
3. project reload succeeds after index changes
|
||||||
|
4. text/search features have the required index support
|
||||||
|
5. persistence and backup assumptions are documented
|
||||||
|
|
||||||
|
## What an LLM should inspect first
|
||||||
|
|
||||||
|
When asked to design or review the data layer on this starter, inspect in this order:
|
||||||
|
|
||||||
|
1. `tibi-server/docs/12-deployment.md`
|
||||||
|
2. `tibi-server/docs/02-configuration.md`
|
||||||
|
3. `tibi-server/docs/04-collections.md`
|
||||||
|
4. current collection YAML for indexes/search
|
||||||
|
5. actual query, sort, audit, and search requirements
|
||||||
|
|
||||||
|
This keeps index design tied to real runtime behavior and not just to field definitions.
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
---
|
||||||
|
name: monitoring-and-performance
|
||||||
|
description: Configure and verify observability for tibi website projects. Covers OpenAPI exposure, Prometheus metrics, Sentry wiring, health/reachability checks, and the operator-facing validation that should exist before a project is considered production-ready.
|
||||||
|
---
|
||||||
|
|
||||||
|
# monitoring-and-performance
|
||||||
|
|
||||||
|
## When to use this skill
|
||||||
|
|
||||||
|
Use this skill when:
|
||||||
|
|
||||||
|
- a project needs operator-facing visibility beyond “the page loads”
|
||||||
|
- you need OpenAPI output for integrations or documentation
|
||||||
|
- you need Prometheus/Grafana visibility
|
||||||
|
- you need Sentry or similar error visibility in frontend or server flows
|
||||||
|
- you want to define the minimum health and observability checks for deploys
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Give later agents a concrete workflow for deciding what should be observable, how it is exposed, and how to verify that exposure.
|
||||||
|
|
||||||
|
This skill is not about arbitrary performance tuning. It is about making the running system inspectable enough that operators and developers can see whether it is healthy.
|
||||||
|
|
||||||
|
## Source of truth
|
||||||
|
|
||||||
|
Use these sources when implementing or reviewing observability:
|
||||||
|
|
||||||
|
- `tibi-server/docs/13-openapi-metrics.md`
|
||||||
|
- `tibi-server/docs/02-configuration.md`
|
||||||
|
- `.agents/skills/deployment/SKILL.md`
|
||||||
|
- `frontend/src/config.ts`
|
||||||
|
- relevant deploy scripts and env/config files
|
||||||
|
|
||||||
|
## OpenAPI exposure
|
||||||
|
|
||||||
|
Tibi-server generates an OpenAPI spec per project:
|
||||||
|
|
||||||
|
```text
|
||||||
|
GET /api/v1/{namespace}/openapi
|
||||||
|
```
|
||||||
|
|
||||||
|
Use this when:
|
||||||
|
|
||||||
|
- a project exposes public API surfaces that need documentation
|
||||||
|
- integrations or client generators benefit from a machine-readable contract
|
||||||
|
|
||||||
|
Collection-level OpenAPI customization lives in collection metadata via `meta.openapi`.
|
||||||
|
|
||||||
|
Use that metadata deliberately to:
|
||||||
|
|
||||||
|
- hide endpoints that should not appear in the spec
|
||||||
|
- add summaries and descriptions
|
||||||
|
- keep the public API contract readable
|
||||||
|
|
||||||
|
## Metrics exposure
|
||||||
|
|
||||||
|
Prometheus metrics are exposed at:
|
||||||
|
|
||||||
|
```text
|
||||||
|
GET /metrics
|
||||||
|
```
|
||||||
|
|
||||||
|
Key upstream metric documented today:
|
||||||
|
|
||||||
|
- `tibi_request_duration_seconds`
|
||||||
|
|
||||||
|
This is useful for:
|
||||||
|
|
||||||
|
- request latency visibility
|
||||||
|
- collection-level timing comparisons
|
||||||
|
- basic traffic and error observation in Grafana/Prometheus
|
||||||
|
|
||||||
|
Do not enable metrics-like operator expectations in a project and then forget to verify the endpoint actually works in the target environment.
|
||||||
|
|
||||||
|
## Sentry and error visibility
|
||||||
|
|
||||||
|
This stack can surface errors through Sentry-related configuration.
|
||||||
|
|
||||||
|
Relevant surfaces include:
|
||||||
|
|
||||||
|
- server-level `sentry` config in tibi-server
|
||||||
|
- frontend runtime wiring in `frontend/src/config.ts`
|
||||||
|
- deploy-time release/build metadata injection where the project uses it
|
||||||
|
|
||||||
|
Use Sentry deliberately:
|
||||||
|
|
||||||
|
- define DSN, environment, and release expectations
|
||||||
|
- know whether tracing is wanted or only error capture
|
||||||
|
- make sure deploy scripts and build metadata agree with the runtime setup
|
||||||
|
|
||||||
|
Do not leave a half-configured Sentry setup that looks present but produces unusable traces.
|
||||||
|
|
||||||
|
## Health and reachability checks
|
||||||
|
|
||||||
|
At minimum, operators should be able to verify:
|
||||||
|
|
||||||
|
- website URL responds
|
||||||
|
- admin URL responds
|
||||||
|
- API responds
|
||||||
|
- OpenAPI and metrics endpoints respond when they are intended to be used
|
||||||
|
|
||||||
|
In this repo family, simple reachability probes are often the first useful health signal. For project delivery, these checks belong next to deploy and sign-off work, not only in ad-hoc troubleshooting.
|
||||||
|
|
||||||
|
## Recommended patterns
|
||||||
|
|
||||||
|
### Public API projects
|
||||||
|
|
||||||
|
Recommended shape:
|
||||||
|
|
||||||
|
- expose OpenAPI intentionally
|
||||||
|
- add `meta.openapi` summaries for meaningful endpoints
|
||||||
|
- verify the spec against the current collection model
|
||||||
|
|
||||||
|
### Operated production projects
|
||||||
|
|
||||||
|
Recommended shape:
|
||||||
|
|
||||||
|
- metrics endpoint reachable in the target environment
|
||||||
|
- at least one documented Grafana/Prometheus use case for request timing
|
||||||
|
- explicit decision whether Sentry is used or intentionally not used
|
||||||
|
|
||||||
|
### Basic website deployments
|
||||||
|
|
||||||
|
Recommended shape:
|
||||||
|
|
||||||
|
- website/admin/API reachability checks are part of deploy verification
|
||||||
|
- observability is documented enough that later operators know what exists
|
||||||
|
|
||||||
|
## Anti-patterns
|
||||||
|
|
||||||
|
- treating observability as optional once the build passes
|
||||||
|
- exposing OpenAPI or metrics accidentally without deciding who uses them
|
||||||
|
- half-configured Sentry with no useful environment or release handling
|
||||||
|
- relying on manual browser clicks as the only production health check
|
||||||
|
|
||||||
|
## Verification checklist
|
||||||
|
|
||||||
|
After observability-related work, verify all of these:
|
||||||
|
|
||||||
|
1. intended OpenAPI exposure works and reflects the current collection config
|
||||||
|
2. intended metrics exposure works in the target environment
|
||||||
|
3. Sentry/error visibility is either intentionally configured or intentionally absent
|
||||||
|
4. deploy-time reachability checks cover website, admin, and API
|
||||||
|
5. `yarn build`, `yarn build:server`, and `yarn validate` still pass when observability wiring touched frontend/server config
|
||||||
|
|
||||||
|
## What an LLM should inspect first
|
||||||
|
|
||||||
|
When asked to set up monitoring or observability on this starter, inspect in this order:
|
||||||
|
|
||||||
|
1. `tibi-server/docs/13-openapi-metrics.md`
|
||||||
|
2. `tibi-server/docs/02-configuration.md`
|
||||||
|
3. deploy scripts and env/config files
|
||||||
|
4. `frontend/src/config.ts`
|
||||||
|
5. whether the project truly needs OpenAPI, metrics, Sentry, or only reachability checks
|
||||||
|
|
||||||
|
This prevents over-documenting features that are not actually wired and under-documenting the ones that matter operationally.
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
---
|
||||||
|
name: multi-tenancy-and-orgs
|
||||||
|
description: Model org/team-aware tibi projects. Covers the org-team-user hierarchy, visibility vs working rights, permission resolution, project assignment, audit visibility, and how later agents should make an explicit single-tenant vs org/team decision.
|
||||||
|
---
|
||||||
|
|
||||||
|
# multi-tenancy-and-orgs
|
||||||
|
|
||||||
|
## When to use this skill
|
||||||
|
|
||||||
|
Use this skill when:
|
||||||
|
|
||||||
|
- a project might need org/team-aware isolation or working rights
|
||||||
|
- multiple organizations or departments share one tibi installation
|
||||||
|
- project visibility and editing rights must be separated cleanly
|
||||||
|
- LLM budget ownership or audit visibility must follow org boundaries
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Give later agents a concrete implementation workflow for enterprise-aware projects and a clear off-ramp for single-tenant projects.
|
||||||
|
|
||||||
|
The most important decision is not how to model orgs. It is whether the project should use org/team features at all.
|
||||||
|
|
||||||
|
## Source of truth
|
||||||
|
|
||||||
|
Use these sources when implementing or reviewing org/team-aware projects:
|
||||||
|
|
||||||
|
- `tibi-server/docs/18-orgs-teams.md`
|
||||||
|
- `tibi-server/docs/05-authentication.md`
|
||||||
|
- `tibi-server/docs/17-field-level-permissions.md`
|
||||||
|
- `.agents/skills/permissions-and-editor-workflows/SKILL.md`
|
||||||
|
- `.agents/skills/search-and-embeddings/SKILL.md` when LLM budget or shared AI/search budgets matter
|
||||||
|
|
||||||
|
## First decision: single-tenant or org/team-aware
|
||||||
|
|
||||||
|
Make this decision explicitly.
|
||||||
|
|
||||||
|
### Single-tenant projects
|
||||||
|
|
||||||
|
Recommended shape:
|
||||||
|
|
||||||
|
- do not model org/team support by default
|
||||||
|
- document that visibility and permissions are handled without enterprise isolation
|
||||||
|
- keep the project simpler unless there is a real multi-tenant requirement
|
||||||
|
|
||||||
|
### Org/team-aware projects
|
||||||
|
|
||||||
|
Use this branch only when you actually need:
|
||||||
|
|
||||||
|
- org-scoped project visibility
|
||||||
|
- team-based working rights
|
||||||
|
- cross-user governance inside shared organizations
|
||||||
|
- org-level budget ownership or audit visibility constraints
|
||||||
|
|
||||||
|
## Core model
|
||||||
|
|
||||||
|
The upstream hierarchy is:
|
||||||
|
|
||||||
|
- org = visibility boundary
|
||||||
|
- team = working-rights unit
|
||||||
|
- user = member of orgs and teams
|
||||||
|
|
||||||
|
Key fields include:
|
||||||
|
|
||||||
|
- `project.orgId`
|
||||||
|
- `project.teams[]`
|
||||||
|
- `user.orgs[]`
|
||||||
|
- `user.teams[]`
|
||||||
|
- `user.primaryOrgId`
|
||||||
|
|
||||||
|
Do not flatten these concepts into a generic “role” discussion. Org visibility and team working rights are different concerns.
|
||||||
|
|
||||||
|
## Visibility vs working rights
|
||||||
|
|
||||||
|
Important rule:
|
||||||
|
|
||||||
|
- org membership controls which projects a user can see
|
||||||
|
- team assignment controls which permission sets a user gets inside those visible projects
|
||||||
|
|
||||||
|
This is the main conceptual boundary later agents must keep intact.
|
||||||
|
|
||||||
|
If a user can see a project but cannot edit it, that can be correct. Visibility is not the same as edit access.
|
||||||
|
|
||||||
|
## Permission resolution
|
||||||
|
|
||||||
|
Team permissions map onto collection permission keys.
|
||||||
|
|
||||||
|
Example idea:
|
||||||
|
|
||||||
|
- team carries `permissions: ["editor"]`
|
||||||
|
- collection defines an `editor` permission set
|
||||||
|
- assigned team members inherit that working-rights set on the project
|
||||||
|
|
||||||
|
Also important:
|
||||||
|
|
||||||
|
- multiple team permissions merge as a union
|
||||||
|
- admin tokens and system admins still sit above team-based resolution
|
||||||
|
- token/public/user/team/custom permission order matters
|
||||||
|
|
||||||
|
## Project assignment rules
|
||||||
|
|
||||||
|
For org/team-aware projects, later agents must design all of these deliberately:
|
||||||
|
|
||||||
|
- which org owns the project
|
||||||
|
- which teams are assigned to the project
|
||||||
|
- who is allowed to manage those assignments
|
||||||
|
- how custom collection permission keys map to teams
|
||||||
|
|
||||||
|
Half-modeled org/team setups create the worst of both worlds: extra complexity without trustworthy isolation.
|
||||||
|
|
||||||
|
## Audit and LLM implications
|
||||||
|
|
||||||
|
Enterprise modeling affects more than CRUD visibility.
|
||||||
|
|
||||||
|
Relevant side effects:
|
||||||
|
|
||||||
|
- audit visibility follows the team → project → collection access chain
|
||||||
|
- non-admin users do not see system audit entries
|
||||||
|
- LLM/org budget logic uses `user.primaryOrgId`
|
||||||
|
|
||||||
|
That means enterprise design can affect audit reviews and AI cost ownership even if the public website itself looks simple.
|
||||||
|
|
||||||
|
## Anti-patterns
|
||||||
|
|
||||||
|
- enabling org/team concepts “just in case”
|
||||||
|
- using teams as visibility boundaries instead of orgs
|
||||||
|
- mixing custom collection permissions and team permissions without naming discipline
|
||||||
|
- forgetting that users can belong to multiple teams
|
||||||
|
- introducing org-aware billing or audit expectations without modeling `primaryOrgId`
|
||||||
|
|
||||||
|
## Verification checklist
|
||||||
|
|
||||||
|
After org/team-related changes, verify all of these:
|
||||||
|
|
||||||
|
1. the project explicitly states single-tenant or org/team-aware
|
||||||
|
2. org ownership and team assignment are defined for enterprise projects
|
||||||
|
3. collection permission keys map cleanly to team permissions
|
||||||
|
4. representative visibility and edit-rights scenarios behave as designed
|
||||||
|
5. audit and LLM budget implications are understood when those features are in scope
|
||||||
|
|
||||||
|
## What an LLM should inspect first
|
||||||
|
|
||||||
|
When asked whether a project needs enterprise features, inspect in this order:
|
||||||
|
|
||||||
|
1. the project's actual tenancy requirement
|
||||||
|
2. `tibi-server/docs/18-orgs-teams.md`
|
||||||
|
3. current permission model in collection configs
|
||||||
|
4. whether audit visibility or LLM budgets depend on org ownership
|
||||||
|
5. whether the simpler single-tenant branch is the right answer
|
||||||
|
|
||||||
|
This prevents unnecessary enterprise complexity in projects that only need normal editorial permissions.
|
||||||
@@ -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:
|
Use these sources when implementing or reviewing AI-backed website features:
|
||||||
|
|
||||||
- `tibi-server/docs/09-llm-integration.md`
|
- `tibi-server/docs/09-llm-integration.md`
|
||||||
- `tibi-admin-nova/docs/collection-config.md`
|
|
||||||
- `tibi-admin-nova/types/admin.d.ts`
|
- `tibi-admin-nova/types/admin.d.ts`
|
||||||
|
- `tibi-admin-nova/docs/collection-config.md`
|
||||||
- `api/config.yml`
|
- `api/config.yml`
|
||||||
- the project's actual Nova runtime config when such a file exists
|
- 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:
|
Use these sources when implementing or reviewing navigation modeling:
|
||||||
|
|
||||||
|
- `tibi-admin-nova/types/admin.d.ts`
|
||||||
- `tibi-admin-nova/docs/collection-config.md`
|
- `tibi-admin-nova/docs/collection-config.md`
|
||||||
- `api/collections/navigation.yml`
|
- `api/collections/navigation.yml`
|
||||||
- `frontend/src/App.svelte`
|
- `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:
|
When asked to work on navigation in this starter, inspect in this order:
|
||||||
|
|
||||||
1. `api/collections/navigation.yml`
|
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
|
3. the frontend navigation loading/rendering path
|
||||||
4. SSR assumptions around header/footer shell data
|
4. SSR assumptions around header/footer shell data
|
||||||
5. the website's language and information-architecture requirements
|
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
|
### 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:
|
The concrete chain is:
|
||||||
|
|
||||||
@@ -207,7 +207,7 @@ The concrete chain is:
|
|||||||
3. build the admin bundle with `yarn build`
|
3. build the admin bundle with `yarn build`
|
||||||
4. point the collection field or collection meta to the built module
|
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:
|
Typical starter pattern:
|
||||||
|
|
||||||
@@ -238,7 +238,7 @@ meta:
|
|||||||
file: /_/assets/dist/admin.mjs?v=${ADMIN_ASSET_VERSION}
|
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 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
|
- 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
|
- 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
|
- 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
|
- 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.
|
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
|
- unrelated API fetching
|
||||||
- page-level navigation concerns
|
- 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
|
### 8. Prepare for styling consistency across blocks
|
||||||
|
|
||||||
A block system works better when blocks share a few stable layout conventions.
|
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.
|
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
|
## Dynamic field rules
|
||||||
|
|
||||||
Use eval-based field rules when permissions depend on document state.
|
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.
|
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
|
## 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.
|
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.
|
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
|
## Tokens and integrations
|
||||||
|
|
||||||
Remember that token-based integrations can have their own permission sets.
|
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.
|
1. Collection methods match the intended role model.
|
||||||
2. Hidden and readonly field behavior is correct on API reads/writes.
|
2. Hidden and readonly field behavior is correct on API reads/writes.
|
||||||
3. Dynamic eval rules behave correctly for the intended document states.
|
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.
|
4. At least one representative allowed write and one denied write were checked for each important workflow state.
|
||||||
5. Token/integration permissions are narrower than admin access when possible.
|
5. Non-admin config delivery still makes sense for the admin UI and field layout.
|
||||||
6. `yarn validate` stays clean.
|
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
|
## What an LLM should inspect first
|
||||||
|
|
||||||
|
|||||||
@@ -202,6 +202,19 @@ When adding new deterministic coverage, extend the seed data instead of assertin
|
|||||||
|
|
||||||
## Which test type to use
|
## 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
|
### API tests
|
||||||
|
|
||||||
Use `tests/api/` when validating:
|
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.
|
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
|
## Admin config coverage strategy
|
||||||
|
|
||||||
Use a hybrid approach:
|
Use a hybrid approach:
|
||||||
@@ -429,3 +464,83 @@ When extending or fixing tests:
|
|||||||
5. Fix selectors or seed shape before widening scope.
|
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.
|
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
|
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
|
# security-hardening-and-token-strategy
|
||||||
@@ -9,150 +9,199 @@ description: Apply current tibi-server security practices to website projects. C
|
|||||||
|
|
||||||
Use this skill when:
|
Use this skill when:
|
||||||
|
|
||||||
- Setting up or reviewing authentication and token use on this stack
|
- setting up or reviewing authentication and token usage on this stack
|
||||||
- Deciding how admin tokens, JWT auth, and token permissions should be used
|
- deciding how admin tokens, JWT auth, and token permissions should be used
|
||||||
- Hardening hooks, actions, and project config against obvious security mistakes
|
- hardening hooks, actions, and project config against current upstream security risks
|
||||||
- Reviewing bulk permissions, secrets, cookie settings, or risky server-side capabilities
|
- reviewing bulk permissions, rate limiting, cookies, secrets, or risky server-side capabilities
|
||||||
|
|
||||||
## Goal
|
## 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
|
## 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/05-authentication.md`
|
||||||
- `tibi-server/docs/14-security.md`
|
- `tibi-server/docs/14-security.md`
|
||||||
- relevant collection/action permissions in `api/`
|
- `tibi-server/docs/17-field-level-permissions.md`
|
||||||
- project environment/config files
|
- `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
|
## Authentication surfaces
|
||||||
|
|
||||||
This stack exposes multiple auth mechanisms:
|
This stack exposes multiple auth mechanisms. Do not mix them casually.
|
||||||
|
|
||||||
- JWT user auth
|
- JWT user auth
|
||||||
- refresh token cookie flow
|
- refresh-token cookie flow
|
||||||
- admin tokens
|
- 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
|
Use the right header for the right surface:
|
||||||
- **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
|
|
||||||
|
|
||||||
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
|
- JWT secrets
|
||||||
- SMTP credentials
|
- SMTP credentials
|
||||||
- LLM API keys
|
- admin tokens
|
||||||
- external API tokens
|
- external API keys
|
||||||
- admin token values
|
- 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 permission safety
|
||||||
|
|
||||||
Bulk mutations are explicitly more dangerous than single-document mutations.
|
Bulk operations are more dangerous than single-document mutations.
|
||||||
|
|
||||||
Important rule:
|
Important rule:
|
||||||
|
|
||||||
- boolean `post: true` / `put: true` / `delete: true` does not imply bulk access
|
- boolean `post: true` / `put: true` / `delete: true` does not imply bulk access
|
||||||
- bulk requires object-form permissions with `bulk: true`
|
- 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
|
## 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
|
- `api.secureCookies`
|
||||||
- local non-HTTPS development may need explicit relaxation
|
- HTTPS vs local HTTP expectations
|
||||||
- do not debug production cookie issues by weakening production defaults globally
|
- 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:
|
Particularly important:
|
||||||
|
|
||||||
- `http.fetch` / `http.fetchStream` can create SSRF risk
|
- `context.http.fetch()` / `context.http.fetchStream()` can create SSRF risk
|
||||||
- `exec.command` can create command-execution risk
|
- `context.exec.command()` can create command-execution risk
|
||||||
- broad filesystem/network access in hooks should not be treated as harmless
|
- 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.
|
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.
|
- why they are necessary
|
||||||
|
- what the allowed target surface is
|
||||||
Prefer header-based token transport when possible.
|
- what the safer rejected alternatives were
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
## CORS configuration
|
## 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 |
|
Levels:
|
||||||
|-------|----------------------|-------|
|
|
||||||
| Server | tibi-server `config.yml` | Global default |
|
|
||||||
| Project | `api/config.yml` → `cors:` | Per project |
|
|
||||||
| Collection/Action | Collection or action YAML → `cors:` | Per endpoint |
|
|
||||||
|
|
||||||
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:
|
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.
|
||||||
- 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
|
|
||||||
|
|
||||||
See `tibi-server/docs/02-configuration.md` (section "CORS Configuration Hierarchy") for details.
|
## Recommended implementation patterns
|
||||||
|
|
||||||
## Secure implementation patterns
|
### Public form workflow
|
||||||
|
|
||||||
### Public form endpoint
|
|
||||||
|
|
||||||
Recommended shape:
|
Recommended shape:
|
||||||
|
|
||||||
- public action with narrow allowed methods
|
- public action with narrow methods
|
||||||
- server-side validation
|
- server-side validation
|
||||||
- no broad admin tokens in the browser
|
- no admin token in the browser
|
||||||
- no unnecessary collection write permissions exposed publicly
|
- separate internal persistence only when truly required
|
||||||
|
|
||||||
### Integration token
|
### Integration token
|
||||||
|
|
||||||
@@ -162,32 +211,42 @@ Recommended shape:
|
|||||||
- minimal collection/action scope
|
- minimal collection/action scope
|
||||||
- header-based transport preferred
|
- header-based transport preferred
|
||||||
|
|
||||||
### Hook that calls external services
|
### Sensitive internal fields
|
||||||
|
|
||||||
Recommended shape:
|
Recommended shape:
|
||||||
|
|
||||||
- fixed or validated destination URLs
|
- use hidden/readonly restrictions explicitly
|
||||||
- no arbitrary user-controlled internal target fetching
|
- keep admin UI aligned with those restrictions
|
||||||
- minimal capability needed for the feature
|
- 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
|
## Anti-patterns
|
||||||
|
|
||||||
- Hardcoded production secrets in committed config
|
- hardcoded production secrets in committed config
|
||||||
- Using admin tokens for routine frontend or integration traffic
|
- broad admin tokens used for normal frontend or integration traffic
|
||||||
- Enabling bulk write permissions without a strong operational reason
|
- bulk permissions enabled without a concrete operator need
|
||||||
- Treating hook `http.fetch` and `exec.command` as risk-free utilities
|
- risky hook capabilities treated as harmless helpers
|
||||||
- Solving access control in the UI instead of on the server
|
- collection security solved in the UI instead of the server
|
||||||
|
- production cookie or rate-limit settings weakened for convenience
|
||||||
|
|
||||||
## Verification checklist
|
## Verification checklist
|
||||||
|
|
||||||
After security-relevant changes, verify all of these:
|
After security-relevant changes, verify all of these:
|
||||||
|
|
||||||
1. Secrets are sourced appropriately.
|
1. secrets are sourced appropriately
|
||||||
2. Token type matches the intended actor and scope.
|
2. token type matches the intended actor and scope
|
||||||
3. Bulk permissions are not broader than necessary.
|
3. bulk permissions are not broader than necessary
|
||||||
4. Public endpoints expose only the required methods.
|
4. readonly/hidden behavior is correct on the API
|
||||||
5. Risky hook capabilities are constrained by design.
|
5. rate limiting and cookie settings match the environment
|
||||||
6. `yarn validate` stays clean.
|
6. risky hook capabilities are constrained by design
|
||||||
|
7. `yarn validate` stays clean
|
||||||
|
|
||||||
## What an LLM should inspect first
|
## 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`
|
1. `tibi-server/docs/05-authentication.md`
|
||||||
2. `tibi-server/docs/14-security.md`
|
2. `tibi-server/docs/14-security.md`
|
||||||
3. the relevant collection/action permission sets
|
3. `tibi-server/docs/17-field-level-permissions.md`
|
||||||
4. secret sourcing in config/env
|
4. the relevant collection/action permission sets
|
||||||
5. whether hooks use risky capabilities like outbound fetch or exec
|
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
|
- persistence
|
||||||
- normalization of response shape
|
- 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
|
## Response design
|
||||||
|
|
||||||
Return a small stable payload that the frontend can rely on.
|
Return a small stable payload that the frontend can rely on.
|
||||||
|
|||||||
@@ -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.
|
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
|
## Hook file structure
|
||||||
|
|
||||||
Wrap every hook in an IIFE:
|
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.
|
- Avoid `@ts-ignore`; use proper casting instead.
|
||||||
- Use `const` and `let` instead of `var` — the goja runtime supports them.
|
- 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 — Go object quirk
|
||||||
|
|
||||||
`context.filter` is a Go object, not a regular JS object. Even when empty, it is **truthy**.
|
`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
|
- custom form/action validation
|
||||||
- audit-output sanitizing for sensitive fields
|
- 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
|
## Common pitfalls
|
||||||
|
|
||||||
- Do not assume browser/Node APIs in hooks. The runtime is goja-based server-side JS.
|
- 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 treat actions as fake collections unless there is a good reason.
|
||||||
- Do not assume bulk hooks run per document.
|
- Do not assume bulk hooks run per document.
|
||||||
- Do not build SSR/cache logic into frontend code when the invalidation belongs in hooks.
|
- 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.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: tibi-project-setup
|
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
|
# 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:
|
Use this skill when:
|
||||||
|
|
||||||
- Creating a new project from the `tibi-svelte-starter` template
|
- creating a new project from `tibi-svelte-starter`
|
||||||
- Onboarding into a freshly cloned starter project where placeholders haven't been replaced yet
|
- onboarding into a freshly cloned project where starter placeholders are still present
|
||||||
- The user asks to "set up", "initialize", or "bootstrap" a new tibi project
|
- 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
|
## Prerequisites
|
||||||
|
|
||||||
- Code-Server environment at `*.code.testversion.online`
|
- `git`, `yarn`, `make`, `docker compose`, `curl`
|
||||||
- `git`, `yarn`, `make`, `docker compose` available
|
- current Code-Server / Docker environment for `*.code.testversion.online`
|
||||||
- Traefik reverse proxy running on the host (manages `*.code.testversion.online` subdomains automatically via Docker labels)
|
- 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
|
```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
|
git clone https://gitbase.de/cms/tibi-svelte-starter.git my-project
|
||||||
cd my-project
|
cd my-project
|
||||||
git remote rename origin template
|
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>/<repo>.git
|
||||||
# git remote add origin https://gitbase.de/org/my-project.git
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Verify:** `git remote -v` shows `template` pointing to the starter and optionally `origin` pointing to the new repo.
|
## Step 2 — Replace 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 |
|
Minimum placeholders to replace:
|
||||||
| -------------------- | ---------------------------------------------- | --------------------------------------------------------- | ------------ |
|
|
||||||
| `__PROJECT_NAME__` | `.env` | kebab-case (used for URLs, Docker containers, subdomains) | `my-project` |
|
- `__PROJECT_NAME__`
|
||||||
| `__TIBI_NAMESPACE__` | `.env`, `api/config.yml`, `frontend/.htaccess` | kebab-case, same value as `PROJECT_NAME` | `my-project` |
|
- `__TIBI_NAMESPACE__`
|
||||||
|
- `__ORG__`
|
||||||
|
- `__PROJECT__`
|
||||||
|
|
||||||
|
Verify with:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
PROJECT=my-project # kebab-case
|
rg '__[A-Z0-9_]+__' . --glob '*.{yml,js,env,htaccess,json,md,ts,svelte}'
|
||||||
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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
```sh
|
||||||
# api/hooks/config-client.js has: const originURL = "https://__PROJECT__.code.testversion.online"
|
token=$(openssl rand -hex 20)
|
||||||
sed -i "s/__PROJECT__/$PROJECT/g" api/hooks/config-client.js
|
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
|
- `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}`
|
||||||
grep -n '__PROJECT_NAME__\|__TIBI_NAMESPACE__\|__PROJECT__\|__ORG__' .env api/config.yml frontend/.htaccess api/hooks/config-client.js
|
- the current deploy scripts also use the same secret as a bearer token on the project-local reload endpoint
|
||||||
# Expected: no output (all placeholders replaced)
|
- `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
|
Use the Docker targets from the project. Do not try to start the frontend with local dev servers.
|
||||||
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
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
yarn install
|
yarn install
|
||||||
make docker-up # Start stack in background
|
make docker-up
|
||||||
# or
|
|
||||||
make docker-start # Start stack in foreground (CTRL-C to stop)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
```sh
|
||||||
make docker-ps # All containers should be "Up"
|
curl -I "$CODING_URL"
|
||||||
make docker-logs # Check for errors
|
curl -I "$CODING_TIBIADMIN_URL"
|
||||||
|
curl -I "$CODING_URL/api/content?limit=1"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Step 6 — Verify URLs
|
If the current environment also exposes a raw tibi-server host, add:
|
||||||
|
|
||||||
Traefik picks up Docker labels automatically — no manual config needed. After `make docker-up`, these URLs become available:
|
|
||||||
|
|
||||||
| Service | URL |
|
|
||||||
| --------------------- | ------------------------------------------------------------ |
|
|
||||||
| Website (BrowserSync) | `https://{PROJECT_NAME}.code.testversion.online/` |
|
|
||||||
| Tibi Admin | `https://{PROJECT_NAME}-tibiadmin.code.testversion.online/` |
|
|
||||||
| Tibi Server API | `https://{PROJECT_NAME}-tibiserver.code.testversion.online/` |
|
|
||||||
| Maildev | `https://{PROJECT_NAME}-maildev.code.testversion.online/` |
|
|
||||||
|
|
||||||
The subdomains are registered via the Docker label `online.testversion.code.subdomain=${PROJECT_NAME}`. Traefik watches Docker events and creates routes dynamically.
|
|
||||||
|
|
||||||
**Verify:** `curl -sI https://{PROJECT_NAME}.code.testversion.online/ | head -1` returns `HTTP/2 200`.
|
|
||||||
|
|
||||||
## Step 7 — Mock mode (optional)
|
|
||||||
|
|
||||||
For frontend development without a running tibi-server backend:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Set in .env:
|
curl -I "$CODING_TIBISERVER_URL/api/v1/version"
|
||||||
MOCK=1
|
|
||||||
# Then full restart (env change requires docker-down first):
|
|
||||||
make docker-down && make docker-up
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**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 |
|
```sh
|
||||||
| ---------------------------------- | ------------------------------------------------------ |
|
curl -s -X POST "$CODING_TIBISERVER_URL/api/v1/project" \
|
||||||
| `frontend/src/blocks/` | Demo block components (HeroBlock, RichtextBlock, etc.) |
|
-H "Content-Type: application/json" \
|
||||||
| `frontend/mocking/content.json` | Demo mock data for content |
|
-H "X-Admin-Token: $ADMIN_TOKEN" \
|
||||||
| `frontend/mocking/navigation.json` | Demo mock data for navigation |
|
-d '{
|
||||||
| `api/collections/content.yml` | Content collection config |
|
"name": "<PROJECT_NAME>",
|
||||||
| `api/collections/navigation.yml` | Navigation collection config |
|
"description": "...",
|
||||||
| `tests/e2e/` | Demo E2E tests |
|
"configFile": "/data/api/config.yml",
|
||||||
| `video-tours/tours/` | Demo video tour |
|
"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
|
### Token header distinction
|
||||||
- i18n route model
|
|
||||||
- pagebuilder-based content collection
|
|
||||||
- navigation collection
|
|
||||||
- media library and image handling
|
|
||||||
- tests / tours as scaffolding
|
|
||||||
|
|
||||||
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.
|
Do not mix these headers casually. A working collection token does not imply project-admin access.
|
||||||
- **Delete immediately** if you're starting with a completely custom design and the demo files would only cause confusion.
|
|
||||||
|
|
||||||
## Step 9 — Build and validate
|
## 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
|
```sh
|
||||||
yarn build
|
yarn build
|
||||||
yarn build:server
|
yarn build:server
|
||||||
yarn validate
|
yarn validate
|
||||||
```
|
```
|
||||||
|
|
||||||
Then also verify:
|
The project is not considered bootstrapped until all three succeed.
|
||||||
|
|
||||||
- the public site loads via the website URL
|
## Step 10 — Optional immediate follow-up work
|
||||||
- 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
|
|
||||||
|
|
||||||
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.
|
||||||
|
|||||||
@@ -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.
|
- **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.
|
- **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.
|
- **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.
|
||||||
@@ -13,6 +13,8 @@ playwright/.cache/
|
|||||||
visual-review/
|
visual-review/
|
||||||
video-tours/output/
|
video-tours/output/
|
||||||
.playwright-mcp/
|
.playwright-mcp/
|
||||||
|
.agents/STARTER_ALIGNMENT_*.md
|
||||||
|
|
||||||
.yarn/*
|
.yarn/*
|
||||||
!.yarn/cache
|
!.yarn/cache
|
||||||
!.yarn/patches
|
!.yarn/patches
|
||||||
|
|||||||
@@ -2,6 +2,27 @@
|
|||||||
|
|
||||||
Tibi CMS starter template — Svelte 5 SPA with esbuild, SSR via goja, and Playwright tests.
|
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
|
## Project overview
|
||||||
|
|
||||||
- **Frontend**: Svelte 5 SPA in `frontend/src/`, esbuild, Tailwind CSS 4.
|
- **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`.
|
- `yarn` is for standalone tasks: `build`, `build:server`, `validate`.
|
||||||
- Bootstrap details (placeholder replacement, docker setup) → `tibi-project-setup` skill.
|
- Bootstrap details (placeholder replacement, docker setup) → `tibi-project-setup` skill.
|
||||||
- Build checklist for full website projects → `.agents/BUILD_CHECKLIST.md`.
|
- 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
|
## Quick reference
|
||||||
|
|
||||||
@@ -39,9 +77,10 @@ Tibi CMS starter template — Svelte 5 SPA with esbuild, SSR via goja, and Playw
|
|||||||
|
|
||||||
## API access
|
## 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:`.
|
- 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.
|
- 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
|
## 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.
|
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
|
## TypeScript
|
||||||
|
|
||||||
```json
|
```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.
|
- **Zero warnings policy**: always run `yarn validate` after changes; fix all warnings.
|
||||||
- Svelte 5 with Runes: `$props()`, `$state()`, `$derived()`, `$effect()`.
|
- 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
|
## Tailwind CSS 4
|
||||||
|
|
||||||
Canonical v4 syntax — no v3 legacy classes:
|
Canonical v4 syntax — no v3 legacy classes:
|
||||||
@@ -92,7 +158,7 @@ Canonical v4 syntax — no v3 legacy classes:
|
|||||||
| v3 | v4 |
|
| v3 | v4 |
|
||||||
|----|----|
|
|----|----|
|
||||||
| `bg-gradient-to-*` | `bg-linear-to-*` |
|
| `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!` |
|
| `!bg-brand-600` | `bg-brand-600!` |
|
||||||
| `hover:!bg-brand-700` | `hover:bg-brand-700!` |
|
| `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 |
|
| 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-pagebuilder-modeling` | Block schemas, preview, drillDown, dependsOn |
|
||||||
| `nova-navigation-modeling` | Header/footer trees, viewHint.navigation, declaredTrees |
|
| `nova-navigation-modeling` | Header/footer trees, viewHint.navigation, declaredTrees |
|
||||||
| `admin-ui-config` | Field widgets, sidebar, layout, choices, foreign refs |
|
| `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 |
|
| `tibi-actions-and-forms` | Contact forms, newsletter, webhooks, action endpoints |
|
||||||
| `scheduled-jobs-and-automation` | Cron tasks, cleanup, reports, sync |
|
| `scheduled-jobs-and-automation` | Cron tasks, cleanup, reports, sync |
|
||||||
| `realtime-and-live-workflows` | SSE channels, live notifications, preview refresh |
|
| `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
|
### 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 |
|
| `frontend-architecture` | SPA router, stores, Svelte 5 patterns, API layer |
|
||||||
| `tibi-ssr-caching` | SSR rendering, cache invalidation, 404 signaling |
|
| `tibi-ssr-caching` | SSR rendering, cache invalidation, 404 signaling |
|
||||||
| `playwright-testing` | Deterministic seed data, API/E2E/admin/visual tests |
|
| `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 |
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ Starter Kit für SPAs(s) `;)` mit Svelte und TibiCMS inkl. SSR
|
|||||||
[ ] Repository geklont und Remotes konfiguriert
|
[ ] Repository geklont und Remotes konfiguriert
|
||||||
[ ] __PROJECT_NAME__ in .env ersetzt (kebab-case)
|
[ ] __PROJECT_NAME__ in .env ersetzt (kebab-case)
|
||||||
[ ] __TIBI_NAMESPACE__ in .env ersetzt (snake_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)
|
[ ] Keine verbleibenden __*__-Platzhalter (mit grep prüfen)
|
||||||
[ ] App.svelte hat <svelte:head> mit <title>
|
[ ] App.svelte hat <svelte:head> mit <title>
|
||||||
[ ] ADMIN_TOKEN in api/config.yml.env gesetzt
|
[ ] ADMIN_TOKEN in api/config.yml.env gesetzt
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
########################################################################
|
########################################################################
|
||||||
|
|
||||||
name: content
|
name: content
|
||||||
|
uploadPath: ../media/content
|
||||||
meta:
|
meta:
|
||||||
label: { de: "Inhalte", en: "Content" }
|
label: { de: "Inhalte", en: "Content" }
|
||||||
muiIcon: article
|
muiIcon: article
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
########################################################################
|
########################################################################
|
||||||
|
|
||||||
name: medialib
|
name: medialib
|
||||||
|
uploadPath: ../media/medialib
|
||||||
meta:
|
meta:
|
||||||
label: { de: "Mediathek", en: "Media Library" }
|
label: { de: "Mediathek", en: "Media Library" }
|
||||||
muiIcon: image_multiple
|
muiIcon: image_multiple
|
||||||
@@ -50,6 +51,12 @@ permissions:
|
|||||||
post: true
|
post: true
|
||||||
put: true
|
put: true
|
||||||
delete: true
|
delete: true
|
||||||
|
"token:${ADMIN_TOKEN}":
|
||||||
|
methods:
|
||||||
|
get: true
|
||||||
|
post: true
|
||||||
|
put: true
|
||||||
|
delete: true
|
||||||
|
|
||||||
imageFilter:
|
imageFilter:
|
||||||
xs-webp:
|
xs-webp:
|
||||||
|
|||||||
@@ -2,6 +2,14 @@
|
|||||||
|
|
||||||
Hook files run inside the tibi-server Go runtime (goja).
|
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
|
## Conventions
|
||||||
|
|
||||||
- Wrap hook files in an IIFE: `;(function () { ... })()`.
|
- 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`.
|
- Use inline type casting with `/** @type {TypeName} */ (value)` and typed collection entries from `types/global.d.ts`.
|
||||||
- Avoid `@ts-ignore`; use proper casting instead.
|
- Avoid `@ts-ignore`; use proper casting instead.
|
||||||
- Use `const` and `let` instead of `var`. The tibi-server runtime supports modern JS declarations.
|
- 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
|
## 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/`.
|
- 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`.
|
- 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.
|
- 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.
|
||||||
|
|||||||
@@ -2,6 +2,14 @@
|
|||||||
|
|
||||||
Server-side rendering via goja (Go JS runtime) with HTML caching.
|
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
|
## Request flow
|
||||||
|
|
||||||
1. `get_read.js` receives the request and calls `lib/ssr-server.js`.
|
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"`.
|
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.
|
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
|
## Build
|
||||||
|
|
||||||
- SSR bundle is built via `yarn build:server` and outputs to `lib/app.server.js`.
|
- 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`.
|
- 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.
|
- 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.
|
- `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
|
## 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.
|
`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.
|
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
@@ -230,7 +230,7 @@ if (process.argv[2] == "start") {
|
|||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
logLevel: "debug",
|
logLevel: "debug",
|
||||||
pathRewrite: function (path, req) {
|
pathRewrite: function (path, req) {
|
||||||
return "/ssr?url=" + encodeURIComponent(path)
|
return "/ssr?url=" + encodeURIComponent(path) + "&noCache=1"
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|||||||
+10
-1
@@ -10,6 +10,13 @@ Svelte 5 SPA bundled with esbuild and styled with Tailwind CSS 4.
|
|||||||
- `src/css/` — global styles and Tailwind imports.
|
- `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`.
|
- `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
|
## Routing
|
||||||
|
|
||||||
This project uses a **custom SPA router** (NOT SvelteKit, NOT file-based routing). Pages are CMS content entries loaded dynamically by URL path.
|
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.
|
- Keep code and comments in English.
|
||||||
- SSR safety: guard browser-only code with `typeof window !== "undefined"`.
|
- 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`.
|
- 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
|
## 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.
|
- Only use arbitrary values (`[...]`) when no standard utility covers the needed value.
|
||||||
|
|
||||||
## i18n
|
## i18n
|
||||||
|
|||||||
@@ -165,7 +165,8 @@
|
|||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
{ lookup: NAVIGATION_CONTENT_LOOKUP }
|
undefined,
|
||||||
|
NAVIGATION_CONTENT_LOOKUP
|
||||||
),
|
),
|
||||||
getCachedEntries<"navigation">(
|
getCachedEntries<"navigation">(
|
||||||
"navigation",
|
"navigation",
|
||||||
@@ -174,13 +175,14 @@
|
|||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
{ lookup: NAVIGATION_CONTENT_LOOKUP }
|
undefined,
|
||||||
|
NAVIGATION_CONTENT_LOOKUP
|
||||||
),
|
),
|
||||||
])
|
])
|
||||||
headerNav = headerEntries[0] || null
|
headerNav = headerEntries[0] || null
|
||||||
footerNav = footerEntries[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">(
|
const contentEntries = await getCachedEntries<"content">(
|
||||||
"content",
|
"content",
|
||||||
{
|
{
|
||||||
@@ -189,10 +191,11 @@
|
|||||||
active: true,
|
active: true,
|
||||||
},
|
},
|
||||||
"sort",
|
"sort",
|
||||||
|
1,
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
{ lookup: CONTENT_MEDIA_LOOKUP }
|
CONTENT_MEDIA_LOOKUP
|
||||||
)
|
)
|
||||||
|
|
||||||
if (contentEntries.length > 0) {
|
if (contentEntries.length > 0) {
|
||||||
|
|||||||
+12
-1
@@ -1,4 +1,5 @@
|
|||||||
import { mount, unmount, type Component, type SvelteComponent } from "svelte"
|
import { mount, unmount, type Component, type SvelteComponent } from "svelte"
|
||||||
|
import { apiBaseOverride } from "./lib/store"
|
||||||
import BlockRenderer from "./blocks/BlockRenderer.svelte"
|
import BlockRenderer from "./blocks/BlockRenderer.svelte"
|
||||||
|
|
||||||
const previewCssUrl = new URL("./index.css", import.meta.url).toString()
|
const previewCssUrl = new URL("./index.css", import.meta.url).toString()
|
||||||
@@ -86,6 +87,11 @@ function createContentBlockDefinition(presentation: BlockPresentation): BlockDef
|
|||||||
position: "relative",
|
position: "relative",
|
||||||
},
|
},
|
||||||
render(container, row, context) {
|
render(container, row, context) {
|
||||||
|
const previewApiBase = context?.projectBase || context?.apiBase
|
||||||
|
if (previewApiBase) {
|
||||||
|
apiBaseOverride.set(String(previewApiBase))
|
||||||
|
}
|
||||||
|
|
||||||
const target = document.createElement("div")
|
const target = document.createElement("div")
|
||||||
target.dataset.adminPreview = "true"
|
target.dataset.adminPreview = "true"
|
||||||
container.appendChild(target)
|
container.appendChild(target)
|
||||||
@@ -99,7 +105,12 @@ function createContentBlockDefinition(presentation: BlockPresentation): BlockDef
|
|||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
update(nextRow) {
|
update(nextRow, nextContext) {
|
||||||
|
const nextPreviewApiBase = nextContext?.projectBase || nextContext?.apiBase
|
||||||
|
if (nextPreviewApiBase) {
|
||||||
|
apiBaseOverride.set(String(nextPreviewApiBase))
|
||||||
|
}
|
||||||
|
|
||||||
unmount(mountedComponent)
|
unmount(mountedComponent)
|
||||||
target.innerHTML = ""
|
target.innerHTML = ""
|
||||||
mountedComponent = mount(BlockRenderer as Component<any>, {
|
mountedComponent = mount(BlockRenderer as Component<any>, {
|
||||||
|
|||||||
+16
-9
@@ -137,12 +137,14 @@ const CACHE_TTL = 1000 * 60 * 60 // 1 hour
|
|||||||
// Generic collection helpers
|
// Generic collection helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
type CollectionNameT = "medialib" | "content" | string
|
type CollectionNameT = "medialib" | "content" | "navigation" | string
|
||||||
|
|
||||||
type EntryTypeSwitch<T extends string> = T extends "medialib"
|
type EntryTypeSwitch<T extends string> = T extends "medialib"
|
||||||
? MedialibEntry
|
? MedialibEntry
|
||||||
: T extends "content"
|
: T extends "content"
|
||||||
? ContentEntry
|
? ContentEntry
|
||||||
|
: T extends "navigation"
|
||||||
|
? NavigationEntry
|
||||||
: Record<string, unknown>
|
: Record<string, unknown>
|
||||||
|
|
||||||
export async function getDBEntries<T extends CollectionNameT>(
|
export async function getDBEntries<T extends CollectionNameT>(
|
||||||
@@ -152,7 +154,8 @@ export async function getDBEntries<T extends CollectionNameT>(
|
|||||||
limit?: number,
|
limit?: number,
|
||||||
offset?: number,
|
offset?: number,
|
||||||
projection?: string,
|
projection?: string,
|
||||||
params?: Record<string, string>
|
params?: Record<string, string>,
|
||||||
|
lookup?: string
|
||||||
): Promise<EntryTypeSwitch<T>[]> {
|
): Promise<EntryTypeSwitch<T>[]> {
|
||||||
const c = await api<EntryTypeSwitch<T>[]>(collectionName, {
|
const c = await api<EntryTypeSwitch<T>[]>(collectionName, {
|
||||||
filter,
|
filter,
|
||||||
@@ -161,6 +164,7 @@ export async function getDBEntries<T extends CollectionNameT>(
|
|||||||
offset,
|
offset,
|
||||||
projection,
|
projection,
|
||||||
params,
|
params,
|
||||||
|
lookup,
|
||||||
})
|
})
|
||||||
return c.data
|
return c.data
|
||||||
}
|
}
|
||||||
@@ -172,13 +176,14 @@ export async function getCachedEntries<T extends CollectionNameT>(
|
|||||||
limit?: number,
|
limit?: number,
|
||||||
offset?: number,
|
offset?: number,
|
||||||
projection?: string,
|
projection?: string,
|
||||||
params?: Record<string, string>
|
params?: Record<string, string>,
|
||||||
|
lookup?: string
|
||||||
): Promise<EntryTypeSwitch<T>[]> {
|
): 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()) {
|
if (cache[filterStr] && cache[filterStr].expire >= Date.now()) {
|
||||||
return cache[filterStr].data as EntryTypeSwitch<T>[]
|
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 }
|
cache[filterStr] = { expire: Date.now() + CACHE_TTL, data: entries }
|
||||||
return entries
|
return entries
|
||||||
}
|
}
|
||||||
@@ -187,18 +192,20 @@ export async function getDBEntry<T extends CollectionNameT>(
|
|||||||
collectionName: T,
|
collectionName: T,
|
||||||
filter: MongoFilter,
|
filter: MongoFilter,
|
||||||
projection?: string,
|
projection?: string,
|
||||||
params?: Record<string, string>
|
params?: Record<string, string>,
|
||||||
|
lookup?: string
|
||||||
): Promise<EntryTypeSwitch<T> | undefined> {
|
): 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>(
|
export async function getCachedEntry<T extends CollectionNameT>(
|
||||||
collectionName: T,
|
collectionName: T,
|
||||||
filter: MongoFilter,
|
filter: MongoFilter,
|
||||||
projection?: string,
|
projection?: string,
|
||||||
params?: Record<string, string>
|
params?: Record<string, string>,
|
||||||
|
lookup?: string
|
||||||
): Promise<EntryTypeSwitch<T> | undefined> {
|
): 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>(
|
export async function postDBEntry<T extends CollectionNameT>(
|
||||||
|
|||||||
@@ -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>(
|
export function debounce<T extends (...args: never[]) => void>(
|
||||||
func: T,
|
func: T,
|
||||||
wait: number
|
wait: number
|
||||||
|
|||||||
@@ -1,30 +1,36 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { apiBaseURL } from "../config"
|
import { apiBaseURL } from "../config"
|
||||||
import { currentLanguage, DEFAULT_LANGUAGE } from "../lib/i18n"
|
import { currentLanguage, DEFAULT_LANGUAGE } from "../lib/i18n"
|
||||||
|
import { apiBaseOverride } from "../lib/store"
|
||||||
|
import { resolveApiAssetUrl } from "../lib/utils"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string
|
id?: string
|
||||||
entry?: MedialibEntry | null
|
entry?: MedialibEntry | null
|
||||||
filter?: string | null
|
filter?: string | null
|
||||||
noPlaceholder?: boolean
|
noPlaceholder?: boolean
|
||||||
|
alt?: string
|
||||||
caption?: string
|
caption?: string
|
||||||
showCaption?: boolean
|
showCaption?: boolean
|
||||||
minWidth?: number
|
minWidth?: number
|
||||||
widthMultiplier?: number
|
widthMultiplier?: number
|
||||||
lazy?: boolean
|
lazy?: boolean
|
||||||
|
class?: string
|
||||||
style?: string
|
style?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
id,
|
id = "",
|
||||||
entry = null,
|
entry = null,
|
||||||
filter = null,
|
filter = null,
|
||||||
noPlaceholder = false,
|
noPlaceholder = false,
|
||||||
|
alt = "",
|
||||||
caption = "",
|
caption = "",
|
||||||
showCaption = false,
|
showCaption = false,
|
||||||
minWidth = 0,
|
minWidth = 0,
|
||||||
widthMultiplier = 1,
|
widthMultiplier = 1,
|
||||||
lazy = false,
|
lazy = false,
|
||||||
|
class: className = "",
|
||||||
style = "",
|
style = "",
|
||||||
}: Props = $props()
|
}: Props = $props()
|
||||||
|
|
||||||
@@ -32,15 +38,38 @@
|
|||||||
let currentFilter = $state<string>("l-webp")
|
let currentFilter = $state<string>("l-webp")
|
||||||
const effectiveId = $derived(entry?.id || entry?._id || id || "")
|
const effectiveId = $derived(entry?.id || entry?._id || id || "")
|
||||||
const fileSrc = $derived(resolveFileSrc(entry?.file?.src, entry?.id || entry?._id || effectiveId))
|
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(() => {
|
$effect(() => {
|
||||||
if (filter) currentFilter = filter
|
if (filter) {
|
||||||
|
currentFilter = filter
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (minWidth) {
|
||||||
|
currentFilter = getFilterForWidth(minWidth * (widthMultiplier || 1))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function getAutoFilter(imgWidth: number): string {
|
function getMeasuredWidth(node: HTMLImageElement): number {
|
||||||
const width = minWidth ? Math.max(imgWidth, minWidth) : imgWidth
|
const pictureElement = node.closest("picture") as HTMLElement | null
|
||||||
const effectiveWidth = width * (widthMultiplier || 1)
|
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 <= 90) return "xs-webp"
|
||||||
if (effectiveWidth <= 300) return "s-webp"
|
if (effectiveWidth <= 300) return "s-webp"
|
||||||
@@ -50,6 +79,31 @@
|
|||||||
return "xxl-webp"
|
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 {
|
function isRasterImage(entry: MedialibEntry | null): boolean {
|
||||||
if (entry?.file?.type?.match(/^image\/(png|jpe?g|webp)/)) return true
|
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)
|
// 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 {
|
function resolveFileSrc(src: string | undefined, entryId: string | undefined): string | null {
|
||||||
if (!src) return 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
|
if (!entryId) return null
|
||||||
const normalizedApiBase = apiBaseURL.replace(/\/+$/, "")
|
const normalizedApiBase = ($apiBaseOverride || apiBaseURL).replace(/\/+$/, "")
|
||||||
return `${normalizedApiBase}/medialib/${entryId}/${src.replace(/^\/+/, "")}`
|
return `${normalizedApiBase}/medialib/${entryId}/${src.replace(/^\/+/, "")}`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResizeObserver: only when no explicit filter and raster image
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const el = imgEl
|
const el = imgEl
|
||||||
if (!el || filter || !entry || !isRasterImage(entry)) return
|
if (!el || filter || !entry || !isRasterImage(entry)) return
|
||||||
if (typeof ResizeObserver === "undefined") return
|
if (typeof ResizeObserver === "undefined") return
|
||||||
|
|
||||||
let maxObservedWidth = 0
|
let maxObservedWidth = 0
|
||||||
|
let deferredUpdateFrame: number | null = null
|
||||||
|
|
||||||
const observer = new ResizeObserver(() => {
|
const updateFilter = () => {
|
||||||
const newWidth = el.clientWidth
|
const width = getMeasuredWidth(el)
|
||||||
if (newWidth <= maxObservedWidth) return // only scale up
|
if (width <= maxObservedWidth) return
|
||||||
maxObservedWidth = newWidth
|
maxObservedWidth = width
|
||||||
currentFilter = getAutoFilter(newWidth)
|
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 {
|
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 (!isRasterImage(entry)) return src
|
||||||
if (filter) return src + `?filter=${filter}`
|
if (filter) return src + `?filter=${filter}`
|
||||||
return src + `?filter=${currentFilter}`
|
return src + `?filter=${currentFilter}`
|
||||||
@@ -97,9 +174,12 @@
|
|||||||
|
|
||||||
return value[lang] || value[DEFAULT_LANGUAGE] || Object.values(value).find((entry) => !!entry) || ""
|
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>
|
</script>
|
||||||
|
|
||||||
{#if effectiveId}
|
|
||||||
{#if entry && fileSrc}
|
{#if entry && fileSrc}
|
||||||
{#if showCaption && caption}
|
{#if showCaption && caption}
|
||||||
<figure>
|
<figure>
|
||||||
@@ -107,8 +187,10 @@
|
|||||||
<img
|
<img
|
||||||
bind:this={imgEl}
|
bind:this={imgEl}
|
||||||
src={getSrc(fileSrc, entry)}
|
src={getSrc(fileSrc, entry)}
|
||||||
alt={resolveLocalizedText(entry.alt, $currentLanguage)}
|
alt={getAltText(entry)}
|
||||||
data-entry-id={id}
|
data-entry-id={effectiveId}
|
||||||
|
class={className}
|
||||||
|
style:object-position={getObjectPosition(entry)}
|
||||||
loading={lazy ? "lazy" : undefined}
|
loading={lazy ? "lazy" : undefined}
|
||||||
{style}
|
{style}
|
||||||
/>
|
/>
|
||||||
@@ -122,16 +204,17 @@
|
|||||||
<img
|
<img
|
||||||
bind:this={imgEl}
|
bind:this={imgEl}
|
||||||
src={getSrc(fileSrc, entry)}
|
src={getSrc(fileSrc, entry)}
|
||||||
alt={resolveLocalizedText(entry.alt, $currentLanguage)}
|
alt={getAltText(entry)}
|
||||||
data-entry-id={id}
|
data-entry-id={effectiveId}
|
||||||
|
class={className}
|
||||||
|
style:object-position={getObjectPosition(entry)}
|
||||||
loading={lazy ? "lazy" : undefined}
|
loading={lazy ? "lazy" : undefined}
|
||||||
{style}
|
{style}
|
||||||
/>
|
/>
|
||||||
</picture>
|
</picture>
|
||||||
{/if}
|
{/if}
|
||||||
{:else if !noPlaceholder}
|
{:else if !noPlaceholder && (effectiveId || entry)}
|
||||||
<picture>
|
<picture>
|
||||||
<img src="/assets/img/placeholder-image.svg" alt="not found" data-entry-id={effectiveId} />
|
<img src={placeholderSrc} alt="not found" data-entry-id={effectiveId} class={className} />
|
||||||
</picture>
|
</picture>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
|
||||||
|
|||||||
@@ -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.
|
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
|
## Running tests
|
||||||
|
|
||||||
- All tests: `yarn test`
|
- 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/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/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/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-visual/fixtures.ts` — Visual test helpers (`waitForVisualReady`, `hideDynamicContent`, `prepareForScreenshot`, `expectScreenshot`, `getDynamicMasks`).
|
||||||
- `e2e-mobile/fixtures.ts` — Mobile helpers (`openHamburgerMenu`, `isMobileViewport`, `isTabletViewport`, `isBelowLg`).
|
- `e2e-mobile/fixtures.ts` — Mobile helpers (`openHamburgerMenu`, `isMobileViewport`, `isTabletViewport`, `isBelowLg`).
|
||||||
- `api/fixtures.ts` — API fixtures (`api`, `adminApi`).
|
- `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`).
|
- `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 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.
|
- 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.
|
- `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
|
## Visual regression
|
||||||
|
|||||||
@@ -1,5 +1,32 @@
|
|||||||
import { APIRequestContext, request } from "@playwright/test"
|
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.
|
* 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_USER: Basic auth username (default: code)
|
||||||
* - MAILDEV_PASS: Basic auth password
|
* - 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_USER = process.env.MAILDEV_USER || "code"
|
||||||
const MAILDEV_PASS = process.env.MAILDEV_PASS || ""
|
const MAILDEV_PASS = process.env.MAILDEV_PASS || ""
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ type ContentEntry = {
|
|||||||
_testdata?: boolean
|
_testdata?: boolean
|
||||||
translationKey?: string
|
translationKey?: string
|
||||||
path?: string
|
path?: string
|
||||||
|
title?: string
|
||||||
[key: string]: unknown
|
[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_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_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 {
|
function isSeededContentEntry(entry: ContentEntry): boolean {
|
||||||
if (entry._testdata === true) {
|
if (entry._testdata === true) {
|
||||||
@@ -36,7 +39,28 @@ function isSeededContentEntry(entry: ContentEntry): boolean {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const SEEDED_CONTENT_ENTRIES = [
|
function isSeededMedialibEntry(entry: ContentEntry): boolean {
|
||||||
|
return entry._testdata === true
|
||||||
|
}
|
||||||
|
|
||||||
|
const SEEDED_MEDIALIB_ENTRIES = [
|
||||||
|
{
|
||||||
|
_testdata: true,
|
||||||
|
title: "Playwright Seed Image",
|
||||||
|
alt: {
|
||||||
|
de: "Playwright Seed Bild",
|
||||||
|
en: "Playwright seed image",
|
||||||
|
},
|
||||||
|
description: "Deterministisches Medialib-Bild fuer Admin- und Preview-Tests.",
|
||||||
|
tags: ["playwright", "seed", "preview"],
|
||||||
|
file: {
|
||||||
|
src: SEEDED_MEDIA_DATA_URI,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] as const
|
||||||
|
|
||||||
|
function getSeededContentEntries(previewImageId: string) {
|
||||||
|
return [
|
||||||
{
|
{
|
||||||
_testdata: true,
|
_testdata: true,
|
||||||
active: true,
|
active: true,
|
||||||
@@ -199,6 +223,44 @@ const SEEDED_CONTENT_ENTRIES = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
_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,
|
_testdata: true,
|
||||||
active: true,
|
active: true,
|
||||||
@@ -286,6 +348,43 @@ const SEEDED_CONTENT_ENTRIES = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
] as const
|
] 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> {
|
export async function cleanupSeededTestContent(baseURL: string): Promise<number> {
|
||||||
const contentEntries = await listCollectionEntries<ContentEntry>(baseURL, "content")
|
const contentEntries = await listCollectionEntries<ContentEntry>(baseURL, "content")
|
||||||
@@ -302,12 +401,15 @@ export async function cleanupSeededTestContent(baseURL: string): Promise<number>
|
|||||||
if (ok) deleted++
|
if (ok) deleted++
|
||||||
}
|
}
|
||||||
|
|
||||||
return deleted
|
const deletedMediaEntries = await cleanupSeededMedialibEntries(baseURL)
|
||||||
|
return deleted + deletedMediaEntries
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function seedTestContent(baseURL: string): Promise<number> {
|
export async function seedTestContent(baseURL: string): Promise<number> {
|
||||||
let created = 0
|
const mediaSeed = await seedMedialibEntries(baseURL)
|
||||||
for (const entry of SEEDED_CONTENT_ENTRIES) {
|
let created = mediaSeed.created
|
||||||
|
|
||||||
|
for (const entry of getSeededContentEntries(mediaSeed.previewImageId)) {
|
||||||
await createCollectionEntry(baseURL, "content", entry as unknown as Record<string, unknown>)
|
await createCollectionEntry(baseURL, "content", entry as unknown as Record<string, unknown>)
|
||||||
created++
|
created++
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ export const test = base.extend({
|
|||||||
export async function loginToAdmin(page: Page): Promise<void> {
|
export async function loginToAdmin(page: Page): Promise<void> {
|
||||||
await page.goto(`${TEST_ADMIN_BASE_URL}/login`)
|
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(/Benutzername|Username/i).fill(ADMIN_UI_CREDENTIALS.username)
|
||||||
await page.getByLabel(/Passwort|Password/i).fill(ADMIN_UI_CREDENTIALS.password)
|
await page.getByLabel(/Passwort|Password/i).fill(ADMIN_UI_CREDENTIALS.password)
|
||||||
await page.getByRole("button", { name: /Anmelden|Sign in|Login/i }).click()
|
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")
|
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> {
|
export async function openNavigationCollection(page: Page): Promise<void> {
|
||||||
await openCollection(page, /Navigation/, "navigation", "Navigation")
|
await openCollection(page, /Navigation/, "navigation", "Navigation")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
Vendored
+4
@@ -57,6 +57,10 @@ export const SEEDED_TEST_CONTENT = {
|
|||||||
translationKey: "pw-e2e-home",
|
translationKey: "pw-e2e-home",
|
||||||
path: "/playwright-e2e-home",
|
path: "/playwright-e2e-home",
|
||||||
},
|
},
|
||||||
|
pagebuilderPreview: {
|
||||||
|
translationKey: "pw-e2e-pagebuilder-preview",
|
||||||
|
path: "/playwright-e2e-pagebuilder-preview",
|
||||||
|
},
|
||||||
contact: {
|
contact: {
|
||||||
translationKey: "pw-e2e-contact",
|
translationKey: "pw-e2e-contact",
|
||||||
path: "/playwright-e2e-contact",
|
path: "/playwright-e2e-contact",
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ async function globalSetup() {
|
|||||||
|
|
||||||
const result = await ensureSeededTestContent(baseURL)
|
const result = await ensureSeededTestContent(baseURL)
|
||||||
console.log(
|
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 {
|
} finally {
|
||||||
await ctx.dispose()
|
await ctx.dispose()
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ async function globalTeardown() {
|
|||||||
const result = await cleanupAllTestData(baseURL)
|
const result = await cleanupAllTestData(baseURL)
|
||||||
if (deletedSeedEntries > 0 || result.users > 0) {
|
if (deletedSeedEntries > 0 || result.users > 0) {
|
||||||
console.log(
|
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) {
|
} catch (err) {
|
||||||
|
|||||||
Reference in New Issue
Block a user