Themes: Hummingbird and Classic

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.

A PrestaShop theme is the whole front office of a shop: the HTML templates written in Smarty, the CSS and JavaScript, the image sizes, the layout of the pages, and the list of modules that must be hooked where. Because modules draw into the page through hooks and the theme decides where those hooks are, a theme is also the thing that makes a module look right or wrong. This page explains how a PrestaShop 9 theme is built and how to customise one without losing the ability to update it. It covers the two themes that ship with 9.2.0, Hummingbird 2.1.2 (the default for new installs since 9.1) and Classic 3.1.2 (deprecated for new development), the complete theme.yml reference, layouts and template inheritance, how files are resolved between a child theme, its parent and the modules, assets, translations, validation, export, the Bootstrap 4 to 5 migration that affects every module template, and practical guidance on theme design and SEO. The text follows the official developer documentation, checked on 2026-10-27, and was written against PrestaShop 9.2.0 (Symfony 6.4.46, Smarty 4.5.7, PHP 8.5.12 in the Docker image).

The running example is the Pocket Coffee storefront. The page builds a child theme of Hummingbird called pcfgcoffee that shows a roast-date badge on the product page, registers its own stylesheets, adds a "Coffee Club" page layout with a banner, overrides the featured-products block, and declares a custom hook. The child theme was activated in a disposable PrestaShop 9.2.0 installation (so that the shared test shop kept its own theme), and the templates, assets, layout, module override, template resolution, export, validation and CLI behaviours described below were observed there. The section What was run and what was not lists the exact limits. Related pages: Hooks for the hook mechanism, Front Controllers and Templates for module templates, Module Translations for the translation system, and Customers, Multistore and i18n for per-shop themes.

Hummingbird and Classic

PrestaShop 9.2.0 installs two themes in themes/: Hummingbird is the default and Classic is still shipped. The Docker image of 9.2.0 installs Hummingbird as the active theme (ps_shop.theme_name is hummingbird). PrestaShop’s documentation states that Hummingbird is the reference implementation for modern theme development and that Classic is deprecated for new development; existing stores upgraded from 9.0.x keep their theme.

Hummingbird 2.1.2 Classic 3.1.2

Status in 9.2

Default theme for new installs since 9.1. Compatibility declared as from: 9.2.0, to: ~9.2.0

Deprecated for new development, still shipped. Compatibility from: 9.0.0, no upper bound

CSS framework

Bootstrap 5.3.3 (framework: bootstrap-v5.3.3)

Bootstrap 4.0.0-alpha.5 (framework: bootstrap-v4.0.0-alpha.5)

Theme JavaScript

TypeScript compiled with webpack 5, no jQuery in theme code, data-ps-* attributes as hooks

jQuery based

CSS architecture

SCSS with CSS @layer, BEM class names

Bootstrap 4 SCSS

Layout templates

Six: layout-both-columns, layout-full-width, layout-left-column, layout-right-column, layout-content-only, layout-error

Six, same names

Template files

161 .tpl files (25 not in Classic, among them a components/ directory of reusable pieces)

144 .tpl files

Image types declared

17

7

assets/css/theme.css

353,410 bytes

196,290 bytes

assets/js/theme.js

211,560 bytes

204,440 bytes

Accessibility

Built to the European Accessibility Act, ARIA and keyboard navigation documented

Not documented for Classic

Build tools

Node (the package.json of 2.1.2 asks for Node 24 or newer, .nvmrc v24.21.0), npm, webpack, Storybook

Compiled assets are shipped, no source folder in the package

Dark mode

Bootstrap 5.3 colour modes

Not documented for Classic

The numbers in the table were read from the files of the 9.2.0 image: config/theme.yml, the templates/ trees and the assets/ folders. Both themes load themes/core.js (131,542 bytes), which still brings jQuery and the prestashop event object for module compatibility; Hummingbird’s own code does not use jQuery. The official themes documentation says Hummingbird needs Node.js 20.x, while the 2.1.2 package.json declares engines.node >= 24 and the 9.2.0 release notes mention Node 24 and npm 11. The shop image has no Node, so the Hummingbird source build was not run; building a child theme that only contains templates and plain CSS needs no build at all, which is the approach used in this page.

Classic’s templates and Hummingbird’s templates share most file names and the same Smarty block names in the page layouts, so a module that only renders content through hooks usually works in both. What differs is the markup inside the templates, the Bootstrap version and the hooks that exist (see From Bootstrap 4 to Bootstrap 5 and the hook table below).

Hook changes between the themes

Change Detail

displaySearch

Removed from Hummingbird; the search bar module is hooked to displayTop instead (ps_searchbar moved).

displayOrderDetail

Replaces the legacy $HOOK_DISPLAYORDERDETAIL template variable in Hummingbird.

displayModalContent

New in Hummingbird 9.1 for modal windows.

displayOrderDetailProductLine

Called by Hummingbird 2.1.2 in the order detail product line.

manufacturer_product_page

New image type in 2.1.2 (98 x 98, fitment bound) used by the product page brand block.

Image fitment

Image types can declare image_fitment: bound, fit or crop since the 9.2 core feature.

Anatomy of a theme

A theme is a folder in themes/ named exactly like the name key of its config/theme.yml:

themes/pcfgcoffee/
├── assets/
│   └── css/
│       ├── pocket-coffee.css
│       └── roast-badge.css
├── config/
│   └── theme.yml
├── modules/
│   └── ps_featuredproducts/views/templates/hook/ps_featuredproducts.tpl
├── templates/
│   ├── catalog/product.tpl
│   └── layouts/layout-club.tpl
└── preview.png

The documented folders of a complete theme are assets/ (css/, js/, plus img/ and fonts/ by convention), config/ (theme.yml), modules/ (overrides of module templates and assets), plugins/ (custom Smarty plugins), templates/ (with _partials/, catalog/, checkout/, cms/, components/, customer/, errors/ and layouts/), translations/ and preview.png (500 x 746 pixels, shown in the back office theme selector). Source directories for the build (src/, _dev/) are not part of a distributed theme. The child theme above contains only what it changes: PrestaShop finds everything else in the parent.

Hummingbird’s templates/ tree has these top-level groups: _partials/ (head, header, footer, breadcrumb, notifications, pagination, microdata), catalog/ (product, listing pages, miniatures, filters), checkout/ (cart, steps, order confirmation), cms/, components/ (title sections, toasts, quantity input, progress circle, module product lists), customer/ (account pages and forms), errors/, layouts/, and contact.tpl, index.tpl, page.tpl at the root.

The theme.yml reference

config/theme.yml is required: PrestaShop uses it to recognise and configure the theme. The table lists every key found in the documentation and in the two shipped themes.

Key Meaning Notes

name

Technical name.

Must equal the folder name.

display_name

Name shown in the back office.

version

Theme version.

author.name, author.email, author.url

Author.

author.name is required by the validator.

parent

Name of the parent theme folder.

Makes this a child theme.

meta.compatibility.from, .to, .framework

PrestaShop version range and CSS framework.

from is required; to and framework are metadata, not enforced.

meta.available_layouts

Layouts the back office offers, with name and description.

Required; the keys are file names in templates/layouts/ (layout-full-width is layout-full-width.tpl).

assets.use_parent_assets

Child themes: load the parent’s assets.

With true the variables theme_assets, css_url, js_url, img_url point to the parent and child_theme_assets, child_css_url, child_js_url, child_img_url to the child.

assets.css, assets.js

Extra stylesheets and scripts, per page key or all.

Page key is the controller’s php_self. Items have id, path, media, priority, position, inline, server, attribute.

global_settings.configuration

Configuration values written when the theme is activated.

For example PS_IMAGE_QUALITY: png, NEW_PRODUCTS_NBR: 4.

global_settings.modules.to_enable, to_disable, to_reset

Modules enabled (installed if needed), disabled or reset at activation.

Hummingbird disables blockwishlist, ps_brandlist, ps_supplierlist.

global_settings.hooks.custom_hooks

New hooks to register (name, title, description).

global_settings.hooks.modules_to_hook

Modules attached to hooks at activation, ~ means "keep the existing ones here".

Applied only at activation.

global_settings.hooks.modules_to_unhook

Modules detached from hooks at activation.

Applied only at activation.

global_settings.image_types

Image sizes: width, height, scope (products, categories, manufacturers, suppliers, stores), optional image_fitment.

The six legacy types are required: cart_default, small_default, medium_default, large_default, home_default, category_default.

theme_settings.default_layout

Layout used when a page has none.

Required.

theme_settings.layouts

Layout per page key.

Hummingbird sets category, best-sales, new-products, prices-drop, contact, manufacturer, supplier to layout-left-column.

theme_settings.rtl_generation

Generate RTL stylesheets at activation.

false in Hummingbird.

theme_settings.core_scripts

false stops core.js from loading.

Removes the prestashop event object and jQuery; most modules need them.

dependencies.modules

Modules bundled when the theme is exported.

Layout choices made in Design > Theme & Logo > Choose layouts are saved, according to the documentation, in a settings_<shopId>.yml file, and theme.yml stays unchanged; the shop id in the file name is what makes the choice per shop in a multistore installation.

What Hummingbird’s theme.yml does at activation

Hummingbird’s file sets configuration values (four products on the home page, PSR_ICON_COLOR, PSR_TEXT_COLOR for the reassurance block), disables three modules, moves modules between hooks (ps_searchbar and ps_mainmenu into displayTop, ps_languageselector, ps_currencyselector, ps_customersignin and ps_shoppingcart into displayNav2, the home blocks into displayHome, the footer blocks into displayFooterBefore, displayFooter and displayFooterAfter) and declares 17 image types. Activating a theme replaces every existing image type with the list in its theme.yml and does not regenerate the images. In the test installation, activating Classic after Hummingbird left 7 image types in ps_image_type (Classic’s six legacy types and stores_default) and Hummingbird’s 17 returned when it was activated again (count 17 after the switch back); after any switch run Design > Image Settings > Regenerate thumbnails, or bin/console prestashop:thumbnails:regenerate.

Required properties and files

The validator ThemeValidator runs before activation. It requires the 13 properties name, display_name, version, author.name, meta.compatibility.from, meta.available_layouts, the six global_settings.image_types.* legacy types and theme_settings.default_layout, and 32 files: preview.png, config/theme.yml, assets/css/theme.css, assets/js/theme.js, templates/index.tpl, templates/contact.tpl, templates/_partials/form-fields.tpl, the product, product list, cart, cart empty, checkout and order confirmation templates, the four cms/ templates, the thirteen customer/ templates and errors/404.tpl and errors/forbidden.tpl. For a child theme, each file is looked for in the child and then in the parent, so a missing file is an error only if both lack it. The properties are not looked up in the parent.

That last point contradicts the "minimal child theme" in the developer guide, which lists only theme.yml and preview.png and a three-line theme.yml. The first child theme built for this page failed:

$ bin/console prestashop:theme:enable pcfgcoffee
 [ERROR] The selected theme "pcfgcoffee" is invalid

and the validator’s error list (obtained with the ThemeValidator service) was:

An error occurred. The information "global_settings.image_types.cart_default" is missing.
An error occurred. The information "global_settings.image_types.small_default" is missing.
An error occurred. The information "global_settings.image_types.medium_default" is missing.
An error occurred. The information "global_settings.image_types.large_default" is missing.
An error occurred. The information "global_settings.image_types.home_default" is missing.
An error occurred. The information "global_settings.image_types.category_default" is missing.

Copying the image_types block of the parent into the child’s theme.yml fixed it: the theme was enabled, ps_shop.theme_name became pcfgcoffee and ps_image_type held the 17 types. A child theme therefore carries its own image-type list, which is reasonable because activation replaces the shop’s image types with it.

Layouts and template inheritance

Layouts and page templates

A layout (templates/layouts/layout-*.tpl) is the HTML shell: <html>, <head>, <body>, the header, the breadcrumb, the columns and the footer. A page template (catalog/product.tpl, index.tpl, checkout/checkout.tpl) extends the layout held in the Smarty variable $layout, which the front controller sets from the theme settings, and fills the blocks. Hummingbird’s layout-both-columns.tpl is the root; it declares 21 named blocks: head, top_content, skip_to_main_content, hook_after_body_opening_tag, product_activation, header, breadcrumb, main_content, notifications, content_columns, container_class, left_column, content_wrapper, content, right_column, footer, javascript_bottom, bottom_elements, hook_before_body_closing_tag, modal_container and back_to_top. The other layouts only redefine the column blocks.

flowchart TD BC["layouts/layout-both-columns.tpl (html, head, header, columns, footer)"] BC --> FW["layout-full-width.tpl"] BC --> LC["layout-left-column.tpl"] BC --> RC["layout-right-column.tpl"] BC --> CO["layout-content-only.tpl"] CL["child theme: layout-club.tpl"] --> FW ERR["layout-error.tpl (standalone)"] FW -. "set as $layout for a page" .-> PG["page.tpl"] FW -.-> PR["catalog/product.tpl"] FW -.-> IX["index.tpl"] FW -.-> PL["catalog/listing/product-list.tpl"] PG --> CMS["cms/page.tpl"] PG --> CT["contact.tpl"] PL --> CAT["catalog/listing/category.tpl"]

The dotted arrows are the {extends file=$layout} of the page templates, which change with the layout chosen for each page; the solid arrows are fixed {extends file='...'} relations. All of this was read from Hummingbird 2.1.2: layout-full-width, layout-left-column, layout-right-column and layout-content-only each extend layout-both-columns, page.tpl, catalog/product.tpl, catalog/listing/product-list.tpl, checkout/checkout.tpl and index.tpl extend $layout, cms/page.tpl and contact.tpl extend page.tpl, and catalog/listing/category.tpl extends catalog/listing/product-list.tpl.

Block strategies

A child template redefines only the blocks it needs. Smarty has three strategies, all of which were used in the child theme:

Strategy Syntax Effect

Replace

{block name='foo'}...{/block}

Replaces the parent’s block.

Append

{block name='foo' append}...{/block}

Parent content first, then yours.

Prepend

{block name='foo' prepend}...{/block}

Yours first, then the parent content.

Parent reference

{$smarty.block.parent} inside the block

Inserts the parent’s content at a chosen position.

Child reference

{$smarty.block.child} in the parent

Wraps what children inject.

The Pocket Coffee product template appends the roast-date badge after the price block and exposes a custom hook for modules:

{extends file='parent:catalog/product.tpl'}

{block name='product_prices' append}
  {foreach from=$product.grouped_features item=feature}
    {if $feature.name === 'Roast date'}
      <p class="roast-badge" data-ps-ref="roast-badge">
        <span class="roast-badge__label">{l s='Roasted on' d='Shop.Pocketcoffee'}</span>
        <span class="roast-badge__date">{$feature.value}</span>
      </p>
    {/if}
  {/foreach}
  {hook h='displayPcfgRoastBadge' product=$product}
{/block}

With the product feature Roast date = 2026-10-26 attached to product 1, the rendered page contained:

<p class="roast-badge" data-ps-ref="roast-badge">
  <span class="roast-badge__label">Roasted on</span>
  <span class="roast-badge__date">2026-10-26</span>
</p>

The Coffee Club layout prepends a banner to the breadcrumb block of the full-width layout and is assigned to the CMS pages through theme_settings.layouts.cms:

{extends file='layouts/layout-full-width.tpl'}

{block name='breadcrumb' prepend}
  <div class="club-banner">
    {l s='Coffee Club members get 10% off beans and free shipping above 40 EUR' d='Shop.Pocketcoffee'}
  </div>
{/block}

The delivery page (a CMS page) then had <body id="cms" class="... layout-club page-cms ..."> and contained the banner.

The parent: prefix and self-extension

A child template with the same path as a parent template replaces it. To start from the parent version of the same path, extend it with the parent: prefix, as the product template above does. Without the prefix, {extends file='catalog/listing/category.tpl'} written inside the child’s catalog/listing/category.tpl points at itself. In the test shop this was not a Smarty error: the request died, the Apache log reported child pid ... exit signal Segmentation fault and a PHP fatal error Allowed memory size of 536870912 bytes exhausted in Smarty’s inheritance runtime, and the client received no response at all. With the parent: prefix the same template rendered normally. Extending a different base template (the catalog/listing/category-4.tpl below extending catalog/listing/category.tpl) is fine.

Template resolution order

PrestaShop looks for a page template by locale and entity id, from the most specific name to the default, and each name is searched in the active theme first and in its parent second. For product 2 in en-US the order is:

  1. en-US/catalog/product-2.tpl

  2. catalog/product-2.tpl

  3. en-US/catalog/product.tpl

  4. catalog/product.tpl

Listing pages add a fallback to catalog/listing/product-list.tpl. In developer mode every template is wrapped in HTML comments that name the file that was actually used, which makes the order easy to confirm. Three files were created in the child theme (templates/catalog/listing/category-4.tpl, templates/catalog/product-1.tpl and templates/en-US/catalog/product-2.tpl) and the three pages showed <!-- begin catalog/listing/category-4.tpl, <!-- begin catalog/product-1.tpl and <!-- begin en-US/catalog/product-2.tpl, while the other products kept using catalog/product.tpl. An empty template file renders nothing: an empty override replaces the page with a blank one, which is how the first attempts at these files looked before they received content.

How PrestaShop finds a theme file: page templates are tried from the most specific name to the default, first in the child theme and then in the parent; assets use the child file when it exists and the parent file otherwise; module templates are looked up in the child modules folder, then the parent modules folder, then the module itself

Assets

Core assets and registration

PrestaShop loads theme.css (id theme-main, priority 50), rtl.css for RTL languages (900), custom.css (1000), core.js (priority 0), theme.js (50) and custom.js (1000) on every page. Use custom.css and custom.js for quick overrides in a theme that is not compiled.

Extra files are registered in theme.yml (per page key or all), from a module with actionFrontControllerSetMedia, or in a controller’s setMedia():

assets:
  use_parent_assets: true
  css:
    product:
      - id: pcfg-roast-badge
        path: assets/css/roast-badge.css
        media: all
        priority: 200
    all:
      - id: pcfg-custom
        path: assets/css/pocket-coffee.css
        priority: 210

The product page of the test shop loaded the parent’s theme.css, then roast-badge.css and pocket-coffee.css from the child; the home page loaded only pocket-coffee.css from the child. The options are:

Stylesheet option Meaning

media

CSS media type, default all.

priority

0 to 999, lower is earlier, default 50. Equal values keep registration order.

inline

Put the content in a <style> tag.

version

Cache-busting query string.

needRtl

Load the _rtl variant if it exists, default true.

JavaScript option Meaning

position

head or bottom (default).

priority

0 to 999.

inline

Inline <script>.

attributes (PHP) / attribute (theme.yml)

async or defer. The key is singular in theme.yml and plural in the PHP API.

server

local or remote for a CDN URL.

version

Cache-busting string.

Registering an id that already exists replaces the asset with that id. To remove an asset of a module, register nothing under its path by placing an empty file at themes/<theme>/modules/<module>/views/css/style.css (only for local, relative paths), or call unregisterStylesheet('id') and unregisterJavascript('id') from actionFrontControllerSetMedia.

The child theme and theme.js

With use_parent_assets: true the parent’s compiled theme.css and theme.js are used. The rule PrestaShop applies is per file: it takes the child’s file if it exists and the parent’s file otherwise. That has a consequence that cost an hour in the test: the first version of the child theme contained an empty assets/js/theme.js (created to satisfy the idea that the validator wants it), and the product page then loaded the child’s empty script at the end of the page and none of Hummingbird’s 211,560-byte theme.js, so every interaction of the theme was gone. After the empty file was deleted the page loaded themes/hummingbird/assets/js/theme.js. The same holds for theme.css: do not create the file in a child theme unless you mean to replace the parent’s stylesheet (in that case use_parent_assets is usually false). A child with its own styles uses the registered files above and leaves theme.css and theme.js to the parent.

Compiling Hummingbird and the CSS architecture

If you fork Hummingbird instead of using a child theme, the source is TypeScript and SCSS built with webpack. The scripts are npm ci, npm run build, npm run watch, npm run lint, npm run stylelint, npm run format, npm run test (Jest) and npm run storybook. Edit src/scss/ and src/js/, not the compiled assets/ output. The SCSS layout is abstract/ (variables, mixins), bootstrap/ (variable overrides and component restyling), prestashop/ (base/, components/, layout/, modules/, pages/) and vendors/, with entry points theme.scss, rtl.scss and error.scss. theme.scss fixes the CSS layer order:

@layer vendors, bs-base, bs-components, bs-custom-components, ps-base, ps-components, ps-pages, ps-modules, utilities;

Unlayered CSS, for example from modules, wins over layered CSS; this is intentional for backward compatibility. Custom classes follow BEM (.product-card, .product-card__title, .product-card--featured), selectors use classes only, no ids or tag selectors, and JavaScript hooks use data-ps-* attributes (data-ps-ref, data-ps-action, data-ps-target, data-ps-component, data-ps-state, data-ps-context, data-ps-observe, data-ps-data) rather than js- classes, although both coexist in 2.1.2. Events travel through the prestashop object: prestashop.on('updatedProduct', fn), prestashop.emit('myEvent', data), with constants in Theme.events.

Module overrides in a theme

A theme can replace a module’s templates, CSS and JavaScript by mirroring the module’s paths under themes/<theme>/modules/<module>/. PrestaShop uses the theme file when it exists and falls back to the module. Hummingbird itself overrides the templates of core modules this way (its modules/ folder holds blockreassurance, blockwishlist, contactform, productcomments, ps_banner, ps_bestsellers and many more), and the child theme’s lookup is child, then parent, then module.

The Pocket Coffee override changes the title and CSS name of the featured-products block. Hummingbird’s own override extends components/module-products.tpl; the child extends the same base:

{extends file='components/module-products.tpl'}

{block name='module_products_name'}pcfg-featured-beans{/block}

{block name='module_products_title'}
  {include file='components/section-title.tpl' title={l s='Our beans' d='Shop.Pocketcoffee'}}
{/block}

The home page then contained <section class="pcfg-featured-beans"> and <h2 class="section-title">Our beans</h2>, with the dev comment <!-- begin module:ps_featuredproducts/views/templates/hook/ps_featuredproducts.tpl -->. The other modules the child did not touch, for example ps_contactinfo, still rendered from the parent (<!-- begin /var/www/html/themes/hummingbird/modules/ps_contactinfo/nav.tpl -->).

Two rules matter. A template that includes a partial with a relative path ({include file='./_partials/card.tpl'}) always loads the partial from the module, so a theme override of the partial is ignored; a module that includes with the module: prefix ({include file='module:ps_featuredproducts/views/templates/front/_partials/card.tpl'}) goes through the override chain. And an empty override file tells PrestaShop to render nothing for that template: use it only as a last resort.

Hooks in themes

Templates call hooks with {hook h='displayFooter'} and the optional mod and excl parameters; extra parameters are passed on to the modules. Themes can register hooks of their own in theme.yml:

global_settings:
  hooks:
    custom_hooks:
      - name: displayPcfgRoastBadge
        title: Pocket Coffee roast badge
        description: Shown next to the price of a product that has a roast date.

After activation the hook existed in ps_hook (id 1267 in the test shop) and a module can attach to it with registerHook('displayPcfgRoastBadge'). modules_to_hook and modules_to_unhook run only at activation, so changing them in an already active theme does nothing until the theme is re-activated. Hummingbird publishes visual maps of its front-office hooks per page (home, category, product, cart, checkout flow, my account, contact, login).

Translations

Wordings in templates use {l} with a domain: {l s='Read more' d='Shop.Pocketcoffee'}. The theme part of the domain starts with a capital letter and has no other capitals (Shop.Mytheme is right, Shop.MyTheme is not). Core domains such as Shop.Theme.Actions, Shop.Theme.Catalog, Shop.Theme.Checkout and Shop.Theme.Global are already translated in the language packs, so reuse them for "Add to cart" and similar. The sprintf parameter inserts values, js=1 escapes for JavaScript, and translated strings reach scripts through a Smarty-rendered variable. In International > Translations, install the languages first (Add / Update a language), then choose Front office translations, the theme and the language to fill in the theme’s own domain. Export a language to put the .xlf files in the theme’s translations/ folder, which is bundled when the theme is exported. The full list of Smarty helpers: {l}, {url}, {hook}, {render}, {form_field}, {widget}, {widget_block} and the modifiers classname and classnames.

RTL: when an RTL language is active the <body> gets the class lang-rtl and PrestaShop loads rtl.css (priority 900) and the _rtl variant of each stylesheet. Design > Theme & Logo > Adaptation to right-to-left languages generates _rtl.css files and never overwrites an existing one; put corrections in a .rtlfix file with the same name as the original. Prefer CSS logical properties (margin-inline-start).

From Bootstrap 4 to Bootstrap 5

Classic uses Bootstrap 4, Hummingbird uses Bootstrap 5.3. Modules written for 1.7 and 8.x assume the BS4 markup, and the PrestaShop documentation lists the three families of breaking changes: attributes are namespaced (data-toggle becomes data-bs-toggle, data-target becomes data-bs-target), utility classes are renamed (.ml-* and .mr-* become .ms-* and .me-*, .pl-* and .pr-* become .ps-* and .pe-*, .sr-only becomes .visually-hidden, .float-left and .float-right become .float-start and .float-end, .text-left and .text-right become .text-start and .text-end, .badge-* becomes .text-bg-*), and jQuery is no longer a dependency of the framework ($('#modal').modal() becomes bootstrap.Modal). Without changes, a BS4 module in Hummingbird loses spacing and its modals and tooltips stop opening.

BS4 BS5 Note

data-toggle="modal" data-target="#m"

data-bs-toggle="modal" data-bs-target="#m"

Namespaced data attributes.

class="ml-2 mr-2"

class="ms-2 me-2"

Logical start and end.

class="sr-only"

class="visually-hidden"

Screen-reader text.

class="form-group"

grid and spacing utilities

form-group was removed.

class="btn btn-default"

class="btn btn-outline-secondary"

btn-default was removed in 4 and has no BS5 equivalent.

$('.dropdown-toggle').dropdown()

new bootstrap.Dropdown(el)

No jQuery plugin interface without a bridge.

The Bootstrap Compatibility Layer is a separate npm package, not part of Hummingbird. It is a runtime shim that maps data-* to data-bs-*, renames the utility classes and bridges the jQuery plugins; a module includes it from actionFrontControllerSetMedia. The documentation calls it a temporary patch: new modules for PrestaShop 9 should use BS5.3 markup directly. The migration guide of Bootstrap lists every change.

Building the Pocket Coffee child theme

Create, validate, activate

The following steps were run in the disposable 9.2.0 installation. The theme folder was copied to themes/pcfgcoffee and given to the web user.

# themes/pcfgcoffee/config/theme.yml
parent: hummingbird
name: pcfgcoffee
display_name: Pocket Coffee
version: 1.0.0
author:
  name: "Pocket Coffee"
  url: "https://example.com"

meta:
  compatibility:
    from: 9.2.0
    to: ~9.2.0
    framework: bootstrap-v5.3.3
  available_layouts:
    layout-full-width:
      name: Full Width
      description: No side columns, ideal for product pages.
    layout-both-columns:
      name: Three Columns
      description: One large central column and 2 side columns.
    layout-left-column:
      name: Two Columns, small left column
      description: Two columns with a small left column
    layout-right-column:
      name: Two Columns, small right column
      description: Two columns with a small right column
    layout-club:
      name: Coffee Club
      description: Full-width page with a club banner above the content.

assets:
  use_parent_assets: true
  css:
    product:
      - id: pcfg-roast-badge
        path: assets/css/roast-badge.css
        media: all
        priority: 200
    all:
      - id: pcfg-custom
        path: assets/css/pocket-coffee.css
        priority: 210

global_settings:
  image_types:
    cart_default:      { width: 125, height: 125, scope: [products] }
    small_default:     { width: 98,  height: 98,  scope: [products, categories, manufacturers, suppliers] }
    medium_default:    { width: 452, height: 452, scope: [products, manufacturers, suppliers] }
    large_default:     { width: 800, height: 800, scope: [products, manufacturers, suppliers] }
    home_default:      { width: 250, height: 250, scope: [products] }
    category_default:  { width: 180, height: 180, scope: [categories] }
    # ... the remaining image types of the parent (stores_default, default_xs ... manufacturer_product_page)
  hooks:
    custom_hooks:
      - name: displayPcfgRoastBadge
        title: Pocket Coffee roast badge
        description: Shown next to the price of a product that has a roast date.

theme_settings:
  default_layout: layout-full-width
  layouts:
    cms: layout-club
docker cp pcfgcoffee ec-ps:/var/www/html/themes/
docker exec ec-ps chown -R www-data:www-data /var/www/html/themes/pcfgcoffee
docker exec -u www-data -w /var/www/html ec-ps php bin/console prestashop:theme:enable pcfgcoffee
#  [OK] Theme "pcfgcoffee" enabled with success.

After activation: ps_shop.theme_name was pcfgcoffee, the product page carried the badge, the child’s two stylesheets and the parent’s theme.css and theme.js, the home page showed the pcfg-featured-beans section, and the CMS pages used the layout-club body class. During development, set Smarty to force compilation (Advanced Parameters > Performance) and disable CCC so that CSS and JavaScript are served as separate files; the image default of PS_CSS_THEME_CACHE=1 combines them into assets/cache/theme-*.css.

Export and distribution

bin/console prestashop:theme:export pcfgcoffee (or Design > Theme & Logo > Export current theme) writes a ZIP. The command printed Your theme has been correctly exported: /var/www/html/themes/pcfgcoffee.zip. The archive contained every file of the theme folder, an index.php guard in each directory, the module overrides and a translations/en-US/ folder generated for the theme. There is no exclusion list for files: clean node_modules/, .git/, source folders, IDE and CI files and any assistant-context files before exporting. Test activation on a clean installation and keep preview.png at 500 x 746 pixels. The Addons marketplace requires its own validation; the developer documentation has no theme checklist for Addons in 9.x, only the generic validation described above.

Switching themes from the command line

prestashop:theme:enable is the only theme command besides prestashop:theme:export. In 9.2.0 it failed in the test installation with Cannot build Language context as no languageId has been defined whenever activating the theme had to change the state of a module: switching from Classic to Hummingbird failed while blockwishlist was enabled (Hummingbird disables it), and switching to Classic failed while it was disabled. Disabling or enabling the module first with bin/console prestashop:module disable blockwishlist (or enable) made the same command succeed. The cause is in the command class: prestashop:module prepares the language context through ContextBuilderPreparer, prestashop:theme:enable does not. Activating from the back office is not affected, and a script can call $container->get(ContextBuilderPreparer::class)->prepareLanguageId(1) before dispatching EnableThemeCommand.

Theme design and SEO practice

The official documentation explains how to build a theme; the books in the field (Hashim for PrestaShop 1.3, Pérez Castaño for 1.6, Ballard for Shopify) explain how to design one. Their advice is still sound once their code is replaced by the current architecture. The older books describe themes made of header.tpl, footer.tpl and CSS files such as global.css, maintenance.css and scenes.css edited by hand, and modules positioned with "transplanting" in the back office; none of that applies to a Symfony-era theme with theme.yml, layouts and Smarty inheritance, and none of them covers Bootstrap 5, @layer, accessibility regulations or JSON-LD.

Page by page

  • Home: the one decision is what the first screen proves. For Pocket Coffee that is a roast calendar, the three origins and the club offer. Home blocks are modules hooked to displayHome; choose their order in modules_to_hook and keep the number of blocks small, since each one is a database query and an image set.

  • Category (listing): filters, sort order and a short description above the products. Hummingbird’s listing template is built from blocks (product_list_header, product_list_top, product_list_active_filters, product_list, product_list_bottom, product_list_footer), so a theme customises one block instead of copying the page. Give each category a unique title, meta description and friendly URL: the SEO recipes for categories and products in PrestaShop Recipes use the same fields that PrestaShop still has in the category and product forms.

  • Product: the information hierarchy is photo, name, price and availability, the add-to-cart control, a short description, then the long description, features and reviews. Ballard’s hierarchy argument fits the Hummingbird template, which orders product_cover_thumbnails, product_header, product_prices, product_description_short, product_customization, product_buy, product_variants, product_add_to_cart, product_additional_info, product_tabs and product_footer. A roast date belongs next to the price, which is why the example appends to product_prices.

  • Cart and checkout: reduce choices. Core features (guest checkout, a single-page flow in 9.2 behind a flag) and the theme’s checkout templates must be tested together; a theme that rewrites the checkout markup also has to keep the selectors that the One Page Checkout module expects.

  • Account and content pages: use layouts for variety instead of copying templates; the CMS layout layout-club shows a banner on content pages only.

SEO in the theme

Hummingbird’s _partials/head.tpl is the place to look. The same blocks produced these tags on the product page of the test shop:

  • the <title> and <meta name="description"> from the page meta ($page.meta.title, $page.meta.description), editable per category, product and CMS page in the back office, and <meta name="robots"> when the page is not index;

  • <link rel="canonical" href="http://localhost:8091/1-hummingbird-printed-t-shirt.html">;

  • a <link rel="alternate" hreflang="..."> for every active language, for example hreflang="en-us" and hreflang="es-es" pointing at the English page and /es/1-hummingbird-printed-t-shirt.html;

  • Open Graph tags og:title, og:description, og:url, og:site_name, og:type and, on products, og:image;

  • five application/ld+json scripts on a product page: Organization, WebSite with a SearchAction, WebPage, BreadcrumbList and Product. In 9.2 the core builds structured data and the theme prints $structured_data; the legacy partials under _partials/microdata/ remain as a fallback and are announced as to be removed;

  • _partials/pagination-seo.tpl, which computes the previous and next page URLs of a paginated listing from the canonical URL, and _partials/preload.tpl, which prints the build-generated assets/preload.html with the theme path fixed.

SEO habits that belong in theme work: one <h1> per page (Hummingbird prints the product name as h1 and uses h2 for section titles), friendly URLs on, a sitemap (a CMS page and a module), canonical URLs for filtered listings, image alt text from the product, and speed. The books tie speed to SEO (PrestaShop Recipes has a chapter on it); for a theme that means few render-blocking files (the build produces one theme.css), sized images from image_types with default_xs to default_xl for responsive sets, product_main and product_main_2x for retina screens, and the CCC options in Advanced Parameters > Performance in production.

Accessibility

Hummingbird is written against the European Accessibility Act: landmark elements (<nav>, <main>, <header>, <footer>), a "skip to main content" link (layout-both-columns has the block skip_to_main_content), aria-label on regions, keyboard navigation and visible focus. A child theme inherits all of it and should not remove it; custom templates follow the coding standards: semantic HTML5, BEM classes, 2-space indentation, <button> for actions and <a> for navigation.

What was run and what was not

  • Run in a disposable 9.2.0 installation: creating the child theme, the validation error list, enabling the theme from the command line, product, category and CMS pages in the shop, template resolution by entity id and locale, append and prepend, parent: extension and the self-extension crash, the module override and parent fallback, the two registered stylesheets, the theme.js inheritance, the custom hook, prestashop:theme:export and the archive listing, the comparison of the asset files of both themes, and the switch to Classic and back with the blockwishlist observation.

  • Run in the shared shop, read only: the file trees, theme.yml and templates of the installed Hummingbird and Classic themes.

  • Not run: building Hummingbird from source (Node is not in the shop image), uploading a ZIP through the back office, the layout chooser and the generated settings_<shopId>.yml, RTL generation, Storybook, Addons validation and the One Page Checkout templates.

  • Docs inconsistencies: the minimal child theme in the guide does not pass validation in 9.2.0 without image_types; the requirements page says Node 20.x while the Hummingbird 2.1.2 package.json needs 24 or newer.