Themes: Block 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 WooCommerce store has two layers of presentation. The plugin produces the data and a set of default templates and blocks; the active WordPress theme decides how the pages around them look and, increasingly, which templates exist at all. This page explains both generations of theme on the version baseline WordPress 7.1.3 and WooCommerce 11.2.1 (the 11.2 series was released on 2026-10-07 and 11.2.1 on 2026-10-09), checked on 2026-10-27: the block theme (HTML templates, theme.json, the Site Editor), the classic theme (PHP templates, hooks, the woocommerce/ override folder), the two default themes WooCommerce points people to (Purple, a block theme, and Storefront, a classic theme), and the practice around them: child themes, why template overrides break, conditional tags, and the theme design and SEO decisions that books on WooCommerce theming still teach well. Everything here applies to self-hosted WooCommerce on WordPress; a theme is a WordPress concept, so it applies to any plan of a hosting provider that lets you install themes.

The running example is the Pocket Coffee shop. Two throw-away themes were written and exercised on a local store: a block child theme of Twenty Twenty-Five, pocket-coffee-themes-roast, that styles the WooCommerce blocks with theme.json, shows the roast date of a product with Block Bindings and provides single product and catalogue templates; and a classic child theme of Storefront, pocket-coffee-themes-storefront-child, that prints the same roast date through a hook and carries one deliberately outdated template override. Neither was activated: the store keeps its own active theme, and the examples were rendered through the theme preview mechanism described in the next section. The roast date itself, the order data model and the extension plugin structure are covered in Extension Development and CRUD Objects and Data Stores; the emails that also read theme template files are on Emails; the blocks used inside the templates are on Product Collection and Blocks and Cart and Checkout Blocks.

How the examples were exercised

A theme can be tried without becoming the active theme. WordPress honours a wp_theme_preview query argument for any administrator: the argument replaces the stylesheet and template options for that single request, so the page is built with the previewed theme while every other visitor keeps seeing the active one. It is the mechanism of the "Live Preview" in the Site Editor. The examples below fetch pages with an administrator cookie and the argument, and look for a marker paragraph that each throw-away template prints (template: single-product (Pocket Coffee Roast)), so the answer to "which file rendered this page" is read from the HTML and not guessed.

# Log in once and keep the cookie jar. The password comes from the environment, never from the command line history.
curl -s -c cookies.txt -b cookies.txt -o /dev/null http://localhost:8081/wp-login.php
curl -s -c cookies.txt -b cookies.txt -o /dev/null -H 'Cookie: wordpress_test_cookie=WP%20Cookie%20check' \
  -d "log=admin&pwd=${WP_ADMIN_PASSWORD}&wp-submit=Log+In&testcookie=1" http://localhost:8081/wp-login.php

# Render the single product page with a theme that is installed but not active.
curl -s -b cookies.txt \
  'http://localhost:8081/product/themes-test-kit/?wp_theme_preview=pocket-coffee-themes-roast' \
  | grep -o 'template: [^<]*'

Output seen:

template: single-product (Pocket Coffee Roast)

For code that is not a page (the System Status check, the template listing), the same trick is applied inside one WP-CLI process: two add_filter calls on stylesheet and template make wp_get_theme() and the template functions answer for the throw-away theme, and nothing is written to the database. One detail matters when you do this yourself: WordPress 7.1 caches the theme paths in globals used by locate_template(), so call wp_set_template_globals() after adding the filters.

The test store had Twenty Twenty-Five 1.5, Twenty Twenty-Four 1.6, Twenty Twenty-Three 1.7, Storefront 4.6.2 and Purple 1.0.3 installed, with the shared child theme ec-block-child active. The two throw-away themes and Purple were added as inactive themes.

wp theme list --fields=name,status,version

Output seen (the shared child theme ec-block-child is the only active one):

name	status	version
ec-block-child	active	0.1.0
pocket-coffee-themes-roast	inactive	0.1.0
pocket-coffee-themes-storefront-child	inactive	0.1.0
purple	inactive	1.0.3
storefront	inactive	4.6.2
twentytwentyfive	parent	1.5
twentytwentyfour	inactive	1.6
twentytwentythree	inactive	1.7

Two kinds of theme and how WooCommerce chooses

WordPress themes come in two generations, and WooCommerce supports both. A classic theme builds pages from PHP template files (header.php, single.php, page.php) and is configured in the Customizer; a block theme builds pages from HTML files that contain block markup (templates/single.html, parts/header.html), and everything visual is edited in the Site Editor (Appearance > Editor) and stored in theme.json. A theme is a block theme when it has templates/index.html; wp_is_block_theme() tells you. Both generations are explained without WooCommerce in Themes Overview and Templates and Template Parts.

WooCommerce decides what to do in one function, wc_current_theme_supports_woocommerce_or_fse() (added in 6.0.0): the theme is supported when it declared add_theme_support( 'woocommerce' ) or when it is a block theme. Block themes need no declaration, because WooCommerce ships its own block templates for them. A classic theme that declares nothing is an unsupported theme: WooCommerce does not load its templates; it hooks the_content and the_title to print the shop and product pages inside the theme’s generic page template, which works but looks plain. Newer stores should not be in that last state.

flowchart TD A[Front-end request] --> B{Theme supports WooCommerce or is a block theme} B -- no --> U[Unsupported theme: content filters print the shop and product inside page.php] B -- yes --> C{Block theme} C -- yes --> D[Block templates: Site Editor edit, then child theme, parent theme, WooCommerce] C -- no --> E[WC_Template_Loader picks a PHP page file] E --> F[wc_locate_template picks every template part] D --> G[Blocks print the product data] F --> H[Hooks print the product data]

The practical consequences are the headings of the rest of this page. In a block theme you change a page by editing block markup and theme.json; in a classic theme you change it with hooks and, as a last resort, by copying a PHP template. Cart and Checkout are the exception to the rule that a theme owns the page: they are blocks stored in the content of the Cart and Checkout pages, so they look the same in either kind of theme (see Cart and Checkout Blocks).

Version history

Version Change that matters to themes

6.0.0

wc_current_theme_supports_woocommerce_or_fse() introduced: block themes are supported without add_theme_support.

7.8

wc-cart-fragments is only enqueued for the legacy mini-cart widget; a theme that relies on it for its own header cart must keep the legacy widget.

8.3

Cart and Checkout blocks are the default for new installs; upgraded stores keep the checkout they had.

10.4

The Interactivity API Mini-Cart block is the default.

11.1.0 (2026-09-03)

WordPress 7.0 becomes the minimum; Purple (block theme) requires WooCommerce 11.1 or newer.

11.2.0 (2026-10-07)

single-product/meta.php is bumped to 11.2.0 and gains the woocommerce_product_meta_category_orderby filter: every older copy of that template is now reported as outdated.

11.3.0-beta.1 (2026-10-26)

Beta only: on block themes the Shop page is associated with the Product Catalog template. Not part of the stable baseline.

Block themes

The files of a block theme

A block theme is a directory in wp-content/themes/. This is the whole file list of the Pocket Coffee child theme that was tested:

pocket-coffee-themes-roast/
  style.css                          theme header only (no layout CSS needed)
  theme.json                         settings and styles, schema version 3
  functions.php                      registers the roast_date meta
  templates/
    single-product.html              Woo template: product page
    archive-product.html             Woo template: shop and category archives
    single-product-themes-test-kit.html      (added later: one specific product)
    taxonomy-product_cat.html                (added later: all categories)
    taxonomy-product_cat-themes-test-beans.html   (added later: one category)

style.css is not where a block theme keeps its design; it only has to carry the header WordPress reads (name, parent, minimum versions). Everything visual goes into theme.json and the templates. The folders have fixed meanings: templates/ (one file per template), parts/ (reusable pieces such as the header and footer), patterns/ (block patterns, PHP files with a header comment, or HTML) and styles/ (style variations: alternative theme.json files the user picks in the Site Editor).

style.css and child themes

A child theme names its parent in the Template header. WordPress then loads the child’s files first and falls back to the parent for anything the child does not provide, and it merges the child’s theme.json over the parent’s. For a block parent such as Twenty Twenty-Five the child can be just these files, which is why the update path stays clean: parent updates keep flowing in and only the customisations live in the child.

/*
Theme Name: Pocket Coffee Roast
Description: Block child theme of Twenty Twenty-Five for the Pocket Coffee shop.
Template: twentytwentyfive
Version: 0.1.0
Requires at least: 7.0
Requires PHP: 7.4
Text Domain: pocket-coffee-roast
License: GPL-2.0-or-later
*/

Template is the directory name of the parent, not its display name. Requires at least and Requires PHP stop WordPress from installing the theme on an older site. A classic child theme has the same header; the difference is that it must also enqueue the parent stylesheet (shown in the classic section below), while a block child theme normally needs no stylesheet at all, because the design lives in theme.json, which WordPress merges with the parent’s.

theme.json

theme.json is the single source of truth for a block theme’s design system. It has three top-level parts that matter here: settings (which options the editor offers and which presets exist: colours, spacing sizes, font families, layout widths), styles (the actual CSS values, applied to the whole site, to elements such as links, and to individual blocks by name), and customTemplates/templateParts (names for templates a user can pick). version is the schema version (the test theme and Twenty Twenty-Five use 3) and $schema lets an editor validate the file. WordPress turns every preset into a CSS custom property (--wp--preset--color--roast) and every block style into a scoped rule, printing only what the page uses.

{
	"$schema": "https://schemas.wp.org/trunk/theme.json",
	"version": 3,
	"settings": {
		"color": {
			"palette": [
				{ "slug": "roast", "name": "Roast", "color": "#4b2e1e" },
				{ "slug": "crema", "name": "Crema", "color": "#f3e3cf" },
				{ "slug": "green", "name": "Green bean", "color": "#6b7f4f" }
			]
		},
		"spacing": {
			"spacingSizes": [
				{ "slug": "30", "name": "Small", "size": "0.75rem" },
				{ "slug": "50", "name": "Medium", "size": "1.5rem" }
			]
		},
		"layout": { "contentSize": "720px", "wideSize": "1160px" }
	},
	"styles": {
		"blocks": {
			"woocommerce/product-price": {
				"color": { "text": "var(--wp--preset--color--roast)" },
				"typography": { "fontWeight": "700" }
			},
			"woocommerce/product-button": {
				"color": {
					"background": "var(--wp--preset--color--roast)",
					"text": "var(--wp--preset--color--crema)"
				}
			}
		},
		"css": ".pc-which { font-size: 0.75rem; opacity: 0.6; }"
	}
}

The file is plain JSON, so a syntax error disables the whole file silently; validate it before deploying (python3 -c "import json; json.load(open('theme.json'))" is enough for syntax). The effect was read back from the page rendered with the theme preview:

curl -s -b cookies.txt 'http://localhost:8081/product/themes-test-kit/?wp_theme_preview=pocket-coffee-themes-roast' > single.html
grep -o -- '--wp--preset--color--roast:[^;]*' single.html | head -2
grep -o '.wp-block-woocommerce-product-price[^}]*}' single.html | head -3

Output seen:

--wp--preset--color--roast: #4b2e1e
.wp-block-woocommerce-product-price .wc-block-components-product-price){color: var(--wp--preset--color--roast);font-weight: 700;}

The reference for Woo blocks is the block name: every WooCommerce block can be styled under styles.blocks with its registered name (woocommerce/product-price, woocommerce/product-button, and so on; the list is in the block reference). Prefer this to plain CSS for the reasons the WooCommerce documentation gives: only the CSS the page uses is printed, users can edit the values in Appearance > Editor > Styles, conflicts between themes and plugins resolve gracefully, and the styles survive markup changes in the blocks. The css key (and the Additional CSS panel of the Site Editor) remains the escape hatch for what the schema cannot express.

The values a user saves in the Site Editor’s Styles panel are stored in the database (a wp_global_styles post) and sit on top of the theme’s theme.json; a theme update therefore never overwrites what the shop owner changed, and "Reset styles" in the editor goes back to the file.

Style variations are further theme.json files in styles/: Purple ships styles/colors with 9 palettes and styles/typography with 10 font pairings, plus styles/block with 5 block styles, so one theme can look like several.

functions.php and Block Bindings

Block themes may still have a functions.php. The Pocket Coffee theme uses it for one thing: registering the product meta key roast_date with show_in_rest, which makes the key available to the editor and to Block Bindings, the WordPress mechanism that binds an attribute of a core block to a data source. A paragraph bound to the core/post-meta source prints the value of a custom field without PHP in the template.

<?php
/**
 * Pocket Coffee Roast: theme setup.
 *
 * @package PocketCoffeeRoast
 */

defined( 'ABSPATH' ) || exit;

add_action(
	'init',
	static function () {
		// Expose the roast date to the block editor and to Block Bindings.
		register_post_meta(
			'product',
			'roast_date',
			array(
				'type'         => 'string',
				'single'       => true,
				'show_in_rest' => true,
			)
		);
	}
);

The single product template (the next section explains where it lives and why it wins) uses the binding on a core paragraph:

<!-- wp:template-part {"slug":"header"} /-->

<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
<main class="wp-block-group">
	<!-- wp:paragraph {"className":"pc-which"} -->
	<p class="pc-which">template: single-product (Pocket Coffee Roast)</p>
	<!-- /wp:paragraph -->

	<!-- wp:woocommerce/store-notices /-->
	<!-- wp:post-title {"level":1} /-->
	<!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"core/post-meta","args":{"key":"roast_date"}}}}} -->
	<p></p>
	<!-- /wp:paragraph -->
	<!-- wp:woocommerce/product-price {"isDescendentOfSingleProductTemplate":true} /-->
	<!-- wp:woocommerce/add-to-cart-form /-->
</main>
<!-- /wp:group -->

<!-- wp:template-part {"slug":"footer"} /-->

With the meta roast_date set to 2026-10-20 on the test product (wp post meta update <id> roast_date 2026-10-20), the page contained:

<p class="wp-block-paragraph">2026-10-20</p>

Block markup is HTML comments with a JSON attribute object; the editor writes it for you, and for a template this small it is normal to write it by hand. The pc-which paragraph is a development aid that prints the name of the template that rendered the page, and is removed before release. The roast_date meta is a product (post) meta key, so reading it from PHP in a plugin or a classic theme must go through the CRUD object ($product->get_meta( 'roast_date' )), as shown in the classic section.

Patterns

A pattern is a reusable chunk of block markup. In a theme it is a PHP file in patterns/ whose header comment names it (Title, Slug, Categories); templates insert it with <!-- wp:pattern {"slug":"theme/pattern-name"} /-->. Purple uses this heavily: its templates/single-product.html is eight lines and delegates the whole product layout to the pattern purple/hidden-single-product, so a shop owner can change the layout from the pattern, and a developer changes one file for several templates. Purple ships 63 patterns (hero sections, product grids, about pages, footers, a cart page title, and more). WooCommerce itself ships patterns (for example woocommerce-blocks/related-products, used by its own single product template).

WooCommerce block templates and parts

WooCommerce registers its templates with WordPress, so they appear in the Site Editor (Appearance > Editor > Templates) next to the theme’s own, with names such as "Product Catalog". On 11.2.1 the following templates and template parts exist (names and slugs are the registered ones; a theme overrides one by providing a file with the same slug in templates/ or parts/).

Slug Title in the editor What it renders

single-product

Single Product

The product page.

archive-product

Product Catalog

The shop page and product archives; the fallback of the three taxonomy templates below.

taxonomy-product_cat

Products by Category

A category archive. Falls back to archive-product when the theme does not provide it.

taxonomy-product_tag

Products by Tag

A tag archive; falls back to archive-product.

taxonomy-product_attribute

Products by Attribute

An attribute term archive; falls back to archive-product.

taxonomy-product_brand

Products by Brand

A brand archive (when product brands are used); falls back to archive-product.

product-search-results

Product Search Results

The result page of a product search.

page-cart, page-checkout

Page: Cart, Page: Checkout

The frame around the Cart and Checkout pages; the blocks themselves stay in the page content.

order-confirmation

Order Confirmation

The order received page.

coming-soon

Page: Coming soon

The coming soon page. The documentation calls it page-coming-soon; the slug registered by 11.2.1 is coming-soon.

Template part slug Used by

mini-cart

The drawer of the Mini-Cart block.

checkout-header

The header of the Checkout template.

coming-soon-social-links

The social links of the coming soon page.

simple-product-add-to-cart-with-options, variable-product-add-to-cart-with-options, grouped-product-add-to-cart-with-options, external-product-add-to-cart-with-options

The add to cart form with options, one part per product type, used inside the add to cart form with options block.

The registered list was read from WordPress with the throw-away theme standing in as the active one:

// Treat the throw-away block theme as active for this process only.
add_filter( 'stylesheet', fn() => 'pocket-coffee-themes-roast' );
add_filter( 'template', fn() => 'twentytwentyfive' );

foreach ( array( 'wp_template', 'wp_template_part' ) as $type ) {
	$templates = get_block_templates( array(), $type );
	usort( $templates, fn( $a, $b ) => strcmp( $a->slug, $b->slug ) );
	foreach ( $templates as $template ) {
		if ( 'plugin' === $template->source || 'theme' === $template->source && false !== strpos( $template->slug, 'product' ) ) {
			printf( "%-17s %-48s %-10s %s\n", $type, $template->slug, $template->source, $template->title );
		}
	}
}

Run with wp eval, it printed:

wp_template       archive-product                                  theme      Product Catalog
wp_template       coming-soon                                      plugin     Page: Coming soon
wp_template       order-confirmation                               plugin     Order Confirmation
wp_template       page-cart                                        plugin     Page: Cart
wp_template       page-checkout                                    plugin     Page: Checkout
wp_template       product-search-results                           plugin     Product Search Results
wp_template       single-product                                   theme      Single Product
wp_template       taxonomy-product_attribute                       plugin     Products by Attribute
wp_template_part  checkout-header                                  plugin     Checkout Header
wp_template_part  coming-soon-social-links                         plugin     Coming Soon Social Links
wp_template_part  external-product-add-to-cart-with-options        plugin     External Product Add to Cart + Options
wp_template_part  grouped-product-add-to-cart-with-options         plugin     Grouped Product Add to Cart + Options
wp_template_part  mini-cart                                        plugin     Mini-Cart
wp_template_part  simple-product-add-to-cart-with-options          plugin     Simple Product Add to Cart + Options
wp_template_part  variable-product-add-to-cart-with-options        plugin     Variable Product Add to Cart + Options

The source column is the point: theme means a file of the active theme (the two files of the Pocket Coffee theme), plugin means WooCommerce’s default, and a third value, custom, means the template was edited in the Site Editor and lives in the database. The category and tag templates are not in the list: WooCommerce serves those pages from archive-product unless a theme provides a template of its own.

Which template renders a page

WordPress looks for the most specific template and falls back. For WooCommerce pages the specific names are built from the product slug and the term slug, exactly as in the documentation: single-product-<product-slug>.html for one product, taxonomy-product_cat-<category-slug>.html for one category, then the general taxonomy-product_cat.html, then archive-product.html. The lookup of each slug goes through four places in a fixed order: a template edited in the Site Editor (database), the child theme, the parent theme, and WooCommerce.

Block theme template resolution for WooCommerce pages, from the most specific slug to the fallback, and the four places each slug is looked up

The order was measured by adding and removing files in the throw-away theme and rendering the same pages with the preview argument:

P='wp_theme_preview=pocket-coffee-themes-roast'
g() { echo "$1 -> $(curl -s -b cookies.txt "http://localhost:8081/$1?$P" | grep -o 'template: [^<]*')"; }
g product/themes-test-kit/
g product-category/themes-test-beans/
g shop/

With all five template files present in the theme (single-product, single-product-themes-test-kit, archive-product, taxonomy-product_cat, taxonomy-product_cat-themes-test-beans):

product/themes-test-kit/ -> template: single-product-themes-test-kit (Pocket Coffee Roast)
product-category/themes-test-beans/ -> template: taxonomy-product_cat-themes-test-beans (Pocket Coffee Roast)
shop/ -> template: archive-product (Pocket Coffee Roast)

Then deleting taxonomy-product_cat-themes-test-beans.html, taxonomy-product_cat.html and single-product-themes-test-kit.html one after the other and rendering again gave, in this order:

product-category/themes-test-beans/ -> template: taxonomy-product_cat (Pocket Coffee Roast)
product-category/themes-test-beans/ -> template: archive-product (Pocket Coffee Roast)
product/themes-test-kit/ -> template: single-product (Pocket Coffee Roast)

A product search (?s=kit&post_type=product) did not print the marker because the theme has no product-search-results.html: WooCommerce’s own template rendered, which is the fourth place in the figure and the reason a theme only needs the templates it wants to change.

Site Editor edits win over theme files

Saving a template in the Site Editor creates a wp_template post tagged with the theme’s slug in the wp_theme taxonomy. When WordPress resolves a slug it looks at these posts first. The following reproduces what the editor does and shows the effect (the post was created for the throw-away theme only and deleted afterwards):

$id = wp_insert_post(
	array(
		'post_type'    => 'wp_template',
		'post_name'    => 'single-product',
		'post_title'   => 'Single Product',
		'post_status'  => 'publish',
		'post_content' => '<!-- wp:paragraph {"className":"pc-which"} --><p class="pc-which">template: single-product (database, Site Editor)</p><!-- /wp:paragraph -->',
	)
);
wp_set_object_terms( $id, 'pocket-coffee-themes-roast', 'wp_theme' );

The theme term is assigned with wp_set_object_terms() on purpose: the tax_input argument of wp_insert_post() checks the capabilities of the current user, and under WP-CLI without --user it silently assigns nothing, which leaves a template that belongs to no theme and is never used.

Rendering the product page again printed template: single-product (database, Site Editor) instead of the theme file, and reading the template back gave custom as its source:

custom | origin='plugin' | theme=pocket-coffee-themes-roast

The check that read it back:

add_filter( 'stylesheet', fn() => 'pocket-coffee-themes-roast' );
add_filter( 'template', fn() => 'twentytwentyfive' );
$t = get_block_template( 'pocket-coffee-themes-roast//single-product', 'wp_template' );
echo $t->source, ' | origin=', var_export( $t->origin, true ), ' | theme=', $t->theme, "\n";

The origin stays plugin because the customised template started as the WooCommerce one. In practice this is the most common reason a shop owner’s change "does nothing after a theme update": the database copy keeps being used and the new file in the theme is never consulted. The cure is in the editor (the template’s "Clear customizations" or "Reset" action), or deleting the wp_template post. The same applies to template parts and to Global Styles. Keep this in mind before debugging markup: ask first whether the page is rendering a theme file at all.

Cart and Checkout page templates

page-cart.html and page-checkout.html render the content of the pages that WooCommerce assigns as the Cart and Checkout pages, so the theme only decides the frame (header, footer, title). The default template wraps the content in the woocommerce/page-content-wrapper block; when you override it, keep the post content block and do not place the Cart or Checkout block directly in the template, otherwise the page editor and what shoppers see drift apart and features that inspect the page content (for example the checks that decide whether the page uses the blocks) cannot find the block.

<!-- wp:template-part {"slug":"header","tagName":"header"} /-->
<!-- wp:woocommerce/page-content-wrapper {"page":"cart"} -->
<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
<main class="wp-block-group">
	<!-- wp:woocommerce/store-notices /-->
	<!-- wp:post-title {"align":"wide", "level":1} /-->
	<!-- wp:post-content {"align":"wide"} /-->
</main>
<!-- /wp:group -->
<!-- /wp:woocommerce/page-content-wrapper -->
<!-- wp:template-part {"slug":"footer","tagName":"footer"} /-->

This is the markup of the default page-cart.html of 11.2.1 (the checkout one is identical except "page":"checkout" and no title block). Use it as the starting point of an override.

The catalogue template

The catalogue template is the Product Collection block with the query set to inherit, wrapped in a heading, a results count and a sort control. The Pocket Coffee theme starts from WooCommerce’s own file, and the only edit is the marker paragraph.

<!-- wp:template-part {"slug":"header"} /-->

<!-- wp:group {"tagName":"main","layout":{"inherit":true,"type":"constrained"}} -->
<main class="wp-block-group">
	<!-- wp:paragraph {"className":"pc-which"} -->
	<p class="pc-which">template: archive-product (Pocket Coffee Roast)</p>
	<!-- /wp:paragraph -->

	<!-- wp:woocommerce/breadcrumbs /-->

	<!-- wp:query-title {"type":"archive","showPrefix":false,"align":"wide"} /-->

	<!-- wp:term-description {"align":"wide"} /-->

	<!-- wp:woocommerce/store-notices /-->

	<!-- wp:group {"layout":{"type":"flex","flexWrap":"nowrap","justifyContent":"space-between"}} -->
	<div class="wp-block-group alignwide">
		<!-- wp:woocommerce/product-results-count /-->
		<!-- wp:woocommerce/catalog-sorting /-->
	</div>
	<!-- /wp:group -->
	<!-- wp:woocommerce/product-collection {"queryId":0,"query":{"woocommerceAttributes":[],"woocommerceStockStatus":["instock","outofstock","onbackorder"],"taxQuery":{},"isProductCollectionBlock":true,"perPage":10,"pages":0,"offset":0,"postType":"product","order":"asc","orderBy":"title","author":"","search":"","exclude":[],"sticky":"","inherit":true},"tagName":"div","dimensions":{"widthType":"fill","fixedWidth":""},"displayLayout":{"type":"flex","columns":3,"shrinkColumns":true},"convertedFromProducts":false,"queryContextIncludes":["collection"],"align":"wide"} -->
	<div class="wp-block-woocommerce-product-collection alignwide">
		<!-- wp:woocommerce/product-template -->
		<!-- wp:woocommerce/product-image {"showSaleBadge":false,"imageSizing":"thumbnail","isDescendentOfQueryLoop":true} -->
			<!-- wp:woocommerce/product-sale-badge {"isDescendentOfQueryLoop":true,"align":"right"} /-->
		<!-- /wp:woocommerce/product-image -->
		<!-- wp:post-title {"textAlign":"center","level":2,"isLink":true,"fontSize":"medium","__woocommerceNamespace":"woocommerce/product-collection/product-title","style":{"typography":{"lineHeight":"1.4"}}} /-->
		<!-- wp:woocommerce/product-price {"textAlign":"center","isDescendentOfQueryLoop":true,"fontSize":"small","style":{"spacing":{"margin":{"bottom":"1rem"}}}} /-->
		<!-- wp:woocommerce/product-button {"textAlign":"center","isDescendentOfQueryLoop":true,"fontSize":"small","style":{"spacing":{"margin":{"bottom":"1rem"}}}} /-->
		<!-- /wp:woocommerce/product-template -->

		<!-- wp:query-pagination {"layout":{"type":"flex","justifyContent":"center"}} -->
		<!-- wp:query-pagination-previous /-->
		<!-- wp:query-pagination-numbers /-->
		<!-- wp:query-pagination-next /-->
		<!-- /wp:query-pagination -->

		<!-- wp:woocommerce/product-collection-no-results -->
		<!-- wp:group {"layout":{"type":"flex","orientation":"vertical","justifyContent":"center","flexWrap":"wrap"}} -->
		<div class="wp-block-group">
			<!-- wp:pattern {"slug":"woocommerce/no-products-found-clear-filters"} /-->
		</div>
		<!-- /wp:group -->
		<!-- /wp:woocommerce/product-collection-no-results -->
	</div>
	<!-- /wp:woocommerce/product-collection -->
</main>
<!-- /wp:group -->

<!-- wp:template-part {"slug":"footer"} /-->

Two lessons from reading WooCommerce’s own template: the attribute inherit: true makes the block use the main query of the page, which is what lets one file serve the shop, a category and a tag; and the many isDescendentOf... attributes are written by the editor to tell inner blocks which context they are in, so when you hand-write a template, copy a working one instead of starting from nothing. Since 11.3 (beta) the Shop page is mapped to this template for block themes; on 11.2.1 the shop page already renders this template through the same archive-product lookup, as the shop/ line above shows.

Classic themes

A classic theme that wants WooCommerce’s templates declares support in functions.php, and may describe the image sizes and grid it designed for. The support array keys read by the code of 11.2.1 are thumbnail_image_width, single_image_width and product_grid with default_rows, min_rows, max_rows, default_columns, min_columns and max_columns; the three gallery features are separate add_theme_support calls.

<?php
add_action(
	'after_setup_theme',
	static function () {
		add_theme_support(
			'woocommerce',
			array(
				'thumbnail_image_width' => 324,
				'single_image_width'    => 416,
				'product_grid'          => array(
					'default_columns' => 3,
					'default_rows'    => 4,
					'min_columns'     => 1,
					'max_columns'     => 6,
					'min_rows'        => 1,
				),
			)
		);
		add_theme_support( 'wc-product-gallery-zoom' );
		add_theme_support( 'wc-product-gallery-lightbox' );
		add_theme_support( 'wc-product-gallery-slider' );
	}
);

These are the values Storefront 4.6.2 passes (through its own storefront_woocommerce_args filter). The image widths feed WooCommerce’s image sizes (woocommerce_thumbnail, woocommerce_single); the product_grid values are the defaults and limits of the catalogue columns and rows the customer sets in the Customizer. A theme with woocommerce.php in its root can wrap all shop pages in one file; note that when it exists it wins over every more specific WooCommerce template and the documentation warns that a woocommerce/archive-product.php override is then ignored.

How a classic theme loads a page

WC_Template_Loader hooks template_include for supported themes. For a product page it first builds a list of file names and asks WordPress to locate the first one that exists in the child or parent theme: files added by the filter woocommerce_template_loader_files, then woocommerce.php, then the page template chosen for the page, then single-product-<slug>.php, then the default single-product.php; only when none exists does it fall back to the file inside the plugin (templates/single-product.php). Taxonomy archives follow the same pattern with taxonomy-product_cat-<slug>.php, taxonomy-product_cat.php (also searched in the woocommerce/ folder and in the form with dashes, taxonomy-product-cat.php) and the default archive-product.php. Inside those page files, the pieces of a page are template parts (single-product/meta.php, loop/price.php) located by wc_locate_template(): first the woocommerce/ folder of the child theme, then the parent theme’s woocommerce/ folder, then the same path at the theme root, then the plugin.

How a classic theme resolves WooCommerce templates: the order of files for a page, the order for a template part, and the version check that reports outdated overrides

Hooks first, overrides last

Every WooCommerce template is made of do_action() calls, and the cheapest, most upgrade-safe customisation is to attach to one. The Pocket Coffee roast date is an action on woocommerce_single_product_summary, priority 15 (after the price at 10 and before the excerpt at 20). It reads the date through the product object, as HPOS-era code must, and the output is escaped.

<?php
/**
 * Pocket Coffee Storefront child theme.
 *
 * @package PocketCoffeeStorefrontChild
 */

defined( 'ABSPATH' ) || exit;

/**
 * Load the parent stylesheet first, then the child one.
 */
function pocket_coffee_sf_styles() {
	wp_enqueue_style( 'storefront-style', get_template_directory_uri() . '/style.css', array(), wp_get_theme( 'storefront' )->get( 'Version' ) );
	wp_enqueue_style( 'pocket-coffee-sf', get_stylesheet_uri(), array( 'storefront-style' ), wp_get_theme()->get( 'Version' ) );
}
add_action( 'wp_enqueue_scripts', 'pocket_coffee_sf_styles' );

/**
 * Show the roast date on the single product page, below the price.
 */
function pocket_coffee_sf_roast_date() {
	global $product;

	if ( ! $product instanceof WC_Product ) {
		return;
	}

	$roast_date = $product->get_meta( 'roast_date' );
	if ( '' === $roast_date ) {
		return;
	}

	printf(
		'<p class="pocket-coffee-roast-date">%s</p>',
		esc_html( sprintf( /* translators: %s: roast date */ __( 'Roasted on %s', 'pocket-coffee-storefront-child' ), $roast_date ) )
	);
}
add_action( 'woocommerce_single_product_summary', 'pocket_coffee_sf_roast_date', 15 );

The child theme’s style.css header names Template: storefront. With the theme preview argument the page contained:

pocket-coffee-roast-date">Roasted on 2026-10-20

Storefront adds its own layer of hooks on top of WooCommerce’s: 51 distinct storefront_* actions (storefront_header, storefront_before_content, storefront_footer, the homepage section hooks, and so on) and the matching storefront_* functions in its inc/ folder. They are the "Storefront child theme" recipes of the 2015 to 2018 books: they still run in Storefront 4.6.2, but they exist only in Storefront, so a recipe written for them does not survive a change of theme.

Overriding a template

When the markup itself must change, copy the template from wp-content/plugins/woocommerce/templates/ into wp-content/themes/<child>/woocommerce/ keeping the sub-path (without the templates/ segment) and edit the copy. The Pocket Coffee child theme hides the tag list in the product meta by overriding single-product/meta.php; the copy below is trimmed to what the shop wanted and deliberately taken from an older WooCommerce release (header @version 9.7.0) to show what happens later.

<?php
/**
 * Single Product Meta
 *
 * This template can be overridden by copying it to yourtheme/woocommerce/single-product/meta.php.
 *
 * @see         https://woocommerce.com/document/template-structure/
 * @package     WooCommerce\Templates
 * @version     9.7.0
 */

use Automattic\WooCommerce\Enums\ProductType;

defined( 'ABSPATH' ) || exit;

global $product;
?>
<div class="product_meta">

	<?php
	/**
	 * Start of the product meta.
	 *
	 * @since 9.7.0
	 */
	do_action( 'woocommerce_product_meta_start' );
	?>

	<?php if ( wc_product_sku_enabled() && ( $product->get_sku() || $product->is_type( ProductType::VARIABLE ) ) ) : ?>
		<span class="sku_wrapper"><?php esc_html_e( 'SKU:', 'woocommerce' ); ?> <span class="sku"><?php echo esc_html( $product->get_sku() ? $product->get_sku() : __( 'N/A', 'woocommerce' ) ); ?></span></span>
	<?php endif; ?>

	<?php echo wc_get_product_category_list( $product->get_id(), ', ', '<span class="posted_in">' . _n( 'Category:', 'Categories:', count( $product->get_category_ids() ), 'woocommerce' ) . ' ', '</span>' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>

	<?php /* Pocket Coffee: the tag list is intentionally not shown. */ ?>

	<?php
	/**
	 * End of the product meta.
	 *
	 * @since 9.7.0
	 */
	do_action( 'woocommerce_product_meta_end' );
	?>

</div>

Rendered with the preview argument, the page contained the SKU PC-THEMES-KIT and the category line, and no tagged_as element (the class WooCommerce gives the tag list): the override was used.

Purple and Storefront

WooCommerce points new stores at two themes from the same company, one per generation.

Purple is the block theme. Its WordPress.org slug is purple; version 1.0.3 was published on 2026-10-09. The style header reads Requires at least: 7.1, Requires PHP: 7.4, WC requires at least: 11.1 and WC tested up to: 11.2, so it needs WordPress 7.1 or newer, newer than the plugin’s own minimum of 7.0. It ships 19 templates (covering every WooCommerce template in the table above except the brand and tag fallbacks, plus pages such as front-page, home, search and blank and cover page templates), 11 template parts, 63 patterns, 9 colour variations and 10 typography variations. Its single product, cart and checkout templates are thin and delegate to patterns. It also loads a small stylesheet per WooCommerce release (assets/css/woocommerce-11.1.css and woocommerce-11.2.css) and enqueues only those that apply to the running WooCommerce version, which is a neat answer to the "core changed its markup" problem of classic overrides. In October 2026 its development moved into the WooCommerce monorepo.

Storefront is the classic theme. Version 4.6.2 (2025-12-09) is the latest release on WordPress.org; its header says Requires PHP: 5.6.0 and Tested up to: 6.7, so it has not been re-tested against the 7.x series by its own header, though it kept working on 7.1.3 in the tests of this page. It is customised in the Customizer, extended through its storefront_* hooks and child themes, and has no theme.json. The WooCommerce developer documentation calls it "Woo’s flagship classic theme", but no official page says whether it is still actively developed: the last release is still the 2025-12-09 one, the header above still says Tested up to: 6.7, and the GitHub repository is not archived. Treat its maintenance status as unknown and check the release history before relying on it.

Purple, a block theme, compared with Storefront, a classic theme: requirements, layout files, design settings, extension points and typical breakage

How to choose, for a new store: take a block theme. It gets the Site Editor, Global Styles, patterns, and WooCommerce’s block templates without copies that go out of date. Purple is the WooCommerce-maintained starting point; Twenty Twenty-Five or any block theme that looks the way you want is a legitimate base too, since WooCommerce supplies the missing store templates. Stay on a classic theme when the store depends on extensions or custom code written for it (a page builder, a PHP template library, Storefront-specific extensions) and plan the migration; a classic theme is supported, not deprecated.

Template overrides and why they break

An override is a frozen copy of the markup of one release. WooCommerce edits its templates on major and minor releases (the template documentation lists the changes in each release), and when it does it bumps the @version number in the file’s header. Your copy keeps the old code, so it silently misses new filters, new arguments, accessibility fixes and security fixes. The template used as the example in this test shows it: single-product/meta.php became @version 11.2.0 in this release and gained the woocommerce_product_meta_category_orderby filter; the copy at 9.7.0 does not have it, so a store that depends on that filter sees no effect and no error. The same happens with a security or accessibility fix: the copy keeps the vulnerable or inaccessible markup until someone notices.

Finding outdated overrides

WooCommerce > Status > System status lists, at the bottom, every template overridden by the active theme and warns when the override is older than the core template. The same data is in the REST system status response (theme.overrides, theme.has_outdated_templates), built by get_theme_info() of the controller. This script reproduces the report for the throw-away classic theme without activating it; it clears the one-hour cache of the report first (see the pitfall below) and does not store the result.

// Pretend, for this one process only, that the Pocket Coffee child theme is the active theme.
add_filter( 'stylesheet', fn() => 'pocket-coffee-themes-storefront-child' );
add_filter( 'template', fn() => 'storefront' );
wp_set_template_globals();

// The report caches its answer for an hour: clear the cache, and do not store this answer.
delete_transient( 'wc_system_status_theme_info' );
add_filter( 'pre_set_transient_wc_system_status_theme_info', fn( $value ) => $value );

$controller = new WC_REST_System_Status_V2_Controller();
$info       = $controller->get_theme_info();
echo wp_json_encode(
	array_intersect_key( $info, array_flip( array( 'name', 'is_child_theme', 'is_block_theme', 'has_outdated_templates', 'overrides' ) ) ),
	JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES
), "\n";

Output seen:

{
    "name": "Pocket Coffee Storefront Child",
    "is_child_theme": true,
    "is_block_theme": false,
    "has_outdated_templates": true,
    "overrides": [
        {
            "file": "wp-content/themes/pocket-coffee-themes-storefront-child/woocommerce/single-product/meta.php",
            "version": "9.7.0",
            "core_version": "11.2.0"
        }
    ]
}

Replacing the copy with the current core file (and then reapplying the edit) changed the answer to "has_outdated_templates": false, with "version": "11.2.0" equal to "core_version": "11.2.0". Two behaviours are worth knowing. The report only scans the templates of the WooCommerce core templates/ folder, so an override of a template that belongs to an extension (including the Pocket Coffee email template of the Emails page, which the same child theme overrides) is never flagged. And the report is cached in the transient wc_system_status_theme_info for an hour: after fixing a template the screen may still show the old warning until the transient expires or a template/theme change clears it.

Fixing them

The documentation’s procedure is the one to follow, in this order: open the status screen and note the outdated templates; save a backup of each; copy the current template from wp-content/plugins/woocommerce/templates/<path> into the child theme (the latest release, which you can also download from WordPress.org or GitHub); and replay your edits on top of the new file by diffing the backup against the old core version. The point is that the process is manual by design: no tool can merge markup for thousands of themes. Reduce the future cost of it:

  • Prefer a hook or a filter to an override; if the change is a single line, there probably is one.

  • Keep each override as small as possible, and put a comment at the top that says what you changed and why, so the diff against the new core file is quick.

  • Do not copy templates "just in case". A template you did not change should not be in the theme.

  • Track the template changes of each WooCommerce release (the developer blog lists them) before updating a production store.

  • Use WC_TEMPLATE_DEBUG_MODE only while debugging: while it is true, wc_locate_template() ignores theme overrides and loads core files, which is a quick way to prove that an override causes a bug.

  • On a block theme the equivalent of the override is a template or part file with the same slug (with the Site Editor caveat from above), and the equivalent of the version check is reading the WooCommerce release notes for template changes; the blocks inside the templates can change between releases too, which is what Purple’s per-version stylesheets compensate for.

The folder name woocommerce/ that the theme is searched in comes from WC()->template_path() and can be changed with the woocommerce_template_path filter; the path of a located template can be altered with woocommerce_locate_template. Both are for plugins that ship their own templates. The email screens of the Emails page use a third filter, woocommerce_template_directory, only for the "copy file to theme" action.

Conditional tags

Code that runs on every page (a script, a banner, a body class) needs to know which kind of page it is on. WordPress has its own conditional tags (is_single(), is_page()); WooCommerce adds a family, defined in wc-conditional-functions.php and wc-template-functions.php.

Function True when

is_woocommerce()

On any page that WooCommerce itself renders: shop, category, tag, attribute, single product. It is not true on the Cart, Checkout and My Account pages, which are ordinary pages with blocks or shortcodes.

is_shop()

The shop page or the product archive.

is_product_taxonomy(), is_product_category(), is_product_tag()

Any product taxonomy archive, a category archive, a tag archive.

is_product()

A single product page.

is_cart(), is_checkout()

The Cart page, the Checkout page.

is_checkout_pay_page(), is_order_received_page(), is_add_payment_method_page()

The pay-for-order endpoint, the thank-you endpoint, the add payment method endpoint.

is_account_page(), is_view_order_page(), is_edit_account_page(), is_lost_password_page(), is_payment_methods_page()

My Account and its endpoints.

is_wc_endpoint_url( $endpoint )

Any WooCommerce endpoint, or the one you name (orders, order-pay or view-order, for example).

is_filtered()

The catalogue is filtered by layered navigation, a price filter or a rating filter.

is_ajax()

WooCommerce is processing an AJAX request.

is_store_notice_showing()

The store notice is enabled.

taxonomy_is_product_attribute( $taxonomy )

A taxonomy name belongs to a product attribute.

A one-function dump helps while learning. It prints the tags that are true for the current request as an HTML comment and only for an administrator who adds ?pc_conditionals=1:

/**
 * Print the WooCommerce conditional tags that are true on the current request.
 *
 * Debug aid for shop managers: append ?pc_conditionals=1 to any front-end URL.
 */
function pocket_coffee_sf_conditionals() {
	// Read-only debug output, restricted to administrators.
	if ( ! isset( $_GET['pc_conditionals'] ) || ! current_user_can( 'manage_options' ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		return;
	}

	$tags = array(
		'is_woocommerce',
		'is_shop',
		'is_product_taxonomy',
		'is_product_category',
		'is_product_tag',
		'is_product',
		'is_cart',
		'is_checkout',
		'is_checkout_pay_page',
		'is_order_received_page',
		'is_account_page',
		'is_wc_endpoint_url',
		'is_filtered',
	);

	$active = array_filter( $tags, 'call_user_func' );
	printf( "<!-- pc-conditionals: %s -->\n", esc_html( implode( ', ', $active ) ) );
}
add_action( 'wp_footer', 'pocket_coffee_sf_conditionals' );

Results seen on the test store with the Storefront child theme (the Checkout pages needed an item in the cart; an empty cart redirects to the cart):

URL Tags that were true

/

none

/shop/

is_woocommerce, is_shop

/product/themes-test-kit/

is_woocommerce, is_product

/product-category/themes-test-beans/

is_woocommerce, is_product_taxonomy, is_product_category

/shop/?orderby=price&min_price=5

is_woocommerce, is_shop, is_filtered

/?s=kit&post_type=product

is_woocommerce, is_shop

/cart/

is_cart

/checkout/

is_checkout

/checkout/order-pay/15/

is_checkout, is_checkout_pay_page, is_wc_endpoint_url

/my-account/

is_account_page

/my-account/orders/

is_account_page, is_wc_endpoint_url

Three rules follow. Call the tags no earlier than the wp action (template_redirect is the usual hook), because before the main query exists they answer false for everything. Do not use is_woocommerce() to mean "this is a shop page": it excludes cart, checkout and account, so enqueue a store script with is_woocommerce() || is_cart() || is_checkout() || is_account_page() or, better, load it only where it is used. And in a block theme, where there is no PHP page file to put a condition in, prefer to decide with the template choice (a specific template for a specific product or category), with the block’s own visibility settings, or with the render_block filter in a small plugin.

Theme design practice

The books on WooCommerce theming (Ravensbergen’s second edition, Rauland’s cookbook and Sims’s guide) are mostly about classic themes and the Storefront Customizer, so their code is legacy, but their design advice on what a shop’s pages have to do survives the change of technology. Rewritten for a block theme and the current templates:

  • Home. One job: lead to the catalogue. A hero with one message, three or four category entry points, the best sellers and a short statement of what makes the shop trustworthy (shipping, returns, roasting date). Pocket Coffee’s home is a pattern with a hero, a Beans and Equipment category grid (the Product Collection block) and the Coffee Club promise.

  • Category and shop pages. Help the shopper narrow down: breadcrumbs, a result count, sorting and the filter blocks. Keep the product card to image, name, price and one button; the information that decides a purchase (origin, roast date) belongs on the product page.

  • Product page. The order that sells: image, name, price, availability, the choice of variation, add to cart, then the details. For variable products like Pocket Coffee’s beans, make the variation choice obvious before the button, and put the delivery promise next to the button.

  • Cart and checkout. Remove distractions. The shop’s header can be replaced by the checkout-header part (a logo and a back link), which is exactly what that template part is for.

  • Everywhere. A responsive layout (block themes and the Product Collection block are responsive out of the box), readable contrast, visible focus, real headings in order, alt text on product images, and font and image weights that keep the largest content on the page fast.

The first classic habits to drop when you move to blocks: styling with the Customizer and widgets (use Global Styles), hand-placing shortcodes for the shop and cart in pages (use the blocks), and hooks of a theme framework that the next theme will not have.

SEO practice

The theme and WooCommerce together decide what search engines see. What WooCommerce 11.2.1 emitted on the test store, observed rather than assumed:

  • Canonical URL and title. The single product page printed <link rel="canonical" href="http://localhost:8081/product/themes-test-kit/" /> and a title of the form Product name – Site name. Titles and meta descriptions per product come from an SEO plugin or the theme’s title-tag support; block themes print the title from the template.

  • Structured data. WooCommerce prints JSON-LD for products and breadcrumbs, whatever the theme. The page contained one script of type application/ld+json with a BreadcrumbList (Home, category, product) and a Product with name, description, sku, url and an Offer carrying price, priceCurrency, availability and priceValidUntil. It was present on the block theme, on Storefront and on Purple. Check it with a structured data validator after changing templates; hiding the title or price block does not remove it, so keep the visible page and the data consistent.

  • Sitemaps. WordPress’s own sitemap (/wp-sitemap.xml) listed the sitemap of products (wp-sitemap-posts-product-1.xml) and of product categories (wp-sitemap-taxonomies-product_cat-1.xml). An SEO plugin may replace it.

  • robots.txt. The virtual file disallowed /wp-content/uploads/wc-logs/, /wp-content/uploads/woocommerce_uploads/, /*?add-to-cart= and /*?*add-to-cart=, so add-to-cart URLs do not become crawl traps.

  • Pages that must not be indexed. Cart and My Account returned <meta name='robots' content='max-image-preview:large, noindex, follow' /> (Checkout redirected away on an empty cart); the shop page did not. WooCommerce adds the directive for the three assigned pages through the wp_robots filter.

  • URLs. The test store used the post-name permalink structure; a shop’s product base (/product/) and category base (/product-category/) are set under Settings > Permalinks. Pick them once: changing them later needs redirects. Keep filter and sorting parameters out of the index (?orderby=, ?min_price=) with the canonical tag of your SEO plugin.

  • Content. Unique product titles and descriptions, one descriptive heading per page, meaningful image alt text, and the facts a buyer needs in text (origin, roast date, weight) rather than only in images.

For the general HTML side of search optimisation see SEO and the accessibility checklist in Accessibility; the comparison of how the four platforms treat SEO is on Comparison.

Moving from a classic theme to a block theme

A store on Storefront or another classic theme moves in steps, and can be tried with the theme preview before anything goes live:

  1. Update WooCommerce and WordPress first and read the status screen: write down outdated overrides and every plugin that adds to the theme’s hooks.

  2. Install the block theme (Purple or a child of a default block theme) and open its preview (?wp_theme_preview=<slug> or the Site Editor’s browse themes).

  3. Check the pages that matter most: home, shop, a category, a variable product, cart, checkout, My Account, the order confirmation, a search, and a 404. Cart and Checkout keep working because their blocks live in the page content.

  4. Rebuild what the old theme provided: the menu and header as a template part, widgets as blocks or patterns, hook-based additions as Block Bindings, blocks or small plugins.

  5. Activate the theme at a quiet hour, clear page and object caches, and re-run the status screen. Keep the old theme installed for a while: rolling back is one click.

  6. After activation, expect Site Editor edits to take over from the files: from then on, changes made in the editor are the source of truth for those templates.

Things that do not carry over: Customizer settings of the old theme, widgets in its sidebars, shortcodes it relied on, and any code that hooked storefront_* actions. Things that do: products, orders, settings, the Cart and Checkout blocks, plugins that use WooCommerce hooks and the Store API, and the SEO data.

Pitfalls

  • Checking the file when the database is used. A Site Editor edit hides the theme file; see "Site Editor edits win over theme files".

  • Editing the parent theme. Updates delete the edit. Use a child theme, or the Site Editor.

  • Copying many templates. Every copy is a future outdated warning.

  • Assuming is_woocommerce() covers cart, checkout and account. It does not.

  • The cached status report. wc_system_status_theme_info is cached for an hour.

  • A block markup typo. The editor shows a block with "Attempt recovery"; the front end prints the markup as plain HTML. Always open a hand-written template in the editor once.

  • Using classic filters for a block layout. Code in the style of woocommerce_checkout_fields, or the Storefront hooks of old books, has no effect on block pages.

  • Wrong parent slug. Template: must be the directory name of an installed theme, otherwise the child theme is reported broken.

  • Unsupported classic theme. Without add_theme_support( 'woocommerce' ) a classic theme gets the generic fallback layout.

  • woocommerce.php in the theme. It wins over every more specific WooCommerce page file.

  • Two spellings. The PHP files inside the plugin use dashes (taxonomy-product-cat.php) while block template slugs use underscores (taxonomy-product_cat); and the documentation lists a page-coming-soon template while 11.2.1 registers coming-soon. Check the slug your version registers, as in the listing above.

What was run and what was not

Run on WordPress 7.1.3 and WooCommerce 11.2.1 (PHP 8.3.36, MariaDB 11.8.9, HPOS enabled) with the themes installed but never activated: the block child theme of Twenty Twenty-Five and the Storefront child theme of this page (static checks with the WooCommerce and WordPress coding standards, PHPCompatibilityWP: no findings), Storefront 4.6.2 and Purple 1.0.3 (installed from WordPress.org, inspected for counts and requirements, and rendered once through the theme preview argument). Rendered and read back: the template resolution of five templates, the database template override, the theme.json CSS output, the Block Bindings paragraph, the hook and the template override in the classic theme, the system status report with and without an outdated override, the conditional tag table, and the SEO output of single product, cart, My Account, sitemap and robots.txt. Not run: the Site Editor in a browser (so the editing screens, the "Clear customizations" action and the visual result of the styles were not looked at), a screenshot comparison of Purple and Storefront, switching the active theme, Customizer behaviour, and anything that needs an account on WordPress.org or WooCommerce.com. The 11.3 beta behaviour of the Shop page is quoted from the release notes of the beta and was not installed.