Theming Without Pain

Override as little as possible, keep every change in a child theme, and know what a minor upgrade will break.

Every file you copy into a theme is a file you have adopted. The original keeps changing upstream; your copy does not. So the working rule is to override as little as possible, and to pick the cheapest tool that produces the change.

Order of preference

  1. Admin configuration, when the setting exists.
  2. etc/view.xml for image sizes and display variables.
  3. _theme.less variables, then _extend.less rules.
  4. Layout XML: move, reorder, remove, change a block's template or arguments.
  5. A template override, last, and only the one template.

Layout is the step people skip. Moving a block or pointing it at your own template needs no copy of anyone's markup, and it keeps working when the original file changes.

Keep it in a child theme

Never edit Magento/luma, Magento/blank or anything in vendor/magento. Declare a theme with <parent>Magento/luma</parent> and put everything there. An upgrade replaces the parent's files; it does not touch yours.

What a minor upgrade breaks

Copied templates keep rendering old markup. A copied _theme.less loses variables the parent added. Layout that names a block, through move or remove="true", stops applying if the name changes, with no error. After every upgrade, diff your overrides against the new originals, then run bin/magento setup:upgrade, setup:di:compile, setup:static-content:deploy and cache:flush.

Example

Example · xml
<!-- app/design/frontend/Acme/storefront/Magento_Catalog/layout/catalog_product_view.xml -->
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <!-- Review summary to the top of the right-hand column.
             Core puts it inside product.info.price. -->
        <move element="product.info.review" destination="product.info.main" before="-"/>

        <!-- Email-a-friend link is not wanted on this store -->
        <referenceBlock name="product.info.mailto" remove="true"/>

        <!-- Our own markup for the short description, in our module,
             instead of a copy of the core template -->
        <referenceBlock name="product.info.overview"
                        template="Acme_Storefront::product/view/overview.phtml"/>
    </body>
</page>

<!--
    On the storefront: /acme-running-shoe.html shows the star rating above the
    price block, no "Email to a Friend" link, and the short description
    rendered by our template. Three changes, no core template copied.

    Apply: bin/magento cache:flush
-->

When to use it

  • Hand a theme to another developer who then has to survive a 2.4.x patch release.
  • Decide whether a request needs a template override or a layout instruction.
  • Audit an inherited project to see how much of Luma it has quietly copied.
  • Work out why a block you removed came back after an upgrade.

More examples

Auditing what the theme has adopted

Run this on any project you inherit. Each template listed is markup frozen at the version it was copied, and each override directory is a file that no longer receives upstream fixes. A theme with four templates is maintainable; one with sixty is a fork wearing a child theme's name, and the first upgrade will show it.

Example · bash
THEME=app/design/frontend/Acme/storefront

# Templates this theme has taken ownership of
find $THEME -name '*.phtml' | sort

# Layout files that replace rather than extend
find $THEME -path '*/layout/override/*' | sort

# Stylesheets copied from the parent rather than extended
find $THEME -name '_module.less' -o -name '_theme.less' | sort

Checking an override against the new original

The upgrade step nobody schedules. For every template the theme overrides, diff it against the version that just arrived in vendor; the diff shows both your changes and whatever upstream changed underneath them. Do this before the release, because the failure mode is silent: the page renders, it is just missing a fix or an accessibility attribute added two versions ago.

Example · bash
# After composer updates magento/product-community-edition
diff -u \
  vendor/magento/module-catalog/view/frontend/templates/product/list.phtml \
  app/design/frontend/Acme/storefront/Magento_Catalog/templates/product/list.phtml

# Variables the parent theme added since you copied _theme.less
diff -u \
  vendor/magento/theme-frontend-luma/web/css/source/_theme.less \
  app/design/frontend/Acme/storefront/web/css/source/_theme.less

Your markup in your module, not on top of theirs

When new markup is genuinely needed, add a template of your own and point the block at it from layout, as in the primary example. It lives in your module, is versioned with your code, and does not shadow a core path, so an upgrade to the core template cannot conflict with it. You still do not inherit upstream improvements to that block's markup, which is why this is step five and not step one.

Example · php
<?php
// app/code/Acme/Storefront/view/frontend/templates/product/view/overview.phtml

declare(strict_types=1);

/** @var \Magento\Catalog\Block\Product\View\Description $block */
/** @var \Magento\Framework\Escaper $escaper */

$product = $block->getProduct();
$overview = (string) $product->getShortDescription();

if ($overview === '') {
    return;
}
?>
<div class="product attribute overview">
    <strong class="type"><?= $escaper->escapeHtml(__('In short')) ?></strong>
    <div class="value">
        <?= $escaper->escapeHtml($overview, ['p', 'br', 'strong', 'em', 'ul', 'li']) ?>
    </div>
</div>

<?php
// On the storefront: the product page shows this block where
// product.info.overview sits, with the heading "In short".
// Apply: bin/magento cache:flush
//        bin/magento setup:static-content:deploy -f en_US  (outside developer mode)

Discussion

  • Be the first to comment on this lesson.