Dynamic Fields in Product Cards
Static cards go stale. When a card needs to show something that changes — the signed-in visitor's name, their account status, the tenant's catalog — you make it dynamic with the binding contract: {{field}} for author-set values and {{data.<name>.path}} for platform-resolved data.
One thing to be clear about first: this contract belongs to code-type custom components, which render as HTML inside a sandboxed iframe. The built-in Product Card is a React component with typed fields, not a {{ }} template — it has no placeholder syntax. To get a dynamic card, build one in Content → Component Library, or duplicate the built-in card and edit the copy.
{{field}} — author-editable values
Every field you declare on a component becomes a {{fieldName}} token in your HTML. Available field types are text, number, textarea, select, radio, plus two pickers that render a runtime dropdown: product-set-picker and hosted-page-picker.
Substitution is deliberately dumb, and that is the contract:
It is pure replacement — there is no expression evaluation inside
{{ }}. A token is a name or a dotted path, never JavaScript.Only declared field names substitute. An undeclared token is left in the output verbatim, so a typo is visible rather than silently blank.
Every substituted value is HTML-escaped, so a value cannot inject markup.
{{data.<name>.path}} — live data
Declare a data binding on the component: a name you choose, a kind from the platform's registry, and optional params. Reference it as {{data.<name>}}, or drill in with a dotted path such as {{data.account.accountNumber}}. Bindings resolve on the server before the iframe mounts, so the component never holds a token, and every resolution is role-checked and audited.
The built-in kinds are viewer, currentAccount, invoices, payments, subscriptions, usage, productCatalog, conversations, and csrAccountList. viewer and productCatalog are public-callable; the rest require a signed-in viewer plus a scope check.
For account-scoped kinds, pass {"$ctx":"account.id"} as the accountId param rather than a literal id — the resolver fills it from the authenticated viewer, so a customer can only ever read their own record.
Gotchas
productCatalogreturns no prices. Its shape is{ id, name, description, category }only. You cannot print a live price from it — resolve pricing through the runtime bridge instead.A failed binding substitutes an empty string, not an error message. A blank card region usually means the binding was rejected, not that the data is empty.
{{data.…}}prints one value. For arrays — a plan list, an invoice table — callPeakBindings.query("name", params)orwindow.PC.call("data", "pricing-resolve" | "product-set-resolved", …)from inside the component. Directfetchis blocked by the sandbox.
Related
Last updated
Was this helpful?
