✨ 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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user