15 KiB
name, description
| name | description |
|---|---|
| media-seo-publishing | Model media, SEO, and publishing workflows for website projects on this starter. Covers file fields, image validation/filtering, alt texts, social metadata, publication windows, and SSR/cache implications. |
media-seo-publishing
When to use this skill
Use this skill when:
- Designing media-heavy website content models
- Adding image/file fields and image filters
- Modeling SEO fields for pages or reusable content
- 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:
uploadPathbelongs on the collection itself, not on the individualtype: filefield- in this starter, collection YAML files live in
api/collections/, while deploy syncs the repo-rootmedia/directory - the current starter collections can omit
uploadPathand rely on the tibi-server default derived from project config - if you explicitly override
uploadPathin 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:
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:
name: content
uploadPath: ../media/content
fields:
- name: _pagebuilderThumbnail
type: file
Tibi then stores uploads below:
{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:
<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:
<MedialibImage entry={resolvedEntry} class="w-full h-full object-cover" noPlaceholder />
The preferred flow is:
- request medialib references with
lookup - pass the resolved
MedialibEntryto the shared widget - let that widget own URL resolution, filter choice, SSR markup, and admin/pagebuilder compatibility
Typical usage:
<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:
const entries = await getCachedEntries<CollectionName>("your-collection", {
filter: { active: true },
sort: "sortOrder",
lookup: "imageField:medialib",
})
getCachedEntries() expects lookup as an option in the second arguments 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:
- Prefer a resolved
MedialibEntryfrom_lookup - If
entry.file.srcis already absolute, use it directly - Otherwise construct the file URL from
{apiBase}/medialib/{entryId}/{file.src}inside the shared widget - 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.
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:
- explicit
filterprop wins when the caller already knows the right output size - otherwise pass a meaningful
minWidthfor layout-stable contexts such as hero, card, or gallery slots - let the shared widget derive the final filter from the measured width on the client
- keep the width-to-filter mapping centralized in the widget instead of repeating
xs/s/m/l/xllogic 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:
- render a normal
imgwith a realsrcin SSR when a filter is explicit,minWidthis known, or admin rendering requires a fallback filter - emit a
noscriptfallback for raster images so crawlers and JS-disabled clients still receive a concrete image URL - 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
apiBaseOverrideor 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
_lookupdata 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:
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.
For real website projects, these concerns affect:
- collection schema
- admin ergonomics
- frontend rendering
- SSR/cache validity
- editorial quality
Source of truth
Use these sources when implementing or reviewing these areas:
tibi-server/docs/08-file-upload-images.md- the project's medialib collection config
- the relevant content or domain collection configs
tibi-admin-nova/types/admin.d.tstibi-admin-nova/docs/collection-config.md- the project's SSR/runtime config hooks
- the project's frontend media widget/helper implementation
Media modeling
Use file fields deliberately.
Typical choices:
filefor a single image or assetfile[]for galleries or multi-asset attachments- foreign references to a media collection when assets need their own lifecycle or reuse
Choose between inline file fields and dedicated media references based on reuse and editorial workflow, not just convenience.
File validation rules
For serious website builds, do not leave file fields unconstrained.
Define validators where appropriate:
- accepted mime types
- max file size
- min/max image dimensions
- whether mixed media is allowed
This should reflect actual content needs. Hero images, logos, documents, and gallery media often need different constraints.
Image filters
If the project serves resized or transformed assets, define image filters intentionally.
Use filters for:
- thumbnails
- card images
- hero images
- OpenGraph/social images when relevant
Do not leave every consuming component to invent its own ad hoc asset sizes.
Alt texts and captions
Accessibility and SEO-relevant image metadata should be explicit in the model.
Recommended approach:
- store alt text explicitly
- keep captions separate from alt text
- use localized fields if the site is multilingual
- optionally use AI assistance only as a suggestion flow
Do not treat filenames as acceptable alt text.
SEO modeling
Page-like collections should usually model SEO explicitly.
Typical fields:
meta.titlemeta.description- social/share image
- optional canonical information if required
- optional index/follow controls for advanced projects
SEO fields should be easy to find in Nova, usually via sidebar groups or clearly named sections.
Publishing model
If the site uses publication timing, define it intentionally.
Typical concerns:
- draft versus active state
- publication window (
from/to) - visibility of unpublished content in public reads
- SSR cache validity for time-sensitive content
Publishing is not just a boolean. If publication windows exist, they must influence runtime and cache behavior.
SSR implications
Media, SEO, and publishing affect SSR directly.
Examples:
- page meta tags must exist in SSR HTML when relevant
- navigation or content with publication windows must invalidate cached HTML correctly
- image-driven blocks must render stable URLs/markup in SSR
If publication timing can make cached HTML stale, the relevant collections must be accounted for in SSR publish-check logic.
Admin ergonomics
Use current Nova features to make media/SEO workflows usable:
- sidebar groups for SEO/publication fields
viewHint.mediafor media-focused collections- previews for image-bearing entities
- layout grouping so editors do not scroll through one long file/SEO form
Media and SEO fields are often technically present but operationally poor if the admin layout is ignored.
Recommended modeling patterns
Marketing page
Recommended shape:
- main content blocks
- explicit SEO object or fields
- hero/share image strategy
- publication controls in sidebar
Media library entry
Recommended shape:
- file field
- title/name
- alt text / caption
- optional copyright/source
- image-focused admin view
Reusable teaser or card entity
Recommended shape:
- image reference or file
- short label/title
- teaser text
- consistent image filter usage in frontend components
Anti-patterns
- No file validators on public/editor-uploaded images
- 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 hardcodingfile/fileinstead 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:
- currently published entry
- future-scheduled entry
- expired entry
- 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:
- Upload validation matches the intended asset type.
- Image filters are named and used consistently.
- Shared image widgets receive resolved entries instead of rebuilding URLs ad hoc.
- Alt/caption/SEO fields are explicit and editor-friendly.
- Publication state affects public output correctly.
- SSR HTML still reflects the intended published state.
- Admin/pagebuilder preview resolves medialib images correctly.
yarn validatestays clean.
What an LLM should inspect first
When asked to work on media, SEO, or publishing on this starter, inspect in this order:
tibi-server/docs/08-file-upload-images.md- the relevant collection YAML
- the shared media widget/helper layer used by the frontend
- admin layout and previews for those fields
- frontend components consuming the media/SEO data
- SSR publish-check and invalidation logic if timing matters
This prevents “just add an image field” changes that break runtime, editorial UX, or caching.