feat: enhance admin UI configuration and SSR handling

- Add support for number chip arrays and JSON editor in admin UI config.
- Introduce pagebuilder block registry for Svelte components in admin previews.
- Implement custom role names and a 3-layer cascade model for field-level permissions.
- Add CORS configuration hierarchy for better API security.
- Update project setup instructions for admin token and config management.
- Improve SSR 404 signaling with proper context handling in NotFound component.
- Refactor routing structure to separate NotFound page into its own route.
This commit is contained in:
2026-05-12 23:20:31 +00:00
parent 60d5920132
commit 958b45272d
13 changed files with 573 additions and 197 deletions
+248
View File
@@ -0,0 +1,248 @@
# 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.
---
## Phase 0: Project Bootstrap
**Skills:** `tibi-project-setup`
- [ ] Replace all starter placeholders in ALL files:
- `.env`: `__PROJECT_NAME__`, `__TIBI_NAMESPACE__`, `__ORG__`, `__PROJECT__`
- `api/config.yml`: `namespace: __TIBI_NAMESPACE__`
- `frontend/.htaccess`: both `__TIBI_NAMESPACE__` entries
- `api/hooks/config-client.js`: `__PROJECT__` (not `__PROJECT_NAME__`)
- [ ] Configure `.env` URLs: `CODING_URL`, `STAGING_URL`, `CODING_TIBIADMIN_URL`
- [ ] Update `package.json` metadata (name, repository)
- [ ] Run `grep -n '__[A-Z0-9_]\+__' . --include='*.{yml,js,env,htaccess,json}'` to verify no placeholder remains
- [ ] Generate secure `ADMIN_TOKEN` in `api/config.yml.env`
- [ ] Verify `ADMIN_ASSET_VERSION` exists in `api/config.yml.env` (re-generate if missing: `echo "ADMIN_ASSET_VERSION=$(node -e "process.stdout.write(require('crypto').randomBytes(6).toString('hex'))")-dirty-${Date.now()}" >> api/config.yml.env`)
- [ ] `yarn install` — must succeed
- [ ] `make docker-up` — verify all containers "Up"
- [ ] Verify URLs respond: website, tibiadmin, tibiserver API
- [ ] `yarn build && yarn build:server && yarn validate` — 0 errors, 0 warnings
## Phase 1: Solution Architecture
**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
curl "http://tibiserver:8080/api/v1/_/<namespace>/ssr?url=/de/..."
```
- HTTP status correct
- Page content present in HTML
- Navigation labels present in HTML
- `window.__SSR_CACHE__` present
- Second request returns `X-SSR-Cache: true`
- `yarn build:server` succeeds
## Phase 6: Backend Hooks & Actions
**Skills:** `tibi-hook-authoring`, `tibi-actions-and-forms`, `scheduled-jobs-and-automation`, `realtime-and-live-workflows`
- [ ] Create/update public read hooks (`api/hooks/<collection>/get_read.js`):
- Filter inactive/unpublished entries for public users
- Use `filter_public.js` pattern
- [ ] Create/update cache invalidation hooks (`api/hooks/clear_cache.js`):
- Clear SSR cache on content/navigation/medialib changes
- Handle `POST`, `PUT`, `DELETE` for each collection that affects SSR
- [ ] Create action endpoints in `api/actions/`:
- Contact form, newsletter, etc.
- Hook chain: bind → validate → handle → return
- Permissions per action (public vs. authenticated)
- Wire into `api/config.yml` under `actions:`
- [ ] Register all hooks in collection/action YAML files
- [ ] Verify hooks work:
- Public API returns only active entries
- Actions respond correctly to valid/invalid submissions
- Cache clears on content mutation
## Phase 7: Permissions & Security
**Skills:** `permissions-and-editor-workflows`, `security-hardening-and-token-strategy`
- [ ] Configure collection permissions:
- `public` read methods where appropriate
- `user` write methods for editors
- `"token:${ADMIN_TOKEN}"` for seed/test access
- [ ] Configure field-level permissions:
- `readonlyFields` at collection or permissionSet level
- `hiddenFields` for sensitive internal data
- `readonly`/`hidden` with eval for dynamic rules
- [ ] Secure sensitive config:
- Production `ADMIN_TOKEN` uses a real random value
- Hook `http.fetch` and `exec.command` used with caution
- [ ] Verify: non-admin users see only permitted fields/collections
- [ ] Verify CORS if external origins access the API
## Phase 8: Media & SEO
**Skills:** `media-seo-publishing`, `nova-ai-editor-features` (optional)
- [ ] Configure `api/collections/medialib.yml`:
- File field with `widget: image`
- Alt text, caption fields
- Image filters for thumbnails, cards, heroes
- [ ] Add SEO fields to content/page collections:
- `meta.title`, `meta.description`
- Social share image reference
- Sidebar placement for SEO fields
- [ ] Configure publication model:
- `active` boolean
- `publication.from` / `publication.to` if time-based
- SSR-aware cache invalidation for publication changes
- [ ] Verify: SSR HTML includes SEO meta tags
## Phase 9: Testing
**Skills:** `playwright-testing`
- [ ] Extend seed data in `tests/api/helpers/seed-data.ts`:
- Seeded pages for all public routes
- Seeded navigation entries
- Hidden `_testdata: true` marker for seed identity
- [ ] Write API tests for collections:
- Public reads return expected data
- Auth-required writes are enforced
- Actions respond correctly
- [ ] Write E2E tests for critical user journeys:
- Homepage loads
- Language switching works
- SPA navigation works
- Each block type renders
- 404 for non-existent pages
- [ ] Write admin smoke tests if admin workflows are stable:
- Login works
- Core collections are reachable
- [ ] Run affected tests:
```bash
npx playwright test tests/api/health.spec.ts --project=api
npx playwright test tests/e2e/home.spec.ts --project=chromium
```
## Phase 10: Video Tours (optional)
- [ ] Update/create tour files in `video-tours/tours/`:
- Key user flows for documentation/training
- Desktop and mobile variants
- [ ] Run tours and verify output:
```bash
yarn tour && yarn tour:mobile
```
## Phase 11: Final Verification
**Skills:** `tibi-project-setup` (build steps), `playwright-testing` (test suite)
- [ ] `yarn build` — success
- [ ] `yarn build:server` — success
- [ ] `yarn validate` — 0 errors, 0 warnings
- [ ] `rg '__[A-Z0-9_]\+__' . --include='*.{yml,js,env,htaccess,json,ts,svelte}'` — no placeholder remains
- [ ] All Playwright tests pass (or known failures documented)
- [ ] Public site loads at website URL
- [ ] Nova admin loads at admin URL
- [ ] Pages are creatable and editable in admin
- [ ] SSR renders real page content
- [ ] Forms/actions work (if applicable)
- [ ] `config.yml.env` has production-ready `ADMIN_TOKEN`
+105 -6
View File
@@ -108,6 +108,7 @@ fields:
| ----------- | ---------------------- | --------------------------------------------- | | ----------- | ---------------------- | --------------------------------------------- |
| `string` | Text input | Use `inputProps.multiline: true` for textarea | | `string` | Text input | Use `inputProps.multiline: true` for textarea |
| `number` | Number input | | | `number` | Number input | |
| `number[]` | Number chip array | Multiple numeric values |
| `boolean` | Toggle/checkbox | | | `boolean` | Toggle/checkbox | |
| `date` | Date picker | | | `date` | Date picker | |
| `object` | Nested field group | Requires `subFields` | | `object` | Nested field group | Requires `subFields` |
@@ -115,6 +116,7 @@ fields:
| `string[]` | Tag input | | | `string[]` | Tag input | |
| `file` | File upload | | | `file` | File upload | |
| `file[]` | Multi-file upload | | | `file[]` | Multi-file upload | |
| `any` | JSON editor | For mixed/arbitrary data |
### inputProps — widget customization ### inputProps — widget customization
@@ -451,7 +453,79 @@ For complex nested objects, use `drillDown` to render them as a sub-page:
## Admin module (frontend/src/admin.ts) ## Admin module (frontend/src/admin.ts)
The `admin.ts` file exports custom Svelte components for injection into the tibi-admin UI. Components are rendered inside Shadow DOM to isolate styles. The `admin.ts` file exports the **pagebuilder block registry** and optional custom Svelte components for the tibi-admin UI. This is how the admin preview renders your Svelte blocks.
### Pagebuilder block registry
The current starter uses `createContentBlockDefinition()` to register each block type. This mounts real Svelte block components into Shadow DOM for admin previews:
```typescript
import { mount, unmount, type Component, type SvelteComponent } from "svelte"
import BlockRenderer from "./blocks/BlockRenderer.svelte"
// Creates a block definition that renders the same Svelte component
// used in the public frontend. The block is mounted inside Shadow DOM
// for style isolation.
function createContentBlockDefinition(presentation: {
label: string
icon: string
color: string
}) {
return {
css: [previewCssUrl], // CSS files to inject into Shadow DOM
label: presentation.label,
icon: presentation.icon,
color: presentation.color,
previewStyles: {
"background-color": "white",
},
render(container, row, context) {
// Mount the Svelte component inside the admin preview
const target = document.createElement("div")
container.appendChild(target)
let mountedComponent = mount(BlockRenderer as Component<any>, {
target,
props: { blocks: [row], isAdminPreview: true },
})
return {
update(nextRow) {
unmount(mountedComponent)
target.innerHTML = ""
mountedComponent = mount(BlockRenderer as Component<any>, {
target,
props: { blocks: [nextRow], isAdminPreview: true },
})
},
destroy() {
unmount(mountedComponent)
target.remove()
},
}
},
}
}
const blockRegistry = {
hero: createContentBlockDefinition({ label: "Hero", icon: "image", color: "#1d4ed8" }),
richtext: createContentBlockDefinition({ label: "Richtext", icon: "article", color: "#7c3aed" }),
// ... add new blocks here
}
export { blockRegistry }
```
**Key points:**
- Each registry entry wraps the Svelte `BlockRenderer` to render the block in the admin preview.
- The `row` object is the block data (same shape as `ContentBlockEntry`).
- Preview data may contain hydrated `_lookup.<fieldPath>` foreign key data and absolute file URLs — do not prepend `apiBase` or attempt re-fetching.
- The `previewCssUrl` loads the project's `index.css` into Shadow DOM so block styles apply.
- After adding blocks to the registry, run `yarn build` so `frontend/dist/admin.mjs` is regenerated.
### Custom Svelte components (advanced)
For custom dashboard widgets, preview components, or field widgets that require Svelte rendering inside the admin UI, use `getRenderedElement()`:
```typescript ```typescript
import type { SvelteComponent } from "svelte" import type { SvelteComponent } from "svelte"
@@ -462,16 +536,14 @@ function getRenderedElement(
nestedElements?: { tagName: string; className?: string }[] nestedElements?: { tagName: string; className?: string }[]
) { ) {
// Creates a Shadow DOM container, mounts the Svelte component inside // Creates a Shadow DOM container, mounts the Svelte component inside
// addCss: CSS files to inject into Shadow DOM
// nestedElements: wrapper elements inside Shadow DOM
} }
export { getRenderedElement } export { getRenderedElement }
``` ```
Build with `yarn build`. The output includes the admin module and is loaded by tibi-admin-nova as a custom module. ### Build
**Use case:** Custom dashboard widgets, preview components, or field widgets that require Svelte rendering inside the admin UI. Run `yarn build`. The admin module (`frontend/src/admin.ts`) is compiled into `frontend/dist/admin.mjs` as part of the esbuild build pipeline (the same build produces both `index.mjs` for the SPA and `admin.mjs` for the admin module). tibi-admin-nova loads this module from the project's asset path (`/_/assets/dist/admin.mjs`). The `ADMIN_ASSET_VERSION` from `config.yml.env` is appended as a query parameter for cache busting: `admin.mjs?v=${ADMIN_ASSET_VERSION}`.
--- ---
@@ -513,7 +585,7 @@ permissions:
get: true get: true
post: true post: true
put: true put: true
delete: true delete: false # usually false for real editorial workflows
fields: fields:
- name: active - name: active
@@ -583,6 +655,33 @@ fields:
--- ---
## Indexes and search
For production collections with many entries, consider adding indexes in the YAML:
```yaml
name: products
indexes:
- name: price_sort
key: [price]
- name: category_active
key: [category, -active] # -prefix for descending
- name: slug_unique
key: [slug]
unique: true
```
Search configurations can be added for advanced text/vector search:
```yaml
search:
- name: default
mode: text
fields: [name, description]
```
See `tibi-server/docs/04-collections.md` (sections on indexes and search config) for full reference.
## 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.
@@ -49,6 +49,20 @@ At minimum, reason about permissions on these levels:
Do not flatten all of this into one vague notion of “editor access”. Do not flatten all of this into one vague notion of “editor access”.
**Custom role names:** Permission set keys in collection/action YAML are arbitrary strings. You can define any role name (e.g. `editor`, `reviewer`, `publisher`, `seo-manager`) and assign users with matching permissions. Combined with org/team membership (see `tibi-server/docs/18-orgs-teams.md`), this enables fine-grained editorial workflows beyond the built-in `public` and `user` roles.
### The 3-layer cascade model
Field-level permissions follow a strict 3-layer cascade:
1. **Collection-Level** (`collection.readonlyFields`, `collection.hiddenFields`): Base set applied to all permission sets.
2. **PermissionSet-Level** (`permissions.<role>.readonlyFields`, `permissions.<role>.hiddenFields`): Adds to or removes from the collection-level set. Prefix a field with `-` to negate (e.g. `-createdBy` removes it from the effective set).
3. **Field-Definition Override** (`field.readonly`, `field.hidden`): Absolute override — `true` forces the field into the set, `false` forces it out regardless of upper layers.
**Important:** Field-definition `readonly`/`hidden` also supports **eval expressions** (JS) for per-document dynamic evaluation. Eval rules are evaluated in a separate phase after the static cascade (Phase 1 = static cascade, Phase 2 = per-document eval). Admin role (role=0) bypasses all field-level restrictions.
See `tibi-server/docs/17-field-level-permissions.md` for the full reference with examples and eval expression context variables (`$`, `$this`, `$auth`, `$method`, `$project`, `$namespace`).
## Collection-level workflow design ## Collection-level workflow design
Before implementing permissions, define who does what. Before implementing permissions, define who does what.
@@ -124,6 +124,25 @@ Think in terms of:
Do not rely on frontend hiding or convention where server-side permissions should be explicit. Do not rely on frontend hiding or convention where server-side permissions should be explicit.
## 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:
| Level | Configuration location | Scope |
|-------|----------------------|-------|
| Server | tibi-server `config.yml` | Global default |
| Project | `api/config.yml``cors:` | Per project |
| Collection/Action | Collection or action YAML → `cors:` | Per endpoint |
Each level can `merge: true` (append to parent) or `merge: false` (replace entirely).
For a project that serves a browser-based SPA to end users on its own domain and serves API/tibiadmin on separate subdomains, the default (no explicit CORS config) is usually correct since the SPA makes same-origin API calls via the BrowserSync/production reverse proxy. Add explicit CORS only when:
- the API is called from external origins (e.g. third-party integrations)
- the admin UI is served on a different origin than the API
- an action endpoint needs to support cross-origin form submissions
See `tibi-server/docs/02-configuration.md` (section "CORS Configuration Hierarchy") for details.
## Secure implementation patterns ## Secure implementation patterns
### Public form endpoint ### Public form endpoint
+35 -8
View File
@@ -53,12 +53,19 @@ sed -i "s/__PROJECT_NAME__/$PROJECT/g" .env
sed -i "s/__TIBI_NAMESPACE__/$NAMESPACE/g" .env api/config.yml frontend/.htaccess 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`, `api/hooks/config-client.js`, and starter metadata in `package.json`. 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`.
**Verify each replacement:** **Important:** The file `api/hooks/config-client.js` contains a **separate** placeholder `__PROJECT__` (not `__PROJECT_NAME__`):
```sh ```sh
grep -n '__PROJECT_NAME__\|__TIBI_NAMESPACE__' .env api/config.yml frontend/.htaccess # api/hooks/config-client.js has: const originURL = "https://__PROJECT__.code.testversion.online"
sed -i "s/__PROJECT__/$PROJECT/g" api/hooks/config-client.js
```
**Verify all placeholders:**
```sh
grep -n '__PROJECT_NAME__\|__TIBI_NAMESPACE__\|__PROJECT__\|__ORG__' .env api/config.yml frontend/.htaccess api/hooks/config-client.js
# Expected: no output (all placeholders replaced) # Expected: no output (all placeholders replaced)
``` ```
@@ -83,7 +90,9 @@ The page title is set dynamically via `<svelte:head>` in `frontend/src/App.svelt
Also verify that SSR still renders meaningful page content and not just the shell after the rewrite. Also verify that SSR still renders meaningful page content and not just the shell after the rewrite.
## Step 4 — Admin token ## 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: `api/config.yml.env` ships with a default `ADMIN_TOKEN`. For production projects, generate a secure one:
@@ -95,6 +104,8 @@ 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`. **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 ## Step 5 — Install, upgrade, and start
```sh ```sh
@@ -106,6 +117,8 @@ 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. 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.
**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.
**Verify containers are running:** **Verify containers are running:**
```sh ```sh
@@ -135,12 +148,10 @@ For frontend development without a running tibi-server backend:
```sh ```sh
# Set in .env: # Set in .env:
MOCK=1 MOCK=1
# Then restart: # Then full restart (env change requires docker-down first):
make docker-up make docker-down && make docker-up
``` ```
Mock data lives in `frontend/mocking/` as JSON files. Missing endpoints return 404.
**When to use mock mode:** Early UI prototyping, frontend-only work, CI environments without a database. **When to use mock mode:** Early UI prototyping, frontend-only work, CI environments without a database.
## Step 8 — Remove demo content ## Step 8 — Remove demo content
@@ -215,6 +226,22 @@ For tibi-server specifically, decide early whether the site also needs:
- field-level permissions - field-level permissions
- AI/LLM integration for admin/editor workflows - 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 ## Step 11 — Functional verification for a real website project
After the first project shaping pass, verify more than just TypeScript: After the first project shaping pass, verify more than just TypeScript:
+35
View File
@@ -150,6 +150,41 @@ If this mapping is wrong, SSR may appear to work for root pages while returning
- This means SSR is not just HTML prerendering; it also primes client-side data access. - This means SSR is not just HTML prerendering; it also primes client-side data access.
- If HTML renders but `window.__SSR_CACHE__` is missing, the SSR pipeline is incomplete. - If HTML renders but `window.__SSR_CACHE__` is missing, the SSR pipeline is incomplete.
## SSR 404 signaling
When a page is not found during SSR, the framework returns the 404 page but with HTTP status **200** unless a 404 signal is set. The SSR hook (`get_read.js`) checks `context.is404` after rendering:
```js
// get_read.js, after app.default.render()
if (context.is404) {
status = 404
}
```
The signal is set from `NotFound.svelte` — when this component is rendered during SSR, it sets the flag directly. This keeps the 404 logic in the component that owns it:
```ts
// NotFound.svelte — top-level script, runs during render:
if (typeof window === "undefined") {
// @ts-ignore - context is the goja global in SSR runtime
context.is404 = true
}
```
**Why this works:**
- The `tibi-types` package declares `var context: HookContext` as a global (available because goja provides it during SSR).
- During SSR, `loadContent()` runs synchronously (goja transforms `async`/`await`).
- By the time `render(App)` returns in `ssr.ts`, `context.is404` is already `true`.
- `get_read.js` reads it, returns HTTP 404, and the rendered 404 page HTML is sent with the correct status.
- Caching is automatically skipped for 404 responses.
**Verification:** Test with a non-existent URL:
```bash
curl -w "\nHTTP Status: %{http_code}\n" "http://tibiserver:8080/api/v1/_/<namespace>/ssr?url=/de/nicht-existierend"
# Expected: HTTP 404, body contains the 404 page HTML
```
## What an LLM should inspect first when changing SSR ## What an LLM should inspect first when changing SSR
1. `api/hooks/ssr/get_read.js` to understand cache lookup, route validation, and template injection. 1. `api/hooks/ssr/get_read.js` to understand cache lookup, route validation, and template injection.
@@ -115,6 +115,12 @@ Use:
- `singleton` - `singleton`
- foreign previews - foreign previews
**I18n field config:** When modeling multilingual content, decide early whether to use:
- **Field-level i18n** — object fields whose subField names match language codes (`de`, `en`, etc.) are auto-detected and rendered with language tabs in Nova. Configured via `api.meta.i18n` in `config.yml` or per-collection `meta.i18n`.
- **Entry-level i18n** — each entry represents one language, linked by a shared `translationGroup` UUID. The `I18nEntryConfig` type (from `tibi-admin-nova/types/admin.d.ts`) defines `languageField`, `groupField`, and `copyFields`/`clearFields` for translation cloning behavior.
See `tibi-admin-nova/types/admin.d.ts` interfaces `I18nFieldConfig` and `I18nEntryConfig` for the full API.
Do not treat admin config as optional polish. It is part of the solution architecture. Do not treat admin config as optional polish. It is part of the solution architecture.
### 6. Actions and workflows ### 6. Actions and workflows
+4
View File
@@ -30,6 +30,10 @@ jobs:
npm install -g yarn npm install -g yarn
yarn install yarn install
- name: validate
run: |
yarn validate
- name: modify config - name: modify config
run: ./scripts/ci-modify-config.sh run: ./scripts/ci-modify-config.sh
+94 -185
View File
@@ -4,226 +4,135 @@ Tibi CMS starter template — Svelte 5 SPA with esbuild, SSR via goja, and Playw
## Project overview ## Project overview
- **Frontend**: Svelte 5 SPA in `frontend/src/`, bundled with esbuild, styled with Tailwind CSS 4. - **Frontend**: Svelte 5 SPA in `frontend/src/`, esbuild, Tailwind CSS 4.
- **Backend**: tibi-server with API hooks in `api/hooks/`, collections in `api/collections/`. - **Backend**: tibi-server with hooks in `api/hooks/`, collections in `api/collections/`.
- **SSR**: Server-side rendering via goja (Go JS runtime) in `api/hooks/ssr/`. - **SSR**: goja (Go JS runtime) in `api/hooks/ssr/`.
- **Tests**: Playwright for E2E, API, mobile, and visual regression tests in `tests/`. - **Tests**: Playwright in `tests/` (API, E2E, mobile, visual).
- **Types**: Shared TypeScript types in `types/global.d.ts`. Keep `tibi-types/` read-only. - **Types**: `types/global.d.ts` (project), `tibi-types/` (read-only, from sibling repo).
## Project bootstrap ## Key constraints
Before treating this repo as a real project, replace the starter placeholders and initial project values. - **Dev servers always run in Docker** — never `yarn dev` or `yarn start` locally.
- `yarn` is for standalone tasks: `build`, `build:server`, `validate`.
- Bootstrap details (placeholder replacement, docker setup) → `tibi-project-setup` skill.
- Build checklist for full website projects → `.agents/BUILD_CHECKLIST.md`.
Derive these values from the real repo path `gitbase.de/ORG/REPO`: ## Quick reference
- `PROJECT_NAME`: use the repo name in kebab-case. | Action | Command |
- `TIBI_NAMESPACE`: set it equal to `PROJECT_NAME`, i.e. use the same repo name in kebab-case. |--------|---------|
- `STAGING_PATH`: use the real repo org and repo name, i.e. `/staging/ORG/REPO/dev`. | Start dev stack | `make docker-up` or `make docker-start` |
| Restart frontend | `make docker-restart-frontend` |
| Logs | `make docker-logs` |
| Build frontend | `yarn build` |
| Build SSR | `yarn build:server` |
| Validate | `yarn validate` |
| Run all tests | `yarn test` |
| Run E2E tests | `yarn test:e2e` |
| Run API tests | `yarn test:api` |
- `.env`: replace `PROJECT_NAME=__PROJECT_NAME__`, `TIBI_NAMESPACE=__TIBI_NAMESPACE__`, `STAGING_PATH=/staging/__ORG__/__PROJECT__/dev`, `STAGING_URL=https://dev-__PROJECT_NAME__.staging.testversion.online`, and `CODING_URL=https://__PROJECT_NAME__.code.testversion.online`. ## Testing notes
- `api/config.yml`: replace `namespace: __TIBI_NAMESPACE__`.
- `frontend/.htaccess`: replace both `__TIBI_NAMESPACE__` proxy targets.
- `api/hooks/config-client.js`: replace `https://__PROJECT__.code.testversion.online` with the real origin URL.
- `package.json`: adapt starter metadata like `name` and `repository` when creating the real project repo.
- Docker and local URLs derive from `.env`, so `PROJECT_NAME` and `TIBI_NAMESPACE` must be correct before `make docker-up`.
- Playwright seed/API tests read `CODING_URL` from `.env` first. Use the configured host from `.env` whenever it serves both `/` and `/api/...`, even in starter-style local bootstrap setups.
- Recommended check: search for remaining starter placeholders with `rg '__[A-Z0-9_]+__' .`.
## Setup commands - Tests read `CODING_URL` from `.env`. Ensure it serves both `/` and `/api/...`.
- After changes, run only affected specs: `npx playwright test tests/e2e/filename.spec.ts`.
- Install deps: `yarn install` - CI runs `yarn validate` but not Playwright (needs MongoDB + tibi-server).
- Start dev: `make docker-up` or `make docker-start`
- Restart frontend watcher/dev-server: `make docker-restart-frontend`
- View logs: `make docker-logs` or `make docker-logs-X`
- Start with mock data: set `MOCK=1` in `.env`, then use the normal Docker start command
- Build frontend/admin bundle: `yarn build`
- Build SSR bundle: `yarn build:server`
- Validate types: `yarn validate`
## Development workflow
- **Dev servers always run in Docker** — never use `yarn dev` or `yarn start` locally; web access only works through the Docker reverse proxy.
- Local `yarn` is only for standalone tasks: `yarn build`, `yarn build:server`, `yarn validate`.
- **Mock mode**: Set `MOCK=1` to run the frontend without a tibi-server. API calls are served from JSON files in `frontend/mocking/`. Missing mock endpoints return 404.
- Frontend code is automatically rebuilt by the watcher/BrowserSync stack; backend hooks reload on change.
- Read `.env` for environment URLs and secrets.
- `webserver/` is for staging/ops only; use BrowserSync/esbuild for day-to-day dev.
- If development environment is running, access the website at: `https://${PROJECT_NAME}.code.testversion.online/`.
- For a11y testing use MCP a11y tools if available.
- For quick interactive browser testing, ask the user to connect Playwright MCP (preferred) or Browser MCP (only in non-autonomous/chat mode).
## Testing
- Run all tests: `yarn test`
- E2E tests: `yarn test:e2e`
- API tests: `yarn test:api`
- Visual regression: `yarn test:visual`
- Before running Playwright, ensure the `CODING_URL` from `.env` actually serves both `/` and `/api/...` for this repo.
- After code changes, run only affected spec files: `npx playwright test tests/e2e/filename.spec.ts`.
- Write unit tests for new functionality and ensure existing tests pass.
## Video tours
- Video tours are Playwright-based screen recordings (not tests) in `video-tours/`.
- Run all tours (desktop): `yarn tour`
- Run all tours (mobile): `yarn tour:mobile`
- Videos are saved to `video-tours/output/` (git-ignored).
- Tour files use `.tour.ts` suffix in `video-tours/tours/`.
- Helpers: `injectVisibleCursor()`, `moveThenClick()`, `moveThenType()`, `smoothScroll()` in `video-tours/helpers.ts`.
- Fixtures provide a `tourPage` with visible cursor overlay via `video-tours/tours/fixtures.ts`.
## API access ## API access
- API access to collections uses the reverse proxy: `CODING_URL/api/<collection>` (e.g. `CODING_URL/api/content`). - `Token` header with `ADMIN_TOKEN` from `api/config.yml.env`.
- Auth via `Token` header with `ADMIN_TOKEN` from `api/config.yml.env` when a configured token with the required permissions is needed. - Collection `user:` permissions apply to JWT auth (`X-Auth-Token`), not static `Token:`.
- Collection permissions for `user:` apply to JWT-authenticated users (`X-Auth-Token`), not to the static `Token:` header. - For write access via `ADMIN_TOKEN`, add `"token:${ADMIN_TOKEN}": { methods: { post: true, put: true } }` to the collection permissions.
- If a collection should allow writes via `ADMIN_TOKEN`, define an explicit permission block like `"token:${ADMIN_TOKEN}":` with the required methods.
## Required secrets and credentials ## Required secrets
| Secret | Location | Purpose | | Secret | Location | Purpose |
| --- | --- | --- | | --- | --- | --- |
| `ADMIN_TOKEN` | `api/config.yml.env` | Access token for configured API/admin permissions | | `ADMIN_TOKEN` | `api/config.yml.env` | API/admin access token |
| `SENTRY_AUTH_TOKEN` | Gitea repo secrets | Sourcemap upload to Sentry (CI only) | | `ADMIN_ASSET_VERSION` | `api/config.yml.env` | Cache-busting for admin bundle (auto-generated) |
| `.basic-auth-web` | project root (git-ignored) | Basic auth for BrowserSync dev server | | `.basic-auth-web` | project root (git-ignored) | BrowserSync basic auth |
| `.basic-auth-code` | project root (git-ignored) | Basic auth for Code-Server / admin | | `.basic-auth-code` | project root (git-ignored) | Code-Server basic auth |
| `RSYNC_PASS` | Gitea secrets (`github.token`) | rsync deployment password (CI only) |
## Infrastructure prerequisites Generated dev defaults in `api/config.yml.env` may be committed. Production values are overwritten in CI/CD via secrets.
- **Code-Server environment** — This project is designed for development on a Code-Server instance at `*.code.testversion.online` with a **Traefik reverse proxy** managing HTTPS and auto-routing via Docker labels. ## Reference repositories (sibling repos)
- **Without Code-Server/Traefik** — The Docker stack starts but the website is not reachable via hostname. Workaround: access BrowserSync directly via `http://localhost:3000` (requires exposing the port in `docker-compose-local.yml`).
- **Docker + Docker Compose** — Required for all development. Never run `yarn dev` or `yarn start` locally.
- **MongoDB** — Runs as a Docker service (`mongo`). Data persists in `tmp/mongo-data/`.
- **Production** — Deployed via rsync to an existing server running tibi-server + MongoDB. The frontend is a static SPA served by a Node.js webserver (`webserver/webserver.js`) or directly by tibi-server.
- **Staging** — Docker Compose builds a `www` container that connects to an external tibi-server at `dev-tibi-server.staging.testversion.online`.
## Reference repositories | Repo | Path | Key file |
|------|------|----------|
These sibling repos in the workspace provide documentation, types, and reference implementations: | **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.) |
| Repository | Path | Purpose | | **tibi-admin-nova** | `../../cms/tibi-admin-nova` | `types/admin.d.ts` (all admin config types) |
| --- | --- | --- |
| **tibi-types** | `../../cms/tibi-types` | TypeScript type definitions for hooks, collections, permissions, etc. Included via `tsconfig.json`**read-only, do not modify**. Key file: `index.d.ts`. JSON schemas in `schemas/api-config/`. |
| **tibi-server** | `../../cms/tibi-server` | Go source code of the server. `docs/` has detailed documentation (16 files), `examples/` has sample projects. |
| **tibi-admin-nova** | `../../cms/tibi-admin-nova` | Admin UI reference project. Key file: `types/admin.d.ts` (1147 lines — all admin types: `AdminCollection`, `AdminCollectionField`, `AdminCollectionMeta`, `AdminDashboard`, etc.). Use as reference for collection field configs, dashboard setup, and fieldLists. |
### When to consult which repo ### When to consult which repo
- **Write collection YAML** → `tibi-server/docs/04-collections.md` + `tibi-types/schemas/api-config/collection.json` (JSON schema) - **Collection YAML** → `tibi-server/docs/04-collections.md` + `tibi-types/schemas/config/collection.schema.json`
- **Write/debug hooks** → `tibi-server/docs/06-hooks.md` + `tibi-types/index.d.ts` (context types) - **Hooks** → `tibi-server/docs/06-hooks.md` + `tibi-types/index.d.ts`
- **Configure admin UI** → `tibi-admin-nova/types/admin.d.ts` (field types, meta, dashboard) + `tibi-admin-nova/api/collections/` (real examples) - **Admin UI config** → `tibi-admin-nova/types/admin.d.ts` + `tibi-admin-nova/api/collections/` (examples)
- **Permissions** → `tibi-server/docs/05-authentication.md` - **Permissions** → `tibi-server/docs/05-authentication.md`
- **Actions/forms** → `tibi-server/docs/19-actions.md`
- **Realtime/SSE** → `tibi-server/docs/07-realtime.md` - **Realtime/SSE** → `tibi-server/docs/07-realtime.md`
- **Images/uploads** → `tibi-server/docs/08-file-upload-images.md` - **Images/uploads** → `tibi-server/docs/08-file-upload-images.md`
- **LLM integration** → `tibi-server/docs/09-llm-integration.md` - **LLM integration** → `tibi-server/docs/09-llm-integration.md`
- **Field-level permissions** → `tibi-server/docs/17-field-level-permissions.md`
### TypeScript types ## TypeScript
`tibi-types` is included via `tsconfig.json`:
```json ```json
"include": ["frontend/src/**/*", "types/**/*", "./../../cms/tibi-types", "api/**/*"] "include": ["frontend/src/**/*", "types/**/*", "./../../cms/tibi-types", "api/**/*"]
``` ```
Project-specific types (e.g. `Ssr`, `ApiOptions`, `ContentEntry`) live in `types/global.d.ts`. - Domain types (entities, block props, API responses) be strict. `any`/`[key: string]: any` OK for inherently dynamic data (MongoDB filters, CMS fields, hook payloads).
- No `@ts-ignore` — use `/** @type {…} */` cast instead.
- **Zero warnings policy**: always run `yarn validate` after changes; fix all warnings.
- Svelte 5 with Runes: `$props()`, `$state()`, `$derived()`, `$effect()`.
## Code style ## Tailwind CSS 4
- Follow the existing code style and conventions used in the project. Canonical v4 syntax — no v3 legacy classes:
- Write clear and concise comments where necessary to explain complex logic.
- Ensure code is modular and reusable where possible.
- Avoid introducing new dependencies unless absolutely necessary.
- Respect a11y and localization best practices; optimize for WCAG AA standards.
- Check the problems tab for any errors or warnings in the code.
- **Zero warnings policy**: After making changes, always run `yarn validate` and check the IDE problems tab. Fix all TypeScript, Svelte, and Tailwind warnings — the codebase must stay warning-free.
- When Tailwind `suggestCanonicalClasses` warnings appear, always fix the **source** `.svelte`/`.ts`/`.css` file — never the compiled output.
### Architecture skills (loaded on demand) | v3 | v4 |
|----|----|
These skills provide deep-dive documentation. Use them by phase instead of treating them as an unsorted reference list.
#### 1. Project start and solution design
| Skill | When to use |
| --- | --- |
| `tibi-project-setup` | Setting up a new project from scratch |
| `website-solution-architecture` | Translating website requirements into a complete solution across content, admin, SSR, and workflows |
| `security-hardening-and-token-strategy` | Applying secure token, secret, permission, and hook-capability decisions |
#### 2. Content model and editor UX
| Skill | When to use |
| --- | --- |
| `content-authoring` | Adding new pages, content blocks, or collections |
| `nova-pagebuilder-modeling` | Designing editor-friendly block systems, nested block schemas, and pagebuilder UX |
| `nova-navigation-modeling` | Modeling multilingual header/footer/navigation trees with current Nova navigation features |
| `admin-ui-config` | Configuring collection admin views, field widgets, layouts |
| `media-seo-publishing` | Modeling media, SEO, and publication workflows for website projects |
| `permissions-and-editor-workflows` | Designing safe editorial permissions, field rules, and role-aware admin workflows |
| `nova-ai-editor-features` | Applying AI and LLM capabilities in editor workflows and media authoring responsibly |
#### 3. Backend behavior and integrations
| Skill | When to use |
| --- | --- |
| `tibi-hook-authoring` | Writing or debugging server-side hooks |
| `tibi-actions-and-forms` | Building contact forms, workflow endpoints, and other action-based website features |
| `scheduled-jobs-and-automation` | Building cron-based background tasks, cleanups, reports, and sync workflows |
| `realtime-and-live-workflows` | Designing SSE-based live updates, notifications, previews, and status workflows |
#### 4. Frontend runtime and delivery
| Skill | When to use |
| --- | --- |
| `frontend-architecture` | Routing, state management, Svelte 5 patterns, API layer, error handling |
| `tibi-ssr-caching` | SSR rendering and cache invalidation |
| `playwright-testing` | Extending or debugging seeded Playwright API, desktop, mobile, or visual tests |
### Quickstart roadmap for a new website
Use this order when building a project from scratch on this starter:
1. Foundation.
Start with `tibi-project-setup` until Docker, URLs, build, and validate are green.
2. Solution design.
Use `website-solution-architecture` first, then `security-hardening-and-token-strategy`, before writing components or hooks.
3. Content and admin model.
Use the skills from **Content model and editor UX** to shape `api/collections/content.yml`, `api/collections/navigation.yml`, reusable domain collections, and Nova authoring UX.
4. Frontend and runtime.
Use `frontend-architecture` plus `nova-pagebuilder-modeling` when wiring routing, i18n, content loading, `frontend/src/blocks/BlockRenderer.svelte`, and shared types.
5. Backend behavior.
Use the skills from **Backend behavior and integrations** for hooks, forms/actions, jobs, and realtime only where the project actually needs them.
6. SSR and publishing.
Use `tibi-ssr-caching` once routes, navigation, and page-critical data are defined, and use `media-seo-publishing` plus `permissions-and-editor-workflows` where publication, SEO, and editorial restrictions matter.
7. Optional AI editor features.
Use `nova-ai-editor-features` only when there is a concrete editorial workflow for it.
8. Final verification.
Use `playwright-testing` for deterministic API/E2E coverage, confirm public site, admin authoring, pagebuilder rendering, navigation/media resolution, forms/actions, and SSR all work, then run `yarn build`, `yarn build:server`, and `yarn validate`.
For one-off tasks, use the phase tables above as the lookup index instead of maintaining a second skill matrix here.
### Tailwind CSS 4 canonical classes
This project uses Tailwind CSS 4. Always use the canonical v4 syntax:
| Legacy (v3) | Canonical (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` |
| `!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!` |
| `!rounded-xl` | `rounded-xl!` |
Rules: ## Architecture skills (loaded on demand)
- Gradient directions use `bg-linear-to-{direction}` instead of `bg-gradient-to-{direction}`. Use these tables as the lookup index. Each skill is a deep-dive for its domain.
- Bare-ratio aspect values like `aspect-4/3` replace arbitrary `aspect-[4/3]`.
- The `!important` modifier is a **suffix** (`class!`) instead of a **prefix** (`!class`). ### 1. Project start and solution design
| Skill | When |
|-------|------|
| `tibi-project-setup` | Bootstrap a new project from scratch |
| `website-solution-architecture` | Translate requirements into collections, blocks, SSR, workflows |
| `security-hardening-and-token-strategy` | Token strategy, secrets, hook risk surfaces, CORS |
### 2. Content model and editor UX
| Skill | When |
|-------|------|
| `content-authoring` | Add pages, block types, collections |
| `nova-pagebuilder-modeling` | Block schemas, preview, drillDown, dependsOn |
| `nova-navigation-modeling` | Header/footer trees, viewHint.navigation, declaredTrees |
| `admin-ui-config` | Field widgets, sidebar, layout, choices, foreign refs |
| `media-seo-publishing` | Image fields, alt/caption, SEO, publication model |
| `permissions-and-editor-workflows` | Field-level readonly/hidden, 3-layer cascade, custom roles |
| `nova-ai-editor-features` | AI-assisted alt text, LLM actions, token budgets |
### 3. Backend behavior
| Skill | When |
|-------|------|
| `tibi-hook-authoring` | Write/debug server-side hooks (goja) |
| `tibi-actions-and-forms` | Contact forms, newsletter, webhooks, action endpoints |
| `scheduled-jobs-and-automation` | Cron tasks, cleanup, reports, sync |
| `realtime-and-live-workflows` | SSE channels, live notifications, preview refresh |
### 4. Frontend and delivery
| Skill | When |
|-------|------|
| `frontend-architecture` | SPA router, stores, Svelte 5 patterns, API layer |
| `tibi-ssr-caching` | SSR rendering, cache invalidation, 404 signaling |
| `playwright-testing` | Deterministic seed data, API/E2E/admin/visual tests |
+8
View File
@@ -29,3 +29,11 @@ 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.
## 404 signaling
The SSR hook (`get_read.js`) checks `context.is404` after rendering to determine the HTTP status code. If `true`, it returns HTTP 404 with the rendered 404 page HTML (and does not cache the result).
`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.
+1 -1
View File
@@ -8,7 +8,7 @@ Svelte 5 SPA bundled with esbuild and styled with Tailwind CSS 4.
- `src/lib/` — utilities, stores, actions, API layer. - `src/lib/` — utilities, stores, actions, API layer.
- `src/widgets/` — reusable UI components (Button, Input, Form, etc.). - `src/widgets/` — reusable UI components (Button, Input, Form, etc.).
- `src/css/` — global styles and Tailwind imports. - `src/css/` — global styles and Tailwind imports.
- Keep route components in a `src/routes/` folder when needed. - `src/routes/` — page-level components (e.g. `NotFound.svelte` for the 404 page). Not file-based routing — these are manually imported by `App.svelte`.
## Routing ## Routing
+1 -1
View File
@@ -7,7 +7,7 @@
import ToastContainer from "./widgets/ToastContainer.svelte" import ToastContainer from "./widgets/ToastContainer.svelte"
import DebugFooterInfo from "./widgets/DebugFooterInfo.svelte" import DebugFooterInfo from "./widgets/DebugFooterInfo.svelte"
import BlockRenderer from "./blocks/BlockRenderer.svelte" import BlockRenderer from "./blocks/BlockRenderer.svelte"
import NotFound from "./blocks/NotFound.svelte" import NotFound from "./routes/NotFound.svelte"
import { initScrollRestoration } from "./lib/navigation" import { initScrollRestoration } from "./lib/navigation"
import { getCachedEntries } from "./lib/api" import { getCachedEntries } from "./lib/api"
import { import {
@@ -2,6 +2,13 @@
import { _ } from "../lib/i18n/index" import { _ } from "../lib/i18n/index"
import { localizedPath } from "../lib/i18n" import { localizedPath } from "../lib/i18n"
import { reveal } from "../lib/actions/reveal" import { reveal } from "../lib/actions/reveal"
// Signal HTTP 404 to the SSR hook during server-side rendering.
// get_read.js checks context.is404 after render() and returns status 404.
if (typeof window === "undefined") {
// @ts-ignore - context is the goja global in SSR runtime (declared by tibi-types)
context.is404 = true
}
</script> </script>
<section class="min-h-[60vh] flex items-center justify-center bg-gray-50"> <section class="min-h-[60vh] flex items-center justify-center bg-gray-50">