feat: enhance medialib image handling and add asset URL resolution

- Implemented `resolveApiAssetUrl` function to normalize asset URLs based on API base.
- Updated `MedialibImage` component to utilize new asset URL resolution and added support for alt text and class properties.
- Enhanced image loading behavior with improved width measurement and focal point handling.
- Added placeholder image handling and improved accessibility with alt text.
- Introduced new test script for auditing broken links in skill documentation.
- Expanded seeded test content to include medialib entries and updated related tests for pagebuilder previews.
- Improved global setup and teardown logging for clarity on seeded content management.
This commit is contained in:
2026-05-17 00:52:41 +00:00
parent 958b45272d
commit 4020ad62c5
44 changed files with 4276 additions and 867 deletions
+124 -9
View File
@@ -1,6 +1,6 @@
---
name: content-authoring
description: Add new pages, content blocks, and collections to a tibi project. Covers the content-based routing model, block registration in BlockRenderer, collection YAML authoring, and TypeScript type definitions. Use when creating new pages, block types, or collections.
description: Add new pages, content blocks, and collections to a tibi project. Covers the content-based routing model, block registration in BlockRenderer and frontend/src/admin.ts, lookup-aware reference modeling, collection YAML authoring, and TypeScript type ownership. Use when creating new pages, block types, or collections.
---
# content-authoring
@@ -29,6 +29,20 @@ This project does **NOT** use file-based routing (no SvelteKit router). Instead:
**Important:** When adding new page types, inspect both the frontend route/i18n layer and `api/hooks/config.js` (SSR route validation). A page can exist in the DB and still fail under SSR if the public URL shape and `content.path` mapping are not aligned.
## Cross-surface ownership rule
For real project work, treat content authoring as a multi-surface contract.
When you add or change blocks, pages, or collections, check these surfaces together:
1. collection YAML in `api/collections/*.yml`
2. type ownership in `types/global.d.ts`
3. typed API mapping in `frontend/src/lib/api.ts` via `EntryTypeSwitch`
4. public rendering in `frontend/src/blocks/BlockRenderer.svelte`
5. admin pagebuilder preview in `frontend/src/admin.ts`
If one of these surfaces is skipped, the project often still looks half-correct until SSR, admin preview, or typed API usage exposes the mismatch.
---
## Adding a new page
@@ -148,7 +162,30 @@ import MyNewBlock from "./MyNewBlock.svelte"
If block types become numerous, plan for grouping and registry discipline early. A real website built on this starter should not keep extending a demo-style renderer forever without structure.
### Step 3: Extend TypeScript types (if new fields are needed)
### Step 3: Register in the admin block registry
If the block is authored through a pagebuilder field, also register it in `frontend/src/admin.ts`.
Example:
```ts
const blockRegistry = {
hero: createContentBlockDefinition({ label: "Hero", icon: "image", color: "#1d4ed8" }),
"my-new-block": createContentBlockDefinition({
label: "My New Block",
icon: "view_compact",
color: "#0f766e",
}),
}
```
Important:
- `BlockRenderer.svelte` controls public rendering
- `frontend/src/admin.ts` controls Nova pagebuilder preview availability
- both should point at the same block contract instead of drifting into separate preview-only logic
### Step 4: Extend TypeScript types (if new fields are needed)
Edit `types/global.d.ts` — add fields to `ContentBlockEntry`:
@@ -161,7 +198,9 @@ interface ContentBlockEntry {
}
```
### Step 4: Extend collection YAML (if new fields need admin editing)
If the change also introduces a new collection or new API usage surface, update the corresponding entry interfaces in the same change instead of leaving `Record<string, unknown>` as a long-term placeholder.
### Step 5: Extend collection YAML (if new fields need admin editing)
Edit `api/collections/content.yml` — add subFields under `blocks`:
@@ -192,11 +231,13 @@ Use current Nova patterns when extending block schemas:
- `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)
When blocks contain foreign references such as medialib images, model the reference path deliberately so later loaders can request the needed `lookup` data.
### Step 6: Update mock data (if using MOCK=1)
Add a block with your new type to `frontend/mocking/content.json`.
### Step 6: Verify
### Step 7: Verify
```sh
yarn validate # TypeScript check — must be warning-free
@@ -209,6 +250,12 @@ yarn build:server
# then request the SSR endpoint directly and check that the block content appears in HTML
```
For blocks that are authored in pagebuilder and use images or foreign references, also verify:
- the block appears in the admin chooser
- the preview renders in Nova
- image/reference data is present through the intended lookup path
### Existing block types for reference
| Type | Component | Purpose |
@@ -309,17 +356,21 @@ interface MyCollectionEntry {
If you need typed helpers, extend the `EntryTypeSwitch` in `frontend/src/lib/api.ts`:
```typescript
type CollectionNameT = "medialib" | "content" | "mycollection" | string
type CollectionNameT = "medialib" | "content" | "navigation" | "mycollection" | string
type EntryTypeSwitch<T extends string> = T extends "medialib"
? MedialibEntry
: T extends "content"
? ContentEntry
: T extends "mycollection"
? MyCollectionEntry
: Record<string, unknown>
: T extends "navigation"
? NavigationEntry
: T extends "mycollection"
? MyCollectionEntry
: Record<string, unknown>
```
Do not treat `EntryTypeSwitch` as optional cleanup. If the frontend or tests consume the collection in a typed way, update this mapping in the same change.
### Step 5: Add hooks (optional)
Common hook patterns:
@@ -374,8 +425,28 @@ For collections intended for rich editorial usage, also verify in Nova:
- foreign-key displays use meaningful previews
- pagebuilder fields render previews and screenshots correctly
If the collection feeds public pages or admin block previews, also verify that the typed API helpers and runtime components agree on the same data shape.
---
## Seed data pattern (Playwright)
Test seed data uses `_testdata: true` as a hidden marker field. **Real content must NEVER use this flag** — otherwise test teardown will delete it.
```yaml
# Last field in every collection schema
- name: _testdata
type: boolean
meta:
hide: true
```
Test setup:
1. `globalSetup` removes entries with `_testdata: true`, then creates new test entries
2. `globalTeardown` removes entries with `_testdata: true`
3. Real editorial content has no `_testdata` field → survives all test runs
## Common pitfalls
- **Path format**: Content paths do NOT include the language prefix. The path `/ueber-uns` becomes `/{lang}/ueber-uns` via the i18n layer.
@@ -385,3 +456,47 @@ For collections intended for rich editorial usage, also verify in Nova:
- **After adding a collection**: The tibi-server auto-reloads hooks on file change, but a new collection in `config.yml` may require `make docker-restart-frontend` or a full `make docker-up`.
- **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.
## API lookup für aufgelöste Referenzen
Beim Laden von Collections können Fremdschlüssel via `lookup`-Parameter automatisch aufgelöst werden. Der `lookup`-Parameter wird als 8. Argument an `getCachedEntries` übergeben:
```ts
const products = await getCachedEntries<"machines">(
"machines",
{ active: true, category: catId },
"sortOrder",
undefined,
undefined,
undefined,
undefined,
"images:medialib" // lookup: "feld:collection"
)
```
Das Format ist `"feldname:zielcollection"` (z.B. `"images:medialib"`). Die aufgelösten Daten landen in `entry._lookup.feldname` als Array der Ziel-Collection-Objekte. Ohne lookup bleiben `string[]`-Felder reine ID-Arrays.
Wichtig: der `lookup`-Parameter muss auch in `getDBEntries` und `apiRequest` durchgereicht werden (siehe `api.ts`).
Für blockbasierte Inhalte ist der Lookup-Pfad oft verschachtelt, nicht flach. Beispiel:
```ts
const entries = await getCachedEntries<"content">(
"content",
{ active: true, path: "/preview-page" },
"sort",
undefined,
1,
undefined,
undefined,
"blocks.heroImage.image:medialib"
)
```
Merke:
- flache Relationen nutzen Pfade wie `images:medialib`
- block- oder objektverschachtelte Relationen nutzen Dot-Paths wie `blocks.heroImage.image:medialib`
- ohne den passenden Lookup fehlen Admin-Preview, SSR oder Frontend-Rendern oft erst zur Laufzeit
Treat public rendering, SSR rendering, and admin preview as the same reference contract whenever possible. If a block renders a medialib image in the site, the admin preview should usually depend on the same resolved media assumption instead of inventing a separate preview-only data path.