Every Adobe Commerce (Magento) fix we catalog
SEOLZ catalogs 173 fixes for Adobe Commerce (Magento) across 5 areas — SEO, answer-engine readiness, accessibility, security and site-health. Each lists the exact steps for Adobe Commerce (Magento), with a link to the official docs.
SEO · 52 fixes
Catalog coverageModerate effort
Audit your XML sitemap and internal linking so that every product and category page is discoverable and crawlable by Google — not hidden behind JavaScript, pagination, or missing links.
On Adobe Commerce (Magento)
- Admin › Marketing › SEO & Search › Site Map: create or regenerate your sitemap. Set the path and filename (e.g., /pub/sitemap.xml) and configure the frequency and priority for products and categories.
- Admin › Stores › Configuration › Catalog › XML Sitemap: set 'Include in Sitemap' to Yes for Products and Categories. Set a reasonable changefreq (e.g., Daily for products).
- Admin › System › Configuration › General › Web › Search Engines Robots: ensure the robots.txt does not disallow /catalog/ or other product URL paths.
- Admin › Catalog › Products: filter by 'Visibility = Not Visible Individually' — these products will not appear in search or category pages. Change to 'Catalog, Search' for products that should be indexed.
- Check for flat catalog issues: Admin › Stores › Configuration › Catalog › Catalog › Storefront — if 'Use Flat Catalog Category' or 'Use Flat Catalog Product' causes sync issues, reindex via Admin › System › Index Management.
- Submit the sitemap URL to Google Search Console and monitor the Indexed vs Submitted URL counts.
<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<sitemap>
<loc>https://www.yourstore.com/sitemap-1-1.xml</loc>
<lastmod>2024-06-01T00:00:00+00:00</lastmod>
</sitemap>
</sitemapindex>
<!-- Individual product sitemap (sitemap-1-1.xml) -->
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://www.yourstore.com/blue-running-shoes.html</loc>
<lastmod>2024-06-01</lastmod>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
</urlset>Magento sitemap configuration in app/etc/config.php or set via Admin › Marketing › SEO & Search › Site Map. This shows the correct XML output a Magento sitemap generates for products.
Cwv clsModerate effort
Eliminate unexpected layout shifts by reserving space for every image, video, embed, and dynamically injected element so nothing jumps when the page loads.
On Adobe Commerce (Magento)
- Images: In your theme's Magento_Catalog module override (app/design/frontend/<Vendor>/<theme>/Magento_Catalog/templates/product/), open list.phtml and view.phtml. Add width and height attributes to every <img> tag. For the product image widget, set these in the image configuration: app/etc/view.xml under <images> → set width and height for each image ID.
- view.xml: Open app/design/frontend/<Vendor>/<theme>/etc/view.xml and confirm every <image id='...'> entry has both <width> and <height> child elements. These values flow into Magento's image helper and automatically add the HTML attributes.
- Cookie / consent notices: Adobe Commerce ships with no built-in cookie notice. Third-party cookie extension settings should be configured to display as a fixed bottom bar — check the extension's admin configuration under Stores → Configuration → [Extension Name].
- Fonts: In your theme's web/css/source/_typography.less (or .scss equivalent), add font-display: swap to all @font-face rules. Run bin/magento setup:static-content:deploy after changes.
- Page Builder / banners: In Content → Pages or Blocks, edit any Page Builder row containing a banner or slider. Set a fixed minimum height on the row (Advanced → Minimum Height) so the container does not collapse before images load.
- Verify: Run bin/magento cache:flush, then test with PageSpeed Insights; target CLS < 0.1.
<?xml version="1.0"?>
<view xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Config/etc/view.xsd">
<media>
<images module="Magento_Catalog">
<!-- Product page main image -->
<image id="product_page_image_large" type="image">
<width>800</width>
<height>800</height>
</image>
<!-- Category listing thumbnail -->
<image id="category_page_grid" type="small_image">
<width>300</width>
<height>300</height>
</image>
<!-- Cart item thumbnail -->
<image id="cart_page_product_thumbnail" type="thumbnail">
<width>100</width>
<height>100</height>
</image>
</images>
</media>
</view>In your theme's etc/view.xml, declare explicit width and height for every image role. Magento reads these and automatically adds width/height HTML attributes wherever that image ID is used.
Cwv inpLarger project
Reduce Interaction to Next Paint (INP) to under 200 ms by breaking up long JavaScript tasks, deferring non-critical third-party scripts, and lightening event handlers so the page feels fast and responsive after every tap or click.
On Adobe Commerce (Magento)
- In the Admin panel, go to Stores → Configuration → Advanced → Developer → JavaScript Settings. Set 'Merge JavaScript Files' and 'Minify JavaScript Files' to 'Yes' to reduce the number and size of JS files.
- Enable 'Move JS code to the bottom of the page' in the same JavaScript Settings section so scripts don't block early rendering.
- Identify heavy RequireJS modules by running a Performance trace in Chrome DevTools. For custom modules, refactor event listener callbacks in your module's JS to yield using `setTimeout` or the browser's Scheduler API.
- Audit third-party extensions: go to System → Extensions and disable or uninstall any extension that injects frontend scripts and is not actively used.
- Use Varnish cache and a CDN (configured in Stores → Configuration → System → Full Page Cache) to ensure HTML is served fast, reducing total blocking time.
- Deploy changes to staging, run PageSpeed Insights and the Chrome DevTools Performance panel, and verify INP is below 200 ms before promoting to production.
Cwv lcpModerate effort
Reduce Largest Contentful Paint (LCP) to under 2.5 seconds by serving the hero image in a next-gen format (WebP/AVIF), preloading it, and eliminating render-blocking resources.
On Adobe Commerce (Magento)
- IDENTIFY LCP IMAGE: Run PageSpeed Insights on your homepage and category pages. LCP is typically the hero banner image in the CMS page or a slider widget.
- CONVERT TO WEBP: Install the Magento 2 module 'magento/module-webp' (available in Magento 2.4.x) or a third-party module such as 'Yireo_Webp2'. Enable it: Stores › Configuration › Advanced › Developer › Image Processing Settings. Alternatively, use a CDN with on-the-fly conversion (Cloudflare, Fastly).
- PRELOAD: Edit your theme's default_head_blocks.xml (located in app/design/frontend/[Vendor]/[Theme]/Magento_Theme/layout/). Add: <link rel='preload' as='image' href='{{media url="path/to/hero.webp"}}' fetchpriority='high'/>. Run bin/magento cache:clean after saving.
- NO LAZY-LOAD ON HERO: In your theme's hero banner template (.phtml file), ensure the hero <img> tag has loading='eager' fetchpriority='high' and does NOT have loading='lazy'.
- DEFER JS: In Magento Admin › Stores › Configuration › Advanced › Developer › JavaScript Settings: set 'Move JS code to the bottom of the page' to Yes. For more granular control use RequireJS bundles or a performance extension such as 'Hyvä' theme.
- FULL PAGE CACHE + CDN: Enable Magento Full Page Cache (Stores › Configuration › Advanced › System › Full Page Cache) and connect a CDN (Fastly for Magento Commerce, or Cloudflare). This dramatically reduces TTFB which directly reduces LCP.
- VERIFY: Run bin/magento cache:flush, then re-run PageSpeed Insights. Monitor Core Web Vitals in Google Search Console.
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<head>
<!-- Preload LCP hero image -->
<link
rel="preload"
src="{{media url='wysiwyg/hero-banner.webp'}}"
as="image"
fetchpriority="high"
src_type="url"
/>
</head>
</page>Add to app/design/frontend/[Vendor]/[Theme]/Magento_Theme/layout/default_head_blocks.xml. Run bin/magento cache:clean after saving.
Duplicate meta descriptionModerate effort
Write a unique, page-specific meta description for every page on your store so Google can display a compelling, accurate snippet for each one in search results.
On Adobe Commerce (Magento)
- For a product: go to Catalog → Products, open the product, expand the 'Search Engine Optimization' section, and fill in the 'Meta Description' field with unique text.
- For a category: go to Catalog → Categories, select the category, expand 'Search Engine Optimization', and update the 'Meta Description' field.
- For CMS pages: go to Content → Pages, open the page, select the 'Search Engine Optimization' tab, and update 'Meta Description'.
- For bulk product updates: use the product grid in Catalog → Products, select multiple products, choose 'Actions → Update Attributes', and set a shared or templated meta description — note that full uniqueness still requires editing each item individually or using a data import/export CSV.
- To import descriptions in bulk: go to System → Data Transfer → Import, choose 'Products', export a CSV template, populate the 'meta_description' column with unique values per SKU, and import.
- After saving, flush the Magento cache (System → Cache Management → Flush Cache), verify descriptions in the page source, and submit URLs to Google Search Console.
<!-- Rendered by Magento in <head> after saving the Meta Description field in Catalog > Products > [Product] > Search Engine Optimization -->
<meta name="description" content="Shop the Acme Hoops Collection — basketball shoes engineered for speed and support. Available in sizes 6–15. Free shipping over $75.">Magento renders the meta description from the product/category/CMS page SEO fields. This is what the rendered HTML looks like — confirm it in View Source after saving.
Duplicate titleModerate effort
Give every page a unique, descriptive title tag that accurately reflects that page's specific content.
On Adobe Commerce (Magento)
- For individual products: Catalog → Products → Edit → Search Engine Optimization tab → 'Meta Title' field.
- For individual categories: Catalog → Categories → Edit → Search Engine Optimization tab → 'Meta Title'.
- For store-wide title templates: Stores → Configuration → Catalog → Catalog → Storefront → 'Product Title Suffix' / 'Category Title Suffix' (controls the brand suffix appended automatically).
- For bulk updates, use the product import/export CSV (System → Data Transfer → Export with entity type 'Products', edit the meta_title column, then re-import).
- Install an SEO extension (e.g. Mageplaza SEO, Amasty SEO) for advanced template-based title generation across the whole catalog.
<head>
<title>Merino Wool Running Socks – Free UK Shipping | BrandName</title>
</head>Place inside <head>. Make the title unique per page — include the specific item name and brand.
Faceted url indexableModerate effort
Point the rel=canonical on every faceted/filter URL to its clean parent category URL (or add noindex), so Google treats filtered pages as a single authoritative page instead of thousands of duplicates.
On Adobe Commerce (Magento)
- Adobe Commerce (Magento) is a major source of faceted URL indexing issues due to its layered navigation, which creates URLs like /womens-pants.html?color=49&size=91 or /womens-pants/color-navy.html.
- Go to Admin → Stores → Configuration → Catalog → Catalog → Search Engine Optimization. Set 'Use Canonical Link Meta Tag For Categories' to Yes. This tells Magento to output a canonical pointing to the clean category URL on all layered navigation / filtered pages.
- Also set 'Use Canonical Link Meta Tag For Products' to Yes to handle product pages.
- To prevent layered navigation URLs from being crawled at all: go to Admin → Stores → Configuration → General → Web → Search Engine Robots and set the default robots for the store. Alternatively, use the 'Layered Navigation' settings to restrict which attributes generate filterable URLs.
- For finer control, go to Admin → Marketing → SEO & Search → URL Rewrites and review or add rewrites for category pages.
- Consider installing a dedicated Magento SEO extension (e.g., Mageplaza SEO, Mirasvit SEO Suite) which provides per-attribute canonical management, noindex rules for filtered pages, and robots.txt auto-generation for faceted URLs.
- Verify by viewing page source on a filtered category URL and confirming the canonical tag points to the clean category URL (e.g., https://yoursite.com/womens-pants.html).
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd">
<default>
<catalog>
<seo>
<!-- Use canonical link meta tag for category pages (including layered nav) -->
<category_canonical_tag>1</category_canonical_tag>
<!-- Use canonical link meta tag for product pages -->
<product_canonical_tag>1</product_canonical_tag>
</seo>
</catalog>
</default>
</config>In app/code/[Vendor]/[Module]/etc/config.xml or via Admin → Stores → Configuration → Catalog → SEO. This shows the XML config equivalent to enabling canonical for categories.
Images missing altModerate effort
Add descriptive alt text to every product and content image so search engines and screen readers can understand what the image shows.
On Adobe Commerce (Magento)
- Product images: Admin → Catalog → Products → [select product] → Images and Videos tab → hover over the image and click its row to expand → fill in the 'Alt Text' field → Save.
- CMS page / block images: Admin → Content → Pages (or Blocks) → [select item] → in the WYSIWYG editor, right-click the image → Image Properties → fill the 'Alt Text' / 'Image Description' field → OK → Save.
- For bulk updates, use a data import: Admin → System → Import → Entity Type 'Products' → download the sample CSV, populate the 'small_image_label', 'thumbnail_label', and 'base_image_label' columns, then import.
<?php
/** @var $block \Magento\Catalog\Block\Product\View\Gallery */
$images = $block->getGalleryImages();
?>
<?php foreach ($images as $image): ?>
<img
src="<?= $block->escapeUrl($image->getData('medium_image_url')) ?>"
alt="<?= $block->escapeHtmlAttr($image->getData('label') ?: $block->getProduct()->getName()) ?>"
width="700"
height="700"
loading="lazy">
<?php endforeach; ?>In a custom .phtml template, always output the alt attribute from the image data object.
Images missing alt bulkModerate effort
Add descriptive alt text to every product and content image at the template level so search engines and screen readers can understand all images on the page.
On Adobe Commerce (Magento)
- In the Admin, go to Catalog → Products → edit a product → Images and Videos tab. For each image, fill in the 'Alt Text' field. This is the per-image fix.
- For a template-level fix, locate the product image template in your theme: `app/design/frontend/VENDOR/THEME/Magento_Catalog/templates/product/view/gallery.phtml` (or the equivalent Hyva theme file `Magento_Catalog/templates/product/view/gallery.phtml`).
- Ensure the `<img>` tag or the `data-gallery-role` JSON configuration includes an `alt` key populated from `$block->getImageAttribute('image', 'label')` or the product name as fallback.
- For category pages, check `Magento_Catalog/templates/product/list.phtml` and verify the image renderer passes `alt` via `$productImage->getLabel()`.
- Clear the full-page cache: System → Cache Management → Flush Magento Cache, then verify in DevTools.
<?php
/** @var \Magento\Catalog\Block\Product\View\Gallery $block */
$product = $block->getProduct();
$mainImage = $block->getImage($product, 'product_page_image_large');
$altText = $mainImage->getLabel();
if (empty($altText)) {
$altText = $product->getName();
}
?>
<img
src="<?= $block->escapeUrl($mainImage->getImageUrl()) ?>"
alt="<?= $block->escapeHtmlAttr($altText) ?>"
width="<?= (int)$mainImage->getWidth() ?>"
height="<?= (int)$mainImage->getHeight() ?>"
loading="lazy">Snippet from app/design/frontend/VENDOR/THEME/Magento_Catalog/templates/product/view/gallery.phtml. Outputs alt from image label with product name fallback.
Low performance scoreLarger project
Improve your Lighthouse/PageSpeed performance score by reducing page weight, eliminating render-blocking resources, optimising images, and addressing Core Web Vitals so Google ranks your store higher in search results.
On Adobe Commerce (Magento)
- Enable built-in full-page cache: Admin → Stores → Configuration → Advanced → System → Full Page Cache → set to 'Varnish Cache' (recommended) or 'Built-In Cache'.
- Enable JS and CSS bundling and minification: Admin → Stores → Configuration → Advanced → Developer → JavaScript Settings → Minify JavaScript Files = Yes; CSS Settings → Minify CSS Files = Yes.
- Configure a CDN for static assets: Admin → Stores → Configuration → General → Web → Base URLs (Secure) — point static and media base URLs to your CDN endpoint (Fastly, Cloudflare, or CloudFront).
- Optimise images: use a Magento extension such as TinyPNG for Magento or Cloudinary to auto-compress and serve WebP; alternatively pre-compress images before upload via bin/magento catalog:images:resize after optimising source files.
- Enable Brotli/GZIP at the Nginx/Apache or Varnish layer — this is a server configuration change; work with your hosting provider or DevOps engineer if on managed hosting (e.g., Adobe Commerce Cloud has this available via .magento.env.yaml).
- Audit and disable unused third-party modules: Admin → System → Web Setup Wizard → Component Manager, or via CLI: bin/magento module:disable Vendor_ModuleName — each enabled module can add JS/CSS to the frontend.
- Re-test with PageSpeed Insights and monitor via New Relic (included with Adobe Commerce Cloud) for TTFB and server-side bottlenecks.
<!-- In <head>: preconnect to critical third-party origins to reduce connection latency -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- Preload your Largest Contentful Paint image (hero/banner) so it loads earlier -->
<link rel="preload" as="image" href="/images/hero.webp" fetchpriority="high">
<!-- Hero image: eager load, explicit dimensions to prevent CLS -->
<img
src="/images/hero.webp"
alt="Descriptive alt text for your hero image"
width="1200"
height="600"
fetchpriority="high"
decoding="async"
>
<!-- All other images: lazy load, explicit dimensions to prevent CLS -->
<img
src="/images/product-shot.webp"
alt="Descriptive product alt text"
width="800"
height="800"
loading="lazy"
decoding="async"
>
<!-- Defer non-critical third-party scripts (analytics, chat, pixels) -->
<script src="https://example-analytics.com/tracker.js" defer></script>
<!-- Load non-critical CSS asynchronously to unblock render -->
<link rel="preload" href="/css/non-critical.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/css/non-critical.css"></noscript>Paste these patterns into your <head> and image markup to address the most common Lighthouse failures.
Meta description too longQuick win
Shorten your meta description to 150–160 characters so Google displays it in full instead of cutting it off with "…" in search results.
On Adobe Commerce (Magento)
- Log in to the Admin Panel → go to Catalog → Products (or Categories / CMS Pages) → open the item.
- Expand the 'Search Engine Optimization' section.
- Edit the 'Meta Description' field, keeping it under 160 characters.
- For store-wide defaults: go to Stores → Configuration → Catalog → Catalog → 'Product Fields Auto-Generation' to adjust the default meta description template.
- Save, then flush the Magento cache: System → Cache Management → Flush Magento Cache.
<meta name="description" content="Shop Bang & Olufsen headphones, speakers and TVs in Malaysia. Visit our showroom at The Gardens Mall or order online for fast delivery.">Magento renders this automatically from the product/category SEO fields. This is what the <head> output should look like after your edit.
Meta description too shortModerate effort
Expand your meta description to 120–160 characters so Google displays a compelling, complete snippet in search results that drives more clicks to your store.
On Adobe Commerce (Magento)
- Admin → Catalog → Products → select the product → expand the 'Search Engine Optimization' section → update the 'Meta Description' field to 120–160 characters → Save.
- For categories: Admin → Catalog → Categories → select the category → 'Search Engine Optimization' tab → update 'Meta Description' → Save.
- For CMS pages: Admin → Content → Pages → select the page → 'Search Engine Optimization' tab → update 'Meta Description' → Save.
- To set a store-wide default template for meta descriptions, go to Admin → Stores → Configuration → Catalog → Search Engine Optimization → 'Product Meta Description' and use attribute variables like {{description}} as a fallback.
<meta name="description" content="Shop the official Nike national team jerseys — advanced Dri-FIT heat tech meets bold heritage design. Free shipping on orders over $75. Explore the full collection now.">Place this inside the <head> element of your HTML page. Replace the content value with your 120–160 character description.
Missing canonicalModerate effort
Add a self-referencing canonical tag to every page so Google knows which URL is the definitive version of that content.
On Adobe Commerce (Magento)
- In your Adobe Commerce Admin, go to Stores → Configuration → Catalog → Catalog → Search Engine Optimization.
- Set 'Use Canonical Link Meta Tag for Categories' and 'Use Canonical Link Meta Tag for Products' both to 'Yes'. Save Config.
- For CMS pages (like the homepage or landing pages), go to Content → Pages → edit the page → expand 'Search Engine Optimization' → enter the preferred URL in the 'URL Key' field and ensure no duplicate routes exist.
- Clear the full-page cache: System → Cache Management → Flush Magento Cache.
- Verify by visiting a product and category page and viewing page source — search for 'canonical'.
- For custom page types or headless setups, canonical tags must be added programmatically in the relevant layout XML file or via a custom module.
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<head>
<link rel="canonical" src="https://www.yourstore.com/your-preferred-page-url"/>
</head>
</page>If you need to add a canonical programmatically via layout XML for a custom page type, add this to your module's layout XML file (e.g. view/frontend/layout/yourmodule_index_index.xml). The canonical URL value should be set via a backend model or block.
Missing h1Quick win
Add a single, descriptive H1 tag to every page that currently lacks one, clearly stating the page's main topic.
On Adobe Commerce (Magento)
- For product pages: Catalog → Products → open the product → the 'Product Name' field is used as the H1 by default via the page_title block in the product view layout.
- For category pages: Catalog → Categories → open the category → the 'Name' field renders as the H1.
- To override the H1 independently of the product name, go to the product edit page → Search Engine Optimization tab → fill in the 'Meta Title' field; some themes use this as the visible H1 — check your theme's catalog/product/view/title.phtml.
- To modify at the template level: in your custom theme, edit app/design/frontend/<Vendor>/<theme>/Magento_Catalog/templates/product/view/title.phtml and ensure the title output is wrapped in <h1 ...>.
- For CMS pages (Home, About, etc.): Content → Pages → open the page → in the Page Information tab, fill in the 'Content Heading' field — this renders as the H1 above the content block.
- Flush cache under System → Cache Management after saving, then verify with View Page Source.
<?php
/** @var \Magento\Catalog\Block\Product\View $block */
?>
<h1 class="page-title">
<span class="base" data-ui-id="page-title-wrapper" itemprop="name">
<?= $block->escapeHtml($block->getProduct()->getName()) ?>
</span>
</h1>In app/design/frontend/<Vendor>/<theme>/Magento_Catalog/templates/product/view/title.phtml, output the product name as H1.
Missing meta descriptionModerate effort
Write and add a unique meta description (120–160 characters) to every page that is missing one, so Google can display an accurate, compelling snippet in search results.
On Adobe Commerce (Magento)
- For a product: Admin → Catalog → Products → click the product → expand the 'Search Engine Optimization' section → fill in 'Meta Description'.
- For a category: Admin → Catalog → Categories → click the category → 'Search Engine Optimization' tab → 'Meta Description' field.
- For a CMS page (homepage, About, etc.): Admin → Content → Pages → click the page → expand 'Search Engine Optimization' → 'Meta Description' field.
- To set a store-wide default template (fallback for products with no manual description): Stores → Configuration → Catalog → Catalog → 'Product Meta Description' — use variables like {{name}} and {{description}} to auto-generate descriptions.
- Clear the full-page cache after saving: System → Cache Management → Flush Magento Cache.
<!-- In Stores > Configuration > Catalog > Catalog > Product Meta Description -->
<!-- Use Magento variable syntax; this value is entered in the admin text field -->
{{name}} - {{description}}Default meta description template set in Stores → Configuration → Catalog → Catalog → Product Meta Description (use layout XML or config, shown here as the config value).
Missing og descriptionModerate effort
Add an og:description meta tag to every page so that social media platforms display a compelling, accurate preview when your pages are shared.
On Adobe Commerce (Magento)
- In the Admin panel, go to Stores → Configuration → Catalog → Catalog → Product Fields Auto-generation.
- For per-product OG tags, install an SEO extension such as Mageplaza SEO or Mageworx SEO Suite, which adds Open Graph fields to each product and category edit screen.
- Without an extension, edit your theme's layout XML: create (or modify) Magento_Catalog/layout/catalog_product_view.xml in your custom theme, add a block for custom head meta, and in the corresponding .phtml template output <meta property="og:description" content="<?= $escaper->escapeHtmlAttr($product->getShortDescription()) ?>">.
- Clear cache (System → Cache Management → Flush Magento Cache) and verify with Facebook Sharing Debugger.
<?php
/**
* Template: Magento_Catalog/templates/product/og-description.phtml
* Outputs og:description for product pages.
*/
/** @var \Magento\Catalog\Block\Product\View $block */
$product = $block->getProduct();
if ($product) {
$description = $product->getShortDescription()
?: $product->getMetaDescription()
?: '';
$description = strip_tags((string)$description);
$description = mb_strimwidth($description, 0, 160, '');
if (!empty($description)) {
echo '<meta property="og:description" content="'
. $block->escapeHtmlAttr($description)
. '" />' . "\n";
}
}Create this file in your custom theme at Magento_Catalog/templates/product/og-description.phtml and register it via layout XML.
Missing og imageModerate effort
Add an og:image meta tag to every page so that social media platforms and messaging apps display a rich, branded image preview whenever someone shares your store's link.
On Adobe Commerce (Magento)
- In the Magento Admin, go to Stores → Configuration → Catalog → Product → set 'Product Image' as the base image for social sharing (some themes handle this automatically via the product's base_image).
- To add og:image to your theme templates, locate your active theme's layout XML or PHTML files under app/design/frontend/<Vendor>/<theme>/Magento_Catalog/templates/product/ — specifically the view.phtml or a dedicated head block.
- In your theme's default_head_blocks.xml (or equivalent), add a meta block that outputs the og:image tag using the product's getImage() helper, or add the tag directly in the page's head PHTML.
- For a faster, no-code solution, install an extension from the Magento Marketplace such as 'SEO Suite Ultimate by Mageworx' or 'Magento 2 SEO Extension by Mageplaza' — both provide UI fields for og:image on product, category, and CMS pages.
- After deploying, flush the Magento cache: System → Cache Management → Flush Magento Cache.
- Validate using Facebook's Sharing Debugger.
Missing og titleQuick win
Add an og:title meta tag to every page so your store's title appears correctly when shared on social media platforms like Facebook, LinkedIn, and Pinterest.
On Adobe Commerce (Magento)
- In the Admin panel, go to Content → Configuration → select your store view → click 'Edit'.
- Under 'Other Settings', expand the 'HTML Head' section. Here you can add default meta tags, but for og:title specifically you need to edit your theme layout.
- Navigate to your custom theme folder at app/design/frontend/[Vendor]/[Theme]/Magento_Theme/layout/ and open or create default_head_blocks.xml.
- Add an og:title tag using layout XML, or edit your theme's default.xml to include it in the <head> block.
- A more maintainable approach: edit app/design/frontend/[Vendor]/[Theme]/Magento_Theme/templates/html/head/additional.phtml to output: <meta property="og:title" content="<?= $block->escapeHtml($this->getLayout()->getBlock('page.main.title')->getPageTitle()) ?>" />
- For product pages specifically, the og:title should use the product name: in catalog/product/view.phtml or via a plugin/override, output the product name in the og:title tag.
- Flush the Magento cache (System → Cache Management → Flush Cache) and verify using Facebook Sharing Debugger.
<?php
/** @var \Magento\Framework\View\Element\AbstractBlock $block */
$pageTitle = $block->getLayout()->getBlock('page.main.title');
$titleText = $pageTitle ? $pageTitle->getPageTitle() : $block->getLayout()->getBlock('head')->getTitle();
?>
<meta property="og:title" content="<?= $block->escapeHtml($titleText) ?>" />
<meta property="og:type" content="website" />
<meta property="og:url" content="<?= $block->escapeUrl($block->getBaseUrl()) ?>" />Add to your custom theme at app/design/frontend/[Vendor]/[Theme]/Magento_Theme/templates/html/head/additional.phtml. Ensure this template is included via layout XML.
Missing titleQuick win
Add a unique, descriptive title tag (30–60 characters) to every page that is missing one.
On Adobe Commerce (Magento)
- For a product: go to Catalog → Products → select the product → expand the 'Search Engine Optimization' section → fill in the 'Meta Title' field.
- For a category: go to Catalog → Categories → select the category → expand 'Search Engine Optimization' → fill in 'Meta Title'.
- For a CMS page: go to Content → Pages → select the page → expand 'Search Engine Optimization' → fill in 'Meta Title'.
- To set global title templates (applied automatically when no manual title is set): go to Stores → Configuration → Catalog → Catalog → search 'Title' and configure 'Product Title Suffix' or use the 'Meta Title' defaults.
- Click Save and flush the Magento cache: System → Cache Management → Flush Magento Cache.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Slim Bifold Leather Wallet | BrandName</title>
</head>
<body>
<!-- page content -->
</body>
</html>Place this inside the <head> element of every HTML page. Keep the text between 30–60 characters.
Multiple h1Moderate effort
Remove all but one H1 tag from each page so every page has exactly one clear, descriptive primary heading.
On Adobe Commerce (Magento)
- Locate the relevant template file via the Admin or directly in the file system: for product pages it is app/design/frontend/<Vendor>/<theme>/Magento_Catalog/templates/product/view/type/ or the base catalog/product/view.phtml.
- Open the template and search for '<h1' — the product name wrapper (typically <span class="base" itemprop="name">) inside an H1 is correct. Remove or downgrade any additional H1 tags to H2.
- Check category page templates in Magento_Catalog/templates/category/view.phtml — the category name H1 should be the only H1.
- Check layout XML files (e.g. catalog_product_view.xml) for any blocks that inject an H1 via widget or CMS block.
- Clear the Magento cache (System → Cache Management → Flush Magento Cache) after changes.
- Preview pages and confirm with DevTools.
<!-- Correct rendered output for a Magento product page -->
<!-- Product name: the sole H1 -->
<h1 class="page-title">
<span class="base" data-ui-id="page-title-wrapper" itemprop="name">
Men's Endurance Running Jacket
</span>
</h1>
<!-- All subsequent section headings use H2 -->
<h2 class="block-title">More Information</h2>
<h2 class="block-title">Customer Reviews</h2>
<!-- In category listing templates, product card names should be H2 -->
<!-- In Magento_Catalog/templates/product/list.phtml: -->
<h2 class="product-name">
<a class="product-item-link" href="#">Men's Endurance Running Jacket</a>
</h2>The rendered output from app/design/frontend/<Vendor>/<theme>/Magento_Catalog/templates/product/view.phtml — confirm only one H1 appears.
Noindex detectedQuick win
Remove or replace the noindex directive on any page you want to appear in Google search results.
On Adobe Commerce (Magento)
- Global robots setting: Go to Admin → Content → Design → Configuration → select your store view → Edit → Search Engine Robots. Set 'Default Robots' to 'INDEX, FOLLOW'. Save and flush cache.
- Per-page (CMS Pages): Go to Content → Pages → edit the page → Search Engine Optimisation tab. In the 'Robots' field, change from 'NOINDEX, NOFOLLOW' to 'INDEX, FOLLOW'.
- Per-category: Go to Catalog → Categories → select the category → Search Engine Optimization tab → set 'Meta Robots' to 'INDEX, FOLLOW'.
- Per-product: Go to Catalog → Products → edit the product → Search Engine Optimization tab → set 'Meta Robots' to 'INDEX, FOLLOW'.
- Layered navigation / faceted URLs: Magento often auto-generates noindex for filtered URLs to avoid duplicate content. Verify this is intentional before changing it — typically these should remain noindex.
- Flush cache: Admin → System → Cache Management → Flush Magento Cache after every change.
- Verify in Google Search Console → URL Inspection → Request Indexing.
<meta name="robots" content="index,follow">Place inside <head>. This is the CORRECT tag that allows indexing and link-following.
Non self canonicalQuick win
Audit every page whose canonical tag points to a different URL, then either correct it to self-reference or confirm the cross-reference is intentional and correct.
On Adobe Commerce (Magento)
- In the Adobe Commerce / Magento 2 admin, go to Stores → Configuration → Catalog → Catalog → Search Engine Optimization.
- Review settings such as 'Use Canonical Link Meta Tag for Categories' and 'Use Canonical Link Meta Tag for Products' — toggling these controls whether Magento adds canonical tags pointing to the base category/product URL (stripping layered navigation parameters). Set these to 'Yes' to enable automatic self-canonicals.
- For individual product or CMS page overrides: navigate to the product/category/CMS page in the admin, find the 'Search Engine Optimization' section, and check the 'URL Key' and any custom canonical field — ensure no incorrect URL is entered.
- If a third-party SEO extension (e.g., Mageworx SEO Suite, Amasty SEO) is installed, check its per-page canonical override fields in the product/category edit screen and clear incorrect values.
- Flush the Magento cache (System → Cache Management → Flush Cache Storage), then view-source the frontend page and confirm the canonical href matches the intended page URL.
<link rel="canonical" href="https://www.example.com/your-page-url/">Place this tag inside the <head> element of every page. The href must exactly match the page's own preferred URL.
Og url mismatchModerate effort
Set a unique, page-specific og:url meta tag on every page that exactly matches that page's canonical URL.
On Adobe Commerce (Magento)
- In Adobe Commerce / Magento 2 Admin, go to Stores → Configuration → Catalog → Catalog → Product Fields Auto-Generation and review any OG-related settings.
- The primary location for og:url in a Luma-based theme is in app/design/frontend/<Vendor>/<Theme>/Magento_Theme/templates/html/head/. Look for a file that outputs og meta tags (often a custom template or a third-party SEO extension).
- If you use an SEO extension (e.g. Mageworx SEO Suite, Amasty SEO Toolkit), go to that extension's configuration in the Admin and find the Open Graph section — enable 'Use canonical as og:url' or a similar option.
- If og:url is hardcoded in a layout XML or PHTML template, replace the static value with the PHP call to get the current page's canonical URL: $block->getUrl('//*', ['_current' => true, '_use_rewrite' => true]) or retrieve it from the page's canonical helper.
- Run bin/magento cache:flush after making template changes.
- Verify by viewing source on a product page, a category page, and the CMS home page that each og:url is unique and matches its <link rel='canonical'>.
<?php
/** @var \Magento\Framework\View\Element\Template $block */
$canonicalUrl = $block->getUrl(
'*/*/*',
['_current' => true, '_use_rewrite' => true, '_query' => false]
);
?>
<meta property="og:url" content="<?= $block->escapeHtmlAttr($canonicalUrl) ?>" />In your custom theme's PHTML head template or a custom module's layout XML block. This retrieves the current page's canonical URL safely.
Page not foundModerate effort
Fix broken internal links pointing to pages that return a 404 error by either removing those links or setting up a 301 redirect from the missing URL to the correct replacement page.
On Adobe Commerce (Magento)
- Log in to the Adobe Commerce / Magento admin panel and go to Marketing → SEO & Search → URL Rewrites.
- Click 'Add URL Rewrite'. Set Request Path to the old broken URL and Target Path to the new destination. Set Redirect Type to 'Permanent (301)'. Save.
- To fix internal links in CMS pages, go to Content → Pages, open each affected page, and use the WYSIWYG editor to update or remove the broken link.
- For broken links in CMS blocks (headers, footers, widgets), go to Content → Blocks, open the relevant block, and update the link there.
- For links hard-coded in theme template files (PHTML files or layout XMLs), edit them via the file manager or FTP under app/design/frontend/[Vendor]/[theme]/.
- Flush the Magento cache (System → Cache Management → Flush Cache) after making changes, then verify the old URL returns a 301 redirect.
<!-- BEFORE: broken internal link -->
<a href="/old-products/discontinued-item">Shop Now</a>
<!-- AFTER: updated to correct URL -->
<a href="/products/replacement-item">Shop Now</a>Update an internal anchor link to point to the correct URL instead of the broken one.
Page speed warningModerate effort
Reduce your page load time to under 1.5 seconds by optimizing images, eliminating render-blocking resources, and enabling caching and compression.
On Adobe Commerce (Magento)
- Full-page cache: In the Magento admin go to System > Cache Management and ensure 'Full Page Cache' is enabled. For production, configure Varnish Cache as the FPC application (Stores > Configuration > Advanced > System > Full Page Cache > Caching Application = Varnish Cache) — this is the single highest-impact Magento speed improvement.
- JS/CSS bundling: In the admin go to Stores > Configuration > Advanced > Developer > JavaScript Settings: set 'Enable JavaScript Bundling' to Yes, 'Minify JavaScript Files' to Yes; and under CSS Settings set 'Minify CSS Files' to Yes.
- Images: In the admin go to Stores > Configuration > General > Web > Engines and enable WebP conversion. Use a Magento image optimization extension (e.g. TinyPNG for Magento) or optimize images before upload. Ensure the built-in lazy load is active under Catalog > Configuration.
- CDN: In Stores > Configuration > General > Web > Base URLs (Secure), configure a CDN base URL (e.g. Fastly, which is included with Adobe Commerce Cloud) so all static assets are served from edge nodes.
- Third-party modules: In the admin go to System > Web Setup Wizard > Module Manager (or use bin/magento module:status in CLI) and disable any module you are not using — unused modules still load code on every request.
- For Adobe Commerce Cloud: Use the Fastly CDN and image optimisation settings in the Cloud Console under your environment's Fastly configuration panel rather than the Magento admin, as Cloud overrides some admin settings.
Redirect chainModerate effort
Update every internal link that passes through a redirect chain to point directly at the final destination URL, eliminating all intermediate hops.
On Adobe Commerce (Magento)
- Run Screaming Frog or Sitebulb against your store; export the redirect-chain report and record every chained URL plus its final 200 OK destination.
- For links inside CMS Pages or CMS Blocks: go to Content → Pages (or Content → Blocks), open the affected page/block in the WYSIWYG editor, locate the linked text, and change the href to the final URL. Save.
- For links inside category or product descriptions: go to Catalog → Categories or Catalog → Products, open the item, and edit the Description field to update any chained links.
- For hard-coded links in layout XML or PHTML templates: locate the relevant template file under app/design/frontend/<Vendor>/<Theme>/ (or the core module override), find the href, replace it with the final URL, and deploy static content (bin/magento setup:static-content:deploy).
- Review URL Rewrites under Marketing → SEO & Search → URL Rewrites; if a rewrite's Target Path is itself a redirect rather than a final URL, update the Target Path to the true destination and save.
- Clear the full-page cache (System → Cache Management → Flush Magento Cache) and re-crawl to confirm all chains are gone.
<!-- BEFORE: link points to an intermediate URL that itself redirects -->
<a href="https://example.com/old-page">Our Story</a>
<!-- AFTER: link points directly to the final destination URL -->
<a href="https://example.com/about-us">Our Story</a>Replace every intermediate redirect URL in your anchor href with the final destination URL — the one that returns HTTP 200 with no further redirects.
Search results indexableModerate effort
Add a noindex robots meta tag to all internal search result pages and block the search URL path in robots.txt to prevent Google from crawling and indexing thin, duplicate search results.
On Adobe Commerce (Magento)
- Log in to your Magento Admin. Go to Content → Design → Configuration → select your store view → expand 'HTML Head'.
- This approach applies site-wide, so instead edit the search results layout XML file: in your active theme, create or edit app/design/frontend/<Vendor>/<Theme>/Magento_CatalogSearch/layout/catalogsearch_result_index.xml
- Add the following block to inject the noindex meta tag on all search result pages (see code snippet below).
- For robots.txt: go to Content → Design → Configuration → select your store view → expand 'Search Engine Robots' → in the 'Edit Custom Instruction of robots.txt File' textarea, add: Disallow: /catalogsearch/ — then save.
- Flush the Magento cache (System → Cache Management → Flush Magento Cache).
- Verify with Google Search Console URL Inspection on a sample /catalogsearch/result/?q= URL.
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<head>
<meta name="robots" content="NOINDEX,FOLLOW"/>
</head>
</page>Create or edit app/design/frontend/<Vendor>/<Theme>/Magento_CatalogSearch/layout/catalogsearch_result_index.xml
Seo category description missingModerate effort
Write a 100–200 word description for every category page explaining what products it contains and who they are for, so Google can rank it for category-level keywords.
On Adobe Commerce (Magento)
- In the Magento Admin Panel, go to Catalog → Categories.
- Select the category from the category tree on the left.
- Open the 'Content' section (click to expand).
- In the 'Description' field, use the WYSIWYG editor to write or paste your 100–200 word category description.
- Optionally use the 'Add CMS Block' option inside the content section to insert a pre-built block with richer formatted content.
- Click Save (or Save and Continue Edit).
- The description is rendered in the category.phtml template (Magento_Catalog module). It appears above the product grid by default in Luma and most themes. If your theme overrides this template, locate vendor/magento/module-catalog/view/frontend/templates/category/view/description.phtml and confirm it is called.
<?php
/** @var \Magento\Catalog\Block\Category\View $block */
?>
<?php if ($description = $block->getCurrentCategory()->getDescription()): ?>
<div class="category-description">
<?= /* @escapeNotVerified */ $description ?>
</div>
<?php endif; ?>This is the relevant section of vendor/magento/module-catalog/view/frontend/templates/category/view/description.phtml — confirm it exists and is called from your layout XML.
Seo category description thinModerate effort
Expand your category page description to at least 100 words of genuinely helpful, keyword-rich content that explains what shoppers will find in that category.
On Adobe Commerce (Magento)
- Log in to the Adobe Commerce / Magento Admin Panel and go to Catalog > Categories.
- Select the category you want to improve from the category tree on the left.
- In the 'Content' tab, find the 'Description' WYSIWYG editor field.
- Write or paste your 100+ word description, using headings and bullets as appropriate.
- In the 'Search Engine Optimization' tab, also review and update the Meta Description field.
- Click 'Save Category'. Flush the Magento cache under System > Cache Management if the update does not appear immediately on the storefront.
<?php if ($description = $block->getCurrentCategory()->getDescription()): ?>
<div class="category-description">
<?= $block->escapeHtml($description, ['p','a','strong','em','ul','ol','li','h2','h3','br']) ?>
</div>
<?php endif; ?>Confirm this block exists in your theme's Magento_Catalog/templates/category/view.phtml to render the description.
Seo category emptyQuick win
Either populate the empty category with products, or add a noindex tag and remove it from your navigation so Google does not waste crawl budget on a page with no content.
On Adobe Commerce (Magento)
- Log in to the Adobe Commerce / Magento Admin Panel.
- Go to Catalog → Categories and click the empty category in the tree.
- Option A — Add products: Scroll to the 'Products in Category' tab at the bottom of the category editor. Search for and assign relevant products. Save.
- Option B — Noindex: In the category editor, open the 'Search Engine Optimization' tab. Set 'Include in Menu' to 'No'. In the 'Meta Robots' field (if your theme exposes it) or via a third-party SEO extension (e.g., Mageworx SEO Suite), set the value to 'NOINDEX, FOLLOW'. Save and flush the Magento cache (System → Cache Management → Flush Magento Cache).
- Option B (via XML layout): In your theme, create a layout override for catalog_category_view_id_{CATEGORY_ID}.xml and inject the noindex meta tag into the head block.
- Option C — Delete & redirect: In the category editor, click 'Delete Category'. Then go to Marketing → SEO & Search → URL Rewrites, click 'Add URL Rewrite', set Type to 'Custom', enter the old category request path, the target path of the parent category, and set Redirect Type to 'Permanent (301)'. Save and flush cache.
- Remove the category from any navigation menus by going to Content → Elements → Menus (if using a menu builder extension) or by checking 'Include in Menu: No' in the category settings.
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<head>
<!-- Noindex empty categories via a custom block or directly -->
<!-- Best applied via a plugin/observer that checks product count and sets:
$resultPage->getConfig()->setRobots('NOINDEX,FOLLOW');
in a controller plugin for catalog_category_view -->
</head>
</page>
<!-- Recommended: Use a Plugin on Magento\Catalog\Controller\Category\View -->
<!-- In your module's di.xml register a plugin, then in the plugin's afterExecute:
if ($category->getProductCount() === 0) {
$result->getConfig()->setRobots('NOINDEX,FOLLOW');
}
-->Create this file at app/design/frontend/[Vendor]/[theme]/Magento_Catalog/layout/catalog_category_view.xml to inject a noindex meta tag only when the category has no products.
Seo category sparseModerate effort
Add more products to sparse category pages, or consolidate very thin categories into broader, related ones so every indexed category page delivers genuine value to shoppers and search engines.
On Adobe Commerce (Magento)
- In the Magento admin, go to Catalog → Categories and select the sparse category.
- Under 'Content' add a Description (HTML) — this text renders on the category page and is indexed by Google.
- To add products: scroll to the 'Products in Category' grid and use the checkbox to assign additional products, or adjust the category's layered navigation conditions.
- To merge categories: reassign products (individually or via mass action in the product grid) to the surviving category, then in Marketing → SEO & Search → URL Rewrites, create a permanent (301) URL rewrite from the old category path to the new one.
- To noindex: in the category settings under 'Search Engine Optimization', set 'Include in Menu' as needed and paste <meta name="robots" content="noindex, follow"> into the category's 'Meta Title/Description' area, or use a third-party SEO extension (e.g., Mageworx SEO Suite) that adds a per-category noindex toggle.
<meta name="robots" content="noindex, follow">Add inside <head> of any sparse category page you want excluded from Google's index while still being followable by crawlers.
Seo facet url not canonicalizedModerate effort
Add a canonical tag to every filtered, sorted, and paginated collection/category URL pointing back to the clean (unfiltered) category URL, so Google consolidates duplicate pages and focuses crawl budget on your real content.
On Adobe Commerce (Magento)
- In the Admin Panel, go to Stores → Configuration → Catalog → Catalog → Search Engine Optimization.
- Set 'Use Canonical Link Meta Tag for Categories' to 'Yes'. This tells Magento to output a canonical pointing to the clean category URL on all filtered/layered-navigation variants.
- Also set 'Use Canonical Link Meta Tag for Products' to 'Yes' for product pages.
- For pagination: Magento by default sets canonical on paginated pages to page 1. Confirm this is working by viewing source on /category?p=3 and checking the canonical value.
- If you use custom layered navigation extensions (Amasty Improved Navigation, Mirasvit Layered Navigation, etc.), check their SEO settings panel for a 'Canonical URL' option — most expose this as a toggle.
- Clear the full-page cache after making changes: System → Cache Management → Flush Magento Cache. Then verify with Google Search Console URL Inspection.
<head>
<!-- All other head tags -->
<link rel="canonical" href="https://www.example.com/collections/hoops">
</head>Place this inside the <head> of every filtered/sorted/paginated category page. The href must be the full, absolute clean URL (no query parameters).
Seo product content client renderedLarger project
Server-render your product descriptions in the initial HTML so that all crawlers — including Google, AI search bots, and social media scrapers — can read your content without executing JavaScript.
On Adobe Commerce (Magento)
- Adobe Commerce (Magento 2) renders product pages server-side via PHP/PHTML by default. If descriptions are missing from raw HTML, a PWA Studio (Venia) or custom React/Vue headless front-end is the cause.
- For standard Luma/Blank themes: the product description is rendered in app/design/frontend/[Vendor]/[Theme]/Magento_Catalog/templates/product/view/description.phtml. Confirm {{ product.getDescription() }} output exists in the server response.
- For PWA Studio (Venia): switch from client-side fetching to SSR or implement full-page caching with pre-rendered HTML. Adobe recommends using Fastly or Varnish with ESI (Edge Side Includes) to cache server-rendered product HTML.
- For Product JSON-LD: install a schema markup extension such as 'MagePlaza SEO' or 'Amasty SEO' — both inject Product schema via PHP into the page <head> server-side. Alternatively, add a custom PHTML block in the head.additional block of your layout XML that outputs the JSON-LD using PHP-rendered product data.
- In layout XML (catalog_product_view.xml): add a block of type Magento\Framework\View\Element\Template pointing to a .phtml file that echoes a complete JSON-LD script tag populated with $product->getName(), $product->getDescription(), etc.
- Verify with: curl -A 'Googlebot' https://yourstore.com/product-url.html | grep 'application/ld+json' — the JSON-LD must appear in the curl output.
Seo product description minimalModerate effort
Expand your product descriptions to at least 300 words by adding specifications, use cases, and benefits so search engines have enough content to rank and shoppers have enough information to buy.
On Adobe Commerce (Magento)
- Go to Magento Admin → Catalog → Products.
- Click 'Edit' on the product.
- Scroll to the 'Content' section and expand it.
- Find the 'Description' field (the full WYSIWYG editor — not 'Short Description'). Use the editor to write a structured 300+ word description with headings, spec bullet lists, and benefit paragraphs.
- For multi-store-view setups, switch the Store View scope to ensure the description is saved for all relevant locales.
- Click 'Save'. Flush the Magento cache under System → Cache Management, then verify with Google Search Console.
<section class="product-description">
<h2>About the [Product Name]</h2>
<p>
The [Product Name] is a [category] designed for [target audience]. Whether you need
[use case 1] or [use case 2], this [product] delivers [core benefit] without
[common pain point].
</p>
<h3>Key Features & Specifications</h3>
<ul>
<li><strong>Material:</strong> [e.g. 100% organic cotton, BPA-free polypropylene]</li>
<li><strong>Dimensions:</strong> [W × H × D] / Weight: [X kg / X lbs]</li>
<li><strong>Colour options:</strong> [Colour 1], [Colour 2], [Colour 3]</li>
<li><strong>Compatibility:</strong> [e.g. Works with iOS 14+ and Android 10+]</li>
<li><strong>Warranty:</strong> [X-year manufacturer warranty]</li>
</ul>
<h3>Why Shoppers Choose [Product Name]</h3>
<p>
Unlike [generic alternatives], the [Product Name] [specific differentiator]. This means
[tangible benefit for the shopper], saving you [time / money / effort].
</p>
<p>
Ideal for [persona / scenario], the [Product Name] has been tested to [standard or
certification] and ships ready to use — no assembly required.
</p>
<h3>How to Use</h3>
<p>
[Step-by-step or scenario-based usage instructions in 2–4 sentences. Include any
important setup, safety, or care notes here.]
</p>
<h3>In the Box</h3>
<ul>
<li>1 × [Product Name]</li>
<li>1 × [Accessory or manual]</li>
<li>1 × [Charging cable / carry bag / etc.]</li>
</ul>
</section>Structural template for a product description section — paste inside your product page's main content area.
Seo product description missingModerate effort
Write and publish a 200–400 word product description covering what the product is, its key specs, who it's for, and how to use it.
On Adobe Commerce (Magento)
- In the Magento admin panel, go to Catalog → Products.
- Click 'Edit' on the product that is missing a description.
- In the 'Content' section (expand it if collapsed), fill in both the 'Description' field (full 200–400 word text, supports HTML) and optionally the 'Short Description' field (used in category listing pages).
- Scroll to the 'Search Engine Optimization' section to set a custom Meta Description (155–160 chars) and verify the URL key.
- Click 'Save' and clear the full-page cache (System → Cache Management → Flush Magento Cache), then view the live product page.
<section class="product-description">
<h2>About This Product</h2>
<p>
The <strong>Trailblazer 500 Waterproof Hiking Boot</strong> is a lightweight,
full-grain leather boot built for all-day comfort on rugged mountain trails.
Its Gore-Tex membrane keeps feet dry in rain and creek crossings while the
Vibram outsole delivers confident grip on wet rock and loose scree.
</p>
<p>
Designed for day hikers and weekend backpackers who want reliable protection
without the weight of a mountaineering boot, the Trailblazer 500 strikes the
ideal balance between support and agility. The padded collar and gusseted
tongue prevent trail debris from entering the boot, and the cushioned EVA
midsole absorbs impact mile after mile.
</p>
<h3>Key Specifications</h3>
<ul>
<li><strong>Upper:</strong> Full-grain leather with Gore-Tex waterproof lining</li>
<li><strong>Outsole:</strong> Vibram Megagrip rubber</li>
<li><strong>Weight:</strong> 680 g / pair (size UK 9)</li>
<li><strong>Shaft height:</strong> Mid (ankle) cut</li>
<li><strong>Available widths:</strong> Standard and Wide</li>
</ul>
<h3>How to Use</h3>
<p>
Lace firmly for ankle support on uneven terrain. Break the boot in over two
or three shorter walks before a full-day hike. Apply a water-repellent spray
to the leather upper every 20–30 hours of trail use to maintain waterproofing.
</p>
</section>Minimum viable product description markup — paste inside your product page's <main> or article element.
Seo product description thinModerate effort
Expand every product description to at least 300 words of unique, keyword-rich content that answers real shopper questions and gives Google enough text to understand and rank the page.
On Adobe Commerce (Magento)
- Go to Admin → Catalog → Products → [select the product].
- Scroll to the 'Content' section; expand it to reveal both the 'Short Description' (shown in listings) and the main 'Description' (shown on the product page) — expand the main 'Description' field.
- Use the WYSIWYG editor (or switch to HTML view) to write your 300+ word description with proper headings and lists.
- Set the store view scope at the top of the page if you need different descriptions per language/store view.
- Click 'Save' and then flush the Magento cache: System → Cache Management → Flush Magento Cache. Submit the URL in Google Search Console.
<section class="product-description" aria-label="Product description">
<h2>About This Product</h2>
<p>
Write your opening paragraph here (2–3 sentences introducing the product,
its primary benefit, and who it is for). Aim for a natural, engaging tone
that mirrors how your customers talk about this type of product.
</p>
<h3>Key Features</h3>
<ul>
<li><strong>Feature One:</strong> Explain the benefit, not just the spec.</li>
<li><strong>Feature Two:</strong> Include the detail a shopper needs to decide.</li>
<li><strong>Feature Three:</strong> Use the words your customers actually search for.</li>
<li><strong>Feature Four:</strong> Keep each bullet to one clear point.</li>
</ul>
<h3>Materials & Specifications</h3>
<p>
Describe materials, dimensions, weight, compatibility, or other technical
details relevant to your product category. Specificity builds trust and
captures long-tail searches (e.g. "100% organic cotton", "fits 13-inch laptops").
</p>
<h3>How to Use</h3>
<p>
Walk the customer through setup, application, or use in plain language.
Answer the most common pre-purchase questions here so shoppers feel confident.
</p>
<h3>Why Our Customers Love It</h3>
<p>
Reinforce with a sentence or two of social proof context or a unique selling
point that differentiates this product from generic alternatives.
</p>
</section>Ideal semantic structure for a product description section on any platform's product page template.
Seo product image alt filenameModerate effort
Replace auto-generated filename-based alt text on product images with short, descriptive phrases that accurately describe what the image shows.
On Adobe Commerce (Magento)
- Go to Admin → Catalog → Products and open a product.
- Click the 'Images and Videos' section to expand it.
- Click the image thumbnail you want to edit; a detail panel appears.
- Fill in the 'Alt Text' field with your descriptive text.
- Click Save. Repeat for base, small, thumbnail, and swatch images.
- For bulk updates, use a CSV import via Admin → System → Import (use the 'Images' entity type with an alt column) or install an extension such as 'Image Alt Text' by MagePow.
<?php
/** @var \Magento\Catalog\Block\Product\Image $block */
?>
<img
src="<?= $block->escapeUrl($block->getImageUrl()) ?>"
alt="<?= $block->escapeHtmlAttr($block->getLabel()) ?>"
width="<?= $block->escapeHtmlAttr($block->getWidth()) ?>"
height="<?= $block->escapeHtmlAttr($block->getHeight()) ?>"
loading="lazy"
>In a custom module or template override (e.g. Magento_Catalog/templates/product/image.phtml), always render the alt attribute from the product data.
Seo product missing brandModerate effort
Add a `brand` property to your Product schema markup so search engines can match your listings to brand-specific queries.
On Adobe Commerce (Magento)
- In Magento Admin, go to Catalog → Products and confirm that each product has the 'Manufacturer' attribute (or a custom brand attribute) populated.
- Locate your theme's product structured data template, typically at `app/design/frontend/<Vendor>/<Theme>/Magento_Catalog/templates/product/view/structured-data.phtml` — if it doesn't exist, copy the core file from `vendor/magento/module-catalog/view/frontend/templates/product/view/structured-data.phtml`.
- Inside the structured data template, add the brand block: `<?php if ($product->getAttributeText('manufacturer')): ?> "brand": {"@type": "Brand", "name": "<?= $block->escapeJs($product->getAttributeText('manufacturer')) ?>"}, <?php endif; ?>`.
- Alternatively, install a structured data extension from Magento Marketplace (e.g., 'SEO Suite Ultimate' by Mirasvit) which provides a UI to map product attributes to schema fields including brand.
- Flush Magento cache (System → Cache Management → Flush Cache) and test a product URL with Google's Rich Results Test.
<?php
/** @var \Magento\Catalog\Block\Product\View $block */
$product = $block->getProduct();
$brandName = $product->getAttributeText('manufacturer');
?>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Product",
"name": "<?= $block->escapeJs($product->getName()) ?>",
"image": "<?= $block->escapeJs($block->getImage($product, 'product_base_image')->getImageUrl()) ?>",
"description": "<?= $block->escapeJs(strip_tags($product->getDescription())) ?>",
"sku": "<?= $block->escapeJs($product->getSku()) ?>",
<?php if ($brandName): ?>
"brand": {
"@type": "Brand",
"name": "<?= $block->escapeJs($brandName) ?>"
},
<?php endif; ?>
"offers": {
"@type": "Offer",
"url": "<?= $block->escapeJs($product->getProductUrl()) ?>",
"priceCurrency": "<?= $block->escapeJs($block->getCurrentCurrencyCode()) ?>",
"price": "<?= $block->escapeJs($product->getFinalPrice()) ?>",
"availability": "https://schema.org/<?= $product->isAvailable() ? 'InStock' : 'OutOfStock' ?>"
}
}
</script>Place this override at app/design/frontend/<Vendor>/<Theme>/Magento_Catalog/templates/product/view/structured-data.phtml — add the brand block inside the existing JSON-LD object.
Seo product missing gtinModerate effort
Add a GTIN (and/or MPN) field to your Product structured data so Google can match your listings to its product knowledge graph for rich results and Google Shopping.
On Adobe Commerce (Magento)
- Go to Admin → Catalog → Products → [select a product].
- In the 'Search Engine Optimization' tab, you may add custom attributes. First, create a product attribute: Stores → Attributes → Product → Add New Attribute. Set the attribute code to 'gtin', label to 'GTIN', input type 'Text Field', and assign it to the relevant attribute set.
- Return to the product and fill in the GTIN value.
- To inject this into the Product JSON-LD, either: (a) install a structured data extension from the Adobe Commerce Marketplace (e.g. 'Structured Data' by Amasty or 'SEO Suite' by Mageworx) that supports GTIN, or (b) create a custom module that overrides the schema data provider to append 'gtin' and 'mpn' from product attributes.
- If using a marketplace extension, configure it in Admin → Stores → Configuration → [Extension section] and map the GTIN attribute.
- Clear cache (System → Cache Management) and validate with Google's Rich Results Test.
Seo product missing stock statusModerate effort
Add a visible stock availability label and an `availability` property to your Product structured data so Google can display rich results and shoppers know immediately whether a product is in stock.
On Adobe Commerce (Magento)
- Adobe Commerce outputs Product structured data via the `Magento_Catalog` module. The availability field is included by default, but verify it is present by checking `app/code/Magento/Catalog/view/frontend/templates/product/view/structured_data.phtml` (or the equivalent in your theme's override path under `app/design/frontend/<Vendor>/<Theme>/Magento_Catalog/templates/`).
- Ensure each product has stock management enabled: Admin → Catalog → Products → Edit Product → Sources (MSI) or the legacy Stock section — set 'Manage Stock' to Yes and update quantity so the system can output `InStock`/`OutOfStock` correctly.
- To add or fix the `availability` output in the template, locate the `<script type='application/ld+json'>` block and ensure it contains: `"availability": "<?php echo $block->getAvailability(); ?>"` — or use a plugin/preference on `Magento\Catalog\Block\Product\View` to expose a `getAvailability()` method.
- For the visible label, the default Magento theme outputs stock availability text near the price area via `app/design/frontend/.../Magento_Catalog/templates/product/view/form.phtml` — verify the `<div class='stock'>` block is present and not hidden by your theme's CSS.
- Clear cache (bin/magento cache:flush), then validate in the Rich Results Test.
<!-- Product structured data with availability -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Your Product Name",
"description": "A short description of your product.",
"sku": "YOUR-SKU-123",
"image": "https://www.example.com/images/product.jpg",
"brand": {
"@type": "Brand",
"name": "Your Brand Name"
},
"offers": {
"@type": "Offer",
"url": "https://www.example.com/products/your-product",
"priceCurrency": "USD",
"price": "29.99",
"availability": "https://schema.org/InStock",
"itemCondition": "https://schema.org/NewCondition"
}
}
</script>
<!-- Visible stock label near Add-to-Cart button -->
<p class="stock-status" style="font-weight:bold; color:#2a7a2a;">✓ In Stock</p>Paste the complete block into the <head> section of your product page HTML. Change availability value to OutOfStock or PreOrder as needed.
Seo product no imagesModerate effort
Add at least one high-quality image to every product page so shoppers can see what they're buying and Google can surface your products in Image Search.
On Adobe Commerce (Magento)
- Log in to your Adobe Commerce (Magento) Admin Panel and navigate to Catalog → Products.
- Click 'Edit' on the product that is missing images.
- Scroll down to the 'Images and Videos' section and click 'Browse to find or drag image here' to upload files.
- After uploading, click the three-dot menu (⋮) on each image, select 'Alt Text', and enter a descriptive phrase.
- Designate one image as 'Base', 'Small', and 'Thumbnail' by toggling the role checkboxes under each image.
- Click 'Save' (top right), then clear the cache under System → Cache Management and visit the product page to confirm images appear.
<figure class="product-gallery">
<img
src="/images/products/black-leather-wallet-front.jpg"
alt="Black leather wallet with RFID blocking, front view"
width="800"
height="800"
loading="eager"
/>
<img
src="/images/products/black-leather-wallet-open.jpg"
alt="Black leather wallet open showing card slots and cash pocket"
width="800"
height="800"
loading="lazy"
/>
</figure>Basic responsive product image with descriptive alt text — place inside your product page HTML where the gallery/main image appears.
Seo product no spec tableModerate effort
Add a structured specifications table or list to every product page so shoppers, search engines, and AI tools can instantly find and use key product details.
On Adobe Commerce (Magento)
- In the Admin panel go to Catalog → Products → (select product) → 'Attributes' tab. Add values for all relevant attributes already in your attribute set.
- To add new spec attributes: Stores → Attributes → Product → 'Add New Attribute'. Set the input type, label, and assign it to your attribute set. Mark it as 'Visible on Catalog Pages on Storefront'.
- The built-in 'More Information' tab on the product page automatically renders all attributes that are set to visible — no template change needed for basic spec display.
- To customise the layout or style, override the template at 'app/design/frontend/<Vendor>/<theme>/Magento_Catalog/templates/product/view/attributes.phtml' in your custom theme.
- For complex specs, consider installing an extension such as 'Mageplaza Product Specifications Table' from the Magento Marketplace.
- Flush the cache (System → Cache Management → Flush Magento Cache) and verify the spec table appears on the storefront.
Seo product price not in htmlModerate effort
Render your product price in the server-generated HTML so Google and other crawlers can read it directly — without executing JavaScript.
On Adobe Commerce (Magento)
- Magento renders prices via PHP .phtml templates. The core template is vendor/magento/module-catalog/view/frontend/templates/product/price/ (or your theme override in app/design/frontend/<Vendor>/<Theme>/Magento_Catalog/templates/product/price/).
- Confirm the price block uses the PHP price renderer (\Magento\Framework\Pricing\Render) rather than being fetched via an AJAX/JS call after load.
- If Full Page Cache (FPC) with Varnish is enabled and prices are personalised (e.g. B2B tier pricing), Magento uses 'price box' placeholder + JS to inject prices for cache-hit pages. For SEO, configure the default/guest price to be present in the initial HTML by setting the price render to output for non-logged-in users statically.
- For structured data, install an SEO extension (e.g. Mageplaza SEO, Amasty SEO Toolkit) that injects Product JSON-LD with 'offers' price into the server-rendered <head>, or add it manually to your product.phtml template.
- Verify with: curl -A 'Googlebot' https://yourstore.com/product-url | grep -o '[0-9]\.[0-9]' to check the raw response contains the price.
Seo product single imageModerate effort
Add at least 4 product images (multiple angles, detail shots, and lifestyle/in-use photos) to every product page to improve click-through rates, conversions, and Google Images visibility.
On Adobe Commerce (Magento)
- Go to Admin → Catalog → Products → [select your product] → Images and Videos tab.
- Click 'Add Images or Videos' (camera icon) to upload additional image files.
- For each uploaded image, click the three-dot menu → 'Details' to open the image settings panel and fill in the 'Alt Text' field.
- Check the appropriate role checkboxes (Base, Small, Thumbnail, Swatch) for the main image; additional images need no role assigned.
- Drag images to reorder them, then scroll up and click 'Save' to publish the updated gallery.
<figure class="product-gallery">
<!-- Main hero image -->
<img
src="/images/red-leather-wallet-front.webp"
alt="Red leather bifold wallet — front view"
width="2000"
height="2000"
loading="eager"
fetchpriority="high"
/>
</figure>
<!-- Thumbnail strip -->
<ul class="product-thumbnails" role="list">
<li>
<img
src="/images/red-leather-wallet-front-thumb.webp"
alt="Red leather bifold wallet — front view"
width="200"
height="200"
loading="lazy"
/>
</li>
<li>
<img
src="/images/red-leather-wallet-back-thumb.webp"
alt="Red leather bifold wallet — back view"
width="200"
height="200"
loading="lazy"
/>
</li>
<li>
<img
src="/images/red-leather-wallet-interior-thumb.webp"
alt="Red leather bifold wallet — interior card slots"
width="200"
height="200"
loading="lazy"
/>
</li>
<li>
<img
src="/images/red-leather-wallet-lifestyle-thumb.webp"
alt="Red leather bifold wallet shown in a shirt pocket"
width="200"
height="200"
loading="lazy"
/>
</li>
</ul>Basic accessible product image gallery markup. Place inside your product detail page template.
Seo product title genericModerate effort
Rewrite every product page title tag to include the specific product name plus at least one differentiating attribute (variant, material, brand, or model) so Google and shoppers can instantly tell what the page is about.
On Adobe Commerce (Magento)
- Log in to the Admin Panel and go to Catalog → Products.
- Click on the product to edit.
- Expand the 'Search Engine Optimization' section in the product edit page.
- Update the 'Meta Title' field with your descriptive product title. This field populates the HTML <title> tag.
- Click Save.
- For bulk updates, use the product mass-action: select multiple products → Actions → Update Attributes, then set the Meta Title in the bulk attribute update form.
- To set a store-wide default title pattern, go to Stores → Configuration → Catalog → Catalog → Product Fields Auto-generation and set the 'Product Title' mask.
<head>
<title>Magnetic Card Holder Phone Mount – MagSafe Compatible – BrandName</title>
<meta name="description" content="Attach your cards securely to any MagSafe-compatible iPhone with BrandName's slim magnetic card holder mount. Holds 1–3 cards. Available in black and white.">
</head>Place inside the <head> section of every product page. Follow the pattern: Product Name – Key Attribute – Brand.
Seo variant urls in sitemapModerate effort
Remove product variant URLs (e.g. ?variant= or ?sku= query strings) from your XML sitemap and keep only the canonical base product URL for each product.
On Adobe Commerce (Magento)
- Go to Admin Panel → Marketing → SEO & Search → Site Map.
- Click 'Edit' on your sitemap (or 'Add Sitemap' if none exists) and review the generation settings.
- Navigate to Stores → Configuration → Catalog → XML Sitemap — expand 'Product Options' and verify that only 'Use URLs' is set to base product URLs; set 'Use Secure URLs' appropriately.
- Install or configure a sitemap extension (e.g. Amasty SEO Toolkit or Mirasvit SEO) that has an explicit 'Exclude URLs with query parameters' option, and enable it.
- Alternatively, add a custom plugin/observer on the sitemap generation event to filter out any product URL containing a query string before it is written to the sitemap XML.
- Regenerate the sitemap (Marketing → SEO & Search → Site Map → Generate) and re-submit in Google Search Console.
<?php
/**
* Plugin to remove query-parameter (variant) URLs from Magento sitemap generation.
* File: app/code/YourVendor/SitemapFix/Plugin/SitemapPlugin.php
*/
namespace YourVendor\SitemapFix\Plugin;
class SitemapPlugin
{
/**
* Around plugin on \Magento\Sitemap\Model\Sitemap::generateXml
* Strips any URL containing a query string before it is written.
*/
public function aroundGenerateXml(
\Magento\Sitemap\Model\Sitemap $subject,
callable $proceed
) {
// Run original generation
$result = $proceed();
// Post-process: read the written file, filter out variant URLs
$fileName = $subject->getSitemapFilename();
$path = $subject->getSitemapPath();
$fullPath = BP . DIRECTORY_SEPARATOR . ltrim($path, '/') . $fileName;
if (file_exists($fullPath)) {
$xml = file_get_contents($fullPath);
$dom = new \DOMDocument('1.0', 'UTF-8');
$dom->loadXML($xml);
$xpath = new \DOMXPath($dom);
$xpath->registerNamespace('s', 'http://www.sitemaps.org/schemas/sitemap/0.9');
// Find all <loc> nodes whose text contains '?'
$nodes = $xpath->query('//s:url[contains(s:loc, "?")]');
foreach ($nodes as $node) {
$node->parentNode->removeChild($node);
}
file_put_contents($fullPath, $dom->saveXML());
}
return $result;
}
}
// ---------------------------------------------------------------------------
// Register in: app/code/YourVendor/SitemapFix/etc/di.xml
// ---------------------------------------------------------------------------
// <?xml version="1.0"?>
// <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
// xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
// <type name="Magento\Sitemap\Model\Sitemap">
// <plugin name="yourvendor_sitemap_fix" type="YourVendor\SitemapFix\Plugin\SitemapPlugin" />
// </type>
// </config>Plugin class to filter out query-string URLs during sitemap generation. Place in app/code/YourVendor/SitemapFix/Plugin/SitemapPlugin.php and register in di.xml.
Seo variant urls not canonicalizedModerate effort
Add a canonical tag to every product variant URL pointing back to the base product page, so Google consolidates ranking signals on one URL instead of splitting them across hundreds of duplicates.
On Adobe Commerce (Magento)
- Go to Stores → Configuration → Catalog → Catalog → Search Engine Optimization.
- Set 'Use Canonical Link Meta Tag for Products' to Yes. This tells Magento to output a canonical tag on all product pages pointing to the base product URL, ignoring filter/option query parameters.
- Set 'Use Canonical Link Meta Tag for Categories' to Yes for category pages as well.
- Save Config and flush the Magento cache: System → Cache Management → Flush Magento Cache.
- Verify by loading a product URL with parameters (e.g. ?color=12&size=L) and using View Source to confirm the <link rel="canonical"> points to the clean base product URL.
- If you use a third-party SEO extension (e.g. Mageworx SEO Suite, Amasty SEO Toolkit), check its product canonical settings in its own configuration panel to ensure it is not overriding the platform setting with variant-specific URLs.
<!-- File: app/design/frontend/YourVendor/YourTheme/Magento_Catalog/layout/catalog_product_view.xml -->
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<head>
<!-- Magento outputs canonical automatically when config Use Canonical Link Meta Tag for Products = Yes -->
<!-- Verify in: Stores > Configuration > Catalog > Search Engine Optimization -->
<!-- No additional code needed when the store config flag is enabled -->
</head>
</page>This is the app/etc/config.php or Store Config path. For code-based enforcement, add this to your theme's catalog_product_view.xml layout to explicitly set the canonical.
Server errorModerate effort
Identify and fix every page returning an HTTP 5xx server error so search engines can crawl and index those URLs again.
On Adobe Commerce (Magento)
- SSH into your server and tail the application log: tail -f var/log/exception.log and var/log/system.log — these show the exact PHP exception causing the 500.
- Also check your web server error log (Apache: /var/log/apache2/error.log; Nginx: /var/log/nginx/error.log) for upstream PHP-FPM or out-of-memory errors.
- Disable recently installed extensions via the Admin panel at Stores > Configuration > Advanced > Advanced > Disable Modules Output, or via bin/magento module:disable Vendor_Module.
- Run bin/magento setup:upgrade and bin/magento cache:flush from the CLI to clear stale configuration after any module change.
- If the error is theme-related, switch to the Luma (default) theme via Content > Design > Configuration > [store view] > Applied Theme to isolate it.
- Increase PHP memory if needed in php.ini (memory_limit = 756M) or in .htaccess for Apache environments.
- After the fix, submit the URL in Google Search Console > URL Inspection > Request Indexing.
# Tail the Magento exception log in real time
tail -f /var/www/html/var/log/exception.log
# Tail the system log
tail -f /var/www/html/var/log/system.log
# After fixing the issue, flush cache and re-run setup
cd /var/www/html
bin/magento cache:flush
bin/magento setup:upgrade
bin/magento setup:di:compile
bin/magento indexer:reindexRun these commands via SSH on your Magento server to read logs and flush caches after a fix.
Slow pageModerate effort
Reduce your page load time to under 3 seconds by compressing images, eliminating render-blocking scripts and styles, and improving server response speed.
On Adobe Commerce (Magento)
- In the Admin panel go to Stores → Configuration → Advanced → Developer → JavaScript Settings — set 'Merge JavaScript Files', 'Minify JavaScript Files' to Yes.
- Go to Stores → Configuration → Advanced → Developer → CSS Settings — set 'Merge CSS Files' and 'Minify CSS Files' to Yes.
- Go to Stores → Configuration → Advanced → System → Full Page Cache — enable Varnish Cache (recommended) or the built-in Magento FPC.
- Images: Go to Stores → Configuration → General → Web → and enable WebP image conversion if your theme/extension supports it. Use an image optimisation extension (e.g., 'Magento Image Optimizer' from the Marketplace) to bulk-compress the media gallery.
- Enable Magento's built-in lazy loading for catalog images in your theme's XML layout files, or install a performance extension such as 'Mageworx Lazy Load'.
- At the server level (requires developer/sysadmin): enable OPcache, GZIP/Brotli compression, HTTP/2, and deploy a CDN (Fastly is bundled with Adobe Commerce Cloud; on-premise installs should configure Cloudflare or Cloudfront).
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<head>
<!-- Preload LCP hero image for faster render -->
<link
rel="preload"
src="images/hero-banner.webp"
as="image"
type="image/webp"
fetchpriority="high"
/>
</head>
</page>Add to your theme's Magento_Theme/layout/default_head_includes.xml to preload the LCP hero image across all pages.
Thin contentModerate effort
Add meaningful, original written content to pages that contain fewer than 300 words so Google has enough substance to rank and display them confidently.
On Adobe Commerce (Magento)
- Log in to the Admin Panel → Catalog → Products → click 'Edit' on the thin product.
- Expand the 'Content' section. Write your full product description in the 'Description' field (the main WYSIWYG editor). Use the 'Short Description' field for a 2–3 sentence teaser shown in listing pages.
- For category pages: Catalog → Categories → select the category → expand the 'Content' tab → add descriptive text in the 'Description' field and ensure 'Display Mode' is set to 'Static Block and Products' or 'Description and Products' so the text appears.
- For CMS pages: Content → Pages → click 'Edit' → expand the 'Content' section and add text in the Page Builder or WYSIWYG editor.
- Clear the full-page cache after saving: System → Cache Management → Flush Magento Cache — otherwise your changes won't appear on the storefront.
<div class="product-full-description">
<h2>Product Overview</h2>
<p>
Write 2–4 sentences introducing what this product is, who it is for,
and what primary problem it solves or value it delivers.
</p>
<h3>Features & Specifications</h3>
<ul>
<li><strong>Material:</strong> [e.g. 100% Merino wool]</li>
<li><strong>Dimensions:</strong> [e.g. 30 cm × 20 cm × 10 cm]</li>
<li><strong>Weight:</strong> [e.g. 450 g]</li>
<li><strong>Compatibility:</strong> [e.g. Works with X, Y, Z]</li>
<li><strong>Warranty:</strong> [e.g. 2-year manufacturer warranty]</li>
</ul>
<h3>How to Use</h3>
<p>
Provide 3–5 sentences or numbered steps explaining how a customer
uses or installs this product correctly for best results.
</p>
<h3>Frequently Asked Questions</h3>
<p><strong>Q: [Common question 1?]</strong><br />
A: [Clear, helpful answer of 2–3 sentences.]</p>
<p><strong>Q: [Common question 2?]</strong><br />
A: [Clear, helpful answer of 2–3 sentences.]</p>
<p><strong>Q: [Common question 3?]</strong><br />
A: [Clear, helpful answer of 2–3 sentences.]</p>
</div>Example of rich HTML content to paste into the Description WYSIWYG field of a Magento product via Admin → Catalog → Products → Content tab.
Title too longQuick win
Shorten your page title tag to 60 characters or fewer so it displays fully — without truncation — in Google search results.
On Adobe Commerce (Magento)
- For a product: go to Catalog → Products, open the product, expand the 'Search Engine Optimization' section, and edit the 'Meta Title' field (≤60 characters). Leave it blank to let Magento use the product name — ensure the product name itself is short enough.
- For a category: go to Catalog → Categories, open the category, expand 'Search Engine Optimization', and edit 'Meta Title'.
- For CMS pages: go to Content → Pages, open the page, select the 'Meta Data' tab (or 'Search Engine Optimization' in newer versions), and edit 'Meta Title'.
- For global title suffix/prefix: go to Stores → Configuration → Catalog → Catalog → Product Listings in Search Results / Search Engine Optimization and adjust the title suffix so the total remains ≤60 characters.
- Save the configuration and flush the cache (System → Cache Management → Flush Magento Cache).
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<head>
<!-- Keep suffix short so product name + suffix stays ≤60 chars -->
<title/>
</head>
</page>Place in your custom theme's Magento_Theme/layout/default_head_blocks.xml to set a global title suffix so the full title stays under 60 characters.
Title too shortQuick win
Expand your page title to between 30 and 60 characters so Google displays it fully in search results and shoppers can immediately understand what the page is about.
On Adobe Commerce (Magento)
- For a product: go to Catalog → Products → select the product → expand the 'Search Engine Optimization' section → update the 'Meta Title' field (this controls the HTML title tag).
- For a category: go to Catalog → Categories → select the category → click the 'Search Engine Optimization' tab → update the 'Meta Title' field.
- For CMS pages (About, Contact, Homepage): go to Content → Pages → select the page → expand 'Search Engine Optimization' → update 'Meta Title'.
- If 'Meta Title' is left blank, Adobe Commerce auto-generates a title from the product/category name — ensure the generated name itself is at least 30 characters or fill in Meta Title explicitly.
- Clear the full-page cache after saving: System → Cache Management → Flush Magento Cache, so changes appear immediately.
<head>
<meta charset="UTF-8">
<title>Men's Merino Wool Beanies – BrandName</title>
</head>Place inside the <head> element of every page. Replace the example with your actual descriptive title (30–60 chars).
Answer Engine Optimization · 17 fixes
Aeo schema client renderedModerate effort
Move your JSON-LD structured data from client-side JavaScript injection to server-rendered HTML so that AI crawlers and search engines can read it in the raw page source.
On Adobe Commerce (Magento)
- Locate the relevant layout XML file for the page type (e.g., catalog_product_view.xml in your theme's Magento_Catalog/layout/ directory).
- Add a <block> or <referenceContainer> entry that calls a PHP Block class responsible for rendering the JSON-LD <script> tag, using the product's data model ($product->getName(), $product->getFinalPrice(), etc.) to populate values server-side.
- Alternatively, create a .phtml template file in your theme (e.g., Magento_Catalog/templates/product/schema.phtml) that echoes the JSON-LD block using PHP, and reference it from the layout XML.
- If you use an SEO extension (e.g., Mageplaza SEO, Amasty SEO), check its configuration under Stores → Configuration → [Extension Name] → Structured Data and ensure 'Render Method' is set to server-side/PHP, not JavaScript.
- Run bin/magento cache:flush after making template changes, then view the page source to confirm the JSON-LD block is present in the raw HTML response.
<?php
/** @var \Magento\Catalog\Block\Product\View $block */
$product = $block->getProduct();
$priceInfo = $product->getPriceInfo();
$finalPrice = $priceInfo->getPrice('final_price')->getAmount()->getValue();
$currency = $block->getCurrentStore()->getCurrentCurrencyCode();
$availability = $product->isAvailable()
? 'https://schema.org/InStock'
: 'https://schema.org/OutOfStock';
$schema = [
'@context' => 'https://schema.org',
'@type' => 'Product',
'name' => $product->getName(),
'description' => strip_tags((string) $product->getShortDescription()),
'sku' => $product->getSku(),
'brand' => [
'@type' => 'Brand',
'name' => $product->getAttributeText('manufacturer') ?: $block->getStoreConfig('general/store_information/name'),
],
'offers' => [
'@type' => 'Offer',
'url' => $product->getProductUrl(),
'priceCurrency' => $currency,
'price' => number_format((float) $finalPrice, 2, '.', ''),
'availability' => $availability,
'itemCondition' => 'https://schema.org/NewCondition',
],
];
?>
<script type="application/ld+json">
<?= /* @noEscape */ json_encode($schema, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) ?>
</script>Create this file at app/design/frontend/YourVendor/YourTheme/Magento_Catalog/templates/product/schema.phtml and reference it from catalog_product_view.xml.
Article missing authorQuick win
Add a structured-data author field (schema.org Person) to every article or blog post so AI engines and Google can identify and trust who wrote the content.
On Adobe Commerce (Magento)
- Adobe Commerce does not have a built-in blog; most stores use a blog extension such as Magefan Blog or Aheadworks Blog.
- In Magefan Blog: go to Blog → Authors → create or edit the author, filling in name, URL, and social profile URLs. The extension generates Article structured data including the author entity automatically.
- For a custom or headless setup, edit the blog post layout XML file (e.g. blog_post_view.xml) and add a Block that renders the JSON-LD script in the <head> section.
- Create a ViewModel or Block class that fetches author data from the post model and outputs it as a JSON-LD script.
- Alternatively, use a third-party SEO extension (e.g. Mageplaza SEO) that supports Article schema with author fields.
- Validate with the Rich Results Test.
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": "How to Choose the Best Running Shoes",
"datePublished": "2024-03-10T09:00:00+00:00",
"dateModified": "2024-06-20T14:00:00+00:00",
"author": {
"@type": "Person",
"name": "Alex Johnson",
"url": "https://www.yourstore.com/about/alex-johnson",
"sameAs": [
"https://www.linkedin.com/in/alexjohnson",
"https://twitter.com/alexjohnson"
]
},
"publisher": {
"@type": "Organization",
"name": "Your Store Name",
"logo": {
"@type": "ImageObject",
"url": "https://www.yourstore.com/assets/logo.png",
"width": 300,
"height": 60
}
},
"mainEntityOfPage": {
"@type": "WebPage",
"@id": "https://www.yourstore.com/blog/how-to-choose-running-shoes"
}
}
</script>For any static site or custom CMS: paste inside the <head> of each article page and fill in real values.
Faq schema not visibleModerate effort
Render all FAQ answers in the server-delivered HTML so that both Google and your FAQ structured data can see every question and answer — not just the one currently open in a JavaScript accordion.
On Adobe Commerce (Magento)
- Identify the CMS page or block used for your FAQ (Content → Pages or Content → Blocks in the Admin).
- Edit the FAQ page HTML: replace any JavaScript accordion widget (e.g. a jQuery UI accordion) with native <details>/<summary> HTML elements so all answers are present in server-rendered HTML.
- Add the FAQPage JSON-LD block directly inside the page content or, preferably, create a custom layout XML file (e.g. cms_page_view_id_faq.xml) that adds a block injecting the JSON-LD into the <head>.
- Alternatively, install a structured-data extension (e.g. from Magento Marketplace) that supports FAQPage schema and configure it to match your page content.
- Clear the full-page cache (System → Cache Management → Flush Magento Cache), then validate with the Rich Results Test.
<section>
<details>
<summary>What is your return policy?</summary>
<p>We accept returns within 30 days of purchase. Items must be unused and in original packaging. Contact support@example.com to start a return.</p>
</details>
<details>
<summary>How long does shipping take?</summary>
<p>Standard shipping takes 3–5 business days. Express shipping (1–2 business days) is available at checkout.</p>
</details>
<details>
<summary>Do you ship internationally?</summary>
<p>Yes, we ship to over 40 countries. International orders typically arrive within 7–14 business days.</p>
</details>
<details>
<summary>Can I change or cancel my order?</summary>
<p>Orders can be changed or cancelled within 1 hour of placement. After that, please wait for delivery and use our return process.</p>
</details>
</section>Replace JS-only accordion with native <details>/<summary> elements — all answers stay in the HTML at all times, visible to crawlers regardless of open/closed state.
Invalid rating valueModerate effort
Fix the AggregateRating.ratingValue in your structured data so it is a valid number between 1 and 5 (or your declared scale's minimum and maximum).
On Adobe Commerce (Magento)
- AggregateRating structured data in Adobe Commerce / Magento is typically output from the product view template or a dedicated structured data module.
- Check app/design/frontend/<Vendor>/<Theme>/Magento_Catalog/templates/product/view/ or the equivalent in the active theme for any structured data template file (often schema.phtml or structured_data.phtml).
- If using a third-party SEO extension (Mageplaza SEO, Amasty SEO, etc.), navigate to that extension's configuration in Stores → Configuration → [Extension] → Rich Snippets / Structured Data and confirm ratingValue is mapped to the product's actual average rating attribute, not a default of 0.
- In custom code, replace any hardcoded ratingValue with a call to $product->getRatingSummary() or the review model's getReviewsCount() / getRatingSummary() methods, and wrap the block in a conditional that checks review count > 0.
- Run bin/magento cache:clean and bin/magento setup:di:compile after making template or module changes.
- Validate with the Google Rich Results Test on a product URL that has real reviews.
Low answer first scoreModerate effort
Restructure page content to lead with a direct, concise answer, use question-format headings, and add FAQ structured data so search engines and AI assistants can surface your answers prominently.
On Adobe Commerce (Magento)
- Admin → Catalog → Products → Edit the product. In the 'Description' field (full description), place the 160-character answer summary as the first paragraph before any other content.
- For CMS pages: Content → Pages → Edit → Content tab; move the answer summary paragraph to the top of the content area and rewrite any heading widgets as questions.
- To add FAQPage JSON-LD site-wide or per-page: Content → Blocks → Add New Block; paste the JSON-LD <script> in the block content, then assign it to the relevant page layout via Content → Pages → Design → Layout Update XML, or use a layout XML file in your theme.
- Alternatively, install a Magento 2 SEO extension (e.g., Mageplaza SEO, Amasty SEO) which provides a dedicated FAQ schema module with admin UI.
- For theme-level injection, add the JSON-LD to app/design/frontend/<Vendor>/<theme>/Magento_Theme/layout/default_head_blocks.xml or a page-specific layout handle.
- Flush the cache (System → Cache Management → Flush Magento Cache) after any changes and verify with Google's Rich Results Test.
<!-- Answer-first page structure template -->
<!-- 1. Direct-answer summary (≤160 characters) -->
<p class="answer-summary">
[Product Name] does [core benefit] by [key mechanism] — ideal for [target customer].
</p>
<!-- 2. Question-format H2 subheading -->
<h2>What does [Product Name] do?</h2>
<p>Direct 1–2 sentence answer here. Then expand with supporting detail below.</p>
<!-- 3. Another question-format heading -->
<h2>Who is [Product Name] designed for?</h2>
<p>Direct answer. Then elaborate.</p>
<!-- 4. FAQ section (visible on page — required for FAQPage schema) -->
<section id="faq">
<h2>Frequently Asked Questions</h2>
<h3>How long does shipping take?</h3>
<p>Standard shipping takes 3–5 business days. Express options are available at checkout.</p>
<h3>What is your return policy?</h3>
<p>We accept returns within 30 days of delivery on unused items in original packaging.</p>
<h3>Is [Product Name] compatible with [common concern]?</h3>
<p>Yes. [One-sentence direct answer with any important caveats.]</p>
</section>
<!-- 5. FAQPage JSON-LD (copy the JSON-LD snippet above and paste here or in <head>) -->Page content structure template — place this as the opening content block on any product, category, or landing page.
Low review countModerate effort
Collect at least 5 published reviews so Google will display star ratings in search results alongside your product listings.
On Adobe Commerce (Magento)
- Navigate to Stores → Configuration → Catalog → Product Reviews and ensure 'Enabled' is set to Yes.
- Install a review extension from the Adobe Commerce Marketplace — 'Yotpo Reviews', 'Trustpilot', or 'Amasty Review Reminder' all support structured data output.
- If using the built-in review system, verify that your theme outputs AggregateRating JSON-LD. In Luma/Blank theme this is in app/design/frontend/[Vendor]/[theme]/Magento_Review/templates/product/view/list.phtml — ensure the aggregateRating block is rendered.
- Enable automated review reminder emails in the review extension's configuration, or configure a transactional email trigger in Marketing → Email Templates.
- After reaching 5 reviews, test with Google's Rich Results Test and check Reports → Marketing → Reviews for review counts per product.
<!-- Product structured data with AggregateRating -->
<!-- Note: Google will only show review stars when reviewCount >= 5 -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Your Product Name",
"image": [
"https://example.com/images/product-front.jpg",
"https://example.com/images/product-side.jpg"
],
"description": "A clear, accurate description of your product.",
"sku": "SKU-12345",
"mpn": "MPN-12345",
"brand": {
"@type": "Brand",
"name": "Your Brand"
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.5",
"reviewCount": "12",
"bestRating": "5",
"worstRating": "1"
},
"offers": {
"@type": "Offer",
"url": "https://example.com/products/your-product",
"priceCurrency": "USD",
"price": "49.99",
"priceValidUntil": "2026-12-31",
"itemCondition": "https://schema.org/NewCondition",
"availability": "https://schema.org/InStock",
"seller": {
"@type": "Organization",
"name": "Your Store Name"
}
}
}
</script>Drop this complete block into the <head> section of any product page. Swap in your real values for name, image, SKU, price, ratingValue, and reviewCount.
Missing schema articleModerate effort
Add Article schema markup (JSON-LD) to every blog post and editorial page so Google can identify and rich-result-enhance your content in search.
On Adobe Commerce (Magento)
- If you use a blog extension (e.g., Magefan Blog, Aheadworks Blog), check the extension's configuration in Stores → Configuration → [Blog Extension] for a built-in structured data / schema.org toggle and enable it.
- For a custom or theme-level implementation, create or edit the blog post layout XML file (e.g., 'view/frontend/layout/blog_post_view.xml') and add a block that renders a PHTML template containing the JSON-LD script.
- In the PHTML template, use Magento's block methods and the post data object to populate headline, datePublished, dateModified, author, and image fields dynamically.
- Run 'bin/magento cache:flush' after deploying, then validate a live blog post URL with the Rich Results Test.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>How to Choose the Right Running Shoes</title>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": "How to Choose the Right Running Shoes",
"description": "A complete guide to selecting running shoes based on your gait, terrain, and training goals.",
"image": {
"@type": "ImageObject",
"url": "https://www.example.com/images/running-shoes-guide.jpg",
"width": 1200,
"height": 630
},
"author": {
"@type": "Person",
"name": "Jane Smith",
"url": "https://www.example.com/authors/jane-smith"
},
"publisher": {
"@type": "Organization",
"name": "Example Store",
"logo": {
"@type": "ImageObject",
"url": "https://www.example.com/logo.png",
"width": 250,
"height": 60
}
},
"datePublished": "2024-03-15",
"dateModified": "2024-06-01",
"mainEntityOfPage": {
"@type": "WebPage",
"@id": "https://www.example.com/blog/how-to-choose-running-shoes"
},
"url": "https://www.example.com/blog/how-to-choose-running-shoes"
}
</script>
</head>
<body>
<!-- page content -->
</body>
</html>Place this entire block inside the <head> element of any article or blog-post page. Fill in all values before publishing.
Missing schema breadcrumblistModerate effort
Add BreadcrumbList structured data (JSON-LD) to every page so Google can display your site's navigation path directly in search results.
On Adobe Commerce (Magento)
- In your Magento admin, go to Content › Configuration › HTML Head or use a custom layout XML file in your theme: app/design/frontend/<Vendor>/<Theme>/Magento_Theme/layout/default_head_blocks.xml.
- Create or edit a custom PHP template block (e.g. app/design/frontend/<Vendor>/<Theme>/Magento_Theme/templates/breadcrumb_jsonld.phtml) that outputs the JSON-LD using Magento's breadcrumb data model ($block->getCrumbs()).
- Register the block in a layout XML file (e.g. default.xml) with reference name='head.additional' so it outputs on every page.
- Alternatively, install a structured data extension from the Magento Marketplace (e.g. 'Rich Snippets' extensions) that handles BreadcrumbList automatically.
- Clear the Magento cache (System › Cache Management › Flush Cache Storage) and validate with Google's Rich Results Test.
<?php
/** @var \Magento\Framework\View\Element\Template $block */
$crumbs = $block->getLayout()->getBlock('breadcrumbs') ? $block->getLayout()->getBlock('breadcrumbs')->getCrumbs() : [];
$baseUrl = $block->getBaseUrl();
$items = [];
$position = 1;
foreach ( (array) $crumbs as $crumbName => $crumbInfo ) {
$items[] = [
'@type' => 'ListItem',
'position' => $position++,
'name' => $crumbInfo['label'],
'item' => isset( $crumbInfo['link'] ) && $crumbInfo['link'] ? $baseUrl . ltrim( $crumbInfo['link'], '/' ) : $baseUrl,
];
}
$schema = [
'@context' => 'https://schema.org',
'@type' => 'BreadcrumbList',
'itemListElement' => $items,
];
?>
<script type="application/ld+json"><?= /* @noEscape */ json_encode($schema, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?></script>Create this file at app/design/frontend/<Vendor>/<Theme>/Magento_Theme/templates/breadcrumb_jsonld.phtml and register it in a layout XML file.
Missing schema faqpageQuick win
Add FAQPage schema markup to any page that contains a Frequently Asked Questions section so Google can display your answers as rich results directly in search.
On Adobe Commerce (Magento)
- In your Admin Panel, go to Content → Pages (or the relevant CMS page containing your FAQ).
- Open the page, switch the content editor to HTML mode, and paste the JSON-LD <script> block at the top of the HTML content — OR create a new Static Block (Content → Blocks) containing just the script and reference it in the page layout.
- For theme-level integration: add the script to the relevant .phtml template file under app/design/frontend/[Vendor]/[theme]/Magento_Cms/templates/ or the specific product/category template.
- Alternatively, install a schema markup extension from the Adobe Commerce Marketplace (search 'FAQ schema' or 'structured data') for a no-code approach.
- Flush the cache (System → Cache Management → Flush Magento Cache) after saving, then validate with Google's Rich Results Test.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Frequently Asked Questions | Your Store</title>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "What is your return policy?",
"acceptedAnswer": {
"@type": "Answer",
"text": "We accept returns within 30 days of purchase. Items must be unused and in their original packaging."
}
},
{
"@type": "Question",
"name": "How long does shipping take?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Standard shipping takes 5–7 business days. Expedited options are available at checkout."
}
}
]
}
</script>
</head>
<body>
<h1>Frequently Asked Questions</h1>
<h2>What is your return policy?</h2>
<p>We accept returns within 30 days of purchase. Items must be unused and in their original packaging.</p>
<h2>How long does shipping take?</h2>
<p>Standard shipping takes 5–7 business days. Expedited options are available at checkout.</p>
</body>
</html>Full HTML page example showing exactly where to place the FAQPage script block inside the <head>.
Missing schema howtoModerate effort
Add HowTo structured data (JSON-LD) to any page that walks customers through a multi-step process, so Google can display your instructions as rich results in search.
On Adobe Commerce (Magento)
- Identify the CMS Page or CMS Block that contains the how-to content (Content > Pages or Content > Blocks in the admin).
- Edit the page, switch the content editor to HTML mode, and paste your <script type='application/ld+json'> HowTo block at the bottom of the HTML.
- For a more scalable solution, create a custom layout XML file (e.g. cms_page_view_id_your-page-identifier.xml) inside your theme at app/design/frontend/<Vendor>/<theme>/Magento_Cms/layout/ and add a block of type Magento\Framework\View\Element\Template that renders a .phtml file containing the JSON-LD.
- Flush the Magento cache (System > Cache Management > Flush Magento Cache) and validate the live URL with Google's Rich Results Test.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>How to Complete Your Task | Your Store</title>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "HowTo",
"name": "How to Complete Your Task",
"description": "Follow these steps to complete your task easily.",
"totalTime": "PT10M",
"step": [
{
"@type": "HowToStep",
"position": 1,
"name": "Prepare your materials",
"text": "Gather everything you need before you start: list materials here."
},
{
"@type": "HowToStep",
"position": 2,
"name": "Complete the main action",
"text": "Describe exactly what the user should do in this core step."
},
{
"@type": "HowToStep",
"position": 3,
"name": "Verify the result",
"text": "Explain how the user confirms the task was completed successfully."
}
]
}
</script>
</head>
<body>
<!-- Your how-to page content -->
</body>
</html>Place this block inside the <head> element of any static HTML page that contains step-by-step instructions.
Missing schema localbusinessQuick win
Add LocalBusiness JSON-LD structured data to your store so search engines and AI answer engines can surface your business name, address, phone number, and hours in rich results and direct answers.
On Adobe Commerce (Magento)
- SSH into your server (or use your hosting file manager) and navigate to your active theme directory, typically app/design/frontend/<Vendor>/<theme>/Magento_Theme/layout/.
- Open or create the file 'default_head_blocks.xml'.
- Add a block that renders a .phtml template containing your JSON-LD script tag, or directly inject the script via a CMS block.
- Alternatively, go to Admin → Content → Blocks → Add New Block. Paste your JSON-LD script there, then insert the block into your homepage or store's root template via Admin → Content → Pages → Homepage → Design → Layout Update XML using the referenceContainer method.
- Flush the Magento cache (System → Cache Management → Flush Cache Storage) and validate with Google's Rich Results Test.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Your Store</title>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Store",
"name": "Your Business Name",
"url": "https://www.yourdomain.com",
"logo": "https://www.yourdomain.com/logo.png",
"address": {
"@type": "PostalAddress",
"streetAddress": "123 Main Street",
"addressLocality": "Springfield",
"addressRegion": "IL",
"postalCode": "62701",
"addressCountry": "US"
},
"telephone": "+1-217-555-0100",
"openingHoursSpecification": [
{
"@type": "OpeningHoursSpecification",
"dayOfWeek": ["Monday","Tuesday","Wednesday","Thursday","Friday"],
"opens": "09:00",
"closes": "18:00"
}
]
}
</script>
</head>
<body>
<!-- page content -->
</body>
</html>Paste this block anywhere inside the <head> element of your HTML page. Replace all placeholder values before deploying.
Missing schema organizationQuick win
Add an Organization schema markup block to your store's homepage (and/or sitewide template) so search engines and AI assistants can definitively identify your brand.
On Adobe Commerce (Magento)
- SSH into your server and navigate to your active theme directory, typically app/design/frontend/<Vendor>/<theme>/Magento_Theme/layout/.
- Open or create the file default_head_blocks.xml. Add a block that references a new PHTML template file.
- Create the PHTML template (e.g., app/design/frontend/<Vendor>/<theme>/Magento_Theme/templates/organization_schema.phtml) and paste your <script type='application/ld+json'>…</script> Organization block inside it.
- Alternatively, use a third-party extension such as 'MageWorx SEO Suite' or 'Amasty SEO Toolkit', which provide an admin UI for Organization schema — go to Stores → Configuration → MageWorx/Amasty → SEO → Structured Data → Organization.
- Run php bin/magento cache:flush after making template changes.
- Verify using the Rich Results Test on your homepage URL.
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "Acme Store",
"url": "https://www.acmestore.com",
"logo": {
"@type": "ImageObject",
"url": "https://www.acmestore.com/images/logo.png",
"width": 250,
"height": 250
},
"sameAs": [
"https://www.facebook.com/acmestore",
"https://www.instagram.com/acmestore",
"https://twitter.com/acmestore",
"https://www.linkedin.com/company/acmestore",
"https://www.youtube.com/c/acmestore"
],
"contactPoint": {
"@type": "ContactPoint",
"contactType": "customer service",
"email": "hello@acmestore.com"
}
}
</script>Drop this complete block inside your <head>…</head> section on every page. Replace all placeholder values.
Missing schema productModerate effort
Add Product schema markup (JSON-LD) to every product page so search engines can display rich results — including price, availability, and ratings — directly in search.
On Adobe Commerce (Magento)
- In your Magento admin, navigate to Content → Design → Configuration → select your store view → Edit.
- For a code-based approach, create or edit the product view template at 'app/design/frontend/<Vendor>/<Theme>/Magento_Catalog/templates/product/view.phtml' in your custom theme.
- Add the JSON-LD block at the top of that template using PHP/Magento variables: $product->getName(), $product->getFinalPrice(), $product->getSku(), and $block->getImage($product,'product_base_image')->getImageUrl().
- Alternatively, install the free 'MageArray Product Schema Markup' extension from the Magento Marketplace for an admin-driven solution.
- Clear the Magento cache (System → Cache Management → Flush Magento Cache), then validate a product page with Google's Rich Results Test.
Missing schema webpageModerate effort
Add WebPage schema markup to every page so search engines and AI answer engines can precisely understand and describe your content.
On Adobe Commerce (Magento)
- The cleanest approach is to create or edit a layout XML file for the relevant page type, e.g., cms_index_index.xml for the homepage, in your custom theme under app/design/frontend/<Vendor>/<theme>/Magento_Cms/layout/.
- Add a block of type 'Magento\Framework\View\Element\Template' referencing a .phtml template file that outputs your JSON-LD <script> block, and set the block's container to 'head.additional'.
- In the .phtml template, use Magento's escaping helpers and store/page data to dynamically populate name, description, and url fields.
- Alternatively, install a structured data extension from the Magento Marketplace (e.g., 'Schema.org Structured Data' by various vendors) which handles WebPage and other schema types automatically across all page types.
- After deploying (and flushing the full-page cache: System → Cache Management → Flush Magento Cache), validate with the Rich Results Test.
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebPage",
"name": "About Us | Acme Store",
"description": "Learn about Acme Store, our mission, and the team behind our products.",
"url": "https://www.example.com/about/",
"inLanguage": "en-US",
"datePublished": "2022-01-15",
"dateModified": "2024-06-01",
"isPartOf": {
"@type": "WebSite",
"name": "Acme Store",
"url": "https://www.example.com/"
},
"breadcrumb": {
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Home",
"item": "https://www.example.com/"
},
{
"@type": "ListItem",
"position": 2,
"name": "About Us",
"item": "https://www.example.com/about/"
}
]
}
}
</script>Paste this inside the <head> element of any HTML page. Replace all placeholder values with real page data.
Non iso dateQuick win
Change every datePublished (and dateModified) value in your structured data from a human-readable string to a strict ISO-8601 date — formatted as YYYY-MM-DD or YYYY-MM-DDThh:mm:ss+hh:mm — so search engines can reliably parse your content's freshness.
On Adobe Commerce (Magento)
- Schema in Adobe Commerce is typically added via a custom module or a third-party extension (e.g., MageWorx SEO Suite, Amasty SEO Toolkit).
- If using an extension, go to Stores → Configuration → the extension's settings panel and look for a 'Schema / Structured Data' section — update date format to ISO-8601 if the option exists.
- For custom module output, locate the Block or ViewModel PHP class that builds the JSON-LD array, typically in app/code/Vendor/Module/Block/ or similar.
- Replace any date formatting call that produces a human-readable string with PHP's ISO-8601 format: date('c', strtotime($dateString)) or (new \DateTime($dateString))->format(\DateTime::ATOM).
- Flush the Magento cache: System → Cache Management → Flush Cache Storage.
- Validate with Google's Rich Results Test on the affected page type.
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "Your Article Headline",
"datePublished": "2025-03-12",
"dateModified": "2025-03-12",
"author": {
"@type": "Person",
"name": "Author Name"
}
}
</script>Minimum viable JSON-LD block to paste into any HTML <head> — replace values with your own.
Org missing sameasQuick win
Add a `sameAs` array to your Organization schema, linking to your verified social profiles, Wikipedia page, LinkedIn, and other authoritative sources so search engines can confidently identify your brand as a known entity.
On Adobe Commerce (Magento)
- SSH into your server or use your file manager. Navigate to your active theme directory: app/design/frontend/<Vendor>/<theme>/Magento_Theme/layout/.
- Open or create default_head_blocks.xml. Add a block that references a new PHTML template file, or alternatively edit app/design/frontend/<Vendor>/<theme>/Magento_Theme/templates/root.phtml to insert the JSON-LD before </head>.
- Create the PHTML file containing your Organization JSON-LD with sameAs (see generic HTML snippet below, wrapped in <?php echo '...'; ?> or output directly as HTML).
- Run bin/magento cache:flush and bin/magento setup:static-content:deploy.
- Validate the live page with Google's Rich Results Test.
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "Your Brand Name",
"url": "https://www.yourdomain.com",
"logo": "https://www.yourdomain.com/images/logo.png",
"sameAs": [
"https://www.linkedin.com/company/your-company",
"https://www.facebook.com/yourpage",
"https://twitter.com/yourhandle",
"https://www.instagram.com/yourhandle",
"https://www.youtube.com/@yourchannel",
"https://en.wikipedia.org/wiki/Your_Brand",
"https://www.wikidata.org/wiki/QXXXXXXX"
]
}
</script>Paste this directly before the closing </head> tag in your HTML template. Remove any sameAs entries for platforms your brand does not have a presence on.
Page unreachableModerate effort
Ensure every page that should appear in AI-powered answers and rich results is actually reachable by crawlers, then add structured data so search engines and AI systems can read and surface its content.
On Adobe Commerce (Magento)
- In the Admin panel, go to Catalog → Products (or CMS → Pages) and confirm the page/product is set to 'Enabled' and 'Visibility' is appropriate (e.g., 'Catalog, Search' for products).
- Check Stores → Configuration → General → Web → Search Engine Optimization — ensure 'Use Web Server Rewrites' is set to 'Yes' to avoid broken URLs.
- For 404 errors, go to Marketing → SEO & Search → URL Rewrites → Add URL Rewrite to create a custom 301 redirect from the broken path to the correct target.
- Check that important pages are not blocked in your robots.txt (Stores → Configuration → General → Design → HTML Head → robots field or the actual robots.txt file on the server root). Remove any Disallow rules for public pages.
- For structured data, Adobe Commerce includes basic Product schema. To extend it (e.g., add FAQPage schema, aggregate ratings), install a Marketplace extension such as 'MageWorx SEO Suite' or edit the relevant .phtml template files in your theme (e.g., app/design/frontend/[Vendor]/[Theme]/Magento_Catalog/templates/product/view/) to inject a JSON-LD script block.
- Flush the cache (System → Cache Management → Flush Magento Cache), then submit the URL in Google Search Console and validate with the Rich Results Test.
Accessibility (WCAG) · 66 fixes
Aria allowed attrModerate effort
Audit every element that uses ARIA attributes and ensure each attribute is permitted for that element's role — removing or correcting any mismatched aria-* attributes.
On Adobe Commerce (Magento)
- Identify which module or theme template renders the flagged element using the browser inspector (right-click → Inspect).
- In your custom theme directory (app/design/frontend/<Vendor>/<theme>/), locate the .phtml template or layout XML file responsible — common locations are Magento_Theme/templates/, Magento_Catalog/templates/, or a third-party module's template override.
- Edit the template to remove or correct the mismatched aria-* attribute, then save.
- If the attribute is injected by a JavaScript UI component (app/code or vendor module's .js file), create an override of that JS file in your theme and correct the attribute assignment.
- Run bin/magento cache:flush, then test with axe DevTools in your browser to confirm the violation is resolved.
<!-- WRONG: aria-checked is not allowed on role="button" -->
<div role="button" aria-checked="true">Toggle</div>
<!-- CORRECT option A: use a role that supports aria-checked -->
<div role="checkbox" aria-checked="true" tabindex="0">Toggle</div>
<!-- CORRECT option B: remove the unsupported attribute and use a native element -->
<button type="button" aria-pressed="true">Toggle</button>
<!-- WRONG: aria-expanded is not allowed on a <li> with no interactive role -->
<li aria-expanded="true">Menu Item</li>
<!-- CORRECT: put aria-expanded on the button that controls the submenu -->
<li>
<button type="button" aria-expanded="true" aria-controls="submenu-1">Menu Item</button>
<ul id="submenu-1">
<li><a href="/page">Sub-page</a></li>
</ul>
</li>
<!-- WRONG: aria-required on a <div> with no form-field role -->
<div aria-required="true">Enter your email</div>
<!-- CORRECT: aria-required belongs on the actual input, or use the native required attribute -->
<label for="email">Email <span aria-hidden="true">*</span></label>
<input type="email" id="email" name="email" required aria-required="true" />Examples of invalid vs. corrected ARIA attribute usage for common patterns.
Aria allowed roleQuick win
Remove or replace any ARIA `role` attribute that is not permitted on the HTML element it is applied to — in particular, do not add `role="textbox"` (or other interactive roles) to elements that already have a native semantic role which conflicts with it.
On Adobe Commerce (Magento)
- Identify the PHTML template or `.html` Knockout template that renders the flagged element. Use browser DevTools to trace the markup to its source file.
- Locate the file in your custom module or theme under `app/design/frontend/<Vendor>/<theme>/` (or the relevant module's `view/frontend/templates/` directory).
- Open the file and apply the fix: remove the invalid `role` attribute, replace it with a permitted one, or change the HTML element to the correct native tag.
- Run `bin/magento cache:clean` and `bin/magento setup:static-content:deploy`, then re-audit with axe DevTools.
<!-- ❌ WRONG: role="textbox" is not permitted on <select> -->
<select role="textbox" name="country">
<option value="us">United States</option>
</select>
<!-- ✅ FIX A: Remove the invalid role — <select> already has the correct native semantics -->
<select name="country">
<option value="us">United States</option>
</select>
<!-- ❌ WRONG: role="textbox" on a <div> with no keyboard/input handling -->
<div role="textbox" class="fake-input">Enter your name</div>
<!-- ✅ FIX B: Replace with a real <input> element -->
<label for="name">Your name</label>
<input type="text" id="name" name="name" placeholder="Enter your name">
<!-- ❌ WRONG: role="textbox" on a <p> element -->
<p role="textbox">Some editable text</p>
<!-- ✅ FIX C: If you need an editable region, use contenteditable + the permitted role -->
<!-- role="textbox" IS permitted on elements with contenteditable, like a <div> -->
<div role="textbox"
contenteditable="true"
aria-multiline="false"
aria-label="Your name">
</div>Common patterns: wrong role on the left, correct fix on the right (as comments).
Aria command nameModerate effort
Give every button, link, and menuitem a clear, accessible name so screen readers can announce what it does.
On Adobe Commerce (Magento)
- SSH into your server or open your code editor and navigate to your active theme directory: `app/design/frontend/<Vendor>/<theme>/`.
- Identify the PHTML or XML layout file responsible for the flagged element — common locations: `Magento_Theme/templates/html/header.phtml`, `Magento_Checkout/templates/cart/minicart.phtml`, or `Magento_Theme/templates/html/topmenu.phtml`.
- Open the file and find the `<button>`, `<a>`, or element with an ARIA role that has no accessible text.
- Add `aria-label="<?= $escaper->escapeHtmlAttr(__('Descriptive action name')) ?>"` to the element (always use the escaper and translation helper for Magento templates).
- Alternatively, add a `<span class="sr-only">` (define the utility class in your theme's CSS) inside the element.
- Run `bin/magento cache:clean` and `bin/magento setup:static-content:deploy`, then re-test with axe.
<?php
/**
* Magento / Adobe Commerce: icon-only cart button with accessible name.
* File: app/design/frontend/<Vendor>/<Theme>/Magento_Checkout/templates/cart/minicart.phtml
*
* @var \Magento\Checkout\Block\Cart\Sidebar $block
* @var \Magento\Framework\Escaper $escaper
*/
?>
<a
class="action showcart"
href="<?= $escaper->escapeUrl($block->getUrl('checkout/cart')) ?>"
data-bind="scope: 'minicart_content'"
aria-label="<?= $escaper->escapeHtmlAttr(__('Shopping cart')) ?>"
>
<span class="text">
<span class="sr-only"><?= $escaper->escapeHtml(__('Shopping cart')) ?></span>
</span>
<span class="counter qty" aria-label="<?= $escaper->escapeHtmlAttr(__('Cart items count')) ?>">
<span class="counter-number" data-bind="text: getCartParam('summary_count')"></span>
</span>
</a>Edit the relevant .phtml template in your custom theme, e.g. app/design/frontend/Vendor/Theme/Magento_Theme/templates/html/header.phtml
Aria dialog nameModerate effort
Add a visible or programmatic accessible name to every dialog element so screen readers can announce what the dialog is about when it opens.
On Adobe Commerce (Magento)
- Locate the modal template in your theme: app/design/frontend/<Vendor>/<theme>/Magento_Ui/templates/modal/modal-popup.html (or modal-slide.html).
- Find the root element that has role="dialog" (or data-role="modal") and add aria-labelledby="{{ data.modalTitleId }}" if a title element already exists in the template with a matching id, which is the Magento modal component's built-in support.
- If the title element exists but lacks an id, add id="{{ data.modalTitleId }}" to the heading element inside the template.
- If no title is present, add aria-label="{{ data.title }}" to the root dialog element so the component's configured title string is used.
- Run bin/magento cache:flush and bin/magento setup:static-content:deploy, then verify with the axe DevTools browser extension.
<div
data-role="modal"
role="dialog"
aria-modal="true"
aria-labelledby="modal-title-<!-- ko text: data.modalTitleId --><!-- /ko -->"
class="modal-inner-wrap"
data-bind="attr: {'id': data.modalWindowId}, style: {zIndex: data.zIndex}">
<header class="modal-header">
<h1
class="modal-title"
data-bind="attr: {'id': data.modalTitleId}, html: title">
</h1>
<button
data-role="closeBtn"
type="button"
class="action-close"
data-bind="attr: {'aria-label': $t('Close')}">
<span data-bind="i18n: 'Close'"></span>
</button>
</header>
<div class="modal-content" data-bind="html: content"></div>
<footer class="modal-footer" data-bind="blockLoader: isLoading">
<!-- ko foreach: { data: buttons, as: 'button' } -->
<button
type="button"
class="action"
data-bind="
click: button.click.bind($parent),
attr: { 'class': 'action ' + button.class },
html: button.text">
</button>
<!-- /ko -->
</footer>
</div>Edit app/design/frontend/<Vendor>/<theme>/Magento_Ui/templates/modal/modal-popup.html. This is the Knockout JS template Magento uses for all UI modals.
Aria hidden focusModerate effort
Remove `aria-hidden="true"` from any element that contains focusable children (links, buttons, inputs), or remove those focusable children from the keyboard tab order so they cannot receive focus while hidden from assistive technology.
On Adobe Commerce (Magento)
- SSH into your server or use your IDE to locate the offending template. Run the axe scan in Chrome to identify the exact HTML element, then trace it to a .phtml template in your active theme (app/design/frontend/<Vendor>/<theme>/).
- Common sources in Magento: the off-canvas navigation (Magento_Theme/templates/html/header.phtml), the minicart drawer (Magento_Checkout/templates/cart/minicart.phtml), and layered navigation sidebar.
- Create overrides in your custom theme directory rather than editing core files: copy the relevant .phtml to app/design/frontend/<YourVendor>/<YourTheme>/ preserving the module path.
- In the template or its companion RequireJS module (web/js/), update the open/close toggle logic: closed state → aria-hidden='true' on container + tabindex='-1' on all focusable children; open state → remove aria-hidden + remove tabindex='-1'.
- Run bin/magento cache:clean && bin/magento cache:flush, then hard-refresh the storefront and re-run the axe scan to verify the fix.
Aria input field nameModerate effort
Add a descriptive accessible name (aria-label, aria-labelledby, or a visible <label>) to every ARIA input field so assistive technologies can identify it.
On Adobe Commerce (Magento)
- Identify the problematic template file. Common locations: app/design/frontend/<Vendor>/<Theme>/Magento_Checkout/templates/cart/item/default.phtml (quantity), Magento_Search/templates/form.mini.phtml (search), Magento_Customer/templates/form/login.phtml.
- Create an override in your custom theme by copying the file from vendor/magento/module-*/view/frontend/templates/ to app/design/frontend/<Vendor>/<Theme>/<Module>/templates/ preserving the same relative path.
- In the quantity input, add: aria-label="<?= $escaper->escapeHtmlAttr(__('Quantity for %1', $item->getName())) ?>"
- In the mini-search form (form.mini.phtml), ensure the <input type="search"> has: aria-label="<?= $escaper->escapeHtmlAttr(__('Search entire store here...')) ?>"
- Run bin/magento cache:flush after saving template changes.
- Verify with axe DevTools browser extension on the storefront.
<label for="search" class="sr-only">
<?= $escaper->escapeHtml(__('Search entire store here...')) ?>
</label>
<input
id="search"
type="search"
autocomplete="off"
name="q"
value="<?= $escaper->escapeHtmlAttr($helper->getEscapedQueryText()) ?>"
placeholder="<?= $escaper->escapeHtmlAttr(__('Search entire store here...')) ?>"
aria-label="<?= $escaper->escapeHtmlAttr(__('Search entire store here...')) ?>"
class="input-text"
maxlength="128"
>In app/design/frontend/<Vendor>/<Theme>/Magento_Search/templates/form.mini.phtml — search input with accessible label.
Aria meter nameQuick win
Add a descriptive accessible name (via aria-label, aria-labelledby, or a visible <label>) to every element with role="meter" so screen readers can announce what the meter represents.
On Adobe Commerce (Magento)
- Identify the PHTML or HTML UI Component template that renders the meter element — common locations: app/design/frontend/[Vendor]/[Theme]/Magento_Catalog/templates/product/view/type/ or a custom module template.
- Open the file in your IDE or via the admin Theme (if using a page builder).
- Add aria-label="[Descriptive label]" to the role="meter" element, or use aria-labelledby referencing an existing element's id.
- Ensure aria-valuenow, aria-valuemin, and aria-valuemax are set with the correct PHP/templating variable values.
- Run bin/magento cache:flush, then verify the rendered HTML on the frontend using the axe DevTools browser extension.
<!-- Option A: aria-label directly on the element -->
<meter
role="meter"
aria-label="Average customer rating"
aria-valuenow="3.8"
aria-valuemin="0"
aria-valuemax="5"
value="3.8"
min="0"
max="5">
3.8 out of 5
</meter>
<!-- Option B: aria-labelledby pointing to an existing visible element -->
<h3 id="rating-heading">Average customer rating</h3>
<meter
role="meter"
aria-labelledby="rating-heading"
aria-valuenow="3.8"
aria-valuemin="0"
aria-valuemax="5"
value="3.8"
min="0"
max="5">
3.8 out of 5
</meter>
<!-- Option C: visually hidden label for div-based meters -->
<span id="stock-label" class="visually-hidden">Items remaining in stock</span>
<div
role="meter"
aria-labelledby="stock-label"
aria-valuenow="4"
aria-valuemin="0"
aria-valuemax="20">
</div>
<!-- CSS for visually-hidden helper class -->
<style>
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
</style>Place this markup wherever a meter widget is rendered. Replace attribute values with real content.
Aria progressbar nameQuick win
Add an accessible name (aria-label or aria-labelledby) to every progress bar element so screen readers can announce its purpose.
On Adobe Commerce (Magento)
- Progress bars in Adobe Commerce appear in: checkout step progress (Magento_Checkout module), product review rating bars, and password-strength meters.
- For checkout step progress: the template is vendor/magento/module-checkout/view/frontend/templates/progress.phtml (or its override in app/design/frontend/<Vendor>/<Theme>/Magento_Checkout/templates/progress.phtml). Add aria-label='Checkout step progress' to the progress bar element in this file — always use a theme override rather than editing vendor files directly.
- For review/rating bars: override app/design/frontend/<Vendor>/<Theme>/Magento_Review/templates/review.phtml and add aria-label='Customer rating' to the rating bar element.
- For password-strength meters: override the relevant template in Magento_Customer (template: customer/widget/form/element/password.phtml) and add aria-label='Password strength' to the meter/progressbar element.
- Run bin/magento cache:clean and bin/magento setup:static-content:deploy after making template changes.
- Verify with the axe DevTools browser extension.
<?php
/**
* Checkout progress bar — accessible override template
* Path: app/design/frontend/<Vendor>/<Theme>/Magento_Checkout/templates/progress.phtml
*/
?>
<?php $step = $block->getStep(); ?>
<?php $totalSteps = $block->getTotalSteps() ?: 4; ?>
<div class="opc-progress-bar" aria-label="<?= $block->escapeHtmlAttr(__('Checkout step %1 of %2', $step, $totalSteps)) ?>">
<ol class="opc-progress-bar-items">
<li class="opc-progress-bar-item <?= $step >= 1 ? '_complete' : '' ?>">
<span><?= $block->escapeHtml(__('Shipping')) ?></span>
</li>
<li class="opc-progress-bar-item <?= $step >= 2 ? '_complete' : '' ?>">
<span><?= $block->escapeHtml(__('Review & Payments')) ?></span>
</li>
</ol>
</div>Place this file at app/design/frontend/<Vendor>/<Theme>/Magento_Checkout/templates/progress.phtml to override the core checkout progress template.
Aria prohibited attrModerate effort
Remove or replace any ARIA attributes that are prohibited for the role assigned to each HTML element, so assistive technologies receive accurate, unambiguous information.
On Adobe Commerce (Magento)
- Identify the PHTML or HTML template responsible for the flagged element — use bin/magento dev:template-hints:enable in staging to see template paths on the frontend.
- Locate the file under app/design/frontend/<Vendor>/<Theme>/ (custom theme) or vendor/magento/module-*/view/frontend/templates/ (core, copy to theme before editing).
- Open the file and search for the prohibited ARIA attribute; correct or remove it.
- Run bin/magento cache:flush after saving.
- Validate on the frontend with the axe browser extension.
<!-- ❌ WRONG: aria-checked is prohibited on role='heading' -->
<div role="heading" aria-level="2" aria-checked="false">Product Title</div>
<!-- ✅ CORRECT: remove the prohibited aria-checked attribute -->
<div role="heading" aria-level="2">Product Title</div>
<!-- ❌ WRONG: aria-label is prohibited on role='none' / role='presentation' -->
<div role="none" aria-label="Decorative banner"></div>
<!-- ✅ CORRECT: role='none' means purely decorative — remove all ARIA attributes -->
<div role="none"></div>
<!-- ❌ WRONG: aria-expanded is prohibited on a static <p> (implicit role='paragraph') -->
<p aria-expanded="true">Read more about our returns policy.</p>
<!-- ✅ CORRECT: move aria-expanded to the interactive control that actually toggles content -->
<button aria-expanded="true" aria-controls="returns-detail">Read more about our returns policy</button>
<div id="returns-detail">Full returns policy text here.</div>
<!-- ❌ WRONG: aria-required is prohibited on a <div role='alert'> -->
<div role="alert" aria-required="true">Please fill in all fields.</div>
<!-- ✅ CORRECT: remove aria-required; it belongs only on form inputs -->
<div role="alert">Please fill in all fields.</div>Examples of prohibited-attribute violations and their correct fixes.
Aria required attrModerate effort
Add all required ARIA attributes (such as aria-expanded and aria-controls) to every interactive element that uses an ARIA role, so assistive technologies can communicate widget state to users.
On Adobe Commerce (Magento)
- Identify the PHTML template or UI Component responsible for the flagged element — common locations are Magento_Theme/templates/html/header.phtml, Magento_Catalog/templates/navigation/top.phtml, or a third-party module's template.
- Open the file via app/design/frontend/[Vendor]/[theme]/[Module_Name]/templates/ (always work in a custom theme, never core files).
- Add aria-expanded='false' and aria-controls='target-element-id' to the trigger element in the template.
- Locate the corresponding requireJS module (usually lib/web/mage/menu.js or a custom module's JS) and in the toggle/activate method add: $(trigger).attr('aria-expanded', isOpen ? 'true' : 'false');
- Run bin/magento setup:static-content:deploy and flush the cache (bin/magento cache:flush), then test with the axe extension.
<!-- Trigger button (e.g. accordion header or dropdown toggle) -->
<button
id="section1-toggle"
aria-expanded="false"
aria-controls="section1-panel"
>
Product Details
</button>
<!-- Controlled panel -->
<div
id="section1-panel"
role="region"
aria-labelledby="section1-toggle"
hidden
>
<p>Panel content goes here.</p>
</div>
<script>
const toggle = document.getElementById('section1-toggle');
const panel = document.getElementById('section1-panel');
toggle.addEventListener('click', function () {
const isExpanded = this.getAttribute('aria-expanded') === 'true';
this.setAttribute('aria-expanded', String(!isExpanded));
panel.hidden = isExpanded;
});
</script>Place on any trigger element that opens/closes a collapsible section. Update aria-expanded via JS on each toggle.
Aria required childrenModerate effort
Add the required child ARIA roles (e.g. `role="option"` inside `role="listbox"`, or `role="group"`/`role="option"` inside `role="combobox"`) so every composite ARIA widget contains the child roles the ARIA specification demands.
On Adobe Commerce (Magento)
- Identify the PHTML or Knockout JS template rendering the broken widget. Common locations: app/design/frontend/<Vendor>/<theme>/Magento_Catalog/templates/ (product options), app/design/frontend/<Vendor>/<theme>/Magento_LayeredNavigation/templates/ (filters), app/code/<Vendor>/<Module>/view/frontend/templates/.
- Open the relevant .phtml or .html (Knockout) template file and locate the container element with the ARIA container role.
- Add the required child roles to the child elements in the template markup (e.g. role="option" and aria-selected="false" on each filter item <li>).
- Run bin/magento cache:clean and bin/magento setup:static-content:deploy after editing templates.
- If the widget is generated by a third-party module, create a template override in your custom theme rather than editing the module directly.
- Test on your staging environment with the axe browser extension and a screen reader before deploying to production.
Aria required parentModerate effort
Wrap every ARIA role that requires a parent container (e.g. `menuitem`, `option`, `listitem`) inside the correct parent element carrying the matching ARIA role (e.g. `menu`/`menubar`, `listbox`, `list`).
On Adobe Commerce (Magento)
- Locate the PHTML template or XML layout file responsible for the affected component — typically in 'app/design/frontend/<Vendor>/<Theme>/Magento_Theme/templates/' or a module's 'view/frontend/templates/' directory.
- Open the template file and add the required parent role wrapper element around the child-role elements (e.g. wrap `<li role='menuitem'>` items in `<ul role='menu'>`).
- Always make changes in a custom theme (inherit from Luma or Blank) so upgrades don't overwrite your fix.
- Run `bin/magento cache:flush` and `bin/magento setup:static-content:deploy` after editing, then verify with the axe DevTools browser extension.
<!-- INCORRECT: menuitem with no required parent -->
<nav>
<a href="/shop" role="menuitem">Shop</a>
<a href="/about" role="menuitem">About</a>
<a href="/contact" role="menuitem">Contact</a>
</nav>
<!-- CORRECT: menuitem children wrapped in role="menubar" -->
<nav>
<ul role="menubar">
<li role="none"><a href="/shop" role="menuitem">Shop</a></li>
<li role="none"><a href="/about" role="menuitem">About</a></li>
<li role="none"><a href="/contact" role="menuitem">Contact</a></li>
</ul>
</nav>
<!-- CORRECT: option children wrapped in role="listbox" -->
<div role="listbox" aria-label="Select a size">
<div role="option" aria-selected="false">Small</div>
<div role="option" aria-selected="true">Medium</div>
<div role="option" aria-selected="false">Large</div>
</div>
<!-- CORRECT: tab children wrapped in role="tablist" -->
<div role="tablist" aria-label="Product details">
<button role="tab" aria-selected="true" aria-controls="panel-desc">Description</button>
<button role="tab" aria-selected="false" aria-controls="panel-reviews">Reviews</button>
</div>
<div id="panel-desc" role="tabpanel">Description content here.</div>
<div id="panel-reviews" role="tabpanel" hidden>Reviews content here.</div>Correct pattern: child roles (menuitem) wrapped in the required parent role (menu/menubar). Apply the same principle to other role pairs.
Aria rolesQuick win
Audit every element that has a `role` attribute and replace any invalid or misspelled role value with a valid WAI-ARIA role from the official specification.
On Adobe Commerce (Magento)
- Identify the template file responsible for the element by inspecting it in the browser (right-click → Inspect) and noting the block/template hint (enable template hints in Stores → Configuration → Advanced → Developer → Debug → Template Path Hints).
- Locate the `.phtml` template file in your custom theme directory under `app/design/frontend/<Vendor>/<Theme>/`, or find the core template at `vendor/magento/` and copy it to your theme to override it.
- In the template file, find the invalid `role=` attribute and correct or remove it.
- Run `bin/magento cache:clean` and `bin/magento cache:flush` to clear caches.
- If using Page Builder content, edit the block/row in Admin → Content → Pages or Blocks, switch to the HTML editor view, correct the role attribute, and save.
- Re-run axe or WAVE on the storefront page to verify the fix.
<!-- INVALID: typo -->
<!-- <div role="buton">Add to Cart</div> -->
<!-- FIXED: correct spelling -->
<div role="button" tabindex="0">Add to Cart</div>
<!-- BETTER: use native HTML instead of ARIA role wherever possible -->
<button type="button">Add to Cart</button>
<!-- INVALID: invented role -->
<!-- <section role="product-card"> -->
<!-- FIXED: remove non-existent role; let native <section> semantics apply -->
<section aria-label="Product card">
<!-- INVALID: abstract/structural-only role used on a concrete element -->
<!-- <ul role="list widget"> -->
<!-- FIXED: 'list' is valid; 'widget' is abstract and not allowed -->
<ul role="list">
<!-- INVALID: deprecated presentational role used incorrectly -->
<!-- <img role="presentation" alt="Red sneaker"> -->
<!-- FIXED: use role="presentation" only for decorative images with empty alt -->
<img role="presentation" alt="">Replace invalid or misspelled role values with the correct WAI-ARIA role. Examples of common corrections:
Aria toggle field nameQuick win
Add a visible or programmatic accessible name to every toggle control (checkbox, switch, or button with role="switch") so screen readers can announce what the control does.
On Adobe Commerce (Magento)
- In your Adobe Commerce / Magento 2 installation, identify the unlabelled toggle using browser DevTools to find the template file responsible (module name + template path shown in template hints — enable via Stores → Configuration → Advanced → Developer → Debug → Enable Template Path Hints).
- Create or edit a template override in your custom theme at app/design/frontend/Vendor/ThemeName/[Module_Name]/templates/.
- In the template (.phtml file), locate the <input type="checkbox"> or role="switch" element and add a matching <label for="INPUT_ID">Descriptive text</label>, or wrap both in a <label>.
- For icon-only toggle buttons (e.g. mobile menu, wishlist), open the relevant .phtml file and add aria-label="Description" to the <button> element.
- Run bin/magento cache:clean and bin/magento setup:static-content:deploy after making template changes.
- Verify with axe DevTools on the frontend that the accessible name is now present.
<?php
/**
* Accessible checkbox toggle — Adobe Commerce / Magento 2 PHTML override.
* File location example:
* app/design/frontend/Vendor/ThemeName/Magento_Newsletter/templates/subscriber/form.phtml
*/
?>
<div class="field choice">
<input type="checkbox"
name="is_subscribed"
id="is_subscribed"
class="checkbox"
<?php if ($block->getIsSubscribed()): ?>checked="checked"<?php endif; ?>
/>
<label for="is_subscribed" class="label">
<span><?= $block->escapeHtml(__('Subscribe to newsletter')) ?></span>
</label>
</div>PHTML template override — copy the original template to your theme, then apply this accessible checkbox pattern.
Aria tooltip nameModerate effort
Give every tooltip element an accessible name so screen readers can announce its content to users who cannot see the screen.
On Adobe Commerce (Magento)
- SSH into your server (or use your hosting file manager) and locate the relevant .phtml template file — tooltip elements are commonly found in Magento_Ui/view/frontend/templates/ or your custom theme under app/design/frontend/<Vendor>/<Theme>/.
- Search for role="tooltip" across templates: grep -r 'role="tooltip"' app/design/ vendor/magento/.
- Edit the identified template to ensure each tooltip element has descriptive inner text, or add the aria-label attribute: <div role="tooltip" aria-label="Ships in 2–3 business days">...</div>.
- If tooltips are rendered by a UI component (Magento_Ui), override the component's .html knockout template in your custom theme directory and add the aria-label binding: attr: {'aria-label': tooltipText}.
- Run bin/magento cache:flush, clear static content (bin/magento setup:static-content:deploy), reload the page, and verify with the axe browser extension.
<!-- In your .phtml template or Knockout HTML template -->
<span class="field-tooltip toggle">
<span
class="field-tooltip-action action-help"
tabindex="0"
aria-describedby="tooltip-payment-method"
aria-label="Payment method help"
></span>
<div
id="tooltip-payment-method"
role="tooltip"
class="field-tooltip-content"
>
We accept Visa, Mastercard, and PayPal. All transactions are encrypted.
</div>
</span>Override the relevant .phtml or Knockout .html template in your custom theme. This is the corrected tooltip element markup.
Aria valid attrQuick win
Find and replace every misspelled or non-existent ARIA attribute name (e.g. `aria-labeledby`) with the correct, spec-defined name (e.g. `aria-labelledby`) across your entire site's HTML and templates.
On Adobe Commerce (Magento)
- Connect to your server via SSH or use your IDE with SFTP access to your Magento installation.
- Run a global search across your active theme directory (`app/design/frontend/<Vendor>/<theme>/`) for the misspelled attribute: `grep -r 'aria-labeledby' app/design/`
- Also search core overrides and any custom modules: `grep -r 'aria-labeledby' app/code/`
- Open each affected `.phtml` or `.html` (KnockoutJS template) file and correct the attribute name.
- If the issue is in a vendor module under `vendor/`, do not edit it directly — create a theme override or module override in `app/design/` or `app/code/` respectively.
- Run `bin/magento cache:flush` and `bin/magento setup:static-content:deploy` after making changes, then re-scan with axe DevTools on the live or staging environment.
<!-- WRONG: aria-labeledby is not a valid ARIA attribute (one 'l' is a typo) -->
<input type="search" aria-labeledby="search-heading">
<!-- CORRECT: aria-labelledby (double 'l') is the valid ARIA attribute -->
<input type="search" aria-labelledby="search-heading">
<h2 id="search-heading">Search our store</h2>
<!-- Other common invalid → valid corrections:
aria-role → role (not an ARIA attribute at all; 'role' is a plain HTML attribute)
aria-labeledby → aria-labelledby
aria-describeby → aria-describedby
aria-owns-id → aria-owns
-->Before/after comparison showing the most common misspelling corrected. Apply the same principle to any other invalid ARIA attribute name.
Aria valid attr valueModerate effort
Audit every ARIA attribute on your pages and correct any that have invalid or mismatched values so assistive technologies can interpret them correctly.
On Adobe Commerce (Magento)
- Identify which `.phtml` template or `.html` Knockout/RequireJS template renders the flagged element — use browser DevTools to inspect the element and note any identifying class or data attribute.
- Locate the file under `app/design/frontend/<Vendor>/<Theme>/` (custom theme) or the relevant module's `view/frontend/templates/` directory.
- Open the file and search for the ARIA attribute, then correct its value to the valid string or fix the `id` reference.
- If the issue is in a JavaScript component (e.g. a dropdown or modal using ui-component), find the relevant `.js` file and correct the attribute value set via `ko` bindings or DOM manipulation.
- Run `bin/magento cache:flush` and `bin/magento setup:static-content:deploy` after changes, then verify with the axe browser extension on the storefront.
<?php
/**
* Corrected collapsible block template.
* File: app/design/frontend/Vendor/Theme/Magento_Theme/templates/collapsible.phtml
*
* aria-expanded must be the string 'true' or 'false'.
* aria-controls must match the id of the controlled element.
*/
?>
<div class="collapsible-wrapper">
<button
class="collapsible-trigger"
aria-expanded="false"
aria-controls="collapsible-content-<?= $block->escapeHtmlAttr($block->getBlockId()) ?>"
>
<?= $block->escapeHtml($block->getTitle()) ?>
</button>
<div
id="collapsible-content-<?= $block->escapeHtmlAttr($block->getBlockId()) ?>"
class="collapsible-content"
hidden
>
<?= $block->getChildHtml('content') ?>
</div>
</div>Edit the relevant .phtml template in your custom theme at app/design/frontend/<Vendor>/<Theme>/. Example shows a corrected collapsible block.
Autocomplete validModerate effort
Add valid `autocomplete` attribute values to every form field that collects personal information, using the exact tokens specified by the HTML standard.
On Adobe Commerce (Magento)
- Identify the template file responsible for the form field. For checkout, the relevant file is typically `Magento_Checkout/web/template/form/element/input.html` or individual step templates under `Magento_Checkout/view/frontend/web/template/`.
- Create a template override in your custom theme: copy the file to `app/design/frontend/<Vendor>/<Theme>/Magento_Checkout/web/template/` (or the relevant module path) and edit it there — never edit core files directly.
- Locate the `<input>` element in the Knockout.js template and add the `autocomplete` binding: `autocomplete: 'given-name'` inside the `ko` bindings, or add it as a static HTML attribute.
- For customer account forms, override `Magento_Customer/view/frontend/templates/form/` templates (e.g. `edit.phtml`, `register.phtml`) in the same way and add `autocomplete="..."` to each `<input>` tag.
- Run `bin/magento setup:upgrade`, `bin/magento setup:static-content:deploy`, and flush the cache (`bin/magento cache:flush`) after making changes.
- Validate on the frontend with the axe browser extension.
<!-- Excerpt from overridden register.phtml — add autocomplete to each input -->
<input type="text"
name="firstname"
id="firstname"
value="<?= $block->escapeHtmlAttr($block->getFormData()->getFirstname()) ?>"
autocomplete="given-name"
class="input-text"
data-validate="{required:true}" />
<input type="text"
name="lastname"
id="lastname"
value="<?= $block->escapeHtmlAttr($block->getFormData()->getLastname()) ?>"
autocomplete="family-name"
class="input-text"
data-validate="{required:true}" />
<input type="email"
name="email"
id="email_address"
value="<?= $block->escapeHtmlAttr($block->getFormData()->getEmail()) ?>"
autocomplete="email"
class="input-text"
data-validate="{required:true, 'validate-email':true}" />
<input type="password"
name="password"
id="password"
autocomplete="new-password"
class="input-text"
data-validate="{required:true, 'validate-customer-password':true}" />Override Magento_Customer/view/frontend/templates/form/register.phtml in your theme and add autocomplete attributes as shown.
Avoid inline spacingModerate effort
Remove hard-coded text-spacing CSS values from inline `style` attributes so users can override them with their own stylesheets or browser settings.
On Adobe Commerce (Magento)
- SSH into your server (or use your IDE) and search your active theme's template files for inline spacing: `grep -r 'style=' app/design/frontend/YourVendor/YourTheme/`.
- Create or open your theme's `web/css/_module.less` (or `_extend.less`) and define a CSS class with the required spacing values.
- In the corresponding `.phtml` template file, replace the `style="..."` attribute with `class="your-class"` on the affected element.
- Run `bin/magento setup:static-content:deploy` and clear the cache with `bin/magento cache:clean` to ensure the new CSS is compiled and served.
- If a third-party extension's template is the source, copy the template into your custom theme directory (following Magento's theme override pattern) and make the change there — never edit vendor files directly.
- Verify with the axe browser extension on the front end.
<!-- BEFORE (fails WCAG 1.4.12 — inline style blocks user overrides) -->
<p style="line-height: 1.6; letter-spacing: 0.05em; word-spacing: 0.1em;">
Product description text here.
</p>
<!-- AFTER (passes — spacing is in a stylesheet class) -->
<p class="product-description">
Product description text here.
</p>
<!-- In your <head> or external .css file -->
<style>
.product-description {
line-height: 1.6;
letter-spacing: 0.05em;
word-spacing: 0.1em;
}
</style>Before: inline style locks spacing. After: class-based style allows user overrides.
Button nameModerate effort
Add a descriptive, screen-reader-accessible name to every button on your site so assistive technology users know what each button does.
On Adobe Commerce (Magento)
- Identify the offending button in your theme's template files. In your custom theme directory (app/design/frontend/Vendor/ThemeName/), locate the relevant .phtml template — e.g. Magento_Catalog/templates/product/view/addtocart.phtml for Add to Cart, or Magento_Theme/templates/html/header.phtml for header icons.
- Open the template file and add aria-label attributes to any icon-only <button> elements. Example: <button type="button" class="action search" aria-label="Search">.
- For buttons rendered by JavaScript UI components (e.g. KnockoutJS-bound elements), locate the corresponding HTML template in the module's web/template/ directory and add the aria-label there.
- After editing, run bin/magento cache:clean and bin/magento setup:static-content:deploy to apply changes.
- For third-party module buttons you cannot edit directly, create a layout XML override or a plugin/observer to modify the block's template output.
- Test on your staging environment with a screen reader and the axe DevTools extension before deploying to production.
<?php
/**
* Example: Accessible search button
* File: app/design/frontend/Vendor/ThemeName/Magento_Search/templates/form.mini.phtml
*/
?>
<form class="form minisearch" id="search_mini_form" action="<?= $escaper->escapeUrl($helper->getResultUrl()) ?>" method="get">
<label class="label" for="search" data-role="minisearch-label">
<span><?= $escaper->escapeHtml(__('Search')) ?></span>
</label>
<div class="control">
<input id="search"
type="search"
name="q"
aria-label="<?= $escaper->escapeHtmlAttr(__('Search entire store here')) ?>"
class="input-text"
placeholder="<?= $escaper->escapeHtmlAttr(__('Search entire store here')) ?>"
/>
</div>
<div class="actions">
<button type="submit"
title="<?= $escaper->escapeHtmlAttr(__('Search')) ?>"
aria-label="<?= $escaper->escapeHtmlAttr(__('Search')) ?>"
class="action search">
<span><?= $escaper->escapeHtml(__('Search')) ?></span>
</button>
</div>
</form>Edit the relevant .phtml template in your custom theme at app/design/frontend/Vendor/ThemeName/. Run bin/magento cache:clean after saving.
Color contrastQuick win
Increase the contrast ratio between your text color and its background color to meet the WCAG 4.5:1 minimum, so all users — including those with low vision — can read your content clearly.
On Adobe Commerce (Magento)
- Identify the failing CSS rule: use browser DevTools → Inspect the element → find the CSS file and rule responsible for the color.
- In your custom theme directory (app/design/frontend/<Vendor>/<theme>/), locate or create web/css/source/_overrides.less (or .css).
- Add a rule overriding the failing color to an accessible value (see snippet below).
- Run 'php bin/magento setup:static-content:deploy' and clear cache ('php bin/magento cache:clean').
- Verify the change in the browser and re-test contrast.
/* Magento: override insufficient contrast on secondary/muted text */
.secondary,
.price-label,
.product-info-stock-sku {
color: #595959; // was #777777 — now 7.0:1 on white
}Add to app/design/frontend/<Vendor>/<theme>/web/css/source/_overrides.less or a .css file in the same directory.
Definition listModerate effort
Ensure every `<dl>` (definition list) element contains only properly nested `<dt>` and `<dd>` children, with no stray text, divs, or other elements placed directly inside it.
On Adobe Commerce (Magento)
- SSH into your server and locate the relevant PHTML template — common files: app/design/frontend/<Vendor>/<Theme>/Magento_Catalog/templates/product/view/attributes.phtml (product attributes) and Magento_Sales/templates/order/items.phtml.
- If no override exists in your theme, copy the core template into your custom theme directory first (never edit vendor/ files).
- Open the PHTML file and correct the `<dl>` HTML structure so only `<dt>`, `<dd>`, or `<div>` wrappers are direct children.
- Run `bin/magento cache:flush` after saving, then verify the fix with the axe browser extension on the storefront.
<?php
/**
* Correctly structured definition list for Magento product attributes.
* Theme override: Magento_Catalog/templates/product/view/attributes.phtml
*/
?>
<?php foreach ($block->getAdditionalData() as $data): ?>
<?php if (!empty($data['value'])): ?>
<dl class="product attribute <?= $block->escapeHtmlAttr($data['code']) ?>">
<div>
<dt class="type"><?= $block->escapeHtml($data['label']) ?></dt>
<dd class="value"><?= $block->escapeHtml($data['value']) ?></dd>
</div>
</dl>
<?php endif; ?>
<?php endforeach; ?>Place in your custom theme at app/design/frontend/<Vendor>/<Theme>/Magento_Catalog/templates/product/view/attributes.phtml
DlitemModerate effort
Wrap every `<dt>` (term) and `<dd>` (description) element inside a parent `<dl>` element so description list items have their required container.
On Adobe Commerce (Magento)
- Identify which template file renders the offending `<dt>`/`<dd>` elements. Use Magento's template hints (Stores → Configuration → Advanced → Developer → Debug → Enable Template Path Hints) to find the exact .phtml file.
- Navigate to the file in your custom theme under app/design/frontend/[Vendor]/[theme]/. If the file only exists in a module's view/frontend/templates/, copy it into your theme's override path first.
- Open the .phtml file, locate the `<dt>` or `<dd>` tags, and add the appropriate `<dl>` … `</dl>` wrapper.
- Common files to check: Magento_Catalog/templates/product/view/attributes.phtml (product attributes tab), and any custom block templates for FAQs or specifications.
- After saving, run `bin/magento cache:flush` and `bin/magento setup:static-content:deploy` if needed, then verify in browser DevTools.
<?php
/**
* Custom theme override:
* Magento_Catalog/templates/product/view/attributes.phtml
* Renders additional product attributes as a correct <dl> structure.
*/
/** @var \Magento\Catalog\Block\Product\View\Attributes $block */
$_helper = $this->helper(\Magento\Catalog\Helper\Output::class);
$_product = $block->getProduct();
$_attributes = $block->getAdditionalData();
?>
<?php if ($_attributes): ?>
<div class="product-attributes">
<dl class="product-attributes__list">
<?php foreach ($_attributes as $_attribute): ?>
<div class="product-attributes__item">
<dt class="product-attributes__label">
<?php echo $block->escapeHtml(__($_attribute['label'])) ?>
</dt>
<dd class="product-attributes__value">
<?php echo $_helper->productAttribute($_product, $_attribute['value'], $_attribute['code']) ?>
</dd>
</div>
<?php endforeach; ?>
</dl>
</div>
<?php endif; ?>Override in your custom theme at app/design/frontend/[Vendor]/[Theme]/Magento_Catalog/templates/product/view/attributes.phtml
Document titleQuick win
Add a descriptive, non-empty `<title>` element to every page so browsers, screen readers, and search engines can identify it.
On Adobe Commerce (Magento)
- For the default title suffix: Admin → Stores → Configuration → General → Design → HTML Head → 'Default Title' and 'Title Prefix/Suffix' fields.
- For CMS pages: Content → Pages → select a page → 'Search Engine Optimisation' tab → 'Meta Title' field.
- For product pages: Catalog → Products → select a product → 'Search Engine Optimization' tab → 'Meta Title' field.
- For category pages: Catalog → Categories → select a category → 'Search Engine Optimization' section → 'Meta Title' field.
- To fix at the layout level: in your custom theme, edit `Magento_Theme/layout/default_head_blocks.xml` and confirm the `<title>` block is properly included.
- Clear the full-page cache after saving: System → Cache Management → Flush Magento Cache.
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<head>
<!-- Magento renders <title> via the page/html_head block using the meta_title set per entity -->
<!-- Ensure this block is NOT removed in your theme's layout XML -->
<block class="Magento\Theme\Block\Html\Head" name="head" as="head"
template="Magento_Theme::html/head.phtml"/>
</head>
</page>Place in app/design/frontend/<Vendor>/<Theme>/Magento_Theme/layout/default_head_blocks.xml. The title variable is injected by Magento's page/html_head block — this confirms it is included in your custom theme.
Empty headingQuick win
Find every empty heading tag on your store and add visible, descriptive text (or remove the tag entirely) so screen readers and search engines can interpret your page structure.
On Adobe Commerce (Magento)
- Connect to your server via SSH or SFTP and navigate to your active theme directory: `app/design/frontend/<Vendor>/<Theme>/`.
- Search template `.phtml` files for empty heading tags: `grep -rn '<h[1-6][^>]*></h[1-6]>' .`
- Open the relevant `.phtml` file and add a text string or a PHP variable (e.g. `<h2><?= $block->escapeHtml($block->getTitle()) ?></h2>`).
- Wrap dynamic headings in a conditional to prevent rendering when the value is empty: `<?php if ($block->getTitle()): ?><h2><?= $block->escapeHtml($block->getTitle()) ?></h2><?php endif; ?>`
- For CMS Pages/Blocks (Admin → Content → Pages or Blocks): open the page, switch to HTML source mode in the WYSIWYG editor, locate empty `<h*></h*>` tags, and add text or remove them.
- Clear the Magento cache (System → Cache Management → Flush Magento Cache) and verify with the axe DevTools browser extension.
<?php
/**
* Render the section heading only when a title value exists.
* Adjust getTitle() to match the actual method for this block.
*/
if ($block->getTitle()): ?>
<h2 class="block-title">
<?= $block->escapeHtml($block->getTitle()) ?>
</h2>
<?php endif; ?>Inside a .phtml template file — guards against rendering an empty heading when the block title is not set.
Empty table headerQuick win
Add visible or screen-reader-accessible text to every empty `<th>` table header cell so assistive technology users can understand what each column or row represents.
On Adobe Commerce (Magento)
- SSH into your server (or use your hosting file manager) and navigate to your active theme directory: app/design/frontend/<Vendor>/<theme>/.
- Search for PHTML template files that render tables: grep -r '<th' . --include='*.phtml' to find all files with table header markup.
- Open the relevant .phtml file (e.g., Magento_Catalog/templates/product/list.phtml or a comparison template) and add text or `aria-label` to any empty `<th>` elements.
- If using a third-party theme or module, create a template override in your custom theme rather than editing vendor files directly.
- Run bin/magento cache:flush after saving, then verify with axe DevTools.
<?php
/**
* Accessible product comparison table template override.
* Place at: app/design/frontend/<Vendor>/<Theme>/Magento_Catalog/templates/product/compare/list.phtml
*
* Key fix: ensure every <th> has either visible text or an aria-label.
*/
?>
<table class="table-comparison" id="product-comparison">
<caption class="sr-only"><?= $block->escapeHtml(__('Product Comparison')) ?></caption>
<thead>
<tr>
<!-- This corner cell previously rendered empty — now has an aria-label -->
<th class="cell label remove" aria-label="<?= $block->escapeHtml(__('Remove product from comparison')) ?>">
<span class="sr-only"><?= $block->escapeHtml(__('Actions')) ?></span>
</th>
<?php foreach ($block->getItems() as $item): ?>
<th class="cell product" scope="col">
<?= $block->escapeHtml($item->getName()) ?>
</th>
<?php endforeach; ?>
</tr>
</thead>
<tbody>
<?php foreach ($block->getAttributes() as $attribute): ?>
<tr>
<th class="cell label" scope="row">
<?= $block->escapeHtml($attribute->getStoreLabel()) ?>
</th>
<?php foreach ($block->getItems() as $item): ?>
<td class="cell product">
<?= $block->getProductAttributeValue($item, $attribute) ?>
</td>
<?php endforeach; ?>
</tr>
<?php endforeach; ?>
</tbody>
</table>Override the comparison list PHTML template in your custom theme at: app/design/frontend/<Vendor>/<Theme>/Magento_Catalog/templates/product/list/items.phtml
Error not associatedModerate effort
Add `aria-invalid="true"` and a linked `aria-describedby` error message to every form field that fails validation, so screen reader users are told exactly what went wrong.
On Adobe Commerce (Magento)
- Connect to your server via SSH or use your IDE to navigate to the active theme directory: app/design/frontend/VENDOR/THEME/.
- Locate the relevant form template, e.g. Magento_Customer/templates/form/register.phtml for registration, Magento_Checkout/templates/ for checkout forms, or Magento_Contact/templates/form.phtml for contact.
- In each .phtml template, find the <input> elements and add aria-describedby="FIELDNAME-error" as an attribute, then add a sibling <span id="FIELDNAME-error" class="mage-error" role="alert" style="display:none"></span>.
- Magento uses the mage/validation jQuery widget for client-side validation. In your theme's requirejs-config.js or a custom JS mixin, extend the showLabel and highlight methods of the validation widget to additionally set aria-invalid='true' on the field and show/hide the error span.
- Run bin/magento setup:static-content:deploy and bin/magento cache:flush after changes.
- Test on /checkout, /customer/account/create, and /contact using a screen reader.
<?php
/** @var \Magento\Customer\Block\Form\Register $block */
?>
<div class="field email required">
<label class="label" for="email_address">
<span><?= $block->escapeHtml(__('Email')) ?></span>
</label>
<div class="control">
<input
type="email"
name="email"
id="email_address"
autocomplete="email"
class="input-text"
title="<?= $block->escapeHtmlAttr(__('Email')) ?>"
data-validate="{required:true, 'validate-email':true}"
aria-required="true"
aria-describedby="email_address-error"
/>
<span
id="email_address-error"
class="mage-error"
role="alert"
style="display:none"
generated="true"
for="email_address"
></span>
</div>
</div>Replace the email input block in app/design/frontend/VENDOR/THEME/Magento_Customer/templates/form/register.phtml with this accessible version.
Focus not visibleQuick win
Add a clearly visible focus indicator to every interactive element so keyboard users can always see which element is currently selected.
On Adobe Commerce (Magento)
- Locate your active theme directory: app/design/frontend/<Vendor>/<theme>/web/css/.
- Open _module.less / _extend.less or the main source/_theme.less file. Search for 'outline'.
- Remove any `outline: none` declarations on interactive selectors, then add the universal `:focus` / `:focus-visible` rule from the snippet below.
- Run `bin/magento setup:static-content:deploy` and clear the cache (`bin/magento cache:flush`).
- Tab through storefront, product, cart, and checkout pages to confirm focus is visible.
/* Visible focus indicator fix — WCAG 2.4.7 */
*:focus-visible {
outline: 3px solid #005FCC;
outline-offset: 2px;
border-radius: 2px;
}
*:focus {
outline: 3px solid #005FCC;
outline-offset: 2px;
}
*:focus:not(:focus-visible) {
outline: none;
}Add to app/design/frontend/<Vendor>/<theme>/web/css/_extend.less or _theme.less. Run static content deploy after saving.
Frame titleQuick win
Add a descriptive `title` attribute to every `<iframe>` and `<frame>` element on your store so screen-reader users know what each embedded frame contains before entering it.
On Adobe Commerce (Magento)
- Locate iframe tags in your theme's template (.phtml) files under app/design/frontend/<Vendor>/<Theme>/. Use a code search (grep -r '<iframe' app/design/) to find all instances.
- Add title="…" to each iframe in the relevant .phtml template files and deploy the updated theme.
- For iframes injected by third-party Magento extensions, check the extension's template files under vendor/ or app/code/, then override the template in your custom theme rather than editing vendor files directly.
- For iframes added via CMS pages or blocks (Content → Pages or Content → Blocks in the Admin panel), edit the HTML of the block/page using the CMS editor's HTML source view and add the title attribute.
- Run bin/magento cache:clean and bin/magento setup:static-content:deploy, then verify with axe DevTools.
<!-- Good: descriptive title -->
<iframe
src="https://www.google.com/maps/embed?pb=..."
title="Google Map showing our store location at 123 Main St"
width="600"
height="450"
loading="lazy"
referrerpolicy="no-referrer-when-downgrade"
></iframe>
<!-- Good: product video embed -->
<iframe
src="https://www.youtube.com/embed/VIDEO_ID"
title="Product demo video: how to use the XYZ Widget"
width="560"
height="315"
allowfullscreen
></iframe>
<!-- Good: decorative / empty frame — hide from assistive tech -->
<iframe
src="blank.html"
title=""
aria-hidden="true"
tabindex="-1"
></iframe>Add a descriptive title attribute directly to any iframe element.
Heading orderModerate effort
Restructure your page headings so they follow a logical, sequential order (H1 → H2 → H3) with no levels skipped.
On Adobe Commerce (Magento)
- Identify the PHTML template responsible for the heading in question — common files include app/design/frontend/<Vendor>/<theme>/Magento_Catalog/templates/product/view.phtml (product page title) and app/design/frontend/<Vendor>/<theme>/Magento_Catalog/templates/category/view.phtml (category page title).
- Open the file in your theme (always work in a custom theme, never the base theme) and locate the heading tag. Change it to the correct semantic level, e.g., replace <h3 ...> with <h2 ...>, keeping all classes and attributes intact.
- For CMS pages and blocks (Content → Pages or Content → Blocks in the Admin), click 'Edit' on the relevant page or block and use the WYSIWYG editor's heading-style dropdown to select the correct heading level for each heading in the content.
- Run bin/magento cache:flush from the command line after saving template changes to clear the full-page cache.
- Use the axe DevTools browser extension to verify heading order on the live storefront.
<!-- ONE H1 per page: the primary page title -->
<h1>Women's Running Shoes</h1>
<!-- H2 for major page sections -->
<h2>Filter by Category</h2>
<!-- H3 for sub-sections within an H2 -->
<h3>Shoe Type</h3>
<h3>Brand</h3>
<h2>Featured Products</h2>
<!-- H3 for individual product titles within the H2 section -->
<h3>UltraRun Pro 5</h3>
<h3>TrailBlaze X2</h3>
<h2>Customer Reviews</h2>
<h3>Top Rated This Month</h3>
<!-- H4 for sub-items within an H3 — only go deeper when content genuinely nests -->
<h4>Review by Sarah M.</h4>
<h4>Review by James T.</h4>Correct heading hierarchy pattern for a typical ecommerce product or category page.
Html has langQuick win
Add a valid `lang` attribute to the `<html>` element so browsers, screen readers, and search engines know what language your page is written in.
On Adobe Commerce (Magento)
- In your Magento admin, go to Content → Design → Configuration.
- Select the store view you want to update and click Edit.
- Under the HTML Head section, check for any overrides, but the primary control is the locale set in Stores → Configuration → General → General → Locale Options → Locale.
- The `lang` attribute is output in your theme's root template. Navigate to your theme directory (e.g., app/design/frontend/Vendor/theme/Magento_Theme/templates/root.phtml) and verify the `<html>` tag uses `<?php /* @escapeNotVerified */ echo $htmlAttributes ?>` or directly includes the lang attribute.
- If the tag is missing `lang`, add it: `<html lang="<?= $block->escapeHtmlAttr($currentLocale) ?>">` — you may need to pass the locale from your layout XML or block class.
- Clear the Magento cache (System → Cache Management → Flush Magento Cache) and verify via View Source.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Your Page Title</title>
</head>
<body>
<!-- page content -->
</body>
</html>Add the lang attribute to the opening <html> tag of every HTML page. Use a valid BCP 47 language tag.
Html lang validQuick win
Add a valid BCP 47 language code to the `lang` attribute on your site's `<html>` element so browsers and assistive technologies know what language your content is written in.
On Adobe Commerce (Magento)
- In your Magento admin, go to Content → Design → Configuration.
- Select the Store View you want to update and click 'Edit'.
- Under 'HTML Head', find the 'Locale' or review your theme's root template file.
- The `lang` attribute is typically output by the root template. Open `<your-theme>/Magento_Theme/templates/root.phtml` (or the parent theme's equivalent) and locate the `<html` tag.
- Update it to use the store's locale: `<html lang="<?= $block->escapeHtmlAttr(strstr($block->getLang(), '_', true) ?: 'en') ?>">` — or use the full locale with a hyphen, e.g., `lang="en-US"`.
- Clear the Magento cache (System → Cache Management → Flush Cache) and verify the change in page source.
<?php
// Derive a BCP 47 language tag from Magento's locale (e.g. 'en_US' -> 'en-US')
$langCode = str_replace('_', '-', $block->getLang());
?>
<!DOCTYPE html>
<html lang="<?= $block->escapeHtmlAttr($langCode) ?>">
<head><?= $block->getChildHtml('head') ?></head>
<body data-container="body" data-mage-init='{"loaderAjax": {}, "loader": {"icon": "<?= $block->getViewFileUrl('images/loader-2.gif') ?>"}}' <?= $block->getBodyClass() ? 'class="' . $block->escapeHtmlAttr($block->getBodyClass()) . '"' : '' ?>>
<?= $block->getChildHtml('global_notices') ?>
<?= $block->getChildHtml('content') ?>
<?= $block->getChildHtml('before_body_end') ?>
<?php require $block->getTemplateFile('Magento_Theme::js/cookie.phtml') ?>
</body>
</html>In <your-theme>/Magento_Theme/templates/root.phtml, update the <html> tag to output the store locale as a valid lang attribute.
Image altModerate effort
Add descriptive alt text to every meaningful image, and mark purely decorative images with an empty alt attribute or role="presentation".
On Adobe Commerce (Magento)
- Product images: Admin → Catalog → Products → [Product name] → Images and Videos section → hover over an image → click the pencil icon → fill in the 'Alt Text' field → Save.
- CMS Page/Block images: Content → Pages (or Blocks) → [Page/Block] → open the WYSIWYG editor → click an image → click 'Insert/Edit Image' (the image icon) → fill in the 'Image Description' (alt text) field.
- Theme/layout images added via XML or templates: locate the relevant .phtml template file in app/design/frontend/[Vendor]/[Theme]/ → find the <img> tag and add or update the alt attribute, using a translatable string where appropriate (e.g., alt="<?= $block->escapeHtmlAttr(__('Description here')) ?>").
- For bulk product image alt text updates, use the Magento Import/Export feature (System → Data Transfer → Import) with the 'Catalog Products' entity, including the 'small_image_label', 'thumbnail_label', and 'base_image_label' columns.
<?php
/** @var \Magento\Catalog\Block\Product\View\Gallery $block */
$image = $block->getImage($block->getProduct(), 'product_page_image_large');
?>
<img
src="<?= $block->escapeUrl($image->getImageUrl()) ?>"
alt="<?= $block->escapeHtmlAttr($image->getLabel() ?: $block->getProduct()->getName()) ?>"
width="<?= (int)$image->getWidth() ?>"
height="<?= (int)$image->getHeight() ?>"
loading="lazy"
>Example .phtml snippet for a product image with escaped, translatable alt text (place in your custom theme template).
Image redundant altQuick win
Remove or clear the alt text on images whose description is already provided by visible text immediately surrounding them, so screen readers don't read the same information twice.
On Adobe Commerce (Magento)
- In the Admin panel, go to Catalog → Products → open the relevant product.
- Scroll to the Images and Videos section, click the product image, and clear the 'Alt Text' field so it is empty.
- For theme-level control, via FTP/SFTP open your custom theme folder under app/design/frontend/<Vendor>/<theme>/.
- Find the relevant template — typically Magento_Catalog/templates/product/list.phtml for category pages or Magento_Catalog/templates/product/view/gallery.phtml for product pages.
- Locate the <img> tag and ensure the alt attribute outputs an empty string (alt="") when the surrounding markup already contains the product name as visible text.
- Run bin/magento cache:flush and bin/magento setup:static-content:deploy, then re-test with axe DevTools.
<!-- CORRECT: image inside a link that also has visible text -->
<a href="/products/red-sneakers">
<img src="red-sneakers.jpg" alt="" width="300" height="300">
<span>Red Sneakers</span>
</a>
<!-- INCORRECT: alt repeats the visible link text -->
<a href="/products/red-sneakers">
<img src="red-sneakers.jpg" alt="Red Sneakers" width="300" height="300">
<span>Red Sneakers</span>
</a>
<!-- INCORRECT: alt attribute missing entirely (screen reader reads filename) -->
<a href="/products/red-sneakers">
<img src="red-sneakers.jpg" width="300" height="300">
<span>Red Sneakers</span>
</a>Use alt="" (empty string) when the image is inside or adjacent to visible text that already describes it. Never omit the alt attribute entirely.
Input button nameQuick win
Add a visible label or accessible name to every input button so assistive technologies can announce what the button does.
On Adobe Commerce (Magento)
- Empty-value input buttons appear in `.phtml` template files in your custom theme.
- Locate the relevant template under `app/design/frontend/<Vendor>/<theme>/` — common files are `Magento_Checkout/templates/cart/form.phtml` and `Magento_Contact/templates/form.phtml`.
- Search for `<input type="submit"` or `<input type="button"` with `value=""` or no value attribute.
- Add a translatable label: `value="<?= $block->escapeHtmlAttr(__('Place Order')) ?>"` to leverage Magento's i18n system.
- Run `bin/magento cache:flush` and `bin/magento setup:static-content:deploy` after saving, then verify with the axe browser extension.
<?php
/**
* File: app/design/frontend/<Vendor>/<Theme>/Magento_Checkout/templates/cart/form.phtml
* Replace any empty-value input with a translated, escaped label.
*/
?>
<input
type="submit"
value="<?= $block->escapeHtmlAttr(__('Update Shopping Cart')) ?>"
class="action update"
/>In a .phtml template, use the escapeHtmlAttr helper and the translation function for accessible, localised button labels.
Input image altQuick win
Add a descriptive `alt` attribute to every `<input type="image">` element so screen readers can announce its purpose to visually impaired shoppers.
On Adobe Commerce (Magento)
- Locate the relevant `.phtml` template file in your custom theme under `app/design/frontend/<Vendor>/<Theme>/`. Common locations: `Magento_Checkout/templates/`, `Magento_Search/templates/`, or a payment module template.
- Search for `type="image"` within those template files.
- Add `alt="<?= $escaper->escapeHtmlAttr(__('Submit order')) ?>"` (using Magento's escaper and translation helper) to each matching `<input>` tag.
- Run `bin/magento cache:flush` and `bin/magento setup:static-content:deploy` after saving.
- Verify on the frontend with the axe browser extension.
<?php
/** @var \Magento\Framework\Escaper $escaper */
/** @var \Magento\Framework\View\Element\Template $block */
?>
<input
type="image"
src="<?= $escaper->escapeUrl($block->getViewFileUrl('Magento_Theme::images/buy-now.png')) ?>"
alt="<?= $escaper->escapeHtmlAttr(__('Buy now')) ?>"
width="120"
height="40"
>Use inside any .phtml template file in your custom theme. The escaper and translation helper ensure security and i18n compliance.
LabelQuick win
Add a visible or programmatic label to every form input so assistive technologies can identify it for users.
On Adobe Commerce (Magento)
- Locate the relevant `.phtml` template file for the form — e.g. `app/design/frontend/<Vendor>/<Theme>/Magento_Checkout/templates/` for checkout fields, or `app/design/frontend/<Vendor>/<Theme>/Magento_Search/templates/form.mini.phtml` for the search form.
- Add `<label for='input-id' class='label'><span>Descriptive text</span></label>` before the `<input id='input-id'>` element. For visually hidden labels use Magento's built-in `.visually-hidden` utility class on the label.
- For the mini-search input, add `aria-label='<?= $block->escapeHtml(__('Search')) ?>'` directly on the `<input>` element.
- Clear the Magento cache: go to System → Cache Management → Flush Magento Cache, or run `bin/magento cache:flush` from the CLI.
- Verify with the axe browser extension on the storefront.
<!-- Magento search mini-form: add aria-label to the existing input -->
<form class="form minisearch" id="search_mini_form" action="<?= $block->escapeUrl($block->getSearchUrl()) ?>" method="get">
<div class="field search">
<label class="label" for="search" data-role="minisearch-label">
<span><?= $block->escapeHtml(__('Search')) ?></span>
</label>
<div class="control">
<input
id="search"
type="search"
autocomplete="off"
name="q"
value="<?= $block->escapeHtml($block->getQueryText()) ?>"
placeholder="<?= $block->escapeHtml(__('Search entire store here...')) ?>"
maxlength="<?= $block->escapeHtmlAttr($block->getMaxQueryLength()) ?>"
class="input-text"
aria-label="<?= $block->escapeHtml(__('Search')) ?>"
>
</div>
</div>
<div class="actions">
<button type="submit" title="<?= $block->escapeHtml(__('Search')) ?>" class="action search" aria-label="<?= $block->escapeHtml(__('Search')) ?>">
<span><?= $block->escapeHtml(__('Search')) ?></span>
</button>
</div>
</form>Edit the .phtml template for the relevant form (e.g. app/design/frontend/<Vendor>/<Theme>/Magento_Search/templates/form.mini.phtml).
Label title onlyModerate effort
Add a visible text label to every form field so it is not labeled only by a hidden tooltip (title attribute) or supplementary description (aria-describedby).
On Adobe Commerce (Magento)
- Identify the .phtml template responsible for the input: common locations are Magento_Search/templates/form.mini.phtml (search), Magento_Checkout/templates/cart/item/renderer.phtml (quantity), Magento_Customer/templates/form/login.phtml (login).
- In your custom theme (app/design/frontend/<Vendor>/<theme>/), copy the template file from the module's view/frontend/templates/ path, maintaining the same directory structure.
- Open the copied file and add or fix the <label for='input-id'> element so it matches the input's id attribute. Use the $block->escapeHtml() helper for any dynamic label text.
- For the mini search form, the input typically has id='search' — ensure a <label for='search' class='label'><span>Search</span></label> precedes it; add the CSS class 'label' to style it visibly or hide it accessibly using Magento's built-in .visually-hidden / .hidden class.
- Run bin/magento setup:static-content:deploy and bin/magento cache:flush, then hard-refresh the storefront and test with axe.
<?php
/**
* Accessible mini search form override.
* Path: app/design/frontend/<Vendor>/<Theme>/Magento_Search/templates/form.mini.phtml
*/
/** @var \Magento\Search\Block\SearchForm $block */
?>
<form
class="form minisearch"
id="search_mini_form"
action="<?= $block->escapeUrl($block->getSearchUrl()) ?>"
method="get"
role="search"
>
<label
class="label visually-hidden"
for="search"
data-role="minisearch-label"
>
<span><?= $block->escapeHtml(__('Search')) ?></span>
</label>
<div class="control">
<input
id="search"
type="search"
name="q"
value="<?= $block->escapeHtmlAttr($block->getQueryText()) ?>"
placeholder="<?= $block->escapeHtmlAttr(__('Search entire store here...')) ?>"
class="input-text"
maxlength="<?= $block->escapeHtmlAttr($block->getMaxQueryLength()) ?>"
autocomplete="off"
aria-label="<?= $block->escapeHtmlAttr(__('Search')) ?>"
/>
</div>
<div class="actions">
<button
type="submit"
title="<?= $block->escapeHtmlAttr(__('Search')) ?>"
class="action search"
aria-label="<?= $block->escapeHtmlAttr(__('Search')) ?>"
>
<span><?= $block->escapeHtml(__('Search')) ?></span>
</button>
</div>
</form>Place in app/design/frontend/<Vendor>/<Theme>/Magento_Search/templates/form.mini.phtml to override the search form.
Landmark banner is top levelModerate effort
Move your site's `<header>` element (or `role="banner"` landmark) so it is a direct child of `<body>`, not nested inside another landmark region.
On Adobe Commerce (Magento)
- Identify your active theme under Content → Design → Configuration.
- Via SFTP or your IDE, open app/design/frontend/<Vendor>/<theme>/Magento_Theme/templates/root.phtml (or the equivalent layout XML).
- Search for the `<header>` element. In the default Luma theme it already appears as a direct `<body>` child; in custom themes it may be wrapped.
- If wrapped, move the `<header>` block outside the wrapping landmark element.
- Run bin/magento cache:clean, then test with axe DevTools.
<!doctype html>
<html <?= /* @noEscape */ $htmlAttributes ?>>
<head><?= /* @noEscape */ $headContent ?></head>
<body data-container="body" data-mage-init='{"loaderAjax": {}}'
class="<?= /* @noEscape */ $bodyClasses ?>">
<?= /* @noEscape */ $bodyContent /* skip link rendered here */ ?>
<!-- CORRECT: page-header landmark is a direct body child -->
<header class="page-header">
<div class="panel wrapper">
<?= /* @noEscape */ $this->getChildHtml('top.container') ?>
</div>
<div class="header content">
<?= /* @noEscape */ $this->getChildHtml('header') ?>
</div>
</header>
<main id="maincontent" class="page-main">
<?= /* @noEscape */ $this->getChildHtml('global.notices') ?>
<?= /* @noEscape */ $this->getChildHtml('main') ?>
</main>
<footer class="page-footer">
<?= /* @noEscape */ $this->getChildHtml('footer') ?>
</footer>
</body>
</html>In app/design/frontend/<Vendor>/<theme>/Magento_Theme/templates/root.phtml, keep <header> outside <main>.
Landmark complementary is top levelModerate effort
Move your <aside> element (or role="complementary") to the top level of the page so it is not nested inside another landmark region.
On Adobe Commerce (Magento)
- In your Magento admin, go to Content → Design → Configuration → select your store view → Edit, and note the active theme.
- Via FTP/SFTP or the admin theme editor, navigate to `app/design/frontend/[Vendor]/[theme]/` and locate the relevant layout XML file (e.g., `Magento_Catalog/layout/catalog_product_view.xml` for product pages or `Magento_Theme/layout/default.xml` for sitewide layout).
- In the layout XML, find the block or container that renders the sidebar (`sidebar.main` or `sidebar.additional` containers). Check whether it is declared inside the `main` container in the XML hierarchy.
- Move the sidebar container declaration in the XML to be a sibling of `main`, not a child — update the `parent` attribute or restructure the container nesting in the XML.
- In the corresponding `.phtml` template file (e.g., `Magento_Theme/templates/root.phtml` or a page-specific template), ensure the `<aside>` tag wrapping the sidebar renders outside the `<main>` element and add `aria-label="Sidebar"` to the `<aside>`.
- Run `bin/magento cache:clean` and `bin/magento setup:static-content:deploy`, then validate with axe DevTools on the storefront.
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<body>
<!-- Top-level wrapper -->
<referenceContainer name="page.wrapper">
<!-- Main content container -->
<container name="main" htmlTag="main" htmlId="main-content" htmlClass="page-main" label="Main Content">
<container name="main.inner" htmlTag="div" htmlClass="page-main-inner">
<!-- primary content blocks go here -->
</container>
</container>
<!-- Sidebar as a SIBLING of main, NOT a child -->
<container name="sidebar.main" htmlTag="aside"
htmlClass="sidebar sidebar-main"
htmlId="sidebar-main"
label="Sidebar"
before="-">
<!-- sidebar blocks go here -->
</container>
</referenceContainer>
</body>
</page>In layout XML (e.g., default.xml), declare sidebar containers as siblings of the main container, not children.
Landmark contentinfo is top levelQuick win
Move your site's footer element (or role="contentinfo") so it is a direct child of the page body, not nested inside another landmark region.
On Adobe Commerce (Magento)
- Locate your active theme's layout XML and template files, typically under app/design/frontend/<Vendor>/<Theme>/.
- Open the main page layout XML (e.g. default.xml or cms_index_index.xml) and find the block or container that renders the footer (usually named 'footer' or 'footer_content').
- In the layout XML, confirm the footer container is a child of the root <page> container (or <body> in the layout), not nested inside the 'main' container.
- If the footer block is declared inside the 'main' container, move its XML declaration to be a sibling of 'main', placed after it.
- Open the corresponding .phtml template (e.g. Magento_Theme/templates/html/footer.phtml) and verify the <footer> tag is not wrapped in a <main> or other landmark element within the template itself.
- Flush the Magento cache (bin/magento cache:flush), reload the storefront, and verify with axe DevTools.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Store Page</title>
</head>
<body>
<header>
<!-- Site logo, main navigation -->
</header>
<nav aria-label="Main navigation">
<!-- Primary nav links -->
</nav>
<main>
<!-- All primary page content goes here -->
<h1>Page Title</h1>
<p>Page content...</p>
</main>
<!-- CORRECT: <footer> is a direct child of <body>, after </main> -->
<footer>
<p>© 2024 My Store. All rights reserved.</p>
<nav aria-label="Footer navigation">
<a href="/privacy">Privacy Policy</a>
<a href="/contact">Contact</a>
</nav>
</footer>
</body>
</html>Correct top-level landmark structure — <footer> is a direct child of <body>, not nested inside <main> or any other landmark.
Landmark main is top levelQuick win
Move the `<main>` element (or `role="main"`) so it is a direct child of `<body>`, not nested inside any other landmark element such as `<header>`, `<nav>`, `<aside>`, or `<footer>`.
On Adobe Commerce (Magento)
- Locate your active theme directory: app/design/frontend/<Vendor>/<theme>/
- Open Magento_Theme/templates/root.phtml (or the equivalent layout handle file) to inspect the top-level HTML shell.
- Search for `<main` or `role="main"` across your theme's template files (use grep: `grep -r 'role="main"' app/design/frontend/<Vendor>/<theme>/`).
- If the `<main>` element or `role="main"` div is nested inside a `<header>` block or another landmark element, edit the template to move it outside — it should sit between the closing `</header>` and opening `<footer>` as a direct child of `<body>`.
- Run `bin/magento cache:flush` and `bin/magento setup:static-content:deploy` after saving.
- Validate the fix with axe DevTools or WAVE on a frontend page.
<?php
/** @var \Magento\Framework\View\Element\Template $block */
?>
<!DOCTYPE html>
<html lang="<?= $block->escapeHtmlAttr($block->getLang()) ?>">
<head><?= $block->getChildHtml('head') ?></head>
<body <?= $block->getBodyAttributeCode() ?>>
<?= $block->getChildHtml('global_notices') ?>
<header>
<?= $block->getChildHtml('header') ?>
</header>
<!-- CORRECT: <main> is a direct child of <body>, outside <header> -->
<main id="maincontent" class="page-main">
<?= $block->getChildHtml('global_messages') ?>
<?= $block->getChildHtml('main') ?>
</main>
<footer class="page-footer">
<?= $block->getChildHtml('footer') ?>
</footer>
<?= $block->getChildHtml('before_body_end') ?>
</body>
</html>In app/design/frontend/<Vendor>/<theme>/Magento_Theme/templates/root.phtml — ensure <main> sits between closing </header> and opening <footer> at body level.
Landmark no duplicate bannerModerate effort
Ensure your page has only one banner landmark (one `<header>` or `role="banner"` element that is not nested inside another landmark), so screen reader users can navigate your site reliably.
On Adobe Commerce (Magento)
- The header markup is in 'app/design/frontend/<Vendor>/<theme>/Magento_Theme/templates/html/header.phtml' (or the equivalent in your custom theme).
- SSH into your server or use your IDE to search all theme template (.phtml) files for `<header` and `role="banner"`.
- Confirm only 'html/header.phtml' outputs the top-level `<header role="banner">` element.
- Any widget, CMS block, or third-party module that outputs a second `<header>` should be edited: find its template file via bin/magento dev:template-hints:enable, then change `<header>` to `<div>` in that template.
- Run 'bin/magento cache:flush' and 'bin/magento setup:static-content:deploy' after changes.
- Re-scan with an accessibility tool to confirm a single banner landmark.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Store Page</title>
</head>
<body>
<!-- CORRECT: Single top-level <header> = one banner landmark -->
<header>
<a href="/"><img src="logo.svg" alt="Acme Store"></a>
<nav aria-label="Main navigation">
<ul>
<li><a href="/shop">Shop</a></li>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
</header>
<main>
<article>
<!-- CORRECT: <header> nested inside <article> is NOT a banner landmark -->
<header>
<h1>Product Name</h1>
<p>Posted: <time datetime="2024-01-01">January 1, 2024</time></p>
</header>
<p>Product description goes here.</p>
</article>
<!-- CORRECT: promotional strip uses <div>, not <header> -->
<div class="promo-bar" role="region" aria-label="Promotional offer">
<p>Free shipping on orders over $50!</p>
</div>
</main>
<footer>
<p>© 2024 Acme Store</p>
</footer>
</body>
</html>Correct page structure: one top-level <header> as the banner, inner content sections use <div> or are wrapped in a sectioning element.
Landmark no duplicate contentinfoModerate effort
Ensure your page has only one footer/contentinfo landmark by removing or re-marking any duplicate `<footer>` elements or `role="contentinfo"` attributes.
On Adobe Commerce (Magento)
- In your Magento admin, go to Content → Design → Configuration → select your store view → Edit.
- Navigate to your active theme directory on the server: app/design/frontend/<Vendor>/<Theme>/Magento_Theme/templates/html/. Open footer.phtml — this contains your primary <footer> landmark.
- Search across all theme template files (.phtml) and layout XML files for additional <footer> tags or role="contentinfo" attributes using a code editor or grep: grep -r 'contentinfo\|<footer' app/design/frontend/<Vendor>/<Theme>/
- Change any secondary <footer> elements to <div> with an appropriate aria-label.
- If a third-party module is injecting a duplicate, check the module's template file in app/code/<Vendor>/<Module>/view/frontend/templates/ and override it in your theme.
- Clear the Magento cache (System → Cache Management → Flush Magento Cache) and verify with axe DevTools on the storefront.
<!-- CORRECT: One site-wide footer landmark -->
<body>
<header><!-- site header --></header>
<main><!-- page content --></main>
<!-- Secondary informational section — NOT a footer landmark -->
<div class="promo-bar" aria-label="Promotional offers">
<p>Free shipping on orders over $50!</p>
</div>
<!-- The ONE true contentinfo landmark -->
<footer>
<nav aria-label="Footer navigation">
<ul>
<li><a href="/privacy">Privacy Policy</a></li>
<li><a href="/terms">Terms of Service</a></li>
<li><a href="/contact">Contact Us</a></li>
</ul>
</nav>
<p>© 2024 My Store. All rights reserved.</p>
</footer>
</body>Correct pattern: exactly one top-level <footer> per page. Any secondary footer-like section uses <div> with a descriptive aria-label.
Landmark no duplicate mainQuick win
Find and remove every duplicate `<main>` element or `role="main"` attribute so that exactly one main landmark exists on each page.
On Adobe Commerce (Magento)
- Identify the active theme under Content → Design → Configuration.
- In your theme directory (app/design/frontend/<Vendor>/<theme>/) open Magento_Theme/templates/root.phtml (or the equivalent base layout file). Search for '<main' and 'role="main"'.
- Also check layout XML files in Magento_Theme/layout/default.xml and any module layout overrides for blocks that render their own '<main>' wrapper.
- Edit the offending template to replace extra '<main>' tags with '<div>' or '<section>', redeploy static content (bin/magento setup:static-content:deploy), flush cache (bin/magento cache:flush), and verify on the frontend.
<?php
/** @var \Magento\Framework\View\Element\Template $block */
?>
<!DOCTYPE html>
<html <?= $block->escapeHtmlAttr($htmlAttributes) ?>>
<head>
<?= $block->getChildHtml('head') ?>
</head>
<body <?= $block->escapeHtmlAttr($bodyAttributes) ?>>
<?= $block->getChildHtml('global_notices') ?>
<header class="page-header">
<?= $block->getChildHtml('header') ?>
</header>
<!-- ✅ Exactly one <main> element -->
<main id="maincontent" class="page-main" tabindex="-1">
<div class="page-main-inner">
<?= $block->getChildHtml('columns') ?>
</div>
</main>
<footer class="page-footer">
<?= $block->getChildHtml('footer') ?>
</footer>
<?= $block->getChildHtml('before_body_end') ?>
<?php $block->getChildHtml('require_js') ?>
</body>
</html>app/design/frontend/<Vendor>/<theme>/Magento_Theme/templates/root.phtml — keep exactly one <main> wrapping the primary content output.
Landmark one mainQuick win
Add a single `<main>` landmark element (or `role="main"`) to every page so assistive technologies can identify and navigate directly to the primary content.
On Adobe Commerce (Magento)
- In your custom theme directory (`app/design/frontend/<Vendor>/<theme>/`), open `Magento_Theme/templates/root.phtml` (or `page/html/main.phtml` for Magento 2's full-page layout).
- Find the `<div class="page-wrapper">` or equivalent wrapper around `<?= $block->getChildHtml('content') ?>`. Change its tag to `<main class="page-main" id="maincontent">` and update the closing tag.
- In `default.xml` or `cms_index_index.xml` layout files, ensure the `main` container uses the HTML tag `main` — e.g. `<container name="page.main" htmlTag="main" htmlId="maincontent" htmlClass="page-main">`.
- Clear the Magento cache: `bin/magento cache:clean` and `bin/magento cache:flush`.
- Verify on a storefront page with axe DevTools.
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<body>
<referenceContainer name="page.main"
htmlTag="main"
htmlId="maincontent"
htmlClass="page-main"/>
</body>
</page>In app/design/frontend/<Vendor>/<Theme>/Magento_Theme/layout/default.xml — set htmlTag to 'main' on the page.main container.
Landmark uniqueModerate effort
Add a unique aria-label (or aria-labelledby) to every repeated landmark role on the page so screen-reader users can tell them apart.
On Adobe Commerce (Magento)
- Landmark HTML is rendered by Magento's frontend theme. Locate your active theme under app/design/frontend/<Vendor>/<theme>/.
- Identify the template files that output the duplicate landmarks: typically Magento_Theme/templates/html/header.phtml (header/nav), Magento_Theme/templates/html/footer.phtml (footer nav), and Magento_LayeredNavigation/templates/layer/view.phtml (sidebar/aside filters).
- Open each file and add aria-label attributes to the relevant landmark tags. For example, change <nav> to <nav aria-label='Main navigation'> in header.phtml and <nav aria-label='Footer navigation'> in footer.phtml.
- If you are using a third-party theme (e.g. Hyvä, Luma-based), create a template override in your custom theme folder rather than editing the vendor file directly.
- Clear the Magento cache (bin/magento cache:flush) and deploy static content (bin/magento setup:static-content:deploy) to see changes on the storefront.
- Run the axe DevTools browser extension on the storefront to confirm all landmark-unique violations are resolved.
<?php
// In header.phtml — main navigation
?>
<nav aria-label="<?= $block->escapeHtmlAttr(__('Main navigation')) ?>">
<?= $block->getChildHtml('top.menu') ?>
</nav>
<?php
// In footer.phtml — footer navigation
?>
<nav aria-label="<?= $block->escapeHtmlAttr(__('Footer navigation')) ?>">
<ul>
<?php foreach ($block->getLinks() as $link): ?>
<li><a href="<?= $block->escapeUrl($link->getHref()) ?>">
<?= $block->escapeHtml($link->getLabel()) ?>
</a></li>
<?php endforeach; ?>
</ul>
</nav>
<?php
// In view/frontend templates for layered navigation sidebar — aside label
?>
<aside aria-label="<?= $block->escapeHtmlAttr(__('Product filters')) ?>">
<?= $block->getChildHtml() ?>
</aside>Edit app/design/frontend/<Vendor>/<theme>/Magento_Theme/templates/html/header.phtml and footer.phtml.
Link in text blockQuick win
Increase the color contrast between inline links and their surrounding body text to at least 3:1, or add a non-color visual distinction (such as underline) so links are identifiable without relying on color alone.
On Adobe Commerce (Magento)
- In your Magento admin, go to Content → Design → Configuration → select your store view → Edit.
- Scroll to HTML Head → add a CSS override in the 'Miscellaneous HTML' field, or navigate to your theme's web/css/source/_theme.less (or .scss) file.
- Update or add the link rule: a { color: #YOUR_NEW_COLOR; text-decoration: underline; }
- Run bin/magento setup:static-content:deploy after any theme file changes.
- Flush the cache (System → Cache Management → Flush Magento Cache) and verify on a CMS page or product description.
// Magento/Adobe Commerce: update link color and add underline
a {
.lib-css(color, #0057d8); // update to your chosen compliant color
text-decoration: underline;
&:hover {
.lib-css(color, darken(#0057d8, 10%));
text-decoration: underline;
}
}Edit app/design/frontend/[Vendor]/[theme]/web/css/source/_theme.less, then run static-content:deploy.
Link nameQuick win
Add a descriptive, screen-reader-accessible text label to every link on your store so assistive technology can announce its purpose.
On Adobe Commerce (Magento)
- Locate the relevant .phtml template file. Common problem areas: Magento_Theme/templates/html/header.phtml (search/cart icon links), Magento_Catalog/templates/product/list.phtml (product card links), Magento_Theme/templates/html/footer.phtml (social links).
- Always make changes inside a custom theme (app/design/frontend/[Vendor]/[theme]/) rather than editing core files — copy the template into your custom theme directory first.
- For icon-only links in the header, add aria-label: <a href='<?= $block->getUrl('checkout/cart') ?>' aria-label='<?= __('View shopping cart') ?>'>.
- For product image links in the product list, add aria-label='<?= $escaper->escapeHtmlAttr(__('View %1', $product->getName())) ?>' to the wrapping <a> tag.
- Run bin/magento cache:flush and bin/magento setup:static-content:deploy after saving template changes, then verify with axe DevTools.
ListModerate effort
Fix all HTML lists so that only valid child elements appear directly inside `<ul>` and `<ol>` tags, and only `<ul>` or `<ol>` elements wrap `<li>` items.
On Adobe Commerce (Magento)
- SSH or SFTP into your server and navigate to your active theme directory: app/design/frontend/[Vendor]/[Theme]/.
- The most common sources of list errors are: Magento_Theme/templates/html/header.phtml (site header/nav), Magento_Theme/templates/html/footer.phtml, and Magento_Catalog/templates/product/ (product feature lists).
- Open the relevant .phtml file, locate any `<ul>` or `<ol>` blocks, and ensure every direct child is `<li>`. Move any `<div>` or `<p>` wrappers inside `<li>` tags.
- If the nav menu is generated by Magento_Theme/templates/html/topmenu.phtml or a navigation module, open that file and apply the same fix.
- Run `bin/magento cache:clean && bin/magento cache:flush` after saving template changes.
- Re-test on the frontend with the axe browser extension to confirm all list violations are resolved.
<nav aria-label="<?= $block->escapeHtmlAttr(__('Main navigation')) ?>">
<ul class="nav-sections-items">
<?php foreach ($block->getNavigationLinks() as $link): ?>
<li class="nav-sections-item-title">
<a href="<?= $block->escapeUrl($link->getUrl()) ?>">
<?= $block->escapeHtml($link->getTitle()) ?>
</a>
</li>
<?php endforeach; ?>
</ul>
</nav>Place in app/design/frontend/[Vendor]/[Theme]/Magento_Theme/templates/html/header.phtml — correct nav list structure.
ListitemModerate effort
Ensure every `<li>` element is a direct child of a `<ul>` or `<ol>` element (or an element explicitly given `role="list"`), so that screen readers can correctly announce list structure to users.
On Adobe Commerce (Magento)
- SSH into your server (or use your local development environment) and navigate to your active theme directory: `app/design/frontend/<Vendor>/<theme>/`.
- Use a text editor or IDE to search for the template file (.phtml) that renders the broken list — common locations are `Magento_Theme/templates/html/header.phtml`, `Magento_Catalog/templates/product/list.phtml`, or a custom module template.
- In the .phtml file, locate the `<li>` element and confirm its direct parent. If it is a `<div>` or other non-list tag, wrap the `<li>` block in `<ul>...</ul>` (or `<ol>` if appropriate).
- If the layout is controlled by a layout XML file (`default.xml`, `catalog_category_view.xml`, etc.), check whether a container is overriding the list's HTML tag — correct the `htmlTag` attribute to `ul` or `ol` in the relevant XML block.
- Run `bin/magento cache:clean` and `bin/magento cache:flush` after making template changes.
- Verify on the frontend using axe DevTools or the Chrome Accessibility panel to confirm the fix.
<?php
/**
* Example: product feature list in a custom .phtml template.
* Path: app/design/frontend/Vendor/Theme/Magento_Catalog/templates/product/features.phtml
*
* Every <li> is a direct child of <ul>.
*/
/** @var \Magento\Framework\View\Element\Template $block */
$features = $block->getProductFeatures(); // your data source
?>
<?php if (!empty($features)): ?>
<section class="product-features">
<h2><?= $block->escapeHtml(__('Product Highlights')) ?></h2>
<ul class="product-features__list">
<?php foreach ($features as $feature): ?>
<li class="product-features__item"><?= $block->escapeHtml($feature) ?></li>
<?php endforeach; ?>
</ul>
</section>
<?php endif; ?>In a .phtml template override under app/design/frontend/<Vendor>/<theme>/ — ensure <li> elements have <ul> or <ol> as their direct parent.
Meta refreshQuick win
Remove or disable the `<meta http-equiv="refresh">` tag that forces an automatic page reload, or replace any redirect use-case with a server-side redirect instead.
On Adobe Commerce (Magento)
- SSH into the server or use your IDE to open the theme's root layout file, typically `app/design/frontend/<Vendor>/<theme>/Magento_Theme/layout/default_head_blocks.xml` or the base theme's `default.xml`.
- Search all layout XML files and `.phtml` templates for `http-equiv` and remove the relevant `<block>` or inline `<meta>` tag.
- If the redirect is set via a CMS page's 'Custom Layout Update' XML field, go to Content → Pages → Edit the page → Design → Layout Update XML and remove the meta tag there.
- For proper redirects, use Stores → Configuration → Web → URL Options, or add a server-level redirect in your `.htaccess` / Nginx config (e.g., `Redirect 301 /old-path /new-path`).
- Run `bin/magento cache:flush` and re-scan the page.
<?xml version="1.0"?>
<!-- app/design/frontend/<Vendor>/<theme>/Magento_Theme/layout/default_head_blocks.xml -->
<!-- REMOVE any block that renders a meta-refresh, for example: -->
<!--
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<head>
<block class="Magento\Framework\View\Element\Html\Tag" name="meta.refresh">
<arguments>
<argument name="tag" xsi:type="string">meta</argument>
<argument name="attributes" xsi:type="array">
<item name="http-equiv" xsi:type="string">refresh</item>
<item name="content" xsi:type="string">30</item>
</argument>
</arguments>
</block>
</head>
</page>
-->
<!-- Simply delete the entire <block> node above and flush cache. -->Example layout XML showing how to remove a meta-refresh block from default_head_blocks.xml.
Meta viewportQuick win
Remove `user-scalable=no` (and any `maximum-scale` below 5) from your site's `<meta name="viewport">` tag so visitors can pinch-to-zoom on mobile devices.
On Adobe Commerce (Magento)
- Via SFTP or your server's file manager, locate the root layout file for your active theme: app/design/frontend/<Vendor>/<theme>/Magento_Theme/layout/default_head_blocks.xml or the base theme equivalent at vendor/magento/module-theme/view/frontend/layout/default_head_blocks.xml.
- Alternatively, search all theme files for 'user-scalable' to find where the viewport tag is set.
- Edit the relevant .phtml or layout XML file to remove `user-scalable=no` from the viewport content string.
- Run bin/magento cache:flush after saving to clear the full-page cache.
- Verify the change is live by viewing page source on mobile or in DevTools.
<meta name="viewport" content="width=device-width, initial-scale=1">Place this in the <head> of every HTML page. This is the correct, accessibility-compliant viewport tag.
Meta viewport largeQuick win
Remove or raise the `maximum-scale` value in your meta viewport tag so users can pinch-zoom on mobile devices.
On Adobe Commerce (Magento)
- SSH into your server or use your IDE to open your active theme directory, typically located at app/design/frontend/<Vendor>/<theme>/.
- Find or create the file: Magento_Theme/layout/default_head_blocks.xml OR locate the root layout XML that renders the viewport tag.
- Alternatively, search your theme's .phtml or layout XML files for 'viewport' and locate where the tag is output.
- Edit the <meta name="viewport"> content attribute to remove maximum-scale restrictions.
- Run bin/magento cache:flush after saving, then verify on a mobile device.
<meta name="viewport" content="width=device-width, initial-scale=1">Replace any existing <meta name="viewport"> tag in your <head> with this. Omitting maximum-scale and user-scalable entirely is the safest, most accessible choice.
Nested interactiveModerate effort
Remove or restructure nested interactive elements so that no focusable control (button, link, input, etc.) is placed inside another interactive control, and never rely on tabindex="-1" or aria-hidden="true" to hide an inner interactive element from assistive technologies.
On Adobe Commerce (Magento)
- Identify the PHTML template or UI Component file responsible for the affected element — common paths: app/design/frontend/<Vendor>/<Theme>/Magento_Catalog/templates/product/list.phtml, Magento_Checkout/templates/cart/item/default.phtml.
- Open the file in your code editor. Search for `<a` or `<button` tags that contain another `<a>` or `<button>` tag.
- Refactor the markup: replace the redundant inner or outer interactive element with a `<span>` with equivalent CSS classes, keeping a single interactive element per intended action.
- If the issue is in a JavaScript/Knockout or React component (PWA Studio/Venia), locate the .jsx/.js component file and apply the same single-interactive-element rule to the JSX return.
- Run `bin/magento cache:clean` and `bin/magento setup:static-content:deploy`, then test in the browser with keyboard navigation and the axe extension.
<!-- ❌ WRONG: button nested inside a link (invalid HTML, unpredictable in screen readers) -->
<a href="/products/widget">
<button type="button">View Product</button>
</a>
<!-- ✅ CORRECT: single interactive element styled as a button -->
<a href="/products/widget" class="btn btn-primary">View Product</a>
<!-- ❌ WRONG: link nested inside a button -->
<button type="button">
<a href="/cart">Add to Cart</a>
</button>
<!-- ✅ CORRECT: use only the element that matches the action -->
<!-- If it navigates → use <a>; if it performs an action on the page → use <button> -->
<a href="/cart" class="btn btn-primary">Add to Cart</a>
<!-- ❌ WRONG: relying on tabindex or aria-hidden to suppress inner element -->
<a href="/products/widget">
<button type="button" tabindex="-1" aria-hidden="true">View Product</button>
</a>
<!-- ✅ CORRECT: icons and decorative content inside interactive elements must be non-interactive -->
<button type="button">
<svg aria-hidden="true" focusable="false" width="20" height="20">
<use href="#icon-cart"></use>
</svg>
<span>Add to Cart</span>
</button>The WRONG pattern and the correct fix side by side.
Object altQuick win
Add a meaningful text alternative (via `title` attribute or `aria-label`) to every `<object>` element so screen readers can describe its content to users who cannot see it.
On Adobe Commerce (Magento)
- Access your Magento installation via FTP/SSH or the Admin panel's built-in editor.
- Identify the template file containing the <object> tag. Common locations: app/design/frontend/<Vendor>/<theme>/Magento_Catalog/templates/ or a custom module's templates/ directory.
- Open the .phtml file and locate the <object> element.
- Add `title='<?= $block->escapeHtmlAttr(__('Descriptive label here')) ?>'` to the <object> tag, using Magento's escaping helper to prevent XSS.
- If the <object> is in a CMS Page or CMS Block (Content → Pages or Content → Blocks), edit that content, switch to HTML view in the editor, and add the `title` attribute.
- Clear the Magento cache (System → Cache Management → Flush Magento Cache), then verify with axe DevTools.
<?php
/** @var \Magento\Framework\View\Element\Template $block */
$objectTitle = $block->escapeHtmlAttr(__('Winter 2024 Product Catalog (PDF)'));
$objectUrl = $block->escapeUrl($block->getViewFileUrl('YourVendor_YourModule::files/catalog.pdf'));
?>
<object
data="<?= $objectUrl ?>"
type="application/pdf"
width="100%"
height="500"
title="<?= $objectTitle ?>">
<p>
<a href="<?= $objectUrl ?>"><?= $block->escapeHtml(__('Download the catalog')) ?></a>
</p>
</object>Place in the relevant .phtml template file. Uses Magento's escapeHtmlAttr() helper to prevent XSS.
Page has heading oneQuick win
Add a single, descriptive `<h1>` heading to every page so assistive technologies and search engines can identify the page's main topic.
On Adobe Commerce (Magento)
- For product pages: the product name is rendered by app/design/frontend/<Vendor>/<theme>/Magento_Catalog/templates/product/view/details.phtml (or the parent theme equivalent). Confirm the name is wrapped in <h1 class='page-title'>.
- For CMS pages: go to Content → Pages, open the page, and in the Content editor (PageBuilder or standard WYSIWYG) add a Heading row with H1 type, or wrap the title text in a Heading 1 widget.
- For category pages: the category name is typically output as <h1> in app/design/frontend/<Vendor>/<theme>/Magento_Catalog/templates/category/view.phtml — verify and correct if needed.
- If using a custom theme, create an override in your theme folder rather than editing core files.
- Clear the Magento cache (System → Cache Management → Flush Magento Cache) and verify the rendered HTML with browser DevTools.
<h1 class="page-title">
<span class="base" data-ui-id="page-title-wrapper" itemprop="name">
<?= $block->escapeHtml($product->getName()) ?>
</span>
</h1>In your theme's Magento_Catalog/templates/product/view/details.phtml, the product name should be output inside an <h1> with the page-title class.
Presentation role conflictModerate effort
Remove conflicting ARIA attributes (global ARIA properties and tabindex) from any element that has been marked as purely presentational with role="presentation" or role="none".
On Adobe Commerce (Magento)
- Identify the offending template file using Chrome DevTools → Elements panel. Note the surrounding HTML and any nearby Magento block or template hint comments (enable template hints via Stores → Configuration → Advanced → Developer → Debug → Enabled Template Path Hints).
- Locate the corresponding .phtml template file in your custom theme directory: app/design/frontend/<Vendor>/<Theme>/.
- Edit the file to remove the conflicting ARIA attribute or tabindex from the element that carries role="presentation" or role="none".
- If the element is in a core or third-party module template, copy it to your theme directory first (following Magento's theme override pattern) rather than editing core files directly.
- Run bin/magento cache:clean && bin/magento cache:flush from the CLI, then reload your storefront and re-scan with axe DevTools.
<!-- CORRECT: Decorative image — use empty alt, no role needed -->
<img src="decorative-swoosh.png" alt="" />
<!-- CORRECT: If role="none" is used on a layout table, remove all ARIA -->
<table role="none">
<tr><td>Layout cell</td></tr>
</table>
<!-- CORRECT: Meaningful element — remove role="presentation" and use proper semantics -->
<img src="product-badge.png" alt="New arrival" />
<!-- BROKEN (do not do this): role="none" conflicts with aria-label -->
<!-- <img src="decorative-swoosh.png" role="none" aria-label="decorative swirl" /> -->
<!-- BROKEN (do not do this): role="presentation" conflicts with tabindex -->
<!-- <div role="presentation" tabindex="0">layout wrapper</div> -->Decision examples: decorative image (correct), meaningful image (correct), broken pattern (what to avoid).
RegionModerate effort
Wrap all visible page content inside HTML landmark regions (e.g. `<header>`, `<main>`, `<nav>`, `<footer>`) so screen-reader users can navigate the page by section.
On Adobe Commerce (Magento)
- Landmark structure is defined in your theme's layout XML and PHTML templates. Navigate to your custom theme directory: 'app/design/frontend/<Vendor>/<theme>/'.
- Open or create 'Magento_Theme/templates/root.phtml' (the global shell). Confirm the page header area uses `<header role='banner'>`, the main content column uses `<main id='maincontent' role='main'>`, the sidebar uses `<aside role='complementary'>`, and the footer uses `<footer role='contentinfo'>`.
- Check 'Magento_Theme/layout/default.xml' and any page-specific layout handles to ensure content blocks are assigned to the correct structural containers.
- For any blocks rendered via widgets (CMS widgets, promotional banners), ensure the widget template wraps its output in an appropriate landmark or is positioned inside an existing landmark container.
- Run 'bin/magento cache:flush' after template changes, then test with axe DevTools on the live or staging site.
Role img altModerate effort
Add a meaningful aria-label (or aria-labelledby) to every element with role="img" so screen readers can announce what the image represents.
On Adobe Commerce (Magento)
- Identify which template file renders the flagged element: use browser DevTools to find a unique class or ID, then search your theme under app/design/frontend/[Vendor]/[Theme]/ using grep -r 'role="img"' .
- Create or edit the relevant .phtml template file in your custom theme (never edit the core or parent theme directly).
- Add aria-label="<?= $block->escapeHtmlAttr(__('Description here')) ?>" to the element, using Magento's translation and escaping helpers so the label is translatable and safe.
- For SVG icon sprites (common in Luma and Hyva themes), locate the SVG sprite definition, add a <title id="icon-[name]">[Description]</title> inside each <symbol>, and update the <svg role="img" aria-labelledby="icon-[name]"> usage in templates.
- Run bin/magento setup:static-content:deploy and clear cache (bin/magento cache:clean) to see changes.
- Validate with axe DevTools browser extension on the frontend.
<?php
/**
* Accessible icon example for a product badge in a .phtml template.
* File: app/design/frontend/YourVendor/YourTheme/Magento_Catalog/templates/product/badge.phtml
*/
?>
<span
role="img"
aria-label="<?= $block->escapeHtmlAttr(__('New arrival product badge')) ?>"
class="product-badge product-badge--new"
>
<?= $block->escapeHtml(__('New')) ?>
</span>
<?php
/**
* Accessible inline SVG example.
* File: app/design/frontend/YourVendor/YourTheme/Magento_Theme/templates/html/header/logo.phtml
*/
?>
<svg
role="img"
aria-labelledby="store-logo-title"
xmlns="http://www.w3.org/2000/svg"
width="120"
height="40"
focusable="false"
>
<title id="store-logo-title"><?= $block->escapeHtml($block->getLogoAlt()) ?></title>
<!-- SVG paths here -->
</svg>Override the relevant .phtml template in your custom theme: app/design/frontend/[Vendor]/[Theme]/[Module]/templates/
Scrollable region focusableQuick win
Make every scrollable region on your site reachable by keyboard by adding tabindex="0" (or placing focusable content inside it) so keyboard and assistive-technology users can scroll it.
On Adobe Commerce (Magento)
- Locate the relevant PHTML or HTML template in your custom theme under app/design/frontend/<Vendor>/<theme>/Magento_Catalog/templates/ (e.g. product/view/description.phtml) or the relevant module template.
- Add tabindex="0" and aria-label="Product description" to the scrollable wrapper element in the template file.
- In your theme's web/css/source/ directory, add a :focus rule for the scrollable class in the relevant .less file: .product-description:focus { outline: 2px solid #005fcc; }
- Run bin/magento setup:static-content:deploy and bin/magento cache:flush to apply the changes.
- Test in the browser with Tab-key and arrow-key navigation to confirm the region is reachable and scrollable.
<?php
/** @var \Magento\Catalog\Block\Product\View\Description $block */
?>
<div
class="product attribute description"
tabindex="0"
role="region"
aria-label="<?= $block->escapeHtmlAttr(__('Product description')) ?>"
style="max-height: 400px; overflow-y: auto;"
>
<div class="value" itemprop="description">
<?= /* @noEscape */ $block->getProduct()->getDescription() ?>
</div>
</div>Override or edit app/design/frontend/<Vendor>/<Theme>/Magento_Catalog/templates/product/view/description.phtml in your custom theme.
Select nameQuick win
Add a visible or programmatic label to every `<select>` dropdown so assistive technologies can announce its purpose to users.
On Adobe Commerce (Magento)
- Unlabeled selects typically appear in: product option/swatch selects, the layered navigation sort dropdown, country/region selects in checkout, and currency switchers.
- Create a custom theme (or use your existing child theme) under `app/design/frontend/<Vendor>/<theme>/`. Never edit core files directly.
- For product options, override `Magento_Catalog/templates/product/view/options/type/select.phtml`. Locate the `<select>` and add `id="select_{{ $block->getOption()->getId() }}"`. Add `<label for="select_{{ $block->getOption()->getId() }}">{{ $block->getOption()->getTitle() }}</label>` before the select.
- For the sort-by toolbar dropdown, override `Magento_Catalog/templates/product/list/toolbar/viewmode.phtml` and `toolbar.phtml`. Add a `<label>` with a matching `for` and add a matching `id` to the `<select>` for sorting.
- For checkout country/region selects, override `Magento_Checkout/frontend/templates/onepage/shipping.phtml` or use a ViewModel/plugin to inject the correct `id` and matching `<label>` elements.
- Run `php bin/magento cache:flush` and `php bin/magento setup:static-content:deploy`, then test with a screen reader and the axe browser extension.
<?php
/**
* Accessible product custom option select template.
* Path: app/design/frontend/<Vendor>/<Theme>/Magento_Catalog/templates/product/view/options/type/select.phtml
*/
/** @var \Magento\Catalog\Block\Product\View\Options\Type\Select $block */
$option = $block->getOption();
$selectId = 'select_' . $option->getId();
?>
<div class="field required" id="<?= $block->escapeHtmlAttr('option-' . $option->getId()) ?>">
<label class="label" for="<?= $block->escapeHtmlAttr($selectId) ?>">
<span><?= $block->escapeHtml($option->getTitle()) ?></span>
<?php if ($block->getOption()->getIsRequire()): ?>
<span class="required">*</span>
<?php endif; ?>
</label>
<div class="control">
<select
id="<?= $block->escapeHtmlAttr($selectId) ?>"
class="product-custom-option admin__control-select"
name="options[<?= $block->escapeHtmlAttr($option->getId()) ?>]"
<?= ($option->getIsRequire() ? ' required' : '') ?>
>
<option value=""><?= $block->escapeHtml(__('-- Please Select --')) ?></option>
<?php foreach ($block->getValuesHtml() as $value): ?>
<?= $value ?>
<?php endforeach; ?>
</select>
</div>
</div>Override Magento_Catalog/templates/product/view/options/type/select.phtml in your custom theme at app/design/frontend/<Vendor>/<Theme>/Magento_Catalog/templates/product/view/options/type/select.phtml
Skip linkQuick win
Add a valid, focusable target element (with a matching `id`) for every "Skip to main content" link so keyboard and assistive-technology users can bypass repeated navigation.
On Adobe Commerce (Magento)
- In your Magento theme, navigate to `app/design/frontend/<Vendor>/<theme>/Magento_Theme/templates/root.phtml` (or the equivalent file in your custom theme).
- Immediately after the opening `<body>` tag, add: `<a class="skip-link" href="#maincontent">Skip to main content</a>`.
- Open `app/design/frontend/<Vendor>/<theme>/Magento_Theme/layout/default.xml` and ensure the main content wrapper block (usually `main` container) has `id="maincontent"` and `tabindex="-1"`.
- Alternatively, find `Magento_Theme::html/container.phtml` or the `columns.phtml` template that renders `<main>` and set those attributes directly.
- Add the CSS for `.skip-link` to your theme's `web/css/source/_module.less` or equivalent SCSS file (see code snippet below).
- Run `bin/magento setup:static-content:deploy` to compile assets, flush cache, and verify with keyboard testing on the frontend.
Svg img altModerate effort
Add a meaningful text alternative to every SVG image so screen readers can describe it to visually impaired shoppers.
On Adobe Commerce (Magento)
- SVGs in Adobe Commerce live in theme directories under app/design/frontend/<Vendor>/<theme>/web/images/ (SVG asset files) and in .phtml template files or layout XML. Use your IDE or FTP to navigate there.
- For inline SVGs in .phtml templates: open the relevant template file, locate the `<svg>` element, and add `role="img"` and `aria-label="<?= $escaper->escapeHtmlAttr(__('Descriptive text')) ?>"` to the opening tag — using the escaper and translation helper for best practice.
- For SVG asset files referenced in templates or CSS: open the SVG in a text editor, add `<title id="svg-t1">Descriptive text</title>` as the first child of the root `<svg>` element, add `role="img" aria-labelledby="svg-t1"` to the `<svg>` tag, and save. Then run `bin/magento setup:static-content:deploy` to republish static assets.
- For decorative SVGs, add `aria-hidden="true"` to the `<svg>` tag in the relevant .phtml file or SVG asset.
- Flush caches: Admin > System > Cache Management > Flush Magento Cache (or `bin/magento cache:flush`).
- Verify in the browser using axe DevTools or Chrome Accessibility panel.
<?php
// In your .phtml template file (app/design/frontend/Vendor/Theme/templates/...)
// Meaningful SVG with escaped, translatable aria-label
?>
<svg
role="img"
aria-label="<?= $escaper->escapeHtmlAttr(__('Free shipping badge')) ?>"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
focusable="false"
>
<title><?= $escaper->escapeHtml(__('Free shipping badge')) ?></title>
<path d="M1 3h15v13H1zM16 8l5 3v5h-5z" fill="currentColor"/>
</svg>
<?php // Decorative SVG ?>
<svg
aria-hidden="true"
focusable="false"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 300 6"
>
<rect width="300" height="6" rx="3" fill="#dddddd"/>
</svg>Inside a .phtml template file — use Magento's escaper and translation helper for the label value.
TabindexModerate effort
Remove or replace all positive tabindex values (tabindex="1" or higher) on your page elements, using tabindex="0" for focusable elements and tabindex="-1" only when programmatic focus is needed.
On Adobe Commerce (Magento)
- On your server, navigate to your active theme directory: app/design/frontend/[Vendor]/[Theme]/.
- Use a file search tool (e.g., 'grep -r "tabindex" app/design/frontend/ --include=".phtml" --include=".html" --include="*.xml"') to find all occurrences of tabindex in template files.
- Edit the relevant .phtml or .html template files and change any tabindex value greater than 0 to '0', or remove the attribute from native form elements and links.
- Also check JavaScript files under web/js/ in your theme for any dynamic setAttribute('tabindex', N) calls where N is greater than 0.
- After editing, run 'bin/magento setup:static-content:deploy' and 'bin/magento cache:clean' to apply changes.
- If using Page Builder or a third-party extension as the source, check the extension's admin configuration or contact the vendor.
<!-- ❌ WRONG: positive tabindex breaks natural tab order -->
<button tabindex="3">Submit</button>
<a href="/shop" tabindex="1">Shop Now</a>
<div tabindex="2">Custom widget</div>
<!-- ✅ CORRECT: native elements use no tabindex (natural order) -->
<button>Submit</button>
<a href="/shop">Shop Now</a>
<!-- ✅ CORRECT: non-native element made focusable in natural order -->
<div tabindex="0" role="button" aria-label="Custom widget">Custom widget</div>
<!-- ✅ CORRECT: element only focused by JavaScript (e.g., modal), not in tab flow -->
<div tabindex="-1" id="modal-container" role="dialog" aria-modal="true">...</div>Replace any positive tabindex value with 0 (or remove the attribute). Use -1 only for programmatic focus.
Valid langQuick win
Add or correct the `lang` attribute on every HTML element that contains content in a different language, using a valid BCP 47 language code.
On Adobe Commerce (Magento)
- In the Magento admin go to Content → Design → Configuration, select your store view, and confirm the HTML Head locale is set correctly.
- Open your theme's root layout file: app/design/frontend/<Vendor>/<Theme>/Magento_Theme/layout/default_head_blocks.xml — or the equivalent — and verify the html lang attribute is rendered dynamically.
- In Magento 2 the <html> tag lang attribute is output by the root.phtml or html_head.phtml template; open app/design/frontend/<Vendor>/<Theme>/Magento_Theme/templates/root.phtml and ensure it contains: <html lang="<?= $block->escapeHtmlAttr($block->getLang()) ?>">.
- For inline passages in CMS blocks or product descriptions, go to Content → Blocks (or the product edit page), click 'Show/Hide Editor' to access raw HTML, and add lang="de" (or appropriate code) to the wrapping element.
- Run bin/magento cache:flush after saving theme file changes.
<?php
/**
* Root page template
* @var \Magento\Framework\View\Element\Template $block
*/
?>
<!DOCTYPE html>
<html lang="<?= $block->escapeHtmlAttr($block->getLang()) ?>">
<head>
<?= $block->getChildHtml('head') ?>
</head>
<body>
<?= $block->getChildHtml('body') ?>
</body>
</html>In app/design/frontend/<Vendor>/<Theme>/Magento_Theme/templates/root.phtml — getLang() returns the store-view locale as a BCP 47 code.
Security (OWASP) · 26 fixes
Dmarc policy noneModerate effort
Strengthen your DMARC policy from p=none (monitor-only) to p=quarantine, then p=reject, so that emails fraudulently sent from your domain are blocked rather than silently allowed through.
On Adobe Commerce (Magento)
- Adobe Commerce sends transactional email via a configured SMTP server. Go to Admin → Stores → Configuration → Advanced → System → Mail Sending Settings and confirm you are routing through an authenticated SMTP relay (SendGrid, Mailgun, Amazon SES, etc.).
- Complete DKIM setup with your SMTP relay provider and publish the DKIM public key DNS record they provide, plus add their include to your SPF record, at your DNS host.
- At your DNS provider, edit the _dmarc.yourdomain.com TXT record — change p=none to p=quarantine.
- Review rua reports (tools like EasyDMARC, Dmarcian, or Postmark's free report reader help), fix any misaligned senders, then change p=quarantine to p=reject.
v=DMARC1; p=quarantine; sp=quarantine; pct=100; rua=mailto:dmarc-reports@yourdomain.com; ruf=mailto:dmarc-failures@yourdomain.com; ri=86400; aspf=r; adkim=r; fo=1;This is the DNS TXT record value for _dmarc.yourdomain.com — paste this as the record content at your DNS provider. Replace the rua/ruf addresses with your own reporting inbox or DMARC reporting service address.
Hsts disabledQuick win
Set a valid HTTP Strict-Transport-Security (HSTS) header with a max-age of at least 31536000 (one year) and includeSubDomains to force browsers to always use HTTPS for your store.
On Adobe Commerce (Magento)
- In the Magento Admin, go to Stores → Configuration → General → Web → Base URLs (Secure) and ensure all base URLs use https://.
- For the server-level HSTS header on Apache, add to your VirtualHost config or .htaccess: Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains" — then run apachectl configtest && sudo systemctl reload apache2.
- For Nginx, add to your server {} block in /etc/nginx/sites-available/magento.conf (or equivalent): add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; — then reload nginx.
- If you use Adobe Commerce Cloud (the cloud.magento.com / Fastly setup), add the header in .magento.app.yaml or configure it via the Fastly VCL snippet in Admin → Stores → Fastly Configuration → Custom VCL Snippets.
- Flush the Magento and Varnish/full-page cache after changes: Admin → System → Cache Management → Flush Magento Cache.
- Verify with: curl -I https://yourdomain.com | grep -i strict-transport
server {
listen 443 ssl;
server_name yourdomain.com www.yourdomain.com;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# ... rest of Magento nginx config
}Add inside the server { listen 443 ssl; } block of your Magento Nginx config (e.g., /etc/nginx/sites-available/magento.conf), then reload nginx.
Hsts max age too shortQuick win
Increase your HSTS max-age directive to at least 31536000 seconds (1 year) so browsers enforce HTTPS connections for a meaningful period.
On Adobe Commerce (Magento)
- In the Magento Admin, navigate to Stores → Configuration → General → Web → Base URLs (Secure) and ensure 'Use Secure URLs on Storefront' and 'Use Secure URLs in Admin' are both set to Yes.
- HSTS itself must be set at the web server or Varnish/FastCGI layer, not inside Magento's admin UI.
- For Nginx, add inside your HTTPS server {} block: add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
- For Apache, add to the VirtualHost or .htaccess: Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
- For Adobe Commerce Cloud (Magento Cloud), add the header in your .magento.app.yaml or via the Fastly service configuration in the Cloud Console under CDN → Headers.
- Clear Magento's full-page cache after the change: Admin → System → Cache Management → Flush Magento Cache. Verify headers with DevTools or curl -I https://yourdomain.com.
server {
listen 443 ssl;
server_name example.com;
# ... existing Magento Nginx config ...
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
}Add inside your Nginx HTTPS server block for your Magento site, then reload Nginx.
Https not availableQuick win
Enable HTTPS by installing a valid SSL/TLS certificate and redirecting all HTTP traffic to the secure HTTPS version of your store.
On Adobe Commerce (Magento)
- Obtain and install an SSL certificate on your web server (Apache or Nginx) via your hosting provider or Let's Encrypt (e.g. using Certbot: sudo certbot --apache -d yourdomain.com).
- In your Magento Admin, go to Stores → Configuration → General → Web.
- Expand the 'Base URLs (Secure)' section and set 'Secure Base URL' to https://yourdomain.com/.
- Set 'Use Secure URLs on Storefront' and 'Use Secure URLs in Admin' both to 'Yes'.
- Expand the 'URL Options' section and ensure 'Auto-redirect to Base URL' is set to 'Yes (302 Found)' or 'Yes (301 Moved Permanently)' to redirect HTTP to HTTPS.
- Clear the Magento cache: System → Cache Management → Flush Magento Cache. Also flush your full-page cache (Varnish if applicable).
- Update your web server configuration (Apache .htaccess or Nginx server block) to add a server-level 301 redirect from HTTP to HTTPS as an additional safeguard.
Info disclosure serverQuick win
Remove or suppress the HTTP `Server` response header so your web server no longer advertises its software name and version number to the public internet.
On Adobe Commerce (Magento)
- Adobe Commerce (self-hosted or cloud) typically runs on nginx or Apache — the fix is made at the web-server configuration level on your server or in Adobe Commerce Cloud's configuration.
- For *Adobe Commerce Cloud* (PaaS): edit the `.magento/routes.yaml` or the nginx configuration snippets in `.magento.app.yaml`. Under the `web > locations` section you can add custom headers. For suppressing `Server`, the most reliable approach is to add an upstream Fastly (the included CDN) custom VCL snippet to unset `beresp.http.Server` in `vcl_fetch`.
- In the *Fastly admin* (within Adobe Commerce Cloud admin > Stores > Configuration > Advanced > System > Full Page Cache > Fastly Configuration): go to Custom VCL Snippets, add a new snippet of type `fetch` with content: `unset beresp.http.Server;`
- For *self-hosted Magento on nginx*: SSH to the server, open `/etc/nginx/nginx.conf` or your Magento vhost file and add `server_tokens off;` inside the `http {}` or `server {}` block. Reload nginx: `sudo nginx -t && sudo systemctl reload nginx`.
- For *self-hosted on Apache*: in your vhost config or `.htaccess` add `ServerTokens Prod` and `ServerSignature Off`. Restart Apache.
- Verify with `curl -I https://yourdomain.com` — no version number should appear in the `Server` header.
// Fastly VCL fetch snippet — remove Server header from origin responses
sub vcl_fetch {
unset beresp.http.Server;
#FASTLY fetch
}Paste this as a custom VCL snippet of type 'fetch' in Adobe Commerce admin > Stores > Configuration > Advanced > System > Fastly Configuration > Custom VCL Snippets.
Info disclosure x powered byQuick win
Remove or suppress the X-Powered-By HTTP response header so your server stops advertising which technology stack, hosting platform, or software version powers your store.
On Adobe Commerce (Magento)
- Adobe Commerce runs on your own or managed infrastructure (Apache/Nginx + PHP), giving you full control.
- For Nginx: open your store's server block config (e.g. /etc/nginx/sites-available/your-store.conf) and add 'more_clear_headers X-Powered-By;' inside the server {} or location {} block. Reload Nginx.
- For Apache: open .htaccess or your VirtualHost config and add: 'Header unset X-Powered-By' and 'Header always unset X-Powered-By'. Ensure mod_headers is enabled (a2enmod headers).
- To suppress at the PHP level: edit pub/index.php or create app/bootstrap.php customization and add header_remove('X-Powered-By'); at the very top, before any output.
- Alternatively, use the Magento 2 module 'Magento_Framework' custom plugin to intercept responses and strip the header via a before/after plugin on \Magento\Framework\App\Response\Http.
- Flush caches (bin/magento cache:flush) and verify via DevTools.
<?php
/**
* Magento entry point — pub/index.php
* Add the line below at the very top, before any require() calls.
*/
header_remove('X-Powered-By');
// ... rest of the original pub/index.php content follows
require __DIR__ . '/../app/bootstrap.php';
// etc.
?>Add header_remove at the very top of pub/index.php, before the require statements.
Insecure cookieModerate effort
Set the HttpOnly, Secure, and SameSite flags on all session and authentication cookies to prevent them from being stolen or misused by attackers.
On Adobe Commerce (Magento)
- Log in to the Magento Admin Panel.
- Go to Stores → Configuration → General → Web → Session Validation Settings and enable 'Use HTTP Only' = Yes.
- Go to Stores → Configuration → General → Web → Default Cookie Settings: set 'Cookie HttpOnly' = Yes, 'Cookie Secure' = Yes (requires HTTPS), 'Cookie SameSite' = Strict.
- For session cookies, also check app/etc/env.php or app/etc/config.php for session configuration and set 'cookie_httponly' => 1, 'cookie_secure' => 1, 'cookie_samesite' => 'Strict'.
- Run bin/magento cache:flush after making config changes.
- Verify with browser DevTools → Application → Cookies that frontend and adminhtml cookies show all three flags.
<?php
// app/etc/env.php — add or merge 'session' key:
return [
// ... other config ...
'session' => [
'save' => 'files', // or 'redis', etc.
'cookie_httponly' => 1,
'cookie_secure' => 1,
'cookie_samesite' => 'Strict',
'cookie_lifetime' => 3600,
'cookie_path' => '/',
],
];Add to app/etc/env.php inside the 'session' configuration array. Flush cache after saving.
Missing content security policyModerate effort
Add a Content-Security-Policy (CSP) HTTP response header to every page to block unauthorized scripts, styles, and resources from loading in your visitors' browsers.
On Adobe Commerce (Magento)
- Adobe Commerce has native CSP support (introduced in Magento 2.3.5+).
- In the Admin panel go to Stores → Configuration → Security → Content Security Policy.
- Set 'CSP Mode' to 'Restrict Mode' (enforced) — start with 'Report Only Mode' to gather violations first.
- Add your trusted domains for each directive (script-src, style-src, img-src, etc.) in the respective fields.
- To add additional directives or fine-tune, edit app/etc/config.php or create a csp_whitelist.xml file inside your custom module's etc/ directory following the Magento CSP whitelist XML schema.
- Deploy the changes and verify the header appears in browser DevTools.
Missing dmarcQuick win
Add a DMARC DNS TXT record at _dmarc.yourdomain.com to tell receiving mail servers how to handle emails that fail authentication checks for your domain.
On Adobe Commerce (Magento)
- Adobe Commerce does not manage DNS. Log in to your server's hosting control panel or standalone DNS provider (Cloudflare, Route 53, etc.).
- Add a DNS TXT record: Name = _dmarc.yourdomain.com (or _dmarc if the UI appends the domain), Type = TXT, TTL = 3600, Value = v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com.
- In Adobe Commerce Admin → Stores → Configuration → Advanced → System → Mail Sending Settings, confirm outgoing email is routed through an authenticated SMTP relay (SendGrid, Mailgun, Amazon SES) that provides DKIM signing.
- Add that SMTP provider's DKIM TXT record(s) to your DNS first, then publish DMARC at p=none to gather reports before escalating to quarantine or reject.
_dmarc.yourdomain.com. 3600 IN TXT "v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com; ruf=mailto:dmarc-forensic@yourdomain.com; sp=none; adkim=r; aspf=r"Add this as a DNS TXT record at your domain registrar or DNS provider. Replace yourdomain.com and the rua address with your actual domain.
Missing permissions policyQuick win
Add a Permissions-Policy HTTP response header to your store to explicitly restrict which browser features (camera, microphone, geolocation, etc.) pages and embedded third-party scripts are allowed to access.
On Adobe Commerce (Magento)
- For Apache: Edit your virtual host config or the .htaccess in the Magento web root. Add the header inside a <IfModule mod_headers.c> block (see code snippet).
- For Nginx: Edit your Magento nginx.conf or the site-specific config file. Add the add_header line inside the server {} block (see code snippet). Run 'nginx -s reload'.
- Alternatively, create a custom module or plugin: create a custom HTTP response plugin in app/code/YourVendor/SecurityHeaders/Plugin/ResponsePlugin.php that calls $response->setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=(), payment=(), usb=(), interest-cohort=()'); in an aroundSendResponse method.
- If using Cloudflare or Fastly CDN in front of Magento: add the header via a Cloudflare Transform Rule or Fastly VCL snippet for the simplest approach.
- Run 'php bin/magento cache:flush' after any code change, then verify in DevTools.
<!-- NOT recommended — use a real HTTP header instead. Shown for reference only. -->
<meta http-equiv="Permissions-Policy" content="camera=(), microphone=(), geolocation=(), payment=(), usb=(), interest-cohort=()">Meta tag delivery is not recommended — use HTTP headers instead. Included for reference only; browser support is partial.
Missing referrer policyQuick win
Add a Referrer-Policy HTTP response header set to strict-origin-when-cross-origin to control what URL information browsers send when users navigate away from your store.
On Adobe Commerce (Magento)
- For Apache: open your virtual host configuration or .htaccess at the Magento root and add: Header always set Referrer-Policy "strict-origin-when-cross-origin"
- For Nginx: open your nginx.conf or the Magento server block configuration and add inside the server{} block: add_header Referrer-Policy "strict-origin-when-cross-origin" always; — then run 'nginx -t && systemctl reload nginx'.
- Alternatively, create a custom Magento module or use an existing security-header extension from the Magento Marketplace to inject the header programmatically.
- In the Magento Admin, check Stores → Configuration → General → Web → Base URLs to ensure all URLs use HTTPS (required for strict-origin-when-cross-origin to work correctly cross-origin).
- Flush the Magento cache (System → Cache Management → Flush Magento Cache) and verify the header in DevTools.
<meta name="referrer" content="strict-origin-when-cross-origin">Add inside the <head> element of every HTML page as a fallback when HTTP header control is unavailable.
Missing security txtQuick win
Create and publish a /.well-known/security.txt file with at least a Contact: line so security researchers know how to report vulnerabilities responsibly.
On Adobe Commerce (Magento)
- Connect to your server via SFTP/SSH and navigate to the Magento document root (the pub/ folder for Magento 2, or root for Magento 1).
- Create the directory .well-known inside pub/ (Magento 2) or the web root.
- Create the file security.txt inside .well-known with your contact details (see code snippets).
- Ensure your web server (Apache/Nginx) does not block .well-known — add a location block if needed (see code snippets).
- Verify by visiting https://yourdomain.com/.well-known/security.txt.
location ^~ /.well-known/ {
allow all;
root /var/www/html/pub;
try_files $uri =404;
}Add this location block to your Nginx server config to allow the .well-known directory to be served as static files. Place before the main location / block.
Missing spfQuick win
Add a DNS TXT record containing a valid SPF policy to your domain so that email receivers can verify which servers are authorised to send mail on your behalf.
On Adobe Commerce (Magento)
- Adobe Commerce / Magento does not manage DNS — you must update the record at your domain registrar or DNS host (Cloudflare, Route 53, GoDaddy, etc.).
- Magento's default installation sends email via the server's local sendmail/PHP mail; for reliable SPF pass, configure an SMTP extension: Admin → Stores → Configuration → Advanced → System → Mail Sending Settings → set 'Use SMTP' or install an extension such as 'Magento 2 SMTP' from the Marketplace.
- Log in to your DNS provider and add a TXT record: Name = '@', Value = v=spf1 include:[your-smtp-provider] a:your-mail-server-ip -all — replacing placeholders with your actual provider's include string.
- If using Adobe Commerce Cloud (cloud.adobe.com), consult Adobe's documentation for the SendGrid include string they provision (typically include:u[number].wl.sendgrid.net).
- Save, propagate, and verify with MXToolbox SPF Lookup.
- Send a test order confirmation and inspect the email headers to confirm 'spf=pass'.
v=spf1 include:_spf.google.com include:sendgrid.net include:spf.mailgun.org -allAdd this as a DNS TXT record at your domain apex ('@'). Replace the include: values with the exact hostnames from each of your email providers' SPF documentation. Only one SPF TXT record is allowed per domain — combine all providers into one record.
Missing strict transport securityQuick win
Add the Strict-Transport-Security (HSTS) response header to every HTTPS page so browsers always use an encrypted connection to your store.
On Adobe Commerce (Magento)
- SSH into your server or open your hosting control panel.
- For Apache: Edit the VirtualHost block in your HTTPS (port 443) configuration file (e.g. /etc/apache2/sites-available/yourdomain.conf) and add: Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains". Enable mod_headers if not already active: a2enmod headers && systemctl restart apache2.
- For Nginx: Edit your server block config and add: add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; inside the server { listen 443 ssl; } block, then run: nginx -t && systemctl reload nginx.
- Alternatively, in Adobe Commerce Admin go to Stores → Configuration → General → Web → Base URLs (Secure) and ensure all URLs use https://. Then set Use Secure URLs on Storefront and Use Secure URLs in Admin both to Yes. This does not add the HSTS header itself but ensures no HTTP fallback occurs alongside your server-level header.
- Flush the Magento cache: Admin → System → Cache Management → Flush Magento Cache.
- Verify with browser DevTools that the header appears on the storefront, admin, and any subdomain used for static assets.
<!-- HSTS cannot be set via an HTML meta tag. It MUST be sent as an HTTP response header. Use the server-level examples below. -->
<!-- Correct header value -->
<!-- Strict-Transport-Security: max-age=31536000; includeSubDomains -->Meta tags cannot set HSTS — it must be a real HTTP response header. This is a reminder of the correct header value to use at the server level.
Missing x content type optionsQuick win
Add the `X-Content-Type-Options: nosniff` HTTP response header to every page and asset your store serves.
On Adobe Commerce (Magento)
- For Nginx: edit `/etc/nginx/sites-available/your-magento-site.conf`, locate the `server {}` block, and add `add_header X-Content-Type-Options 'nosniff' always;`. Run `nginx -t` to test, then `systemctl reload nginx`.
- For Apache: open `.htaccess` in the Magento root (or your VirtualHost config) and add inside `<IfModule mod_headers.c>`: `Header always set X-Content-Type-Options "nosniff"`. Restart Apache.
- Alternatively, add the header programmatically in `app/code/YourVendor/YourModule/Plugin/ResponsePlugin.php` by creating a plugin on `Magento\Framework\App\Response\Http` that calls `$response->setHeader('X-Content-Type-Options', 'nosniff', true)` before dispatch.
- Flush Magento cache: Admin → System → Cache Management → Flush Magento Cache. Verify with DevTools.
<?php
// File: app/code/YourVendor/YourModule/Plugin/ResponsePlugin.php
namespace YourVendor\YourModule\Plugin;
use Magento\Framework\App\Response\Http as Response;
class ResponsePlugin
{
public function beforeSendResponse(Response $response): void
{
$response->setHeader('X-Content-Type-Options', 'nosniff', true);
}
}Create this file inside a custom module (replace YourVendor/YourModule). Register the plugin in di.xml. This covers all HTTP responses from Magento's front controller.
Missing x frame optionsQuick win
Add the X-Frame-Options HTTP response header set to DENY or SAMEORIGIN to prevent your store's pages from being embedded in iframes on other sites.
On Adobe Commerce (Magento)
- In Adobe Commerce / Magento 2, go to Admin → Stores → Configuration → General → Web → Default Pages — note: this section does not directly set headers, but Magento has a built-in X-Frame-Options setting.
- Navigate to Admin → Stores → Configuration → General → Web → Default Pages (or search 'frame' in config search).
- Alternatively, set it directly in app/etc/env.php or via the Magento CLI: bin/magento config:set web/frame_options SAMEORIGIN
- For Apache-hosted Magento, add to your .htaccess in the Magento root: Header always set X-Frame-Options 'SAMEORIGIN' inside <IfModule mod_headers.c>.
- For Nginx-hosted Magento, add to your server block in the Nginx config: add_header X-Frame-Options 'SAMEORIGIN' always; — then reload Nginx: sudo nginx -s reload.
- Flush Magento cache: Admin → System → Cache Management → Flush Magento Cache, then verify in browser DevTools.
# Set X-Frame-Options to SAMEORIGIN via Magento CLI
bin/magento config:set web/frame_options SAMEORIGIN
# Flush the Magento cache
bin/magento cache:flush
# Verify the setting
bin/magento config:show web/frame_optionsRun this Magento CLI command from your Magento root directory to set X-Frame-Options via Magento's built-in config, then flush cache.
Passive scan onlyModerate effort
Set up regular active security testing (DAST) on a staging copy of your store to catch vulnerabilities that passive header/cookie/TLS scans cannot find.
On Adobe Commerce (Magento)
- Adobe Commerce has native staging built in (Content → Staging & Preview on Commerce Cloud) — clone your environment using the CLI: 'magento-cloud environment:branch staging main'.
- On Magento Open Source, set up a staging server by cloning the codebase and database to a separate vhost or subdomain, then restrict access with .htaccess Basic Auth or an IP allowlist.
- Set the staging base URL: Stores → Configuration → General → Web → Base URLs → update to the staging domain.
- Run OWASP ZAP: Automated Scan → enter staging URL, configure script-based or form-based authentication under Contexts to reach the Magento admin panel and customer account areas.
- Review ZAP's Active Scan results; common Magento findings include exposed /admin path (change via Stores → Config → Advanced → Admin → Admin Base URL), outdated extensions, and session misconfigurations.
- Apply fixes (extension updates via Composer, config hardening via app/etc/env.php or admin panel), re-scan, then deploy to production with 'magento-cloud environment:merge' or your CI/CD pipeline.
<meta name="robots" content="noindex, nofollow">Add this meta tag to your staging site's <head> to prevent search engines from indexing it while DAST scanning is in progress.
Ssl cert expiring soonQuick win
Renew your SSL/TLS certificate before it expires to keep your store secure, trusted, and visible in search results.
On Adobe Commerce (Magento)
- Adobe Commerce does not manage SSL — the certificate lives at your web server (Apache/Nginx) or load balancer layer, or within Adobe Commerce Cloud.
- Adobe Commerce Cloud (Pro/Starter): SSL is managed via Fastly CDN. Log in to the Cloud Console (console.magento.com), navigate to your project → Environments → your environment → click 'Configure domain'. TLS certificates are provisioned and renewed automatically by Fastly for domains routed through it.
- Self-hosted / on-premise: Log in to your server, renew the certificate through your CA or Let's Encrypt (e.g. run: sudo certbot renew), then reload your web server (sudo systemctl reload nginx or apache2).
- After renewal, update the Base URLs in Magento Admin → Stores → Configuration → General → Web → Base URLs (Secure) to confirm https:// is used.
- Verify the certificate in your server's virtual host config file (/etc/nginx/sites-available/yourstore.conf or /etc/apache2/sites-available/yourstore.conf) references the new certificate file paths.
<!-- Place this in your maintenance/holding page only if HTTPS is temporarily unavailable -->
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">Meta-refresh redirect for maintenance: not applicable to SSL renewal. Instead, use this HTML to display a temporary notice if your cert has lapsed and you need to redirect HTTP traffic while fixing HTTPS.
Ssl cert invalidModerate effort
Install a valid SSL/TLS certificate that exactly matches your store's domain name, so browsers trust your site and customer data is encrypted in transit.
On Adobe Commerce (Magento)
- Adobe Commerce (self-hosted or cloud) requires you to manage SSL certificates at the server or cloud infrastructure level.
- For Adobe Commerce Cloud (PaaS): log in to the Cloud Console → select your project/environment → Domains. Add or update your custom domain and Adobe will provision a Let's Encrypt certificate via Fastly CDN automatically.
- For self-hosted Magento on Apache: copy your new certificate (.crt), private key (.key), and CA bundle (.ca-bundle) to the server. Edit your Apache VirtualHost file (e.g. /etc/apache2/sites-available/yourstore.conf): set SSLCertificateFile, SSLCertificateKeyFile, and SSLCACertificateFile to the correct paths. Restart Apache: sudo systemctl restart apache2.
- For self-hosted Magento on Nginx: update your server block with ssl_certificate and ssl_certificate_key pointing to the new files. Run: sudo nginx -t && sudo systemctl reload nginx.
- In Magento Admin → Stores → Configuration → General → Web, ensure 'Base URL (Secure)' is set to https:// with your correct domain, and 'Use Secure URLs on Storefront' and 'Use Secure URLs in Admin' are both set to Yes.
- Run bin/magento cache:flush after making Admin configuration changes.
Ssl errorModerate effort
Replace or reissue your SSL/TLS certificate so it is valid for the exact domain name your store uses, eliminating the hostname mismatch error.
On Adobe Commerce (Magento)
- Adobe Commerce (self-hosted or cloud) requires you or your hosting/DevOps team to manage SSL certificates at the server or load balancer level.
- For Adobe Commerce Cloud: log in to the Cloud Console (cloud.magento.com), select your project and environment, go to Settings → Variables or contact Adobe Support to upload a new SSL certificate via the CLI: `magento-cloud domain:update yourstore.com --cert=cert.pem --key=key.pem --chain=chain.pem`.
- For self-hosted Magento: access your server and use your CA (e.g. Let's Encrypt / Certbot) to issue a new certificate: `sudo certbot --apache -d yourstore.com -d www.yourstore.com` or the nginx equivalent.
- Ensure the certificate SANs cover all domain variants your store uses (bare domain, www, and any subdomains).
- In Magento Admin → Stores → Configuration → Web → Base URLs (Secure), confirm the base URL starts with https:// and matches the exact domain on the certificate.
- Flush Magento's cache (Admin → System → Cache Management → Flush Cache) after any URL or SSL changes.
Ssl not accessibleModerate effort
Enable HTTPS on your store by opening port 443 and installing a valid SSL/TLS certificate so every page is served over a secure connection.
On Adobe Commerce (Magento)
- Adobe Commerce (self-hosted or cloud) requires you to configure SSL at the server and application level.
- Server level: ensure port 443 is open in your server's OS firewall (`ufw allow 443/tcp`) and any cloud security group (AWS Security Group, GCP Firewall Rule).
- Install/renew your TLS certificate: use Let's Encrypt via Certbot (`sudo certbot --nginx -d yourdomain.com`) or upload your purchased certificate via your hosting control panel.
- In Magento Admin → Stores → Configuration → General → Web → Base URLs (Secure), set the Secure Base URL to https://yourdomain.com/.
- Also set 'Use Secure URLs on Storefront' and 'Use Secure URLs in Admin' to 'Yes' in the same configuration section.
- Run `bin/magento cache:flush` from the command line to apply the changes, then verify https:// loads correctly.
- For Adobe Commerce Cloud, SSL and port 443 are managed by Fastly CDN — configure the domain in the Cloud Console and Fastly will handle certificate provisioning automatically.
Vulnerable component axiosModerate effort
Update the Axios JavaScript library to its latest stable release to eliminate known security vulnerabilities in your storefront code.
On Adobe Commerce (Magento)
- On your server, navigate to your Magento root directory and run `npm list axios` (or `yarn list axios`) in any frontend theme or module that uses npm to confirm the installed version.
- Open the relevant package.json file (typically in app/design/frontend/Vendor/theme/ or a custom module's view/frontend/ directory) and update the axios version constraint to `^latest` or the specific latest version number.
- Run `npm install` (or `yarn`) followed by your frontend build command (e.g. `npm run build` or `grunt less` depending on your setup) to compile the updated bundle.
- If Axios is loaded via a CDN <script> tag in a layout XML file (.xml) or a PHTML template, locate the file under app/design/frontend/ or app/code/Vendor/Module/view/frontend/templates/, update the URL, then run `php bin/magento cache:flush`.
- Deploy the updated static content with `php bin/magento setup:static-content:deploy` and verify the new version loads in your browser's DevTools.
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<head>
<!-- Remove old vulnerable Axios if previously added -->
<remove src="js/axios.min.js"/>
<!-- Load updated Axios from CDN -->
<script src="https://cdn.jsdelivr.net/npm/axios@1.7.9/dist/axios.min.js"
src_type="url"/>
</head>
</page>In your theme's default_head_blocks.xml layout file (app/design/frontend/Vendor/Theme/Magento_Theme/layout/), add or update the Axios script reference.
Vulnerable component jqueryModerate effort
Update jQuery to the latest stable version (3.7+) to eliminate known cross-site scripting (XSS) and prototype-pollution vulnerabilities that attackers actively exploit.
On Adobe Commerce (Magento)
- Adobe Commerce (Magento 2) ships jQuery as a RequireJS module in 'lib/web/jquery.js'. Update Adobe Commerce to the latest patch release via Composer: run 'composer update magento/magento2-base' (or the full 'composer update') in your project root — this upgrades the bundled jQuery.
- For Magento Open Source, check the currently bundled jQuery version in 'lib/web/jquery.js' and compare to the current stable. If your Magento version is end-of-life, upgrading Magento itself is required.
- If you must patch jQuery independently (not recommended as a permanent fix), replace 'lib/web/jquery.js' and 'lib/web/jquery.min.js' with the downloaded files from jquery.com, matching the filename exactly.
- Check all installed extensions: in Admin → System → Web Setup Wizard (M2.3) or via Composer, review each extension's 'view/frontend/web/js/' directories for bundled jQuery copies — contact extension vendors for updates.
- Run 'php bin/magento setup:static-content:deploy' and 'php bin/magento cache:flush' after any file changes.
- Verify the fix by loading a storefront page, opening DevTools → Network, and filtering for 'jquery' to confirm the updated version is served.
# Step 1: Update Magento (which upgrades bundled jQuery)
composer update magento/magento2-base --with-dependencies
# Step 2: Redeploy static content for all locales
php bin/magento setup:static-content:deploy -f
# Step 3: Flush all caches
php bin/magento cache:flush
# Step 4: Verify the jQuery version in the deployed file
grep -m1 'jQuery JavaScript Library' pub/static/frontend/*/*/en_US/jquery.jsRun in your Magento project root to update jQuery via Composer, then redeploy static content.
Weak spfQuick win
Add a hard-fail (-all) or soft-fail (~all) terminator to your SPF DNS record so that mail servers reject or flag email sent from servers not explicitly authorized by your domain.
On Adobe Commerce (Magento)
- Adobe Commerce (Magento) is self-hosted or cloud-hosted; DNS is managed at your registrar or hosting provider, not within Magento itself.
- Log in to your DNS provider's control panel (cPanel Zone Editor, Plesk DNS, Cloudflare, Route 53, etc.).
- Locate the TXT record for your root domain beginning with `v=spf1`.
- Edit the record to append `-all` (hardfail) or `~all` (softfail) at the end of the record value.
- Save the record and confirm propagation with `dig TXT yourdomain.com` or an online SPF checker.
v=spf1 include:_spf.google.com include:sendgrid.net -allDNS TXT record value for your root domain (@). Replace the include/redirect mechanisms with the ones your email service provider specifies, and always end with -all or ~all.
X content type options weakQuick win
Set the X-Content-Type-Options response header to exactly `nosniff` (once, not duplicated) on every page and asset your store serves.
On Adobe Commerce (Magento)
- SSH into your server and open your Nginx virtual host config (typically at `/etc/nginx/sites-available/your-store.conf` or inside `/etc/nginx/conf.d/`) or your Apache `.htaccess` / virtual host config.
- For Nginx, add inside the `server {}` block: `add_header X-Content-Type-Options "nosniff" always;` — remove any duplicate `add_header X-Content-Type-Options` lines in the same or parent blocks.
- For Apache, in `.htaccess` or VirtualHost config add: `<IfModule mod_headers.c>` / `Header set X-Content-Type-Options "nosniff"` / `</IfModule>` — remove any duplicate directives.
- Alternatively, configure it in Magento's `pub/.htaccess` or via a custom plugin/module that sets response headers.
- Reload/restart the web server (`sudo nginx -t && sudo systemctl reload nginx` or `sudo systemctl reload apache2`).
- Verify with DevTools or securityheaders.com.
X frame options weakQuick win
Replace the weak X-Frame-Options header value with DENY or SAMEORIGIN to prevent clickjacking attacks on your store.
On Adobe Commerce (Magento)
- SSH into your server or use your hosting control panel to access your Nginx or Apache config.
- For Nginx: edit /etc/nginx/nginx.conf or the Magento vhost config (commonly /etc/nginx/sites-available/magento.conf) and add inside the server{} block: add_header X-Frame-Options "DENY" always; and add_header Content-Security-Policy "frame-ancestors 'none';" always;
- For Apache: edit your .htaccess in the Magento root (pub/.htaccess for Magento 2) and add: Header always set X-Frame-Options "DENY"
- Alternatively in Magento 2 Admin: go to Stores → Configuration → General → Web → and look for the 'X-Frame-Options Header' field (available in some versions) — set it to SAMEORIGIN or DENY.
- If running Magento 2.4+: you can also set this in app/etc/env.php or via a custom module that injects the header through a response plugin (interceptor on Magento\Framework\App\Response\Http).
- After deployment, run: bin/magento cache:flush, then verify headers with DevTools or curl -I https://yourdomain.com.
<?php
// File: app/code/YourVendor/SecurityHeaders/Plugin/AddSecurityHeaders.php
namespace YourVendor\SecurityHeaders\Plugin;
use Magento\Framework\App\Response\Http;
class AddSecurityHeaders
{
public function beforeSendResponse(Http $response): void
{
$response->setHeader('X-Frame-Options', 'DENY', true);
$response->setHeader('Content-Security-Policy', "frame-ancestors 'none';", true);
}
}For Magento 2: create a custom module or add to an existing module's etc/frontend/di.xml to inject the header via a plugin on the HTTP response.
Site Lifecycle · 12 fixes
Cms versionModerate effort
Update your CMS and ecommerce platform to the latest stable version immediately to close known security vulnerabilities and maintain vendor support.
On Adobe Commerce (Magento)
- Log in to your server via SSH. Run 'php bin/magento --version' to confirm the exact installed version.
- Check the current stable release and security patches at experienceleague.adobe.com/docs/commerce-operations/release/versions.html.
- For security patches only (lower risk): download the patch from the Adobe Commerce release page and apply using 'composer require magento/product-community-edition <patch-version> --no-update && composer update'.
- For full version upgrades: follow the official Adobe Commerce Upgrade Guide (experienceleague.adobe.com). Key steps: update composer.json, run 'composer update', then 'php bin/magento setup:upgrade', 'setup:di:compile', 'setup:static-content:deploy', and 'cache:flush'.
- Always test on a staging environment first. Use the Adobe Upgrade Compatibility Tool ('vendor/bin/uct upgrade:check') to identify breaking changes in your custom code before upgrading production.
- After upgrading, reindex: 'php bin/magento indexer:reindex' and verify storefront, admin, and checkout functionality.
#!/bin/bash
# Adobe Commerce / Magento Open Source — version upgrade via Composer
# Run from your Magento root directory as the web-server user (e.g., www-data or magento)
# 1. Enable maintenance mode
php bin/magento maintenance:enable
# 2. Back up the database (replace values with your actual credentials)
mysqldump -u DB_USER -p DB_NAME > backup_$(date +%Y%m%d_%H%M%S).sql
# 3. Update Magento via Composer (replace 2.4.7 with the target version)
composer require magento/product-community-edition 2.4.7 --no-update
composer update
# 4. Run Magento upgrade routines
php bin/magento setup:upgrade
php bin/magento setup:di:compile
php bin/magento setup:static-content:deploy -f
php bin/magento indexer:reindex
php bin/magento cache:flush
# 5. Disable maintenance mode
php bin/magento maintenance:disable
# 6. Verify installed version
php bin/magento --versionRun these commands via SSH in order to apply a Composer-based version upgrade on Adobe Commerce or Magento Open Source. Replace version numbers with the current stable release.
Domain expiryQuick win
Log in to your domain registrar, confirm your domain's expiry date, and enable auto-renew so your store never goes offline due to an expired domain.
On Adobe Commerce (Magento)
- Adobe Commerce does not manage domain registration — the domain was registered through a separate registrar or via your hosting/infrastructure provider.
- Log in to your registrar (often managed by your agency, DevOps team, or infrastructure provider). Find the domain and confirm auto-renew is enabled.
- If you use Cloudflare in front of your Magento store, remember that Cloudflare can act as a registrar — check both your original registrar AND Cloudflare Registrar (dash.cloudflare.com → Domain Registration) to confirm where the authoritative registration lives.
<!-- No HTML change required. Domain renewal is managed at your domain registrar's dashboard (e.g. GoDaddy, Namecheap, Cloudflare Registrar). Log in, find the domain, enable auto-renew, and confirm a valid payment method is on file. -->No code change is needed on your website — this fix is completed entirely in your registrar's dashboard. There is no markup or configuration file involved.
Gtm auditModerate effort
Install Google Tag Manager on your store and configure it to fire GA4 with full ecommerce event tracking (view_item, add_to_cart, purchase, and more).
On Adobe Commerce (Magento)
- Install the 'MagicToolbox GTM' extension or use the community 'Yireo GoogleTagManager2' module via Composer: composer require yireo/magento2-googletagmanager2.
- Go to Stores › Configuration › Sales › Google API › Google Tag Manager, enter your GTM Container ID, and enable.
- The module automatically pushes a data layer with Magento ecommerce events (view_item, add_to_cart, purchase) for GTM to consume.
- In GTM, create GA4 Event tags reading from the data layer and publish the container.
- Clear the Magento cache (System › Cache Management › Flush All) and verify with GTM Preview mode.
<!-- Google Tag Manager (paste in <head>) -->
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-XXXXXXX');</script>
<!-- End Google Tag Manager -->
<!-- Google Tag Manager (noscript) (paste immediately after <body>) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXXXXX"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<!-- End Google Tag Manager (noscript) -->Replace GTM-XXXXXXX with your real Container ID. First snippet goes in <head>, second immediately after <body>.
Html langQuick win
Add a lang attribute to your site's <html> tag that matches the primary language of your store's content.
On Adobe Commerce (Magento)
- In Magento admin, go to Content → Design → Configuration.
- Select your store view and look for HTML Head settings — Magento does not set lang via this UI, so you must edit the root template.
- Via SFTP or your server file manager, open app/design/frontend/<Vendor>/<Theme>/Magento_Theme/templates/root.phtml (for your custom theme) or the Magento_Theme module's root.phtml.
- Find <html and add <?= /* @escapeNotVerified */ $htmlAttributes ?> if it is already templated, or add lang="en" directly: <html lang="en">.
- For multi-store/multi-language setups, use the Magento locale resolver: inject the current locale as the lang value via a Block or ViewModel so each store view outputs its own language code.
- Deploy static content and clear the cache: bin/magento cache:clean, then verify with View Source.
<?php
/** @var \Magento\Framework\View\Element\Template $block */
$locale = $block->getViewFileUrl('') ? substr($block->helper(\Magento\Framework\App\Helper\Context::class)->getLocale()->getLocale(), 0, 2) : 'en';
?>
<!DOCTYPE html>
<html lang="<?= $block->escapeHtmlAttr($locale) ?>">
<head><?= $block->getChildHtml('head') ?></head>
<body<?= $block->getBodyAttribute() ?>>
<?= $block->getChildHtml('body') ?>
</body>
</html>In app/design/frontend/<Vendor>/<Theme>/Magento_Theme/templates/root.phtml — outputs lang from the store's configured locale.
Lifecycle oos schema not updatedModerate effort
Update the `offers.availability` value in your Product structured data from `InStock` to `OutOfStock` (or `PreOrder`) whenever a product sells out, so Google's records always match your actual inventory.
On Adobe Commerce (Magento)
- Adobe Commerce (Magento 2) generates Product schema via the `Magento_ProductAlert` and structured-data modules. The availability is read from the product's `is_saleable` / `isSalable()` method.
- Check whether a third-party SEO extension (e.g. Mageplaza SEO, Mageworx SEO, Amasty SEO) is overriding the schema output. Go to the extension's admin configuration (Stores → Configuration → [Extension Vendor] → SEO) and ensure availability is set to 'Dynamic' rather than a hardcoded value.
- If using a custom theme, locate the schema template — typically in `app/design/frontend/<Vendor>/<theme>/Magento_Catalog/templates/product/structured-data/` or similar. Find the availability condition and ensure it calls `$product->isSalable()` (Magento 2) to output the correct schema.org URL.
- Clear full-page cache after changes: in the admin go to System → Cache Management → Flush Magento Cache.
- Run a crawl or spot-check with Google's Rich Results Test on several out-of-stock product URLs to verify `OutOfStock` is now output.
<?php
/**
* Custom Product structured data template for Adobe Commerce / Magento 2.
* Path: app/design/frontend/<Vendor>/<Theme>/Magento_Catalog/templates/product/view/structured-data.phtml
*/
$product = $block->getProduct();
$availability = $product->isSalable()
? 'https://schema.org/InStock'
: 'https://schema.org/OutOfStock';
$structuredData = [
'@context' => 'https://schema.org',
'@type' => 'Product',
'name' => $product->getName(),
'sku' => $product->getSku(),
'description' => strip_tags($product->getDescription()),
'offers' => [
'@type' => 'Offer',
'url' => $product->getProductUrl(),
'priceCurrency' => $block->getCurrencyCode(),
'price' => number_format((float)$product->getFinalPrice(), 2, '.', ''),
'availability' => $availability,
'itemCondition' => 'https://schema.org/NewCondition',
],
];
?>
<script type="application/ld+json">
<?= /* @noEscape */ json_encode($structuredData, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) ?>
</script>Place this in your custom theme at app/design/frontend/Vendor/theme/Magento_Catalog/templates/product/view/structured-data.phtml (create it if it does not exist) to override availability output.
Lifecycle orphaned productsModerate effort
Add internal links pointing to every orphaned product page from relevant category pages, collection pages, or other on-site content so search crawlers and shoppers can discover them.
On Adobe Commerce (Magento)
- Log in to the Admin Panel and go to Catalog → Products. Open each orphaned product.
- Click the 'Categories' field and assign the product to all relevant categories. Save.
- Go to Catalog → Categories to create a new category if needed, then assign it products via the 'Products in Category' tab.
- Go to Content → Navigation → Top Navigation (or use a menu extension like Mageplaza Mega Menu) to add the category to the storefront nav.
- For related/cross-sell/upsell links, open the product → Related Products, Up-Sells, and Cross-Sells tab → add the orphaned products to related product lists of bestselling items in the same category.
- Flush the Magento cache (System → Cache Management → Flush Magento Cache) and reindex (System → Index Management → Reindex All) after making changes.
<!-- Add this inside a relevant category page, blog post, or 'Featured Products' section -->
<a href="/products/your-orphaned-product-slug">Product Display Name</a>Add a simple text link from any existing page (category, homepage, blog post) pointing directly to the orphaned product URL.
Lifecycle products missing from sitemapQuick win
Add all canonical product URLs to your XML sitemap so search engines discover and index every product page quickly.
On Adobe Commerce (Magento)
- Go to Admin → Marketing → SEO & Search → Site Map → click 'Add Sitemap' or edit the existing one.
- Set Frequency and Start Time, then click 'Save & Generate' to regenerate the sitemap file immediately.
- To configure what is included, go to Admin → Stores → Configuration → Catalog → XML Sitemap → Products section; set 'Frequency', 'Priority', and ensure 'Maximum No of URLs Per File' is large enough to cover your full catalog.
- Confirm products are enabled: Admin → Catalog → Products → filter by Status = 'Disabled' and re-enable any that should be live.
- Check that the sitemap file path and URL match what is referenced in your robots.txt (Admin → Content → Design → Configuration → your store view → 'Edit' → Other Settings → Search Engine Robots → 'Edit custom instruction' to confirm the Sitemap: line).
- Submit the sitemap URL in Google Search Console → Indexing → Sitemaps.
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<!-- Example product page entry -->
<url>
<loc>https://www.example.com/products/blue-running-shoes</loc>
<lastmod>2024-06-01</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<!-- Repeat one <url> block per canonical product page -->
<url>
<loc>https://www.example.com/products/red-yoga-mat</loc>
<lastmod>2024-05-20</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
</urlset>Standard XML sitemap structure. Place this at yourdomain.com/sitemap.xml (or reference it from a sitemap index file).
Lifecycle products too deepModerate effort
Move deeply buried product pages closer to the homepage by flattening your site's navigation structure so every product is reachable within 4 clicks (ideally 3) from the homepage.
On Adobe Commerce (Magento)
- In the Admin Panel go to Catalog → Categories.
- Examine your category tree in the left panel. Any category that is 3 or more levels below the Root Catalog places its products 4+ clicks from the homepage.
- Select an overly nested category and drag it up in the tree to a shallower parent, or edit it and change its parent in the 'Category Information' tab, then save.
- Go to Content → Pages and edit your CMS Homepage. Add a 'Catalog Products List' widget (Insert Widget → Catalog Products List) filtered to the previously buried products to give them a direct 1-click path.
- Go to Catalog → Categories → [Top-Level Category] → Display Settings and enable 'Anchor' so products from child categories are included in the parent category's listing page, effectively reducing the displayed click depth.
- Clear the cache (System → Cache Management → Flush Magento Cache) and re-crawl to verify.
<!-- Featured Products section on homepage or top category page -->
<section aria-label="Featured Products">
<h2>Featured Products</h2>
<ul>
<li><a href="/products/product-name-1">Product Name 1</a></li>
<li><a href="/products/product-name-2">Product Name 2</a></li>
<li><a href="/products/product-name-3">Product Name 3</a></li>
</ul>
</section>Add direct internal links to deep products from your homepage or top-level category pages. Place this markup in a 'Featured Products' or 'Quick Links' section.
Mixed contentModerate effort
Ensure every resource (images, scripts, stylesheets, fonts, iframes) on your store is loaded over HTTPS, never HTTP, so browsers never block or warn about mixed content.
On Adobe Commerce (Magento)
- In the Admin panel, go to Stores → Configuration → General → Web.
- Under 'Base URLs (Secure)', confirm all Base URL, Base Link URL, Base Skin URL, Base Media URL, and Base JavaScript URL values start with 'https://'.
- Under 'Base URLs' (the non-secure section), also set the Base URL to 'https://' to prevent any HTTP asset generation.
- Enable 'Use Secure URLs on Storefront' and 'Use Secure URLs in Admin' — set both to Yes.
- Run bin/magento cache:flush from the command line (or go to System → Cache Management → Flush Magento Cache).
- If hardcoded HTTP URLs persist in CMS pages or blocks, go to Content → Pages / Blocks, edit each, and update URLs in the HTML source view.
- Re-test using browser DevTools Console.
# Set secure base URL (replace with your actual domain)
php bin/magento config:set web/secure/base_url "https://www.yourdomain.com/"
# Force HTTPS on storefront and admin
php bin/magento config:set web/secure/use_in_frontend 1
php bin/magento config:set web/secure/use_in_adminhtml 1
# Flush cache
php bin/magento cache:flushRun this from the Magento root in CLI to update the base secure URL, then flush cache.
Mobile viewportQuick win
Add a responsive viewport meta tag to every page so mobile browsers render your site at the correct width and scale.
On Adobe Commerce (Magento)
- SSH into your server and navigate to your active theme directory: `app/design/frontend/<Vendor>/<theme>/Magento_Theme/layout/`.
- Open (or create) `default_head_blocks.xml`.
- Add the viewport meta tag using the layout XML instruction shown in the code snippets.
- Alternatively, edit your theme's `app/design/frontend/<Vendor>/<theme>/Magento_Theme/templates/root.phtml` and add the tag directly in the `<head>` section.
- Run `bin/magento cache:flush` and `bin/magento setup:static-content:deploy` to apply the change.
- Verify on a mobile device or Chrome DevTools.
<?xml version="1.0"?>
<!-- File: app/design/frontend/<Vendor>/<Theme>/Magento_Theme/layout/default_head_blocks.xml -->
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
</head>
</page>After saving, run: bin/magento cache:flush && bin/magento setup:static-content:deploy
Server versionQuick win
Remove or suppress HTTP response headers that advertise your server software name and version to the public internet.
On Adobe Commerce (Magento)
- In Nginx (recommended for production), add 'server_tokens off;' to the 'http {}' block in '/etc/nginx/nginx.conf', and add 'fastcgi_hide_header X-Powered-By;' in the relevant 'location ~ \.php$' block.
- In Apache, add 'ServerTokens Prod', 'ServerSignature Off', and 'Header unset X-Powered-By' in your VirtualHost config or '.htaccess'.
- In 'php.ini' (or 'php-fpm.conf'), set 'expose_php = Off' to prevent PHP from adding 'X-Powered-By: PHP/x.x' headers.
- If using Varnish in front of Magento, add 'unset beresp.http.X-Powered-By; unset beresp.http.Server;' in your VCL 'vcl_backend_response' subroutine.
- In Adobe Commerce Cloud (Magento Cloud), modify '.magento.app.yaml' or use the Fastly CDN service (included) → navigate to Fastly configuration → add a custom VCL snippet to strip headers.
- Run 'curl -I https://yourdomain.com' from a terminal or use a header-checker tool to confirm no version strings appear in the response.
server {
listen 443 ssl;
server_name example.com;
# Hide Nginx version from Server header
server_tokens off;
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
# Remove PHP version disclosure
fastcgi_hide_header X-Powered-By;
}
# ... rest of your config
}Add to your Nginx server block (e.g. /etc/nginx/sites-available/magento.conf). Restart Nginx after saving.
Ssl expiryQuick win
Renew your TLS/SSL certificate before it expires and enable automatic renewal so your store never goes offline or shows security warnings.
On Adobe Commerce (Magento)
- Adobe Commerce Cloud: SSL certificates are managed by Adobe's infrastructure. Log in to the Cloud Console (https://cloud.magento.com), navigate to your environment, and confirm the domain's SSL status under Environments → [your env] → Routes.
- For self-hosted Magento Open Source: log in to your server, check Certbot with `sudo certbot certificates`, and ensure a renewal cron/timer is active (`sudo systemctl status certbot.timer`).
- After renewal, clear the full-page cache in Admin → System → Cache Management and flush the Varnish cache if applicable.
- Confirm Magento's Base URLs under Admin → Stores → Configuration → General → Web both use 'https://'.
<!-- SERVER-SIDE redirect is strongly preferred. Example nginx config snippet below. -->
<!-- nginx: force HTTPS -->
<!-- server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
} -->Verify your HTTPS redirect is in place at the server/app level. This meta-refresh is a last-resort fallback only — implement a proper server-side 301 redirect instead.