--- 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. --- # content-authoring ## When to use this skill Use this skill when: - Adding a new page to the website - Creating a new content block type (e.g. testimonials, pricing table, gallery) - Adding a new collection to the CMS (e.g. products, events, team members) - Understanding how content is structured and rendered ## Key concept: content-based routing This project does **NOT** use file-based routing (no SvelteKit router). Instead: 1. Pages are **CMS entries** in the `content` collection with a `path` field. 2. Public URLs are typically language-prefixed (`/de/...`, `/en/...`), but the DB entry in `content.path` is stored **without** that language prefix. 3. `App.svelte` reacts to URL changes → strips the language prefix → calls `getCachedEntries("content", { lang, path, active: true })`. 4. The same loading path is used for browser navigation and SSR. 5. The matching `ContentEntry.blocks[]` array is passed to `BlockRenderer.svelte`. 6. Each block has a `type` field that maps to a Svelte component. **Implication:** To add a new page, you create a content entry (via Admin UI or API) — no new Svelte file or route config is needed. **Important:** When adding new page types, inspect both the frontend route/i18n layer and `api/hooks/config.js` (SSR route validation). A page can exist in the DB and still fail under SSR if the public URL shape and `content.path` mapping are not aligned. --- ## Adding a new page ### Option A: Via Admin UI (preferred for content editors) 1. Open the Nova admin at `https://{PROJECT_NAME}-tibiadmin.code.testversion.online/`. 2. Navigate to **Inhalte** (Content) collection. 3. Click **New** and fill in: - `name`: Display name (e.g. "Über uns") - `path`: URL path without language prefix (e.g. `/ueber-uns`) - `lang`: Language code (e.g. `de`) - `active`: `true` - `translationKey`: Shared key for cross-language linking (e.g. `about`) - `blocks`: Add content blocks (see below) - `meta.title` / `meta.description`: SEO metadata 4. Save. The page is immediately available at `/{lang}{path}`. **Nova authoring guidance:** - Prefer meaningful `meta.preview` and field `preview` configs so entries and nested blocks are understandable in breadcrumbs, foreign-key widgets, and arrays. - Use `containerProps.layout.size` to keep editors on one screen instead of stacking every field vertically. - Use `dependsOn` to hide block-specific fields until the relevant block type is selected. - Prefer drill-down editing for larger `object[]` structures instead of flat, folded arrays. ### Option B: Via API ```sh curl -X POST "$CODING_URL/api/content" \ -H "Token: $ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "active": true, "lang": "de", "name": "Über uns", "path": "/ueber-uns", "translationKey": "about", "blocks": [ { "type": "hero", "headline": "Über uns", "subline": "Unser Team" } ], "meta": { "title": "Über uns", "description": "Erfahre mehr über unser Team." } }' ``` ### Option C: Via mock data (for MOCK=1 mode) Add the entry to `frontend/mocking/content.json` — the mock engine supports MongoDB-style filtering. ### Adding to navigation To make the page appear in the header/footer menu, edit the corresponding `navigation` entry: ```sh # Get existing header nav curl "$CODING_URL/api/navigation?filter[type]=header&filter[language]=de" -H "Token: $ADMIN_TOKEN" # Look up the content entry ID for your page curl "$CODING_URL/api/content?filter[path]=/ueber-uns&filter[lang]=de" -H "Token: $ADMIN_TOKEN" # PUT to update elements array (add your page by FK id) curl -X PUT "$CODING_URL/api/navigation/" \ -H "Token: $ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "elements": [ ...existing, { "name": "Über uns", "page": "" } ] }' ``` If navigation drives the public website shell, treat navigation as page-critical SSR data. A page is not fully SSR-ready if only the main content entry exists but header/footer navigation is missing. ### Multi-language pages - Create one `ContentEntry` per language with the **same `translationKey`** but different `lang` and `path`. - The language switcher is path-based and derives the target URL from the current route plus `ROUTE_TRANSLATIONS`. - Add localized route slugs to `ROUTE_TRANSLATIONS` in `frontend/src/lib/i18n.ts` if URLs should differ per language (e.g. `/ueber-uns` vs `/about`). --- ## Adding a new content block type ### Step 1: Create the Svelte component Create `frontend/src/blocks/MyNewBlock.svelte`: ```svelte
{#if block.headline}

{block.headline}

{/if}
``` **Conventions:** - Accept `block: ContentBlockEntry` as the single prop. - Use `block.anchorId` for scroll anchoring. - Respect `block.containerWidth` (`""` = default, `"wide"`, `"full"`). - Guard browser-only code with `typeof window !== "undefined"` (SSR safety). ### Step 2: Register in BlockRenderer Edit `frontend/src/blocks/BlockRenderer.svelte`: ```svelte import MyNewBlock from "./MyNewBlock.svelte" {:else if block.type === "my-new-block"} ``` If block types become numerous, plan for grouping and registry discipline early. A real website built on this starter should not keep extending a demo-style renderer forever without structure. ### Step 3: Extend TypeScript types (if new fields are needed) Edit `types/global.d.ts` — add fields to `ContentBlockEntry`: ```typescript interface ContentBlockEntry { // ... existing fields ... // my-new-block fields myCustomField?: string myItems?: { title: string; description: string }[] } ``` ### Step 4: Extend collection YAML (if new fields need admin editing) Edit `api/collections/content.yml` — add subFields under `blocks`: ```yaml - name: blocks type: object[] subFields: # ... existing subFields ... - name: myCustomField type: string - name: myItems type: object[] meta: drillDown: true preview: title subFields: - name: title type: string - name: description type: string ``` Use current Nova patterns when extending block schemas: - `meta.preview` for entry and block previews - `meta.drillDown: true` for nested arrays that would otherwise become hard to edit - `containerProps.layout.size` for dense editor layouts - `dependsOn` for block-type-specific fields - collection- or field-level `meta.pagebuilder` for registry/default viewport settings ### Step 5: Update mock data (if using MOCK=1) Add a block with your new type to `frontend/mocking/content.json`. ### Step 6: Verify ```sh yarn validate # TypeScript check — must be warning-free ``` For blocks that appear on SSR pages, also verify: ```sh yarn build:server # then request the SSR endpoint directly and check that the block content appears in HTML ``` ### Existing block types for reference | Type | Component | Purpose | | -------------- | ------------------------- | ----------------------------------------- | | `hero` | `HeroBlock.svelte` | Full-width hero with image, headline, CTA | | `richtext` | `RichtextBlock.svelte` | Rich text with optional image | | `accordion` | `AccordionBlock.svelte` | Expandable FAQ/accordion items | | `contact-form` | `ContactFormBlock.svelte` | Contact form | --- ## Adding a new collection ### Step 1: Create collection YAML Create `api/collections/mycollection.yml`. Use `content.yml`, `navigation.yml`, or a current `tibi-admin-nova` example config as a template: ```yaml ######################################################################## # MyCollection — description of what this collection stores ######################################################################## name: mycollection meta: label: { de: "Meine Sammlung", en: "My Collection" } muiIcon: category # Material UI icon name viewHint: table preview: label: name table: - name - source: active label: Active permissions: public: methods: get: true # Public read access user: methods: get: true post: true put: true delete: true fields: - name: active type: boolean meta: label: { de: "Aktiv", en: "Active" } - name: name type: string meta: label: { de: "Name", en: "Name" } # Add more fields as needed ``` Use current Nova config: - `preview` for row/foreign/search display - object-form `singleton` - `sidebar` groups instead of ad hoc sidebars - `pagebuilder` defaults when a collection contains pagebuilder fields - `viewHint` plus `preview.table` for better admin ergonomics **Field types:** `string`, `number`, `boolean`, `object`, `object[]`, `string[]`, `file`, `file[]`. For the full schema reference: `tibi-types/schemas/config/collection.schema.json`. ### Step 2: Include in config.yml Edit `api/config.yml`: ```yaml collections: - !include collections/content.yml - !include collections/navigation.yml - !include collections/ssr.yml - !include collections/mycollection.yml # ← add this line ``` ### Step 3: Add TypeScript types Edit `types/global.d.ts`: ```typescript interface MyCollectionEntry { id?: string _id?: string active?: boolean name?: string // ... fields matching your YAML } ``` ### Step 4: Configure API layer (optional) If you need typed helpers, extend the `EntryTypeSwitch` in `frontend/src/lib/api.ts`: ```typescript type CollectionNameT = "medialib" | "content" | "mycollection" | string type EntryTypeSwitch = T extends "medialib" ? MedialibEntry : T extends "content" ? ContentEntry : T extends "mycollection" ? MyCollectionEntry : Record ``` ### Step 5: Add hooks (optional) Common hook patterns: - **Public filter** — reuse `filter_public.js` to enforce `active: true` for unauthenticated users. - **Write validation** — add method/step hook files such as `api/hooks/mycollection/post_validate.js` or `api/hooks/mycollection/put_validate.js`. - **Cache invalidation** — add your collection to `api/hooks/clear_cache.js` if it affects rendered pages. - **Action endpoints** — prefer `actions:` instead of fake collections when you need forms, newsletters, calculators, imports, or other endpoint-like behavior without CRUD storage. Reference hook in YAML: ```yaml hooks: get: read: type: javascript file: hooks/filter_public.js put: update: type: javascript file: hooks/clear_cache.js post: create: type: javascript file: hooks/clear_cache.js delete: delete: type: javascript file: hooks/clear_cache.js ``` ### Step 6: Add mock data (if using MOCK=1) Create `frontend/mocking/mycollection.json`: ```json [{ "_id": "1", "active": true, "name": "Example Entry" }] ``` ### Step 7: Verify ```sh yarn validate # TypeScript check # If Docker is running, the tibi-server auto-reloads the collection config ``` For collections intended for rich editorial usage, also verify in Nova: - list/table/card previews are readable - nested arrays are editable with drill-down where needed - sidebar groups and layout are usable without scrolling through one long form - foreign-key displays use meaningful previews - pagebuilder fields render previews and screenshots correctly --- ## Common pitfalls - **Path format**: Content paths do NOT include the language prefix. The path `/ueber-uns` becomes `/{lang}/ueber-uns` via the i18n layer. - **Active flag**: Pages with `active: false` are filtered out by `filter_public.js` for public users. The admin can still see them. - **Block `hide` field**: Blocks with `hide: true` are skipped by `BlockRenderer.svelte` — useful for draft blocks. - **Collection YAML indentation**: YAML uses 2-space indentation. Sub-fields under `object[]` require a `subFields` key. - **After adding a collection**: The tibi-server auto-reloads hooks on file change, but a new collection in `config.yml` may require `make docker-restart-frontend` or a full `make docker-up`. - **Do not fake forms as collections** if they are really endpoint logic. Use `actions:` when no CRUD collection is needed. - **Do not overfit to demo blocks**. Real projects should shape block schemas and admin ergonomics around actual editor workflows.