Skip to content
MAGENTO EXTENSIONS

How to Customize the Magento 2 Checkout Success Page by Product

Customize the Magento 2 checkout success page per product: a CMS block for every order, a SKU-checking view model, and the gotchas.

Shane Blandford
Shane Blandford · FOUNDERPUBLISHED · 8 MIN READ

The Magento checkout success page is the same for every order: an order number, a "continue shopping" button, and on guest orders a prompt to create an account. Putting a CMS block on it for everyone takes one layout file. Showing a block only when the order contains a specific product, like a warranty registration link for buyers of one drill, takes a small custom module or an extension.

We build Magento stores for clients, and we also build and sell the OCM Labs Product Messaging module for Magento 2, which does the per-product version. So weigh our opinion accordingly. Below is the native code for both cases first, because for one message on one product, custom code is a fair choice.

What is the checkout success page in Magento 2?

It is the page at checkout/onepage/success, rendered by the Magento\Checkout\Controller\Onepage\Success controller with the layout handle checkout_onepage_success. Core puts two blocks in the content container: checkout.success (class Magento\Checkout\Block\Onepage\Success, template Magento_Checkout::success.phtml) and checkout.registration for the guest account prompt.

The page knows which order was just placed through the checkout session. The success block calls getLastRealOrder() on Magento\Checkout\Model\Session, which loads the order by the last real order increment ID stored in the session. That session lookup is the hook every customization uses, and it is also the source of most of the gotchas below.

How do you add a CMS block to every order's success page?

Create the block in Content > Blocks, then reference it from a checkout_onepage_success.xml layout file in your theme or a custom module. Core's Magento\Cms\Block\BlockByIdentifier renders a block by its identifier and runs it through the normal CMS filter, so widgets and {{store url}} work:

APP/CODE/ACME/SUCCESSPAGE/VIEW/FRONTEND/LAYOUT/CHECKOUT_ONEPAGE_SUCCESS.XML
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceContainer name="content">
            <block class="Magento\Cms\Block\BlockByIdentifier"
                   name="acme.success.notice"
                   after="checkout.success">
                <arguments>
                    <argument name="identifier" xsi:type="string">success-page-notice</argument>
                </arguments>
            </block>
        </referenceContainer>
    </body>
</page>

If the block is disabled or the identifier does not exist, BlockByIdentifier renders an empty string instead of throwing. That makes this a safe way to run a store-wide message, like a holiday shipping cutoff, and turn it off from the admin. It does not know anything about the order.

How do you show success page content only for certain products?

Add a template block with a view model that loads the last order from the checkout session and checks the SKUs. Nest the CMS block as a child and only render it when the check passes. Three files, starting with the layout:

APP/CODE/ACME/SUCCESSPAGE/VIEW/FRONTEND/LAYOUT/CHECKOUT_ONEPAGE_SUCCESS.XML
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceContainer name="content">
            <block class="Magento\Framework\View\Element\Template"
                   name="acme.success.warranty"
                   template="Acme_SuccessPage::warranty.phtml"
                   after="checkout.success"
                   cacheable="false">
                <arguments>
                    <argument name="view_model" xsi:type="object">Acme\SuccessPage\ViewModel\Warranty</argument>
                </arguments>
                <block class="Magento\Cms\Block\BlockByIdentifier" name="acme.success.warranty.cms">
                    <arguments>
                        <argument name="identifier" xsi:type="string">drill-warranty-registration</argument>
                    </arguments>
                </block>
            </block>
        </referenceContainer>
    </body>
</page>

The view model:

APP/CODE/ACME/SUCCESSPAGE/VIEWMODEL/WARRANTY.PHP
<?php
declare(strict_types=1);

namespace Acme\SuccessPage\ViewModel;

use Magento\Checkout\Model\Session as CheckoutSession;
use Magento\Framework\View\Element\Block\ArgumentInterface;

class Warranty implements ArgumentInterface
{
    private const SKUS = ['DRILL-2000', 'DRILL-3000'];

    public function __construct(private readonly CheckoutSession $checkoutSession)
    {
    }

    public function orderHasWarrantyProduct(): bool
    {
        $order = $this->checkoutSession->getLastRealOrder();
        if (!$order->getId()) {
            return false;
        }

        foreach ($order->getAllVisibleItems() as $item) {
            if (in_array($item->getSku(), self::SKUS, true)) {
                return true;
            }
        }

        return false;
    }
}

And the template:

APP/CODE/ACME/SUCCESSPAGE/VIEW/FRONTEND/TEMPLATES/WARRANTY.PHTML
<?php
/** @var \Magento\Framework\View\Element\Template $block */
$viewModel = $block->getData('view_model');
?>
<?php if ($viewModel->orderHasWarrantyProduct()): ?>
    <?= $block->getChildHtml('acme.success.warranty.cms') ?>
<?php endif; ?>

Add the usual registration.php and etc/module.xml, run setup:upgrade, and buyers of either drill see the warranty block. Everyone else sees the stock page.

That is the whole mechanism. What it leaves to you: the SKU list lives in PHP, so marketing cannot change it without a deploy. A second message for a second product line means a second block and view model, or a config structure you invent. The match is exact, so new SKUs in the same family need a code change. And on configurable and bundle products, the SKU on the order line may not be the one you typed into the list, so test with a real order of each product type you sell.

What breaks a custom success page?

Four things come up on almost every build.

Refreshing the page. The success controller runs SuccessValidator first, then calls clearQuote() on the checkout session, which wipes the last success quote ID the validator checks. A refresh, or opening the success URL later, fails validation and redirects to the cart. You cannot test by reloading; you need a fresh order each time. Anything that must survive past this one page view belongs in an email.

The session is the only link to the order. getLastRealOrder() reads from the shopper's session. Open the success URL in a second browser, or from any session that did not place the order, and there is no order and no block. Return early when getId() is empty, as the view model above does.

Multishipping uses a different page. Orders placed through multi-address checkout land on multishipping/checkout/success, layout handle multishipping_checkout_success, which can follow several orders at once. Your checkout_onepage_success.xml never loads there, and that page does not track its orders through getLastRealOrder(). You need a second layout file and a different lookup.

Caching. Core marks the checkout.success block cacheable="false", which keeps the whole success page out of full page cache. Mark your own block cacheable="false" too, so it never becomes the reason a cached variant leaks between customers. Never put a cacheable="false" block in default.xml, though; that turns off full page cache for every page on the site. If you run Varnish with custom rules, check that nothing caches checkout/onepage/success.

Does any of this change on Hyva?

The layout handle and the block classes are the same, so a layout file that adds a block to content, like both examples above, works on Hyva. Templates are where it differs. Hyva strips the block declarations out of core's layout files (a reset theme did this up to Hyva 1.3, the Base Layout Reset module does it from 1.4.0), and the Hyva default theme declares the success page blocks again with its own Magento_Checkout::success.phtml. If your customization overrides core's success template or depends on Luma JavaScript, it will not carry over; check it against the Hyva theme's version. CMS block content should use Tailwind classes instead of Luma CSS.

When does a module make more sense than custom code?

When more than one product has something to say, or when the people writing the messages are not developers. Product Messaging turns the view model above into an admin screen. Each rule at Sales > Product Messaging > Rules has a SKU list and a Show Block on Success Page action that points at an ordinary CMS block.

How the success-page action works, per the success-page block documentation:

  • Per-rule blocks. Turn on the action, pick an active CMS block, save. The next matching order shows it. The rule can match exact SKUs or * wildcards, so DRILL-* covers the whole family.
  • Build the block in place. + Create New Block opens the real CMS block editor, Page Builder or WYSIWYG, in a modal. The result is a normal block in Content > Blocks, and the dropdown refreshes without a page reload.
  • Evaluated live. Blocks are matched against the order on each success page view. There is no queue and no log for this action.
  • Stacking. If an order matches three rules, all three blocks render, in ascending rule ID order. An order with a drill and a battery pack can get the warranty registration link and a "battery ships separately" note together.
  • No empty markup. A non-matching order renders nothing, not an empty wrapper, and a disabled or deleted block renders nothing instead of an error.
  • Scoping. Each rule can be limited to store views and customer groups, so a Wholesale-only manual link stays off retail orders.
  • Multishipping covered. The module adds its block to both success page handles and shows blocks for every order just placed.
  • Luma and Hyva. Output is server-rendered CMS block HTML with no JavaScript, so it renders the same on both without a compatibility package.
  • Kill switch. Stores > Configuration > OCM Labs > Product Messaging > Success Page > Enable Checkout Success-Page Blocks turns off every success-page block at once without touching emails.

The same rule can also send a product-specific email with PDFs attached, which we cover in attaching manuals and PDFs to Magento order emails. A drill rule can show "register your warranty" on the success page and email the manual when the order ships.

The limits are real. Matching is by SKU only, with no category or attribute conditions. Blocks appear on the checkout success page only, not on the customer's order view or in order emails. There is no impression or click tracking for blocks. The module is $250 for Magento Open Source and $500 for Adobe Commerce, and supports 2.4.8 through 2.4.9 on PHP 8.2 through 8.5.

If you have one product and one message, ship the view model above. If you have a catalog full of warranties, manuals, and shipping caveats, that is what we built the module for. More of our modules are on the Magento extensions page.

FAQ

Can you edit the Magento 2 checkout success page without code?+
Not natively. Core Magento has no admin setting for success page content. You need a layout file to place even a store-wide CMS block, or an extension that adds the block placement for you.
Why does refreshing the success page send me to the cart?+
The success controller clears the checkout session's last success quote ID after the first view. On the next request the success validator fails and Magento redirects to checkout/cart. This is core behavior, so test with a new order each time.
Where is the success page template in Magento 2?+
Core uses Magento_Checkout::success.phtml, rendered by Magento\Checkout\Block\Onepage\Success in the checkout_onepage_success layout handle. Hyva ships its own version of that template in its default theme.
Can I show different success page content for different products?+
Yes. Either write a view model that reads the last order from the checkout session and checks SKUs, as shown above, or use a module that maps SKU lists to CMS blocks. With Product Messaging, several matching rules stack their blocks on the same page.
Does the success page work with multishipping checkout?+
Multi-address checkout uses a separate page with the multishipping_checkout_success handle, so customizations made only for checkout_onepage_success do not appear there. Product Messaging adds its blocks to both pages.
Can the success page block also go in the order email?+
Not the block itself. Success-page blocks render only on the success page. For a durable copy, give the same Product Messaging rule an email action, which can include PDF attachments.
Shane Blandford

Shane Blandford

FOUNDER

Founder of Orange Collar Media, a Denver ecommerce agency behind 800+ Magento and Shopify builds. In Magento since version 1.x - building, rescuing, and supporting revenue-critical stores since 2008.

All posts by Shane Blandford→

KEEP READING

Related from the Journal.

Your store can sell more. Let's find out how much.

Start a project