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
- Admin configuration, when the setting exists.
etc/view.xmlfor image sizes and display variables._theme.lessvariables, then_extend.lessrules.- Layout XML: move, reorder, remove, change a block's template or arguments.
- 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
<!-- 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.
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' | sortChecking 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.
# 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.lessYour 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.
<?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