--- name: media-seo-publishing description: 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/` 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 ``` `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 ``` 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 {#if block._lookup?.image} {/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("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: 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. 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. ## 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 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.