Files
my-notes-viewer/.agents/skills/media-seo-publishing/SKILL.md
T
apairon 4020ad62c5 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.
2026-05-17 00:52:41 +00:00

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:

  • 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:

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:

  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:

<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",
    { 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.

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:

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.ts
  • tibi-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:

  • file for a single image or asset
  • file[] 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.title
  • meta.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.media for 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.

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 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. 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

When asked to work on media, SEO, or publishing on this starter, inspect in this order:

  1. tibi-server/docs/08-file-upload-images.md
  2. the relevant collection YAML
  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.