Frontend: Luma Themes

This content was generated with the assistance of AI and should be verified against the official documentation of each platform before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

This page was written against Magento 2.4.9 (generally available 2026-05-12) and checked on 2026-10-27. The theme was built and exercised on Mage-OS 3.6.0, the community distribution of the Magento Open Source 2.4.9 code base (PHP 8.5.12, developer mode), which ships the same Magento/blank and Magento/luma themes as Magento Open Source; the documentation quoted is Adobe’s frontend guide. Luma themes apply to Magento Open Source, to Adobe Commerce on-premises and to Adobe Commerce on cloud infrastructure. They do not apply to Adobe Commerce as a Cloud Service (ACCS), whose documentation states that it does not support Luma storefronts: its storefront is the Commerce Storefront on Edge Delivery Services. No statement about deprecating Luma for Open Source or the PaaS editions was found in Adobe’s documentation on the check date. The alternatives (Hyvä, PWA Studio, Edge Delivery) are on the page Frontend: Hyvä, PWA Studio and Edge Delivery.

Luma is the storefront most Magento 2 shops still run, and thousands of extensions assume it. A Luma-based storefront is a tree of PHP templates, layout XML, LESS, RequireJS modules and Knockout templates, resolved through a chain of themes. This page explains the chain (Magento/blank, then Magento/luma, then your theme), builds the Pocket Coffee child theme PocketCoffee/roast step by step, and ends with the static content deployment that makes it fast in production. It assumes the module basics of Module Anatomy and the layout and template concepts of Routing, Controllers, Blocks, Layouts and Templates. The private content section that the theme’s component displays is built on Caching, Indexing and Performance.

Blank and Luma

Magento ships two storefront themes. Both live in Composer packages (magento/theme-frontend-blank and magento/theme-frontend-luma; mage-os/theme-frontend-blank|luma on Mage-OS) and register themselves under the area frontend.

Theme What it is

Magento/blank

The base theme: the complete set of templates from the modules, the UI library (a LESS library of mixins and variables), a plain, neutral look and the responsive layout. It has no parent. It is the theme to inherit from when you want to design everything yourself.

Magento/luma

A demonstration theme that inherits from Blank and adds the look most people know as "Magento": its own typography, colours, icons, buttons, navigation, product page and checkout styles, and overrides of module templates in more than twenty module directories such as Magento_Catalog, Magento_Checkout and Magento_Customer. Its theme.xml declares Magento/blank as its parent.

The responsive breakpoints come from the UI library, in lib/web/css/source/lib/variables/_responsive.less: @screen__xxs 320px, @screen__xs 480px, @screen__s 640px, @screen__m 768px, @screen__l 1024px and @screen__xl 1440px. The theme compiles two stylesheets, styles-m.css for the mobile and small screens and styles-l.css for the desktop (from 768px), which the browser loads with media queries.

A theme is chosen per store view (Content > Design > Configuration), optionally with exceptions per user agent. A child theme normally inherits from Luma or Blank; this page builds a child of Luma, because most extensions are tested against it.

The structure of a theme

The Pocket Coffee theme is a directory under app/design/frontend/<Vendor>/<name>. (When it is distributed with Composer, the same files live in a package of type magento2-theme.) This is the whole file list of the tested theme:

app/design/frontend/PocketCoffee/roast/
  registration.php
  theme.xml
  composer.json
  etc/view.xml
  media/preview.jpg
  requirejs-config.js
  web/css/source/_extend.less
  web/images/logo.svg
  Magento_Theme/
    layout/default.xml
    templates/roast-badge.phtml
    templates/html/copyright.phtml
    web/js/roast-badge.js
    web/js/roast-badge-mixin.js
    web/template/roast-badge.html

registration.php and composer.json

<?php
declare(strict_types=1);

use Magento\Framework\Component\ComponentRegistrar;

ComponentRegistrar::register(ComponentRegistrar::THEME, 'frontend/PocketCoffee/roast', __DIR__);

ComponentRegistrar::THEME with the name frontend/<Vendor>/<name>: the area (frontend or adminhtml), the vendor directory and the theme directory. The path in this string is how everything else refers to the theme (PocketCoffee/roast). The Composer file makes the theme installable:

{
    "name": "pocketcoffee/theme-frontend-roast",
    "description": "Pocket Coffee storefront theme, a child of Magento Luma",
    "type": "magento2-theme",
    "license": "proprietary",
    "require": {
        "php": "~8.3.0||~8.4.0||~8.5.0",
        "magento/framework": "*",
        "magento/theme-frontend-luma": "*"
    },
    "autoload": {
        "files": ["registration.php"]
    }
}

type must be magento2-theme for the Magento Composer installer to put the package in the right place; autoload.files loads the registration; the requirement on magento/theme-frontend-luma documents (and enforces) the parent. When the theme is in app/design, composer.json is optional.

theme.xml

<?xml version="1.0"?>
<theme xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:noNamespaceSchemaLocation="urn:magento:framework:Config/etc/theme.xsd">
    <title>Pocket Coffee Roast</title>
    <parent>Magento/luma</parent>
    <media>
        <preview_image>media/preview.jpg</preview_image>
    </media>
</theme>

title is shown in the Admin; parent is the path Vendor/name of another theme (never its title), and the parent may belong to another vendor; media/preview_image is a path relative to the theme, copied into the media storage when the theme is registered. Without parent the theme stands alone and has to provide everything itself. The schema is urn:magento:framework:Config/etc/theme.xsd; the file validated with xmllint --schema on the test store.

A pitfall found while testing: the preview image has to be a real image. The first version of the theme shipped a four-byte placeholder named preview.jpg, and bin/magento setup:upgrade failed in "Running data recurring" with Warning: Cannot use bool as array in .../framework/Image/Adapter/AbstractAdapter.php, because the image adapter (GD2 on the test store) could not read the file. Any valid JPEG or PNG makes it pass; the one in the theme was produced with imagejpeg() and is 400 x 300 pixels.

Registering and applying the theme

bin/magento setup:upgrade (or bin/magento setup:upgrade --keep-generated) scans app/design and the Composer packages and fills the table theme. On the test store it produced:

theme_id  parent_id  theme_path                theme_title          area
1         NULL       Magento/blank             Magento Blank        frontend
2         NULL       Magento/backend           Magento 2 backend    adminhtml
3         2          MageOS/m137-admin-theme   M137 Admin Theme     adminhtml
4         1          Magento/luma              Magento Luma         frontend
5         4          PocketCoffee/roast        Pocket Coffee Roast  frontend

The parent_id column is the inheritance chain: 5 (roast) to 4 (Luma) to 1 (Blank). To apply the theme, open Content > Design > Configuration, edit the scope (global, website or store view) and choose it under Applied Theme. From the command line the same setting is the configuration path design/theme/theme_id:

bin/magento config:set design/theme/theme_id 5          # 5 is theme_id of PocketCoffee/roast in the theme table
bin/magento cache:flush

The test store keeps Luma as its default theme. To check the Pocket Coffee theme, design/theme/theme_id was set to 5 for a moment, the home page was fetched and the setting was removed again (its value in core_config_data is absent for the default Luma theme).

etc/view.xml

etc/view.xml holds the presentation settings that are data rather than code: the sizes of catalog images and values such as the gallery options. A theme’s file is merged with its ancestors', so the child lists only what it changes.

<?xml version="1.0"?>
<view xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:noNamespaceSchemaLocation="urn:magento:framework:Config/etc/view.xsd">
    <media>
        <images module="Magento_Catalog">
            <image id="category_page_grid" type="small_image">
                <width>300</width>
                <height>300</height>
            </image>
        </images>
    </media>
    <vars module="Magento_Catalog">
        <var name="gallery">
            <var name="nav">thumbs</var>
            <var name="loop">false</var>
        </var>
    </vars>
</view>

Here the category grid image is 300 x 300 pixels instead of Luma’s 240 x 300. Each image has an id (the name templates use, as in $imageHelper->init($product, 'category_page_grid')), a type (image, small_image, thumbnail, swatch_image and swatch_thumb) and the optional width, height, constrain, aspect_ratio, frame, transparency and background elements. The vars element sets values read with $block->getVar() in templates and JavaScript. After a change run bin/magento catalog:images:resize (a consumer, media.storage.catalog.image.resize, does the same asynchronously).

Inheritance and fallback

Theme inheritance and fallback: PocketCoffee/roast, Magento/luma, Magento/blank and the module, with the rules for templates and static files, for LESS partials and for layout XML

Adobe’s rules, from the inheritance page of the frontend guide:

  • Templates (.phtml) and static files (images, JavaScript, CSS, fonts, HTML templates) use fallback: the system looks in the current theme, then in each ancestor in turn, then in the module’s view/frontend (or view/base) directory, and uses the first file it finds. To override one, place a file with the same relative path in your theme. For a template the path is <theme>/<Vendor>_<Module>/templates/<path>; for module static files <theme>/<Vendor>_<Module>/web/<path>; for theme-level static files <theme>/web/<path>; with an i18n/<locale> folder in front for locale-specific files.

  • Layout XML does not use fallback. All layout files of a name are collected and merged: the module’s, then each ancestor’s from the most distant one, then the theme’s. The recommended way to customise is to extend with a file in <theme>/<Vendor>_<Module>/layout/ that only adds or modifies instructions (a referenceBlock with remove="true" removes a block). Overriding is possible but discouraged: <theme>/<Vendor>_<Module>/layout/override/base/<layout>.xml replaces the module’s file, and <theme>/<Vendor>_<Module>/layout/override/theme/<Parent_Vendor>/<parent_theme>/<layout>.xml replaces the file of an ancestor theme. Page layouts use page_layout instead of layout.

  • The parent is "indirectly active": it never needs to be applied to a store view for the child to use it.

Never edit files in vendor/: they are replaced by the next composer update. Everything below overrides or extends from the child theme.

A layout extension and a template

The theme adds a block to the header panel of every page by extending the default.xml of Magento_Theme. The file in the theme contains only the new instruction; the rest of the page’s layout comes from the modules and the ancestors.

<?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="header.panel">
            <block class="Magento\Framework\View\Element\Template"
                   name="pocketcoffee.roast.badge"
                   template="Magento_Theme::roast-badge.phtml"
                   before="-"/>
        </referenceContainer>
    </body>
</page>
<?php
/** @var \Magento\Framework\View\Element\Template $block */
/** @var \Magento\Framework\Escaper $escaper */
?>
<div class="pocketcoffee-roast-badge" data-bind="scope: 'roastBadge'">
    <!-- ko template: getTemplate() --><!-- /ko -->
</div>
<script type="text/x-magento-init">
{
    "*": {
        "Magento_Ui/js/core/app": {
            "components": {
                "roastBadge": {
                    "component": "roastBadge"
                }
            }
        }
    }
}
</script>

The template is a plain block template in the theme (Magento_Theme::roast-badge.phtml resolves to <theme>/Magento_Theme/templates/roast-badge.phtml). It has a Knockout scope, a placeholder that the component fills, and an initialisation script of type text/x-magento-init, the form that is JSON data, not executable script, so it needs no exception in the content security policy (executable inline scripts do; see the CSP documentation).

To override an existing template, place a file at the same relative path. Luma’s footer uses Magento_Theme/templates/html/copyright.phtml; the theme’s copy appends a phrase:

<?php
/** @var \Magento\Theme\Block\Html\Footer $block */
?>
<small class="copyright">
    <span><?= $block->escapeHtml($block->getCopyright()) ?> &middot; <?= $block->escapeHtml(__('Roasted in small batches')) ?></span>
</small>

On the test store with the theme applied, the home page’s HTML contained the badge inside div.panel.header, the footer ended with "Copyright (c) 2013-present Magento, Inc. All rights reserved. · Roasted in small batches", and the page loaded /static/version.../frontend/PocketCoffee/roast/en_US/css/styles-m.css, images/logo.svg and requirejs-config.js. The logo is replaced by a file of the same name in the theme’s web/images/ (Luma’s logo is images/logo.svg too; the Admin setting Content > Design > Configuration > Header > Logo Image takes precedence over a file). The same fallback gives email templates: a copy of Magento_Email/email/footer.html or of the transactional templates under <theme>/Magento_<Module>/email/ overrides the module’s.

Template debugging is built in: bin/magento dev:template-hints:enable (and disable, status) shows which file produced each block, bin/magento cache:flush afterwards.

LESS

The files

Blank’s web/css/ holds the entry points styles-m.less and styles-l.less. Each one imports the UI library, the theme’s source files and, using the Magento directive //@magento_import, the same-named partial of every module and theme in the chain. The order, simplified, from Blank’s styles-m.less:

@import 'source/lib/_lib.less';            // the UI library: mixins and default variables
@import 'source/_sources.less';            // the theme's styles: typography, layout, tables, forms, buttons, ...
//@magento_import 'source/_module.less';   // every module's styles (Magento_Theme/web/css/source/_module.less, ...)
//@magento_import 'source/_widgets.less';  // widgets
@import 'source/_theme.less';              // the theme's variable overrides
@import 'source/lib/_responsive.less';     // the media-query mixins
//@magento_import 'source/_extend.less';   // theme additions, imported last

The partials that you create in a child theme (in web/css/source/):

File Purpose

_theme.less

Overrides the variables of the UI library. A file with this name in a child theme replaces the parent’s file, it does not extend it (see the pitfall below).

_extend.less

Additions: new rules and mixin uses, imported last, after the parent’s styles. Luma and Blank have none, so it is free to use in a child of either.

_variables.less, _extends.less, _typography.less, _layout.less, …​

Replacements of the corresponding parent partial, by the same fallback rule.

<Module_Name>/web/css/source/_module.less

Styles for one module’s blocks. Blank and Luma use these; a child theme can override a module’s file or add rules in _extend.less.

Overriding variables in a child of Luma

The Pocket Coffee theme changes the page background, the link colours and the primary button. The straightforward-looking way, a _theme.less with the new variables, was tried first and failed on the test store:

variable @copyright__background-color is undefined in file
  .../var/view_preprocessed/pub/static/frontend/PocketCoffee/roast/en_US/Magento_Theme/css/source/_module.less in _module.less on line 284

The cause is the fallback rule: the theme’s _theme.less replaced Luma’s, which defines hundreds of variables (including @copyright__background-color), so every module style that uses one of them broke, and the CSS request returned the compiler error (HTTP 404 with the message) instead of a stylesheet. Blank’s own _theme.less carries a comment that a theme file should only declare overrides of existing variables, and that new variables belong in the theme’s local files; for a child of Luma there are two valid ways: copy Luma’s _theme.less into the theme and edit it (it then has to be merged by hand on Luma upgrades), or use LESS’s lazy evaluation: variables are not evaluated in order, the last definition in a scope wins, and _extend.less is imported last. So the overrides go to _extend.less:

//  _extend.less is imported last, after Luma's _theme.less, so with LESS lazy evaluation
//  (the last definition of a variable in a scope wins) these lines re-define Luma's variables.
@page__background-color: #fbf6ee;
@link__color: #6b3e26;
@link__hover__color: #8c5233;
@button-primary__background: #6b3e26;
@button-primary__hover__background: #4f2c1a;
@button-primary__border: 1px solid #4f2c1a;

//  Rules added on top of Luma, after every module's styles.
.pocketcoffee-badge {
    background: @color-white;
    border: 1px solid @link__color;
    border-radius: 3px;
    color: @link__color;
    display: inline-block;
    font-size: 1.2rem;
    padding: .2rem .8rem;

    &._member {
        background: @link__color;
        color: @color-white;
    }
}

.media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__s) {
    .pocketcoffee-badge {
        display: block;
        text-align: center;
    }
}

Compiled by the server on the test store, the result confirmed it: a:hover changed from Luma’s #006bb4 to #8c5233, the rules .pocketcoffee-badge and .pocketcoffee-badge._member appeared in styles-m.css, and the mobile rule was emitted inside @media only screen and (max-width: 639px), because .media-width(@extremum, @break) with 'max' and @screen__s is the UI library’s way to write a breakpoint (@screen__s is 640px, the media query says 639px). The UI library offers many more mixins (.lib-css, .lib-font-size, .lib-button-primary, …​), documented in the "UI library" pages of the frontend guide.

How the LESS becomes CSS

Mode Behaviour

Server-side compilation (default, dev/front_end_development_workflow/type = server_side_compilation)

Magento compiles the LESS with wikimedia/less.php (5.5.1 on the test store) the first time a stylesheet is requested in developer or default mode and stores the result in pub/static; the preprocessed sources are in var/view_preprocessed. Slow on the first request, no tooling to install.

Client-side compilation

The browser compiles LESS with Less.js (4.2.2 in 2.4.9). Only for debugging.

Grunt

The repository carries Gruntfile.js.sample, grunt-config.json.sample and package.json.sample. Copy them without .sample, register the theme in dev/tools/grunt/configs/themes.js, npm install, then grunt exec:<theme> to compile and grunt less:<theme> or grunt watch for a live reload loop. Grunt is a developer workflow tool and does not replace production deployment.

Production

setup:static-content:deploy (below) compiles everything once; at runtime nothing is compiled.

bin/magento dev:source-theme:deploy [--theme=Vendor/name] [--locale=en_US] [--area=frontend] publishes the LESS sources of a theme, by default css/styles-m and css/styles-l, so that Grunt or an IDE can work on them. The two commands rm -rf pub/static/frontend/Vendor var/view_preprocessed followed by bin/magento cache:flush are the standard reset when a stylesheet looks stale in developer mode. The Magento 2 theme design book of 2016 builds its workflow on Grunt and LESS in the same way and is otherwise outdated for today’s installs (it assumes Magento Connect for extension installation and a local XAMPP stack); the flow above replaces both.

RequireJS

Magento’s JavaScript is a graph of AMD modules loaded by RequireJS. Every module and theme can contribute to the RequireJS configuration through a file called requirejs-config.js at the root of the module’s view/<area>/ directory, or at the root of a theme or a theme’s module directory. Magento collects all of them (modules in load order, then the ancestors, then the theme) and writes one merged requirejs-config.js that the page loads first; the tested theme’s was served at /static/frontend/PocketCoffee/roast/en_US/requirejs-config.js and contained its entries in the merged file.

var config = {
    map: {
        '*': {
            roastBadge: 'Magento_Theme/js/roast-badge'
        }
    },
    config: {
        mixins: {
            'Magento_Theme/js/roast-badge': {
                'Magento_Theme/js/roast-badge-mixin': true
            }
        }
    }
};
Key Meaning

map

Aliases a module id for a given context. '*' is every context. Here roastBadge becomes shorthand for Magento_Theme/js/roast-badge; the component name in x-magento-init uses the alias. map is also how a theme replaces a module’s JavaScript file with its own.

paths

Aliases module ids to paths or URLs (for libraries).

shim

Declares dependencies and exports for non-AMD scripts.

deps

Modules loaded on every page as soon as RequireJS starts.

config.mixins

Mixins: modules that wrap another module without replacing its file. Each key is the target module, each value maps a mixin module to true/false (enabled/disabled).

config.config

Per-module configuration read through the module.config() mechanism.

The module identifier Magento_Theme/js/roast-badge means: module directory Magento_Theme, then web/js/roast-badge.js, resolved with the same fallback as other static files (so it is found in <theme>/Magento_Theme/web/js/). A mixin is the supported way to change a widget or component that you do not own, because it survives upgrades of the target:

define([], function () {
    'use strict';

    // A mixin receives the module it extends and returns the extended module.
    return function (RoastBadge) {
        return RoastBadge.extend({
            /** @inheritdoc */
            initialize: function () {
                this._super();
                this.prefix = 'Pocket Coffee: ';

                return this;
            }
        });
    };
});

How a page starts its JavaScript: blocks output either data-mage-init='{"widgetName": {...}}' on an element or a <script type="text/x-magento-init"> with a selector ("*" means no element) and the module to run. The mage/apply/main module reads them. UI components use the second form with Magento_Ui/js/core/app, as the template above does. Third-party scripts added to pages with <script> tags in templates do not pass through RequireJS and break bundling and deferred loading; wrap them in a module and add them with x-magento-init.

Knockout and UI components

Luma uses Knockout.js for the pieces that change in the browser: the mini cart, the customer menu, the checkout and every UI component. A UI component is a JavaScript class (uiComponent, from Magento_Ui/js/lib/core/collection) that has observable properties and a template; Magento_Ui/js/core/app turns the JSON of x-magento-init into component instances and puts each in a scope that the HTML selects with data-bind="scope: 'name'".

The Pocket Coffee component reads the private content section that the module PocketCoffee_RoastIndex provides (see the caching and indexing page) and shows the greeting:

define([
    'uiComponent',
    'ko',
    'Magento_Customer/js/customer-data'
], function (Component, ko, customerData) {
    'use strict';

    return Component.extend({
        defaults: {
            template: 'Magento_Theme/roast-badge'
        },

        /** @inheritdoc */
        initialize: function () {
            this._super();
            // private content: loaded from customer/section/load and kept in localStorage
            this.club = customerData.get('pocketcoffee_club');
            this.greeting = ko.pureComputed(function () {
                return this.club().greeting || '';
            }, this);
            this.isMember = ko.pureComputed(function () {
                return this.club().customer_group_id > 0;
            }, this);

            return this;
        }
    });
});
<span class="pocketcoffee-badge" data-bind="css: {_member: isMember()}, text: prefix + greeting()"></span>

What the pieces do:

  • uiComponent.extend({ defaults: { template: 'Magento_Theme/roast-badge' } }) names the Knockout template. The id Magento_Theme/roast-badge is resolved to Magento_Theme/web/template/roast-badge.html through RequireJS' text plugin (text!); the template/ directory is added by Magento.

  • customerData.get('pocketcoffee_club') returns a Knockout observable holding the section (loaded from customer/section/load/ and cached in localStorage). The computed observables greeting and isMember read from it, and the template binds them with text: and css:.

  • <!-- ko template: getTemplate() --><!-- /ko --> in the PHTML is the usual container comment: Knockout renders the component’s template there.

  • The mixin from the previous section adds the property prefix, used by the template, to show how a mixin extends a component without touching its file.

The static files of the component were fetched over HTTP on the test store (the JavaScript, the template and the merged RequireJS configuration were served with the expected content, the roastBadge scope and the x-magento-init element were in the page), and node --check accepted the three scripts. The component was not executed in a browser here (the shared browser panel was in use), so the rendered text "Pocket Coffee: Join the Coffee Club for 10% off beans" is the expected result and not an observation. The section data it reads was observed through the endpoint ({"pocketcoffee_club":{"customer_group_id":0,"greeting":"Join the Coffee Club for 10% off beans",...}}).

Static content deployment

What gets deployed

In production mode Magento serves only files that exist in pub/static; nothing is generated on demand. bin/magento setup:static-content:deploy [locales] publishes, for every theme and locale, the compiled CSS, the merged requirejs-config.js, the JavaScript, the Knockout and HTML templates, the images and fonts, and the minified and bundled variants when enabled. In developer mode the files are generated on demand: a request for a missing file is routed to pub/static.php, which publishes it (static files such as the logo become symlinks to the theme’s file, as ls -l showed on the test store; the stylesheets are compiled). The first request to the theme’s styles-m.css on the test store took the LESS compile; later ones were files.

bin/magento setup:static-content:deploy -f --area frontend --theme PocketCoffee/roast --no-parent en_US
# Deploy using quick strategy
# frontend/PocketCoffee/roast/en_US       1826/1826   100%   4 s
# Execution time: 4.72
du -sh pub/static/frontend/PocketCoffee     # 33M for one theme and one locale, parents not compiled

The command was run in developer mode with -f/--force (required outside production mode), for one theme and one locale, without the parent themes (--no-parent, supported by the quick and standard strategies). The deployment of one theme without its parents is only suitable for testing: the child’s CSS is compiled against the parent’s sources, and a real deployment lists the locales and deploys all themes in use, with their parents. The folder was removed afterwards and the store continues to generate files on demand.

Option Behaviour

<languages> argument, -l or --language, --exclude-language

Locales to deploy (en_US es_ES). Default all locales in use.

-a or --area, --exclude-area

frontend, adminhtml; default all.

-t or --theme, --exclude-theme

Themes by path (PocketCoffee/roast); default all registered.

-s or --strategy

quick (the default: for each theme one locale is deployed fully and the other locales get only the files that differ, the rest copied, which minimises the time for several locales), standard (the regular process: all files of all packages are published) or compact (minimises the space by storing similar files in base subdirectories).

-f or --force

Allows the command outside production mode.

-j or --jobs

Parallel processes (needs pcntl).

--no-javascript, --no-css, --no-less, --no-images, --no-fonts, --no-html, --no-misc, --no-html-minify

Skip a file type, to save time when only another changed.

--no-js-bundle

Do not produce the JavaScript bundles.

--no-parent

Do not compile parent themes (quick and standard only).

--symlink-locale

Link locale folders that have no customisations to a base locale.

--content-version, --refresh-content-version-only

Set the static content version (the pub/static/deployed_version.txt value that appears as /static/version<timestamp>/ in URLs) so several web nodes agree, or only refresh the version to invalidate browser and CDN caches.

--max-execution-time

Seconds before the deployment is aborted (default 900).

In a deployment pipeline

The production sequence on a build server, as in Adobe’s deployment-flow guidance, is: composer install --no-dev, bin/magento setup:di:compile, composer dump-autoload -o --apcu (the -o option makes a classmap autoloader and --apcu caches found and not found classes in APCu), then bin/magento setup:static-content:deploy for the locales you serve. Setting production mode with deploy:mode:set production runs the compile and the static deployment for you. The pub/static folder is part of the build artefact, which is why a theme’s CSS cannot be edited on the server: edit, build, redeploy. dev/static/sign (value 1 on the test store) appends the version to static URLs, which is what lets the web server give them one-year immutable expiry. Adobe on cloud infrastructure does the same in its build phase from the .magento.env.yaml settings; the Cloud documentation for it is the page on static content deployment.

Pitfalls

Symptom Cause and fix

setup:upgrade fails in "Running data recurring" after adding a theme

The preview_image of theme.xml is not a valid image. Use a real JPEG or PNG, or leave the media node out.

The stylesheet request returns variable @x is undefined

A _theme.less in the child replaced Luma’s. Delete it and put overrides in _extend.less, or copy the parent’s file.

A change to a .phtml or .less file does not appear

Caches and generated files: cache:flush, and in developer mode rm -rf pub/static/frontend/<Vendor> var/view_preprocessed. In production mode re-run the static deployment.

A layout change has no effect

The file name or directory is wrong (<theme>/<Vendor>_<Module>/layout/<handle>.xml), the handle does not match the page, or the layout cache is stale. Use dev:template-hints and the layout handle shown in the page’s <body> classes.

Layout override/ hides a block added by an extension

Overrides replace a whole file and therefore every instruction in it; extend instead.

A component shows nothing

The scope name in data-bind="scope: ..." differs from the key in x-magento-init, the template id is wrong, or the section data is empty because the section is not registered in etc/frontend/di.xml (see the caching page).

A mixin is ignored

The key of config.mixins has to be the exact module id of the target, after any map alias is resolved, and the mixin file has to return a function.

The theme is not an option in the Admin

app/design path or registration.php is wrong, setup:upgrade was not run, or the theme’s parent is not registered. Check the theme table.

Third-party extensions look broken

Many are written for Luma markup and ignore a child theme’s differences; Hyvä needs compatibility modules for them.

What could not be run

The theme’s XML validated against Magento’s XSDs with xmllint --schema, phpcs --standard=Magento2 found no errors in its PHP and PHTML files (one long-line warning in the copyright template), and node --check accepted its JavaScript. The following were not run:

  • Browser execution of the Knockout component (see above): the JavaScript and the page were checked over HTTP, not rendered.

  • Production mode and a full setup:static-content:deploy, JavaScript bundling and minification, and the Grunt workflow (Node tooling was not installed in the container).

  • Hyvä, Breeze, PWA Studio and Edge Delivery storefronts are covered on their own page and were not installed.

  • Adobe Commerce-specific theming features such as Page Builder templates in the Admin, and the Admin theme (adminhtml) customisation, were not exercised.