Fjord for Craft CMS

Templates

Fjord owns the routing, the state and the offers. Your templates own the markup — the same division Commerce checkout already works on.

What a step template receives

VariableIs
flowthe flow being served
stepthe step being rendered
variantthe split-test arm, or null for the control
sessionthe visitor's place in the funnel
cartthe current cart, or the completed order once there is one
orderthe completed order, or null
offerthe offer on an upsell or downsell step
bumpsthe eligible order bumps for a checkout step
nextUrlwhere "continue" goes from here

craft.fjord

Available in any template, funnel or not:

{{ craft.fjord.session }}                 {# the visitor's place in the funnel #}
{{ craft.fjord.step }}                    {# the step they are on #}
{{ craft.fjord.next(step) }}              {# where "continue" goes #}
{{ craft.fjord.continueUrl }}             {# where a checkout's redirect should point #}
{{ craft.fjord.bumps(step, cart) }}       {# the bumps for this checkout #}
{{ craft.fjord.isOnOrder(offer, cart) }}  {# is this bump already ticked? #}
{{ craft.fjord.grants(order) }}           {# what each offer resolved to #}
{{ craft.fjord.storeCheckoutUrl }}        {# null when no flow has taken it over #}
{{ craft.fjord.flow('springLaunch') }}
{{ craft.fjord.offer('careKit') }}
{{ craft.fjord.isPro }}

The one line to get right

In a checkout template, point Commerce's redirect at craft.fjord.continueUrl:

{{ redirectInput(craft.fjord.continueUrl) }}

Not at a page of your choosing, and not at craft.fjord.next(step).

The next step is decided after the order completes. At render time there is no completed order, so every post-purchase step looks unreachable — and a URL baked into the form at that moment would skip the upsell entirely. continueUrl resolves when it is followed, which is the only moment the answer is right.

Storing the card

A one-click upsell has nothing to charge unless the customer's payment method is reusable. That means the gateway must support payment sources, and the customer must have asked to save the card:

<label>
    <input type="checkbox" name="savePaymentSource" value="1" checked>
    Save my payment details for one-click offers
</label>

Without it, Fjord skips every post-purchase step rather than showing an offer it cannot fulfil.

Front-end actions

ActionDoesParams
fjord/checkout/toggle-bumpTicks or unticks an order bumpoffer, on
fjord/checkout/accept-offerTakes a post-purchase offer with one clickstep
fjord/checkout/decline-offerRefuses it and moves onstep
fjord/checkout/nextMoves on from a stepstep
fjord/checkout/store-checkoutSends a shopper into the store-checkout flow
fjord/flow/continueResolves where the visitor goes next, when they get there

Each answers JSON to an Ajax caller and a redirect to a plain form post, so a funnel can be built either way without the controller caring which:

<form method="post">
    {{ csrfInput() }}
    {{ actionInput('fjord/checkout/accept-offer') }}
    {{ redirectInput(step.getUri()) }}
    {{ hiddenInput('step', step.id) }}
    <button type="submit">{{ offer.getDisplayAcceptLabel() }}</button>
</form>

The JSON reply carries a redirect key with the same answer, for a template that would rather stay on the page and move itself.

Marking a bump on the cart

A bump's line item is stamped, so a cart summary can label it:

{% for item in cart.lineItems %}
    {{ item.description }}
    {% if item.options.fjordOffer is defined %}<em>added offer</em>{% endif %}
{% endfor %}

Templates per step and per variant

A step's Template field takes either a bare name — looked up under the Template Root, so checkout means shop/fjord/checkout — or a path with a slash, used as-is. Left empty, it falls back to the step's type.

A split-test variant can name its own template, or leave it empty to render the step's. An empty-template variant is how a copy-only test works: same markup, different settings.