- Updated `tibi-project-setup` skill to clarify project initialization goals and steps. - Improved `tibi-ssr-caching` skill to detail SSR architecture, responsibilities, and caching mechanisms. - Introduced `website-solution-architecture` skill for translating website requirements into coherent solutions. - Refined `AGENTS.md` to provide a structured roadmap for project development phases. - Added `ADMIN_ASSET_VERSION` to `api/config.yml.env` for asset versioning. - Updated SSR request flow and cache invalidation logic in `api/hooks/ssr/AGENTS.md`. - Removed obsolete `esbuild.config.admin.js` and integrated asset versioning into the main `esbuild.config.js`. - Adjusted `api/collections/content.yml` to utilize asset versioning for admin scripts.
13 KiB
name, description
| name | description |
|---|---|
| content-authoring | 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:
- Pages are CMS entries in the
contentcollection with apathfield. - Public URLs are typically language-prefixed (
/de/...,/en/...), but the DB entry incontent.pathis stored without that language prefix. App.sveltereacts to URL changes → strips the language prefix → callsgetCachedEntries("content", { lang, path, active: true }).- The same loading path is used for browser navigation and SSR.
- The matching
ContentEntry.blocks[]array is passed toBlockRenderer.svelte. - Each block has a
typefield 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)
- Open the Nova admin at
https://{PROJECT_NAME}-tibiadmin.code.testversion.online/. - Navigate to Inhalte (Content) collection.
- 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:truetranslationKey: Shared key for cross-language linking (e.g.about)blocks: Add content blocks (see below)meta.title/meta.description: SEO metadata
- Save. The page is immediately available at
/{lang}{path}.
Nova authoring guidance:
- Prefer meaningful
meta.previewand fieldpreviewconfigs so entries and nested blocks are understandable in breadcrumbs, foreign-key widgets, and arrays. - Use
containerProps.layout.sizeto keep editors on one screen instead of stacking every field vertically. - Use
dependsOnto 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
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:
# 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/<id>" \
-H "Token: $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "elements": [ ...existing, { "name": "Über uns", "page": "<content-id>" } ] }'
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
ContentEntryper language with the sametranslationKeybut differentlangandpath. - The language switcher is path-based and derives the target URL from the current route plus
ROUTE_TRANSLATIONS. - Add localized route slugs to
ROUTE_TRANSLATIONSinfrontend/src/lib/i18n.tsif URLs should differ per language (e.g./ueber-unsvs/about).
Adding a new content block type
Step 1: Create the Svelte component
Create frontend/src/blocks/MyNewBlock.svelte:
<script lang="ts">
let { block }: { block: ContentBlockEntry } = $props()
</script>
<section class="py-16 sm:py-24" id={block.anchorId || undefined}>
<div class="max-w-6xl mx-auto px-6">
{#if block.headline}
<h2 class="text-3xl font-bold mb-6">{block.headline}</h2>
{/if}
<!-- Block-specific content here -->
</div>
</section>
Conventions:
- Accept
block: ContentBlockEntryas the single prop. - Use
block.anchorIdfor 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:
<!-- Add import at the top -->
import MyNewBlock from "./MyNewBlock.svelte"
<!-- Add case in the {#each} block -->
{:else if block.type === "my-new-block"}
<MyNewBlock {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:
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:
- 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.previewfor entry and block previewsmeta.drillDown: truefor nested arrays that would otherwise become hard to editcontainerProps.layout.sizefor dense editor layoutsdependsOnfor block-type-specific fields- collection- or field-level
meta.pagebuilderfor 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
yarn validate # TypeScript check — must be warning-free
For blocks that appear on SSR pages, also verify:
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:
########################################################################
# 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:
previewfor row/foreign/search display- object-form
singleton sidebargroups instead of ad hoc sidebarspagebuilderdefaults when a collection contains pagebuilder fieldsviewHintpluspreview.tablefor 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:
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:
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:
type CollectionNameT = "medialib" | "content" | "mycollection" | string
type EntryTypeSwitch<T extends string> = T extends "medialib"
? MedialibEntry
: T extends "content"
? ContentEntry
: T extends "mycollection"
? MyCollectionEntry
: Record<string, unknown>
Step 5: Add hooks (optional)
Common hook patterns:
- Public filter — reuse
filter_public.jsto enforceactive: truefor unauthenticated users. - Write validation — add method/step hook files such as
api/hooks/mycollection/post_validate.jsorapi/hooks/mycollection/put_validate.js. - Cache invalidation — add your collection to
api/hooks/clear_cache.jsif 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:
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:
[{ "_id": "1", "active": true, "name": "Example Entry" }]
Step 7: Verify
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-unsbecomes/{lang}/ueber-unsvia the i18n layer. - Active flag: Pages with
active: falseare filtered out byfilter_public.jsfor public users. The admin can still see them. - Block
hidefield: Blocks withhide: trueare skipped byBlockRenderer.svelte— useful for draft blocks. - Collection YAML indentation: YAML uses 2-space indentation. Sub-fields under
object[]require asubFieldskey. - After adding a collection: The tibi-server auto-reloads hooks on file change, but a new collection in
config.ymlmay requiremake docker-restart-frontendor a fullmake 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.