Skip to content

fix(templates): interpolate tokens inside htmlAttributes - #353

Open
mostafasadeghidev wants to merge 1 commit into
CoreBunch:mainfrom
mostafasadeghidev:fix/interpolate-tokens-in-html-attributes
Open

fix(templates): interpolate tokens inside htmlAttributes#353
mostafasadeghidev wants to merge 1 commit into
CoreBunch:mainfrom
mostafasadeghidev:fix/interpolate-tokens-in-html-attributes

Conversation

@mostafasadeghidev

Copy link
Copy Markdown

What

resolveDynamicProps now interpolates tokens inside htmlAttributes, not just in string-typed props.

Why

The resolver's second pass reads:

for (const key of Object.keys(target)) {
  const v = target[key]
  if (typeof v !== 'string') continue   // htmlAttributes is an object → skipped
  
}

htmlAttributes is a Record<string, string> — the one prop that holds authored strings a level down — so every token written on an attribute was skipped and shipped to the browser verbatim.

The asymmetry is invisible until you need it. href on a link is a first-class string prop and interpolates fine, so an author reasonably expects src written on a custom tag to behave the same way. It doesn't:

<video>
  <source src="{currentEntry.video-link}">   <!-- published exactly like this -->
</video>

That is the shape a per-item hover video takes on a site imported from another builder, where the <source> is a custom tag inside a loop and its src is bound per row. The failure is completely silent: valid HTML, an attribute that resolves to nothing, no warning anywhere. It took reading the published bytes to find it.

How

The loop now handles htmlAttributes as the string map it is, and leaves everything else exactly as before.

Two deliberate limits:

  • Attribute values are never markdown-rendered. The richtext shim keys off the prop name, and an attribute happening to be called html must not be wrapped in <p> — an attribute is a value, not a body. There is a test for this.
  • Nothing else is descended into. filters on a loop is also an object of strings, but it holds configuration rather than authored output; interpolating it would be surprising. Only the one prop that authors type into gets this.

The copy-on-write behaviour is preserved: a props bag with no tokens is returned by identity, so token-free pages allocate nothing extra (also covered by a test).

User impact

Additive. Every token that resolved before resolves to the same value; tokens that previously leaked as literal text now resolve. All three render surfaces — publisher, editor canvas, ReadOnlyNodeTree — go through this one function, so they stay identical to each other.

Verification

bun test src/__tests__/templates/ src/__tests__/publisher/ src/__tests__/loops/   # 524 pass
bun run build
bun run lint

Seven new tests in src/__tests__/templates/bindingSourcesAndTokens.test.ts. Four of them fail on main and pass with this change — I checked by stashing the source edit and re-running:

(fail) interpolates a token written on an author-set attribute
(fail) interpolates only the attributes that carry tokens
(fail) an unresolvable token becomes empty rather than shipping the literal
(fail) never markdown-renders an attribute value

The other three pin the behaviour that must not change: no needless copy, no mutation of the caller's object, and a malformed bag ignored rather than thrown on.

Also verified end-to-end on a real imported site: the <source src> inside a CMS loop now publishes the row's actual URL instead of the literal token.

Note: the one EBUSY failure in src/__tests__/loops/dataRowsFetch.test.ts is a pre-existing Windows temp-file teardown issue in createTestDb, unrelated to this change.

`resolveDynamicProps` interpolated every string-typed prop, then skipped
anything that was not a string — including `htmlAttributes`, the one prop
that holds strings a level down. Tokens written on an author-set attribute
were never substituted: they shipped to the browser as literal text, so
the attribute pointed nowhere.

The asymmetry was invisible until you needed it. `href` on a link is a
first-class string prop and interpolated fine, so authors reasonably
expected `src` written on a custom tag to behave the same way. It did not:

    <video>
      <source src="{currentEntry.video-link}">   <-- shipped verbatim
    </video>

That is the shape a per-item hover video takes on an imported site, and
the failure is silent — valid HTML, an attribute that resolves to nothing,
no error anywhere.

`htmlAttributes` is now walked as the string map it is. Attribute values
are deliberately NOT run through the richtext shim: an attribute is a
value, not a body, and wrapping a URL in `<p>` would corrupt it. Nothing
else is descended into — `filters` on a loop holds configuration, not
authored output.

The no-token path still returns the caller's object untouched, so pages
that use no tokens allocate nothing extra.

All three render surfaces resolve through this function, so the publisher,
the editor canvas, and ReadOnlyNodeTree stay identical.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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