forked from cms/tibi-svelte-starter
✨ 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:
@@ -15,6 +15,239 @@ Use this skill when:
|
||||
- Defining publication windows and how they interact with runtime and SSR
|
||||
- Building authoring workflows around images, metadata, and release control
|
||||
|
||||
## Medialib file serving
|
||||
|
||||
Images uploaded to medialib (via base64 or multipart) are stored with a relative `file.src` path such as `"file/example.jpg"`. In this starter, `MedialibImage` resolves that stored path together with the medialib entry ID into the project-local URL:
|
||||
|
||||
```
|
||||
/api/medialib/{id}/file/example.jpg
|
||||
```
|
||||
|
||||
Responsive image filters are then applied by the shared widget as query params:
|
||||
|
||||
```
|
||||
/api/medialib/{id}/file/example.jpg?filter=s-webp
|
||||
/api/medialib/{id}/file/example.jpg?filter=m-webp
|
||||
/api/medialib/{id}/file/example.jpg?filter=l-webp
|
||||
```
|
||||
|
||||
Those `/api/...` URLs are starter-local proxy URLs. For frontend rendering in Tibi website projects, the preferred approach is **not** to hand-build medialib URLs in each block or route component.
|
||||
|
||||
## Collection uploadPath
|
||||
|
||||
For Tibi file fields, the storage root is configured per collection via top-level `uploadPath`.
|
||||
|
||||
Important rules:
|
||||
|
||||
- `uploadPath` belongs on the collection itself, not on the individual `type: file` field
|
||||
- in this starter, collection YAML files live in `api/collections/`, while deploy syncs the repo-root `media/` directory
|
||||
- the current starter collections can omit `uploadPath` and rely on the tibi-server default derived from project config
|
||||
- if you explicitly override `uploadPath` in this starter, it should normally point to `../media/<collection-name>` so deploy and runtime stay aligned
|
||||
- do not write uploads into `api/media`; that path is not the persistent deploy target used by the project
|
||||
|
||||
Explicit override example:
|
||||
|
||||
```yaml
|
||||
name: medialib
|
||||
uploadPath: ../media/medialib
|
||||
|
||||
fields:
|
||||
- name: file
|
||||
type: file
|
||||
```
|
||||
|
||||
For hidden thumbnails stored on the `content` collection, the same rule applies when you choose an explicit override at collection level:
|
||||
|
||||
```yaml
|
||||
name: content
|
||||
uploadPath: ../media/content
|
||||
|
||||
fields:
|
||||
- name: _pagebuilderThumbnail
|
||||
type: file
|
||||
```
|
||||
|
||||
Tibi then stores uploads below:
|
||||
|
||||
```text
|
||||
{uploadPath}/{entryId}/{fieldName}/{filename}
|
||||
```
|
||||
|
||||
So an explicitly overridden medialib upload typically lands at `../media/medialib/{entryId}/file/{filename}`, while a content thumbnail lands at `../media/content/{entryId}/_pagebuilderThumbnail/{filename}`.
|
||||
|
||||
## Preferred frontend integration
|
||||
|
||||
Use a shared media widget such as `MedialibImage` as the frontend boundary for medialib-rendered images.
|
||||
|
||||
### MedialibImage with minimal entry (ID only)
|
||||
|
||||
When only a medialib ID is available (no resolved `_lookup` entry), pass a minimal entry structure together with the `id` prop:
|
||||
|
||||
```svelte
|
||||
<MedialibImage
|
||||
id={medialibId}
|
||||
entry={{ file: { src: "file/example.jpg", type: "image/jpeg" } }}
|
||||
class="w-full h-full object-cover"
|
||||
noPlaceholder
|
||||
/>
|
||||
```
|
||||
|
||||
`resolveFileSrc()` in `MedialibImage` kombiniert `entry.file.src` (zum Beispiel `"file/example.jpg"`) mit der `id` zur korrekten URL: `${apiBase}/medialib/{id}/{src}`. Filter werden im Widget als `?filter=...` angehängt. Wenn `_lookup`-Daten mit vollständigem Entry verfügbar sind, bevorzugt diese verwenden:
|
||||
|
||||
```svelte
|
||||
<MedialibImage entry={resolvedEntry} class="w-full h-full object-cover" noPlaceholder />
|
||||
```
|
||||
|
||||
The preferred flow is:
|
||||
|
||||
1. request medialib references with `lookup`
|
||||
2. pass the resolved `MedialibEntry` to the shared widget
|
||||
3. let that widget own URL resolution, filter choice, SSR markup, and admin/pagebuilder compatibility
|
||||
|
||||
Typical usage:
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import MedialibImage from "../widgets/MedialibImage.svelte"
|
||||
|
||||
let { block }: { block: ContentBlockEntry } = $props()
|
||||
</script>
|
||||
|
||||
{#if block._lookup?.image}
|
||||
<MedialibImage
|
||||
entry={block._lookup.image}
|
||||
class="w-full h-full object-cover"
|
||||
minWidth={900}
|
||||
lazy={true}
|
||||
/>
|
||||
{/if}
|
||||
```
|
||||
|
||||
For repeated collection data such as galleries, teaser lists, or detail-page image arrays, also request the lookup instead of rendering from raw ID strings:
|
||||
|
||||
```ts
|
||||
const entries = await getCachedEntries<CollectionName>(
|
||||
"your-collection",
|
||||
{ active: true },
|
||||
"sortOrder",
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
"imageField:medialib"
|
||||
)
|
||||
```
|
||||
|
||||
`getCachedEntries()` expects `lookup` as the 8th argument and as a string, not as part of the `params` object.
|
||||
|
||||
Then consume the resolved entry from `_lookup`, for example `_lookup.imageField` or `_lookup.imageField?.[0]` depending on whether the schema stores one image or an array.
|
||||
|
||||
### URL resolution strategy (frontend)
|
||||
|
||||
At the shared widget boundary, resolve image data with this priority:
|
||||
|
||||
1. Prefer a resolved `MedialibEntry` from `_lookup`
|
||||
2. If `entry.file.src` is already absolute, use it directly
|
||||
3. Otherwise construct the file URL from `{apiBase}/medialib/{entryId}/{file.src}` inside the shared widget
|
||||
4. Only fall back to raw ID-to-URL construction in legacy code paths that cannot yet pass resolved entries
|
||||
|
||||
Do not duplicate medialib URL logic in every block. Keep it in one widget/helper layer so SSR, admin preview, and filter behavior stay consistent.
|
||||
|
||||
```ts
|
||||
function resolveFileSrc(src: string | undefined, entryId: string | undefined, apiBase: string): string | null {
|
||||
if (!src) return null
|
||||
if (/^(https?:)?\/\//.test(src) || src.startsWith("/")) return src
|
||||
if (!entryId) return null
|
||||
return `${apiBase.replace(/\/+$/, "")}/medialib/${entryId}/${src.replace(/^\/+/, "")}`
|
||||
}
|
||||
```
|
||||
|
||||
### Lookup parameter
|
||||
|
||||
Use the `lookup` API parameter to resolve medialib references automatically:
|
||||
|
||||
```
|
||||
/api/{collection}?lookup={fieldPath}:medialib
|
||||
```
|
||||
|
||||
The resolved data is available in the `_lookup` field of the returned object at the corresponding path.
|
||||
|
||||
Typical project patterns:
|
||||
|
||||
- pagebuilder blocks with nested image refs: `blocks.someImageField:medialib`
|
||||
- collection entries with a single image field: `image:medialib`
|
||||
- collection entries with image arrays: `images:medialib`
|
||||
- attachment arrays or document previews: `attachments:medialib`
|
||||
|
||||
Prefer adding the lookup at the data-loading boundary rather than rehydrating IDs later in the component tree.
|
||||
|
||||
## Filter sizing strategy
|
||||
|
||||
Do not hardcode one fixed filter per component unless the rendered width is truly fixed.
|
||||
|
||||
Preferred rules:
|
||||
|
||||
1. explicit `filter` prop wins when the caller already knows the right output size
|
||||
2. otherwise pass a meaningful `minWidth` for layout-stable contexts such as hero, card, or gallery slots
|
||||
3. let the shared widget derive the final filter from the measured width on the client
|
||||
4. keep the width-to-filter mapping centralized in the widget instead of repeating `xs/s/m/l/xl` logic in blocks
|
||||
|
||||
This keeps image payloads reasonable without forcing each block author to manually guess the correct filter.
|
||||
|
||||
Typical examples:
|
||||
|
||||
- hero/background image: `minWidth={1600}`
|
||||
- card image: `minWidth={640}`
|
||||
- product detail main gallery image: `minWidth={960}`
|
||||
- thumbnail image: `minWidth={240}`
|
||||
|
||||
The exact breakpoints can vary per project, but the sizing logic should remain centralized.
|
||||
|
||||
## SSR and no-JS behavior
|
||||
|
||||
For raster images, SSR cannot always know the final client width. The shared widget should therefore render deterministically:
|
||||
|
||||
1. render a normal `img` with a real `src` in SSR when a filter is explicit, `minWidth` is known, or admin rendering requires a fallback filter
|
||||
2. emit a `noscript` fallback for raster images so crawlers and JS-disabled clients still receive a concrete image URL
|
||||
3. avoid unstable per-block SSR hacks that guess widths differently from the client
|
||||
|
||||
This is especially important for image-bearing blocks, teasers, galleries, and detail views where HTML must remain stable between SSR, hydration, and no-JS rendering.
|
||||
|
||||
## Admin and pagebuilder compatibility
|
||||
|
||||
Medialib rendering must also work inside admin/pagebuilder preview contexts, not only on the public website.
|
||||
|
||||
Important rules:
|
||||
|
||||
- respect `apiBaseOverride` or the admin-provided project base when constructing medialib URLs
|
||||
- do not prepend frontend public paths blindly when the admin already passes an absolute file URL
|
||||
- consume hydrated `_lookup` data directly in preview/runtime code instead of trying to re-fetch references inside preview components
|
||||
- keep placeholder asset resolution admin-safe too, not just medialib file URLs
|
||||
|
||||
If a block preview loads collection data in the admin, the preview context must provide the project-specific API base and the frontend code must route requests through that base.
|
||||
|
||||
In practice, that means the media widget and any helper used by it should be aware of `apiBaseOverride` or an equivalent admin-provided API base so the same component works in:
|
||||
|
||||
- public frontend
|
||||
- SSR render
|
||||
- admin pagebuilder preview
|
||||
- collection-driven block previews
|
||||
|
||||
### Base64 file upload via API
|
||||
|
||||
Upload images to medialib via JSON API by including base64 data inline:
|
||||
|
||||
```json
|
||||
POST /api/medialib
|
||||
{
|
||||
"file": { "src": "data:image/jpeg;base64,..." },
|
||||
"title": "Image title",
|
||||
"alt": "Alt text"
|
||||
}
|
||||
```
|
||||
|
||||
The tibi-server saves the file below the collection's `uploadPath`, for example `../media/medialib/{entryId}/file/{filename}`, and creates the medialib entry.
|
||||
|
||||
## Goal
|
||||
|
||||
The goal is to make media, SEO, and publishing part of the actual solution design instead of leaving them as late add-ons.
|
||||
@@ -32,11 +265,12 @@ For real website projects, these concerns affect:
|
||||
Use these sources when implementing or reviewing these areas:
|
||||
|
||||
- `tibi-server/docs/08-file-upload-images.md`
|
||||
- `api/collections/medialib.yml`
|
||||
- `api/collections/content.yml`
|
||||
- the project's medialib collection config
|
||||
- the relevant content or domain collection configs
|
||||
- `tibi-admin-nova/types/admin.d.ts`
|
||||
- `tibi-admin-nova/docs/collection-config.md`
|
||||
- `api/hooks/config.js`
|
||||
- `api/hooks/lib/ssr-server.js`
|
||||
- the project's SSR/runtime config hooks
|
||||
- the project's frontend media widget/helper implementation
|
||||
|
||||
## Media modeling
|
||||
|
||||
@@ -175,19 +409,37 @@ Recommended shape:
|
||||
- No explicit alt field
|
||||
- Mixing captions and alt text into one field
|
||||
- Hardcoding image sizes only in frontend CSS/components
|
||||
- Building raw `/api/medialib/{id}/{src}` URLs in every block or hardcoding `file/file` instead of using one shared widget
|
||||
- Repeating filter breakpoint logic in multiple components
|
||||
- Rendering public image URLs in frontend code that break in admin pagebuilder preview
|
||||
- Treating publication as frontend-only logic
|
||||
- Forgetting that publish windows can invalidate SSR HTML
|
||||
|
||||
## Publication verification matrix
|
||||
|
||||
If the project uses publication timing, do not verify only one happy-path entry.
|
||||
|
||||
Check a small matrix deliberately:
|
||||
|
||||
1. currently published entry
|
||||
2. future-scheduled entry
|
||||
3. expired entry
|
||||
4. token/admin visibility when editorial or operator access should still exist
|
||||
|
||||
This keeps publication modeling tied to the real public-read and SSR behavior instead of to optimistic field design.
|
||||
|
||||
## Verification checklist
|
||||
|
||||
After changing media/SEO/publishing behavior, verify all of these:
|
||||
|
||||
1. Upload validation matches the intended asset type.
|
||||
2. Image filters are named and used consistently.
|
||||
3. Alt/caption/SEO fields are explicit and editor-friendly.
|
||||
4. Publication state affects public output correctly.
|
||||
5. SSR HTML still reflects the intended published state.
|
||||
6. `yarn validate` stays clean.
|
||||
3. Shared image widgets receive resolved entries instead of rebuilding URLs ad hoc.
|
||||
4. Alt/caption/SEO fields are explicit and editor-friendly.
|
||||
5. Publication state affects public output correctly.
|
||||
6. SSR HTML still reflects the intended published state.
|
||||
7. Admin/pagebuilder preview resolves medialib images correctly.
|
||||
8. `yarn validate` stays clean.
|
||||
|
||||
## What an LLM should inspect first
|
||||
|
||||
@@ -195,8 +447,9 @@ When asked to work on media, SEO, or publishing on this starter, inspect in this
|
||||
|
||||
1. `tibi-server/docs/08-file-upload-images.md`
|
||||
2. the relevant collection YAML
|
||||
3. admin layout and previews for those fields
|
||||
4. frontend components consuming the media/SEO data
|
||||
5. SSR publish-check and invalidation logic if timing matters
|
||||
3. the shared media widget/helper layer used by the frontend
|
||||
4. admin layout and previews for those fields
|
||||
5. frontend components consuming the media/SEO data
|
||||
6. SSR publish-check and invalidation logic if timing matters
|
||||
|
||||
This prevents “just add an image field” changes that break runtime, editorial UX, or caching.
|
||||
|
||||
Reference in New Issue
Block a user