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 |
| `number` | Number input | |
| `number[]` | Number chip array | Multiple numeric values |
| `boolean` | Toggle/checkbox | |
| `date` | Date picker | |
| `object` | Nested field group | Requires `subFields` |
@@ -115,6 +116,7 @@ fields:
| `string[]` | Tag input | |
| `file` | File upload | |
| `file[]` | Multi-file upload | |
| `any` | JSON editor | For mixed/arbitrary data |
### 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)
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
import type { SvelteComponent } from "svelte"
@@ -462,16 +536,14 @@ function getRenderedElement(
nestedElements?: { tagName: string; className?: string }[]
) {
// Creates a Shadow DOM container, mounts the Svelte component inside
// addCss: CSS files to inject into Shadow DOM
// nestedElements: wrapper elements inside Shadow DOM
}
export { getRenderedElement }
```
Build with `yarn build`. 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
post: true
put: true
delete: true
delete: false # usually false for real editorial workflows
fields:
- 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
- **`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”.
**Custom role names:** Permission set keys in collection/action YAML are arbitrary strings. You can define any role name (e.g. `editor`, `reviewer`, `publisher`, `seo-manager`) and assign users with matching permissions. Combined with org/team membership (see `tibi-server/docs/18-orgs-teams.md`), this enables fine-grained editorial workflows beyond the built-in `public` and `user` roles.
### The 3-layer cascade model
Field-level permissions follow a strict 3-layer cascade:
1. **Collection-Level** (`collection.readonlyFields`, `collection.hiddenFields`): Base set applied to all permission sets.
2. **PermissionSet-Level** (`permissions.<role>.readonlyFields`, `permissions.<role>.hiddenFields`): Adds to or removes from the collection-level set. Prefix a field with `-` to negate (e.g. `-createdBy` removes it from the effective set).
3. **Field-Definition Override** (`field.readonly`, `field.hidden`): Absolute override — `true` forces the field into the set, `false` forces it out regardless of upper layers.
**Important:** Field-definition `readonly`/`hidden` also supports **eval expressions** (JS) for per-document dynamic evaluation. Eval rules are evaluated in a separate phase after the static cascade (Phase 1 = static cascade, Phase 2 = per-document eval). Admin role (role=0) bypasses all field-level restrictions.
See `tibi-server/docs/17-field-level-permissions.md` for the full reference with examples and eval expression context variables (`$`, `$this`, `$auth`, `$method`, `$project`, `$namespace`).
## Collection-level workflow design
Before implementing permissions, define who does what.
@@ -124,6 +124,25 @@ Think in terms of:
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
### 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
```
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
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)
```
@@ -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.
## 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:
@@ -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`.
**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
@@ -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.
**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:**
```sh
@@ -135,12 +148,10 @@ For frontend development without a running tibi-server backend:
```sh
# Set in .env:
MOCK=1
# Then restart:
make docker-up
# Then full restart (env change requires docker-down first):
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.
## Step 8 — Remove demo content
@@ -215,6 +226,22 @@ For tibi-server specifically, decide early whether the site also needs:
- 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:
+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.
- If HTML renders but `window.__SSR_CACHE__` is missing, the SSR pipeline is incomplete.
## SSR 404 signaling
When a page is not found during SSR, the framework returns the 404 page but with HTTP status **200** unless a 404 signal is set. The SSR hook (`get_read.js`) checks `context.is404` after rendering:
```js
// get_read.js, after app.default.render()
if (context.is404) {
status = 404
}
```
The signal is set from `NotFound.svelte` — when this component is rendered during SSR, it sets the flag directly. This keeps the 404 logic in the component that owns it:
```ts
// NotFound.svelte — top-level script, runs during render:
if (typeof window === "undefined") {
// @ts-ignore - context is the goja global in SSR runtime
context.is404 = true
}
```
**Why this works:**
- The `tibi-types` package declares `var context: HookContext` as a global (available because goja provides it during SSR).
- During SSR, `loadContent()` runs synchronously (goja transforms `async`/`await`).
- By the time `render(App)` returns in `ssr.ts`, `context.is404` is already `true`.
- `get_read.js` reads it, returns HTTP 404, and the rendered 404 page HTML is sent with the correct status.
- Caching is automatically skipped for 404 responses.
**Verification:** Test with a non-existent URL:
```bash
curl -w "\nHTTP Status: %{http_code}\n" "http://tibiserver:8080/api/v1/_/<namespace>/ssr?url=/de/nicht-existierend"
# Expected: HTTP 404, body contains the 404 page HTML
```
## What an LLM should inspect first when changing SSR
1. `api/hooks/ssr/get_read.js` to understand cache lookup, route validation, and template injection.
@@ -115,6 +115,12 @@ Use:
- `singleton`
- foreign previews
**I18n field config:** When modeling multilingual content, decide early whether to use:
- **Field-level i18n** — object fields whose subField names match language codes (`de`, `en`, etc.) are auto-detected and rendered with language tabs in Nova. Configured via `api.meta.i18n` in `config.yml` or per-collection `meta.i18n`.
- **Entry-level i18n** — each entry represents one language, linked by a shared `translationGroup` UUID. The `I18nEntryConfig` type (from `tibi-admin-nova/types/admin.d.ts`) defines `languageField`, `groupField`, and `copyFields`/`clearFields` for translation cloning behavior.
See `tibi-admin-nova/types/admin.d.ts` interfaces `I18nFieldConfig` and `I18nEntryConfig` for the full API.
Do not treat admin config as optional polish. It is part of the solution architecture.
### 6. Actions and workflows