fix(templates): media bindings resolve asset ids and outlets bind any rich field - #334
Open
mostafasadeghidev wants to merge 2 commits into
Conversation
… 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
marked this pull request as ready for review
August 3, 2026 03:53
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
htmlbinding wins)effectiveNodeBindingsspread the implicitcurrentEntry.bodyoutlet binding after the node's persisted overlay, so a persisted customhtmlbinding was always clobbered — an outlet could never render any field exceptbody. 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 cellbody.src/core/templates/dynamicBindings.ts: the implicit binding is now a default; a persistedhtmlbinding (author- or plugin-set) wins. Hand-dropped outlets with no overlay keep the implicit body behaviour (existingoutletEntryBodytests still cover it).html,*richtext) rather than only forbody/bodyMarkdownfields — 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.longTextbound to a textarea) keep their exact previous behaviour.2.
format: 'media'bindings resolve asset ids to URLsThe binding schema and picker offer
format: 'media'(custom media cells store the bare asset id), butresolveBindingValuehad no media branch — binding e.g.base.imagesrcto a custom media cell published<img src="<asset-id>">. Only the pre-materialised aliases (featuredMediaPathetc.) worked.TemplateRenderDataContextgains an optionalmedialookup (ReadonlyMap<string, { publicPath }>, keyed by asset id and public path).resolveBindingValuetranslates 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.publishPageseeds the lookup from the prefetched asset map (composeTemplateContext), covering the public renderer, entry routes, the content-preview endpoint, and the iframe runtime preview.prefetchMediaAssetscollects the bare ids referenced byformat: '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 materializedpublicPathso_resolvedMediaByKeyenrichment survives URL-transformer plugins. It gained arootNodeIdoption mirroringprefetchLoopData.useCmsMediaAssetLookup()(same module-level cache asuseCmsMediaAssetByPath, now keyed by id too). The attachment happens inCanvasRoot— only for client-side rendering surfaces (transform layer, live surface, agent snapshot frame);useTemplatePreviewContextstays serializable for the SSR-preview surfaces (hover preview, preview overlay, script build).mediamember arriving over JSON (Map→{}) and lets the server-side prefetch supply it.Impact
Verification
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 WindowsEBUSYtemp-file cleanup in untoucheddataRowsFetch.test.ts(fails identically onmain).canvas+editor+property-controls+modules+media+site-explorer: all pass except one pre-existingsiteExplorerPanelCSS-extraction failure (fails identically onmain).server: failure set diffed against a freshmainbaseline run — byte-identical except this PR's one new DB-backed test, which fails only the sameEBUSYcleanup everycreateTestDbtest hits on Windows (its assertions were separately verified green via a direct SQLite run).architecture: the same 8 failures asmain(bundle budget over onmain's own build too; circular-dependency gate times out on the loaded machine — directmadge --circularover src+server reports no cycles).publishPage, custom-rich-field markdown rendering, bound-media-cell publishing end-to-end, binding-driven prefetch collection (real SQLite).