The templates below mirror a real production checkout (
checkout.ef). Every directive shown is copied from a page that renders correctly against a live merchant — not invented for the docs.How a checkout page renders
Two passes build the page:- Server pass (
CheckoutProcessor). Runs only if the page has the “Is checkout page” flag on. It resolves the product from the URL, builds thecheckoutscope (customer, totals, subscription, bumps, coupon, wallet settings), and swaps the checkout tags —<checkout-cc-panel>becomes a gateway-specific card<div>,<checkout-wallet>becomes the wallet containers. It injects the checkout payload ontowindow.efScope.checkout. - Client pass (frontend template engine). In the browser, it evaluates every block marked
data-frontend-template="true"against the live scope (including values only known client-side, such as whether a wallet button actually rendered), fills the card/wallet forms, and wires the submit button.
Prerequisites
- Enable “Is checkout page” on the page. Open Page List → Edit Page → toggle Is checkout page on → Save.
- Assign a merchant to the domain (Settings → Domains → Edit → Merchant). The card panel is swapped for a gateway-specific form (Stripe / NMI / Authorize.Net) resolved from this merchant.
- Link at least one product so the page can be reached with
?p=<code>(or a cart).
Step 1 — resolve the product from the URL
A single-product checkout loads its product from thep query parameter: /checkout?p=herpafend_6. The runtime reads req.query.p, looks the product up for the brand, and populates the checkout scope (title, image, price, retail price, and — if the product is a subscription — the full billing cadence).
So the checkout page is generic; the ?p= code decides what it sells. A plan or quantity selector is just a set of links (or a script) that point at the same page with a different p:
If there is no
p in the URL, the runtime falls back to cart-only mode (see Step 2). A page reached with neither ?p= nor cart items cannot resolve anything to sell.checkout.subscription and the totals are already correct on first paint — no client fetch. See Buy links for how funnel buttons build the ?p= link.
Step 2 — single product vs cart
The order-summary area renders one of two shapes, chosen by scope flags:“Carts” here are Shopify-style carts — multi-item carts synced from Shopify, not an á-la-carte cart you build on the ElasticFunnels page. Most direct-response checkouts are single-product (one
?p=); the is_cart branch exists so the same layout can also render a synced Shopify cart with per-line quantities and remove buttons. See Shopify Checkout.data-frontend-template="true" (see Step 6 below). This is the exact production markup:
Step 3 — subscription vs one-time (conditional)
Key idea: the same checkout page serves both one-time and subscription products, because the?p= code decides. So every subscription-specific element must be gated — never assume a subscription. Gate on the checkout.subscription object (it is null for one-time products, so it is falsy):
<template-if data-condition="checkout.subscription">— subscription framing (recurring heading, billing note, “save vs one-time”, “Subscribe” CTA).<template-if data-condition="!checkout.subscription">— one-time order framing.
The billing note (four cases)
Subscription products differ in how the first charge works, so the recurring note has to cover four mutually-exclusive cases. This is the production block verbatim — it readscheckout.subscription.{intro_offer, first_charge_free, trial_days, frequency, frequency_unit} and formats money with formatPrice(...) and values with [[ … ]]:
checkout.subscription.intro_offer.recurring_price is the price charged after the intro ends. The frequency_unit == 'month' ? … ternary just pluralizes “month/months”; other units (day, week, year) are printed as-is. For the full subscription scope shape (price_steps, prepaid, tiers), see Frontend Template Engine: Checkout.
In cart mode subscription data lives on each line (
item.is_subscription, item.subscription), not on checkout.subscription.Step 4 — wallets (Apple Pay / Google Pay)
Place a<checkout-wallet> tag where you want the express buttons. The runtime swaps it for an ef-wallet-standalone container holding #applePay and #googlePay, and the client injects whichever wallet the device supports:
- Buttons appear automatically when the device/browser supports a wallet — you do not render them yourself.
checkout.wallet_visibleflipsfalse → trueonly once a wallet button actually renders (it is promoted, never reset). Gate the “or pay another way” divider on it so the divider never shows above an empty space. Addforce="true"to pre-render the native Apple Pay button on iOS and setwallet_visibleimmediately, avoiding a first-paint layout shift.- Billing address comes from the wallet. When a customer pays with Apple/Google Pay, the correct billing address is taken from the wallet sheet — no extra fields needed in your template.
<checkout-wallet force="true" wallets="applePay,googlePay"/> at the top and disable the card panel’s own wallet tab with data-applepay="false" (next step).
Step 5 — the card panel
Drop in a single<checkout-cc-panel> tag. Do not hand-build card inputs — the runtime swaps this tag for a gateway-specific secure card form resolved from the domain’s merchant:
- Server-side, the tag is replaced with
<div class="checkout-cc-panel" data-gateway="{stripe|nmi|authorize_net}" data-accepted-cards="…">. The gateway is resolved from the merchant assigned to the domain. - Client-side, the secure card fields for that gateway are injected (Stripe Elements, NMI CollectJS, or Authorize.Net Accept.js). A resolved merchant/gateway is required for real tokenization.
data-accepted-cardssets the accepted card brands;data-applepay="false"suppresses the panel’s built-in wallet tab (use it when you place<checkout-wallet>separately).
Step 6 — mark client-side blocks with data-frontend-template="true"
Add data-frontend-template="true" to every template-if, template-else, and template-foreach whose condition depends on data that is only known in the browser: the wallet divider, subscription blocks, order bumps, cart items, coupon state, and any bump.added / checkout.wallet_visible conditional.
Why: the server template pass and the client template pass are separate. Values like checkout.wallet_visible (true only after a wallet renders), bump.added (toggled by the shopper), and coupon state (applied after an async validation) do not exist during the server pass. Marking a block data-frontend-template="true" tells the server pass to leave it alone and hands it to the client engine, which evaluates it against the live scope. Without the flag, the server tries to evaluate the condition against empty scope and the block is dropped or renders wrong.
@if(query.p == 'davincitone-4-weeks')) are the opposite case — those are evaluated during the render and do not take the attribute.
Pair client-side blocks with
data-ef-cloak (and the CSS [data-ef-cloak] { display: none !important; }) on anything that would flash before the engine runs. Also hide the raw template-if / template-foreach tags with display: none !important so unprocessed markup never flashes.Step 7 — order bumps
Bumps come fromcheckout.bump_products — an array with per-bump code, name, description, price (formatted), price_raw, image, added, and added_message. Loop it and render an offer state (!bump.added) and an added state (bump.added). Toggling with @click="bump.added = true/false" mutates the scope and re-renders:
bump.added:
Bumps are set by page events (and can be split-tested)
The bumps incheckout.bump_products are not hard-coded on the page. They are supplied by the funnel graph’s Set Checkout Bumps node (set_checkout_bumps), whose bumps field is an array of bump objects (title, description, price, added_message, product code). The node is only offered when the page is flagged as a checkout page.
Because bumps come from a page-event node, you can A/B test bump configurations: place a Split Test → Traffic Distribution node in front, and give each branch its own Set Checkout Bumps node. Different visitors then get different bumps against the same checkout template, and the split test measures which set converts better. See Page event nodes → Set Checkout Bumps and Split testing.
Step 8 — the formless submit
Checkout submission is not a form submit. The<form> in the examples is only for autofill/semantics (onsubmit="return false;"); the runtime sets its form reference to null and collects every bound field from the page wrapper instead. Submission is a click on a [data-checkout-submit] button that preventDefault()s and posts via fetch — so fields do not have to live inside the <form>, and method="POST" has no effect.
Wire it with three attributes:
data-template-value="checkout.customer.{field}"on each input/select — binds it into the checkout payload (email,phone,shipping_address,shipping_city,shipping_state,shipping_zip,billing_*, …).data-checkout-error="{field}"on an empty element — the validation adapter fills it with that field’s error message.data-checkout-messageon one element — the general status/error line for the whole submit.data-checkout-submiton the pay button (type="button").
Backend config: checkout_settings
Checkout options are set in a <script scope="backend"> block via setVariable("checkout_settings", { … }). This is the production block:
setVariable("checkout_settings", {…}) and assigning the checkout_settings object are the same thing — the object is read by both the render (to build the rendered dropdowns/labels) and process-checkout (to enforce the same rules on submit). Common keys:
Full semantics: Checkout functionality reference →
checkout_settings.
Output bindings reference
Two ways to print scope values, used throughout the examples above:
Use
[[ … ]] where you need a value inside an attribute or mixed with other text (data-code="[[ item.code ]]"); use ef-text / ef-src / ef-alt for a whole element’s text or image. formatPrice(raw) formats a raw number as currency.
<template-foreach> also drives select dropdowns for country/state, each option { value, label }:
Page builder vs code editor
Everything above is code-editor HTML. In the page builder, use the Checkout block category, which maps to the same tags/bindings:- Checkout Details — contact + shipping + billing fields
- Credit Card Form —
<checkout-cc-panel> - Checkout Button —
<button data-checkout-submit> - Price Summary — totals section
- Checkout Coupon — coupon input with apply/clear (
data-apply-coupon,data-clear-coupon) - Checkout Bumps — order bumps
- Single Product Summary / Cart Products — the
is_single_product/is_cartblocks
<checkout-wallet force="true" wallets="applePay,googlePay"/> custom-code block above Checkout Details and set data-applepay="false" on the Credit Card Form.
Related docs
- Checkout functionality reference — scope keys,
checkout_settings, abuse protection, submit flow - Frontend Template Engine: Checkout —
checkout.*bindings, totals, subscription shape - Apple Pay and Google Pay — wallet setup
- Shopify Checkout — where
is_cartcarts come from - Buy links — building the
?p=link - Set Checkout Bumps node and Split testing — configuring / A/B-testing bumps
- Checkout redirect behavior — post-checkout routing