Skip to content

fix(templates): media bindings resolve asset ids and outlets bind any rich field - #334

Open
mostafasadeghidev wants to merge 2 commits into
CoreBunch:mainfrom
mostafasadeghidev:fix/binding-media-resolution-and-outlet-override
Open

fix(templates): media bindings resolve asset ids and outlets bind any rich field#334
mostafasadeghidev wants to merge 2 commits into
CoreBunch:mainfrom
mostafasadeghidev:fix/binding-media-resolution-and-outlet-override

Conversation

@mostafasadeghidev

Copy link
Copy Markdown

Two dynamic-binding engine gaps surfaced while building a real template-generating plugin against the platform.

What changed

1. Outlets can now bind any rich field (persisted html binding wins)

effectiveNodeBindings spread the implicit currentEntry.body outlet binding after the node's persisted overlay, so a persisted custom html binding was always clobbered — an outlet could never render any field except body. Custom data tables with a richText cell (e.g. rich-text) could not render their content on entry routes at all; the only workaround was naming the cell body.

  • The spread order is swapped in src/core/templates/dynamicBindings.ts: the implicit binding is now a default; a persisted html binding (author- or plugin-set) wins. Hand-dropped outlets with no overlay keep the implicit body behaviour (existing outletEntryBody tests still cover it).
  • The html-format markdown shim now also fires when the binding lands in a richtext-typed prop (html, *richtext) rather than only for body/bodyMarkdown fields — a custom richText cell stored as markdown renders to HTML, and one stored as HTML passes through the GFM renderer verbatim. Non-richtext destinations (e.g. longText bound to a textarea) keep their exact previous behaviour.

2. format: 'media' bindings resolve asset ids to URLs

The binding schema and picker offer format: 'media' (custom media cells store the bare asset id), but resolveBindingValue had no media branch — binding e.g. base.image src to a custom media cell published <img src="<asset-id>">. Only the pre-materialised aliases (featuredMediaPath etc.) worked.

  • TemplateRenderDataContext gains an optional media lookup (ReadonlyMap<string, { publicPath }>, keyed by asset id and public path).
  • resolveBindingValue translates bare references through it; values already carrying a path/URL pass through untouched; an unresolvable reference counts as "missing" so the binding's fallback applies instead of the raw id leaking into markup.
  • publishPage seeds the lookup from the prefetched asset map (composeTemplateContext), covering the public renderer, entry routes, the content-preview endpoint, and the iframe runtime preview.
  • prefetchMediaAssets collects the bare ids referenced by format: 'media' bindings (walking the render tree's bindings and reading those fields off the entry stack + loop items — prose scalars stay out of the IN-list), and also keys each asset by its materialized publicPath so _resolvedMediaByKey enrichment survives URL-transformer plugins. It gained a rootNodeId option mirroring prefetchLoopData.
  • The hole and loop fragment endpoints now prefetch media and attach the lookup — appended infinite-scroll items previously rendered with no srcset/alt enrichment at all (pre-existing gap fixed in passing).
  • The canvas attaches the admin media-library cache through a new useCmsMediaAssetLookup() (same module-level cache as useCmsMediaAssetByPath, now keyed by id too). The attachment happens in CanvasRoot — only for client-side rendering surfaces (transform layer, live surface, agent snapshot frame); useTemplatePreviewContext stays serializable for the SSR-preview surfaces (hover preview, preview overlay, script build).
  • The runtime-preview endpoint strips the non-serializable media member arriving over JSON (Map{}) and lets the server-side prefetch supply it.

Impact

  • Plugins/authors can bind outlets to any rich field and bind image/media/url props to custom media cells; both publish and canvas preview resolve correctly.
  • No schema or persisted-data changes; no migrations.

Verification

bun run build   # tsc -b && vite build — clean
bun run lint    # clean

Tests were run in chunks (a single full-suite process exhausts memory on the Windows dev machine used here):

  • templates + publisher + loops: 525/526 — the 1 failure is a Windows EBUSY temp-file cleanup in untouched dataRowsFetch.test.ts (fails identically on main).
  • canvas + editor + property-controls + modules + media + site-explorer: all pass except one pre-existing siteExplorerPanel CSS-extraction failure (fails identically on main).
  • server: failure set diffed against a fresh main baseline run — byte-identical except this PR's one new DB-backed test, which fails only the same EBUSY cleanup every createTestDb test hits on Windows (its assertions were separately verified green via a direct SQLite run).
  • architecture: the same 8 failures as main (bundle budget over on main's own build too; circular-dependency gate times out on the loaded machine — direct madge --circular over src+server reports no cycles).
  • New tests: media-reference translation/fallback/pass-through, outlet override via publishPage, custom-rich-field markdown rendering, bound-media-cell publishing end-to-end, binding-driven prefetch collection (real SQLite).

mostafasadeghidev and others added 2 commits August 3, 2026 05:29
… rich field

Two dynamic-binding engine gaps surfaced while building a template-generating
plugin against the platform:

1. `effectiveNodeBindings` spread the implicit `currentEntry.body` outlet
   binding AFTER the node's persisted overlay, so a persisted custom `html`
   binding was always clobbered — an outlet could never render any field
   except `body`. The spread order is now swapped: the implicit binding is a
   default, a persisted binding wins, so authors and plugins can point an
   outlet at any rich field. The html-format markdown shim also covers
   richtext-destination props now (not just body/bodyMarkdown), so both
   markdown- and HTML-stored richText cells render correctly.

2. `format: 'media'` bindings had no resolver branch — binding e.g. an image
   `src` to a custom media cell published the raw asset id. The render
   context now carries a `media` lookup (id + path keyed):
   - `resolveBindingValue` translates bare references through it; path/URL
     values pass through; unresolvable references fall back instead of
     leaking the id.
   - `publishPage` seeds the lookup from the prefetched asset map; the
     prefetch collects the bare ids referenced by media bindings (walking
     the tree's bindings + entry/loop fields) and also keys assets by their
     materialized publicPath so enrichment survives URL transformers.
   - The hole and loop fragment endpoints now prefetch media too — appended
     infinite-scroll items previously rendered without srcset/alt enrichment.
   - The canvas attaches the admin media-library cache via a new
     `useCmsMediaAssetLookup`, so the editor resolves ids like the publisher.
   - The runtime-preview endpoint strips the non-serializable `media` member
     arriving over JSON and substitutes the server-side prefetch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eview-context hook

useTemplatePreviewContext also serves the SSR-preview surfaces (site-explorer
hover preview, preview overlay, runtime script build), whose context is
serialized to the runtime-preview endpoint — a live media Map is dead weight
there, and the extra listCmsMediaAssets fetch broke the hover preview's
one-request contract. CanvasRoot now enriches the context with
useCmsMediaAssetLookup only for the client-side rendering surfaces
(transform layer, live surface, agent snapshot frame).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mostafasadeghidev
mostafasadeghidev marked this pull request as ready for review August 3, 2026 03:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant