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:
2026-05-17 00:52:41 +00:00
parent 958b45272d
commit 4020ad62c5
44 changed files with 4276 additions and 867 deletions
@@ -198,7 +198,7 @@ The exact shape can vary, but the pattern stays the same: block type first, then
### Step 3a: Build and wire the block registry
For this starter, the pagebuilder registry is not implicit. Nova loads it from the admin bundle via `meta.pagebuilder.blockRegistry.file`.
In current Tibi/Nova projects, the pagebuilder registry is typically not implicit. Nova loads it from the admin bundle via `meta.pagebuilder.blockRegistry.file`.
The concrete chain is:
@@ -207,7 +207,7 @@ The concrete chain is:
3. build the admin bundle with `yarn build`
4. point the collection field or collection meta to the built module
The current starter already does this in `frontend/src/admin.ts` and `api/collections/content.yml`.
The concrete file names vary by project, but the pattern is the same: registry code lives in the admin bundle and collection config points to the built admin asset.
Typical starter pattern:
@@ -238,7 +238,7 @@ meta:
file: /_/assets/dist/admin.mjs?v=${ADMIN_ASSET_VERSION}
```
Important constraints for this starter:
Important constraints for this setup:
- the registry module must be part of the admin bundle, not a random standalone file outside the build pipeline
- the exported registry keys must match the block type values stored in the collection
@@ -246,6 +246,26 @@ Important constraints for this starter:
- if the registry file path in YAML and the built admin asset diverge, Nova can still render the schema but the pagebuilder preview/picker loses its real block definitions
- in Nova pagebuilder preview, file fields are already normalized by the admin backend to absolute `http(s)://...` URLs when appropriate; preview code must not prepend `apiBase`, `projectBase`, or other frontend URL helpers when the value is already absolute
- Nova may also pass preview rows with hydrated `_lookup` data for FK-like fields; the registry/block preview should consume that data directly instead of trying to re-fetch or manually hydrate references inside the admin preview
- For medialib-based images, prefer the same shared frontend widget used on the public site rather than preview-only URL logic. The widget/helper must honor `apiBaseOverride` so filter URLs, placeholders, and medialib files keep working inside admin preview.
- Nova's `render(container, row, context)` provides API path information. **`context.projectBase`** contains the full project-specific API base including namespace. `context.apiBase` may only contain the generic `/api/` root and is often not sufficient for project-scoped collection endpoints. Blocks that load collection data in preview should therefore use the project-specific base when one is available:
```ts
import { apiBaseOverride } from "./lib/store"
import { get } from "svelte/store"
const prev = get(apiBaseOverride)
if (context?.projectBase) apiBaseOverride.set(String(context.projectBase))
// mount(BlockRenderer, ...)
// in destroy(): apiBaseOverride.set(prev)
```
When a pagebuilder block renders images from medialib, prefer this pattern:
1. request lookup-resolved medialib entries in the data load
2. pass the resolved entry into the shared image widget
3. let that widget decide filter sizing from explicit `filter` or `minWidth`
4. rely on `apiBaseOverride` / `context.projectBase` for admin-safe URL resolution
Do not create a second, preview-only image rendering path that diverges from the public frontend. That usually causes broken placeholders, wrong filter URLs, or SSR/admin mismatches later.
Use collection-level `meta.pagebuilder.blockRegistry.file` when several pagebuilder fields share the same registry. Override at field level only when one field genuinely needs a different registry.
@@ -358,6 +378,91 @@ Avoid pushing these concerns into block components unless there is a strong reas
- unrelated API fetching
- page-level navigation concerns
### 7a. Admin pagebuilder preview: CSS custom properties in shadow DOM
The Nova pagebuilder renders block previews in an isolated DOM context (shadow DOM or detached subtree). Tailwind 4's `@theme` directive generates CSS custom properties on `:root`, but these do **not** cascade into shadow DOM contexts.
**Consequence:** Block previews in the admin can have wrong colors (light text instead of dark, missing brand colors) because `var(--color-ink)` resolves to nothing.
**Fix:** Add a `:host` selector in the project's CSS file that redeclares the theme variables for the shadow DOM context. Also set a hardcoded `color` fallback on `[data-admin-preview]` since the Nova preview container has this attribute.
```css
:host,
[data-admin-preview] {
--color-ink: #2c3e45;
/* … all theme color variables … */
font-family: "Inter Tight", system-ui, sans-serif;
color: #2c3e45; /* hardcoded fallback, not var() */
}
```
Verify by checking admin pagebuilder block preview after any CSS theme changes.
### 7b. Admin pagebuilder preview: API calls from dynamic blocks
Blocks that load data via API (e.g. `CategoryGridBlock` using `getCachedEntries`) need the correct API base URL in the admin preview. The Nova pagebuilder's `render()` callback receives a `context` object with optional `apiBase` and `namespace`, but not all versions provide these.
The `admin.mjs` is loaded by the pagebuilder via dynamic `import()` — the URL is resolved relative to the admin page, NOT relative to the API. So `import.meta.url` contains the admin's page URL (e.g. `/_/assets/dist/admin.mjs`), not the tibi-server's API URL. Regex extraction from `import.meta.url` for the pattern `/api/_/{namespace}/` does NOT work for this reason.
**Reliable approach:** use multiple fallbacks in `admin.ts`, with the admin hostname pattern as the most robust:
1. `context.apiBase` (from Nova when available)
2. `context.namespace` (from Nova)
3. `import.meta.url` regex (works when admin serves admin.mjs through its own API proxy)
4. **Hostname extraction**: admin URL is `{project}-tibiadmin.{domain}` → extract project name
5. DOM scan: find any element with `src`/`href` containing `/api/_/{namespace}/`
```ts
const prevApiBase = get(apiBaseOverride)
let ns: string | null = null
if (context?.apiBase) {
apiBaseOverride.set(String(context.apiBase))
} else {
if (context?.namespace) ns = String(context.namespace)
if (!ns) {
try {
ns = ((import.meta as any).url || "").match(/\/api\/_\/([^/]+)\//)?.[1]
} catch {}
}
// Most reliable: admin hostname is always {namespace}-tibiadmin.{domain}
if (!ns && typeof window !== "undefined") {
const h = window.location.hostname.match(/^(.+?)-tibiadmin\./)
if (h) ns = h[1]
}
// Fallback: scan DOM for API references
if (!ns && typeof document !== "undefined") {
const el = document.querySelector('[src*="/api/_/"], [href*="/api/_/"]')
if (el) {
const a = el.getAttribute("src") || el.getAttribute("href") || ""
ns = a.match(/\/api\/_\/([^/]+)\//)?.[1] || null
}
}
if (ns) apiBaseOverride.set(`/api/_/${ns}/`)
}
```
Set the `apiBaseOverride` store BEFORE mounting the block component so API calls inside `$effect` use the correct base.
The Nova pagebuilder renders block previews in an isolated DOM context (shadow DOM or detached subtree). Tailwind 4's `@theme` directive generates CSS custom properties on `:root`, but these do **not** cascade into shadow DOM contexts.
**Consequence:** Block previews in the admin can have wrong colors (light text instead of dark, missing brand colors) because `var(--color-ink)` resolves to nothing.
**Fix:** Add a `:host` selector in the project's CSS file that redeclares the theme variables for the shadow DOM context. Also set a hardcoded `color` fallback on `[data-admin-preview]` since the Nova preview container has this attribute.
```css
:host,
[data-admin-preview] {
--color-ink: #2c3e45;
--color-ink-2: #3a4d56;
/* … all theme color variables … */
font-family: "Inter Tight", system-ui, sans-serif;
color: #2c3e45; /* hardcoded fallback, not var() */
}
```
Verify by checking admin pagebuilder block preview after any CSS theme changes.
### 8. Prepare for styling consistency across blocks
A block system works better when blocks share a few stable layout conventions.