Every Magento Open Source fix we catalog
SEOLZ catalogs 113 fixes for Magento Open Source across 5 areas — SEO, answer-engine readiness, accessibility, security and site-health. Each lists the exact steps for Magento Open Source, with a link to the official docs.
SEO · 35 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 Magento Open Source
- Follow the same steps as Adobe Commerce (Magento) above — the admin paths and sitemap configuration are identical in Magento Open Source.
- Run bin/magento indexer:reindex after making visibility or category changes to ensure the sitemap and flat tables are up to date.
- Check pub/robots.txt via Admin › Content › Design › Configuration to confirm crawl paths are open.
<!-- CORRECT: Standard crawlable link -->
<a href="/products/blue-running-shoes">Blue Running Shoes</a>
<!-- WRONG: JavaScript-only — Googlebot cannot follow this -->
<span onclick="navigate('/products/blue-running-shoes')">Blue Running Shoes</span>Standard crawlable product link — use plain <a href> tags, never JavaScript-only click handlers, for navigation links that Google must follow.
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 Magento Open Source
- Follow all Adobe Commerce (Magento) steps above — the admin paths and configuration options are identical.
- Additionally, use the free Yireo Webp2 or Hyva theme (if applicable) — the Hyva theme replaces Magento's heavy RequireJS/KnockoutJS stack with lightweight Alpine.js + Tailwind CSS, which dramatically reduces INP on many stores.
- Run `bin/magento setup:static-content:deploy` and `bin/magento cache:flush` after any JS configuration change.
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 Magento Open Source
- Same steps as Adobe Commerce (Magento) above — go to Stores → Configuration → Catalog → Catalog → Search Engine Optimization and set 'Use Canonical Link Meta Tag for Categories' to 'Yes'.
- For community extensions that add faceted/layered navigation, check the extension's configuration for SEO options including canonical and noindex settings.
- After configuration, flush the cache via System → Cache Management and verify by inspecting a filtered URL's page source.
<head>
<!-- Canonical points to the CLEAN category URL, not the filtered variant -->
<link rel="canonical" href="https://www.example.com/womens-pants">
</head>Place this in the <head> of every filtered/faceted page. The href must be the clean parent category URL — never the filtered URL itself.
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 Magento Open Source
- FOR PRODUCT IMAGES: Admin > Catalog > Products > open product > Images and Videos section > hover over image > gear icon > fill 'Alt Text' field > Save.
- FOR CMS CONTENT: Admin > Content > Pages or Blocks > open item > WYSIWYG editor > click image > Insert Image icon > fill 'Image Description' (alt) field.
- FOR BULK IMPORT: System > Data Transfer > Import > select Products entity type > prepare CSV with additional_image_labels column populated > upload and import.
<!-- BEFORE (missing alt — bad) -->
<img src="/images/red-leather-bag.jpg">
<!-- AFTER (descriptive alt — correct) -->
<img src="/images/red-leather-bag.jpg" alt="Red leather crossbody bag with gold buckle clasp">
<!-- Decorative image (intentionally empty alt — correct) -->
<img src="/images/divider-line.png" alt="">Place this wherever an <img> tag appears in your HTML. Replace the placeholder text with your real description.
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 Magento Open Source
- Follow the same steps as Adobe Commerce (Magento) above — the template paths and admin UI are identical.
- If using a third-party theme (e.g. Luma child theme), copy the gallery template to your child theme's override path before editing.
<!-- Informative product image — describe what the image shows -->
<img src="/images/red-running-shoes-side-view.jpg"
alt="Red canvas running shoes, side view, white rubber sole"
width="800" height="800">
<!-- Decorative image — empty alt tells screen readers to skip it -->
<img src="/images/divider-wave.svg"
alt=""
role="presentation"
width="1200" height="40">Correct alt text pattern for informative images vs. decorative images.
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 Magento Open Source
- Enable full-page cache: Admin → System → Cache Management → enable 'Full Page Cache'; configure Varnish via Admin → Stores → Configuration → Advanced → System → Full Page Cache.
- Enable JS/CSS minification: Admin → Stores → Configuration → Advanced → Developer → set Minify JavaScript Files and Minify CSS Files to Yes (use in production mode only).
- Set up a CDN: configure static and media base URLs to point to a CDN endpoint in Admin → Stores → Configuration → General → Web.
- Compress images in bulk using a free Magento 2 extension (e.g., Kraken Image Optimizer) or pre-process images before uploading to pub/media/.
- Enable GZIP at the server level in your Nginx config: add 'gzip on; gzip_types text/css application/javascript image/svg+xml;' to the server block, then reload Nginx.
- Run bin/magento deploy:mode:set production and bin/magento setup:static-content:deploy to bundle and minify all static assets.
- Monitor performance with PageSpeed Insights after each change.
<!-- 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 Magento Open Source
- Follow the same steps as Adobe Commerce (Magento) — the admin path and SEO fields are identical.
- After saving, flush the cache via System → Cache Management → Flush Magento Cache.
<meta name="description" content="Award-winning Bang & Olufsen headphones, speakers and TVs. Visit our Malaysia showroom at The Gardens Mall, Level 2. Free shipping on orders over RM500.">Place this inside the <head> element of every page. Replace the content value with your page-specific description (max 160 characters).
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 Magento Open Source
- Admin → Catalog → Products → select the product → 'Search Engine Optimization' section → update 'Meta Description' → Save.
- For categories: Admin → Catalog → Categories → select category → 'Search Engine Optimization' tab → update 'Meta Description' → Save.
- For CMS pages: Admin → Content → Pages → select page → 'Search Engine Optimization' tab → update 'Meta Description' → Save.
<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 Magento Open Source
- Follow identical steps to Adobe Commerce above — the canonical settings under Stores → Configuration → Catalog → Search Engine Optimization apply to Magento Open Source as well.
- Set both 'Use Canonical Link Meta Tag for Categories' and 'Use Canonical Link Meta Tag for Products' to 'Yes'.
- Flush the cache via System → Cache Management → Flush Magento Cache.
- Verify via page source on a product and category page.
<link rel="canonical" href="https://www.yourstore.com/your-page/" />Paste this inside the <head> element of every page. Replace the href value with the full, absolute preferred URL of that specific page.
Missing h1Quick win
Add a single, descriptive H1 tag to every page that currently lacks one, clearly stating the page's main topic.
On Magento Open Source
- Follow the same steps as Adobe Commerce (Magento) above — the admin paths and template structure are identical in Magento Open Source.
<h1>Women's Running Shoes</h1>Place exactly one of these per page, as the first heading in the main content area.
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 Magento Open Source
- Identical to Adobe Commerce steps above. Navigate Admin → Catalog → Products (or Categories / Content → Pages) → open the item → 'Search Engine Optimization' section → 'Meta Description'.
- Flush cache: System → Cache Management → Flush Magento Cache.
<head>
<meta charset="UTF-8">
<meta name="description" content="Shop our full range of handmade leather wallets. Free shipping on orders over $50. Slim, durable & personalised — order yours today.">
<title>Handmade Leather Wallets | YourStoreName</title>
</head>Place this tag inside the <head> element of every HTML page. Keep the content between 120–160 characters.
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 Magento Open Source
- Follow the same steps as Adobe Commerce (Magento) above — the theme file structure and extension approach are identical.
<head>
<!-- Open Graph / Social Sharing -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://www.example.com/page-url/" />
<meta property="og:title" content="Page Title – Brand Name" />
<meta property="og:description" content="A concise, compelling summary of this page in 100–160 characters that encourages clicks." />
<meta property="og:image" content="https://www.example.com/images/social-preview.jpg" />
</head>Place all Open Graph tags inside the <head> element of every page.
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 Magento Open Source
- Follow the same steps as Adobe Commerce (Magento) above — the admin path, layout XML approach, and recommended extensions are identical for Magento Open Source.
Missing titleQuick win
Add a unique, descriptive title tag (30–60 characters) to every page that is missing one.
On Magento Open Source
- Same as Adobe Commerce — go to Catalog → Products (or Categories) or Content → Pages → select the item → expand 'Search Engine Optimization' → fill in 'Meta Title'.
- Flush the cache after saving: 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 Magento Open Source
- Follow the same steps as Adobe Commerce (Magento) above — the template file paths and cache-clearing process are identical for Magento Open Source.
<!-- CORRECT: One H1 per page -->
<h1>Blue Running Shoes – Men's Lightweight Trainers</h1>
<!-- Major sections use H2 -->
<h2>Product Details</h2>
<p>Breathable mesh upper, responsive foam midsole...</p>
<h2>Customer Reviews</h2>
<!-- Sub-sections within a major section use H3 -->
<h3>Top Review: 5 Stars</h3>
<p>Best shoes I've ever bought!</p>
<!-- WRONG: Multiple H1 tags — do NOT do this -->
<!--
<h1>Blue Running Shoes</h1>
<h1>Product Details</h1>
<h1>Customer Reviews</h1>
-->Correct semantic heading structure for an ecommerce page — one H1, everything else H2 or lower.
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 Magento Open Source
- Follow the same steps as Adobe Commerce (Magento) above — Stores → Configuration → Catalog → Search Engine Optimization to manage canonical tag settings.
- For individual pages, edit the product, category, or CMS page and check the 'Search Engine Optimization' panel for any custom canonical URL field; clear or correct it.
- Flush cache via System → Cache Management after making changes, then verify the canonical in the page source.
<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.
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 Magento Open Source
- Follow the same steps as Adobe Commerce (Magento) above — the URL Rewrites manager and CMS editors are identical in Magento Open Source.
- Go to Marketing → SEO & Search → URL Rewrites → Add URL Rewrite. Set the old path as Request Path, the new destination as Target Path, and Redirect Type as 'Permanent (301)'.
- Update or remove the broken internal links in Content → Pages or Content → Blocks as described above.
- Flush the cache via System → Cache Management after saving all changes.
<!-- 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.
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 Magento Open Source
- Follow the same steps as Adobe Commerce (Magento) above — the admin path and layout XML approach are identical for Magento Open Source.
- Edit app/design/frontend/<Vendor>/<Theme>/Magento_CatalogSearch/layout/catalogsearch_result_index.xml to inject the noindex meta tag.
- Add Disallow: /catalogsearch/ to your robots.txt via Content → Design → Configuration → Search Engine Robots.
- Flush cache via System → Cache Management.
<meta name="robots" content="noindex,follow">Place this tag inside the <head> element of every search results page template.
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 Magento Open Source
- Follow the same steps as Adobe Commerce (Magento) above — the Catalog → Categories → Content → Description path is identical in Magento Open Source.
- After saving, run php bin/magento cache:flush from your server terminal to clear the full-page cache so the description appears immediately on the live site.
<section class="category-description">
<h1>Women's Waterproof Hiking Boots</h1>
<p>
Our women's waterproof hiking boots are built for trail runners, weekend
trekkers, and serious mountaineers who refuse to let wet weather slow them
down. Every pair in this collection features a fully sealed waterproof
membrane, aggressive rubber outsoles for grip on loose or muddy terrain,
and reinforced ankle support to protect against sprains on uneven ground.
Whether you're day-hiking a local forest trail or taking on a multi-day
alpine route, you'll find boots here rated for every conditions and
experience level. Lightweight options keep pack weight low on fast-and-light
adventures, while insulated models keep feet warm below freezing. Browse the
full range below to find your perfect fit.
</p>
</section>Place this block near the top of your category page, above the product grid.
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 Magento Open Source
- Log in to the Magento Open Source Admin and navigate to Catalog > Categories.
- Select the target category from the tree.
- Open the 'Content' accordion and expand the 'Description' field (WYSIWYG editor).
- Enter at least 100 words of unique, relevant description text.
- Save and then flush the cache via System > Cache Management.
<section class="category-description">
<h2>Shop Our Women's Running Shoes</h2>
<p>
Discover our curated collection of women's running shoes, designed for every
type of runner — from casual joggers to marathon enthusiasts. Whether you
need extra cushioning for long-distance road running, a lightweight racer
for speed training, or a grippy trail shoe for off-road adventures, you'll
find the perfect fit here.
</p>
<p>
We stock top brands including Brooks, ASICS, New Balance, and Hoka, all
rigorously tested for comfort, durability, and performance. Each pair is
available in a wide range of widths and sizes, so you never have to
compromise on fit. Browse by surface type, cushion level, or pronation
support to narrow down your ideal shoe in seconds.
</p>
<p>
Not sure where to start? Check out our
<a href="/guides/running-shoe-guide">Running Shoe Buying Guide</a> or
explore our <a href="/collections/running-accessories">Running Accessories</a>
collection to complete your kit.
</p>
</section>Paste this pattern into any category page template. Replace placeholder text with your real content.
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 Magento Open Source
- Follow the same steps as Adobe Commerce (Magento) above — the Catalog → Categories path, SEO tab, and URL Rewrites are identical in Magento Open Source.
<meta name="robots" content="noindex, follow">Place inside the <head> element of an empty category page to prevent Google from indexing it.
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 Magento Open Source
- Follow the same steps as Adobe Commerce (Magento) above — the Catalog → Categories path and URL Rewrite manager are identical in Magento Open Source.
- For the noindex option, install a community SEO extension (e.g., Mageplaza SEO) if the core admin does not expose a per-category noindex field.
<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 Magento Open Source
- Follow the same steps as Adobe Commerce above: Stores → Configuration → Catalog → Catalog → Search Engine Optimization → set both canonical meta tag options to 'Yes'.
- Flush cache at System → Cache Management after saving.
- Verify by loading a filtered category URL, viewing page source, and confirming <link rel="canonical"> points to the base category URL.
<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 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 Magento Open Source
- In the admin panel, go to Catalog → Products → Edit the product.
- Expand the 'Content' section and add your full description in the 'Description' WYSIWYG editor.
- Expand the 'Search Engine Optimization' section and fill in the Meta Description field.
- Save the product and flush caches under System → Cache Management.
<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 Magento Open Source
- Same path as Adobe Commerce: Admin → Catalog → Products → [product] → Content section → Description field.
- Edit using the WYSIWYG editor, save, and flush cache via System → Cache Management.
<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 Magento Open Source
- Follow the same Admin → Catalog → Products → Images and Videos → Alt Text path as Adobe Commerce.
- For mass updates, use Admin → System → Data Transfer → Import with a CSV containing SKU and image alt columns.
<!-- BAD: filename-based alt text -->
<img src="/images/IMG_4823_blue_v2.webp" alt="IMG_4823_blue_v2.webp">
<!-- BAD: empty alt text -->
<img src="/images/blue-sweater.webp" alt="">
<!-- GOOD: descriptive alt text -->
<img src="/images/blue-sweater.webp" alt="Blue cotton crew-neck sweater, front view">The correct pattern for a product image alt attribute — descriptive, specific, keyword-natural.
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 Magento Open Source
- Follow the same steps as Adobe Commerce above — the theme template path and attribute approach is identical for Magento Open Source.
- Use the same `structured-data.phtml` override method and map `manufacturer` or your custom brand attribute into the JSON-LD output.
- Flush cache after saving and validate with Google's Rich Results Test.
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 Magento Open Source
- Same process as Adobe Commerce: create a 'gtin' product attribute, populate it per product, and use a structured data module or custom module to output it in JSON-LD.
- Popular open-source options: 'mageplaza/module-seo' supports product schema and can be configured to include custom attributes like GTIN.
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 Magento Open Source
- Follow the same steps as Adobe Commerce above — the structured data template and admin inventory paths are identical in Magento Open Source.
- If using a third-party theme (e.g. Luma child, Porto, Amasty), check whether the theme overrides `Magento_Catalog/templates/product/view/structured_data.phtml` and ensure `availability` is preserved in that override.
- Install the free *MageWorx SEO Suite* or *Amasty SEO Toolkit* extension if you need a UI-driven way to control structured data fields including `availability` without editing PHP templates.
<!-- 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 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 Magento Open Source
- Follow the same attribute and template steps as Adobe Commerce above — the attribute management path and 'More Information' tab rendering are identical in Magento Open Source.
- Override 'attributes.phtml' in your custom theme to restyle the spec table without modifying core files.
- Flush the cache after any template or attribute change.
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 Magento Open Source
- Follow the same steps as Adobe Commerce (Magento) above — the template and rendering pipeline are identical.
- Use app/design/frontend/<Vendor>/<Theme>/Magento_Catalog/templates/product/price/final_price.phtml to override price rendering if needed.
- Add JSON-LD Product schema with price via a layout XML update (catalog_product_view.xml) that adds a new block rendering the JSON-LD partial.
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 Magento Open Source
- Steps are identical to Adobe Commerce (Magento). Go to Catalog → Products → edit a product → Search Engine Optimization section → update Meta Title.
- For store-wide default title templates, go to Stores → Configuration → Catalog → Catalog → Product Fields Auto-generation.
<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 Magento Open Source
- Follow the same steps as Adobe Commerce (Magento) above — the admin path and configuration keys are identical.
- For programmatic sitemap generation via cron, edit app/code/[Vendor]/Sitemap/Model/Sitemap.php (or the equivalent community module) to filter getCollection() results and exclude URLs with query parameters.
- Run bin/magento cron:run --group=index to regenerate, then verify the output XML file.
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<!-- CORRECT: canonical base product URL -->
<url>
<loc>https://www.example.com/products/blue-sneaker</loc>
<lastmod>2024-06-01</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<!-- WRONG — never include variant query strings:
<url>
<loc>https://www.example.com/products/blue-sneaker?variant=12345</loc>
</url>
<url>
<loc>https://www.example.com/products/blue-sneaker?sku=blue-large</loc>
</url>
-->
</urlset>Correct sitemap entry — clean base product URL only, no query parameters.
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 Magento Open Source
- Identical to Adobe Commerce: Stores → Configuration → Catalog → Catalog → Search Engine Optimization → set 'Use Canonical Link Meta Tag for Products' to Yes.
- Flush cache after saving: System → Cache Management → Flush Magento Cache.
- Verify with View Source on a variant URL that the canonical points to the base product URL.
<head>
<!-- All variant URLs (e.g. ?color=Red, ?size=M, ?variant=123) point canonical to the base product URL -->
<link rel="canonical" href="https://www.example.com/products/my-product-name">
</head>Place this inside the <head> of EVERY variant URL. The href must be the clean, base product URL — never the variant URL itself.
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 Magento Open Source
- Follow the same steps as Adobe Commerce (Magento): Catalog → Products / Categories, expand 'Search Engine Optimization', edit 'Meta Title' to ≤60 characters.
- Flush the cache via System → Cache Management after saving.
<head>
<meta charset="UTF-8">
<title>Running Shoes for Men | YourBrand</title>
</head>Place inside the <head> element of every HTML page. Keep the total character count at or under 60.
Answer Engine Optimization · 5 fixes
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 Magento Open Source
- Same approach as Adobe Commerce above — use Magefan Blog or a similar community blog module.
- Edit the blog post phtml template to inject the JSON-LD block, referencing the author's name, bio URL, and sameAs links from the post or author model.
- 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.
Low review countModerate effort
Collect at least 5 published reviews so Google will display star ratings in search results alongside your product listings.
On Magento Open Source
- Follow the same Adobe Commerce steps above — the native review system and file paths are identical in the open-source edition.
- For structured data, add or verify the AggregateRating block in your theme's product view template, or install a free Magento 2 SEO extension such as 'SEO Suite by Mageworx' that handles this automatically.
- Use the built-in cron-triggered reminder email module, or install 'Mirasvit Follow Up Email' for post-purchase review request automation.
<!-- 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 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 Magento Open Source
- Same approach as Adobe Commerce: Content → Design → Configuration → Edit your store view → HTML Head → Scripts and Style Sheets field.
- Paste the <script type='application/ld+json'>…</script> block and save, then flush caches via System → Cache Management.
- For version-controlled deployments, add the script via app/design/frontend/<Vendor>/<theme>/Magento_Theme/layout/default_head_blocks.xml or a custom module.
<!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 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 Magento Open Source
- Follow the same steps as Adobe Commerce above — the template path and PHP variable approach is identical in Magento Open Source.
- Flush the cache after any template change via bin/magento cache:flush in the CLI or via the admin Cache Management panel.
- Validate with Google's Rich Results Test.
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 Magento Open Source
- Follow the same steps as Adobe Commerce (Magento) above — the codebase and approach are identical for structured data fixes.
- After editing theme/module files, run bin/magento cache:flush from the CLI to clear all caches.
<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.
Accessibility (WCAG) · 41 fixes
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 Magento Open Source
- Follow the same steps as Adobe Commerce (Magento) above — template overrides live under `app/design/frontend/<Vendor>/<theme>/`.
- After editing, run `bin/magento cache:clean` and redeploy static content, then re-audit.
<!-- ❌ 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 Magento Open Source
- Follow the same theme-override and template-editing steps as Adobe Commerce (Magento) above.
- Locate the relevant .phtml or Knockout .html template, copy it to your custom theme, add `aria-label` (and `aria-hidden="true"` on decorative SVGs), flush cache, and re-deploy static content.
<!-- OPTION 1: aria-label on an icon-only button -->
<button type="button" aria-label="Open cart">
<svg aria-hidden="true" focusable="false" width="24" height="24" viewBox="0 0 24 24">
<path d="M6 2L3 6v14a2 2 0 002 2h14a2 2 0 002-2V6l-3-4z"/>
<line x1="3" y1="6" x2="21" y2="6"/>
<path d="M16 10a4 4 0 01-8 0"/>
</svg>
</button>
<!-- OPTION 2: visually-hidden span inside the button -->
<button type="button">
<svg aria-hidden="true" focusable="false" width="24" height="24" viewBox="0 0 24 24">
<path d="M6 2L3 6v14a2 2 0 002 2h14a2 2 0 002-2V6l-3-4z"/>
</svg>
<span class="sr-only">Open cart</span>
</button>
<!-- OPTION 3: aria-labelledby pointing to existing visible text -->
<h2 id="cart-heading">Your Cart</h2>
<button type="button" aria-labelledby="cart-heading">View</button>
<!-- OPTION 4: icon-only link with aria-label -->
<a href="/cart" aria-label="View shopping cart">
<img src="cart-icon.svg" alt="" role="presentation" />
</a>Apply one of these patterns to any button, link, or menuitem that contains only an icon or image.
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 Magento Open Source
- Follow the same steps as Adobe Commerce (Magento) above — the template override approach and RequireJS module pattern are identical.
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 Magento Open Source
- Follow the same steps as Adobe Commerce (Magento) above — the template override process and file paths are identical for Magento Open Source 2.x.
<label for="customer-email">Email address</label>
<input type="email" id="customer-email" name="email" autocomplete="email">Option 1 (preferred): Visible label associated via 'for' + 'id'. Use this whenever a visible label fits the design.
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 Magento Open Source
- Follow the same steps as Adobe Commerce above — template overrides in app/design/frontend/[Vendor]/[theme]/, JS customisation via requireJS mixins, and static content deployment after changes.
<!-- 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 Magento Open Source
- Follow the same steps as Adobe Commerce above — the template override mechanism is identical in Magento Open Source.
- Use bin/magento cache:flush after any template change.
- Verify with axe DevTools browser extension.
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 Magento Open Source
- Follow the same steps as Adobe Commerce (Magento) above — locate the relevant PHTML template in your custom theme, add the parent role wrapper, flush caches, and verify with axe.
<!-- 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.
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 Magento Open Source
- Follow the same steps as Adobe Commerce above — template paths and cache-clearing commands are identical for Magento Open Source.
<!-- PATTERN 1: Icon-only button — add aria-label directly on the button -->
<button type="button" aria-label="Open search">
<svg aria-hidden="true" focusable="false" width="24" height="24" viewBox="0 0 24 24">
<path d="M21 21l-4.35-4.35M17 11A6 6 0 1 1 5 11a6 6 0 0 1 12 0z"
stroke="currentColor" stroke-width="2" fill="none"/>
</svg>
</button>
<!-- PATTERN 2: Button with an <img> icon — give the img a descriptive alt -->
<button type="button">
<img src="/icons/cart.svg" alt="View shopping cart" width="24" height="24">
</button>
<!-- PATTERN 3: Button labelled by visible text elsewhere on the page -->
<h2 id="newsletter-heading">Subscribe to our newsletter</h2>
<!-- ... form fields ... -->
<button type="submit" aria-labelledby="newsletter-heading">Subscribe</button>
<!-- PATTERN 4: Visible text button — just ensure the text is real HTML text -->
<button type="button">Add to Cart</button>
<!-- PATTERN 5: Quantity stepper buttons -->
<button type="button" aria-label="Decrease quantity">−</button>
<input type="number" value="1" aria-label="Product quantity">
<button type="button" aria-label="Increase quantity">+</button>Use one of these patterns depending on your button's content. Apply the correct pattern to every icon-only or ambiguous button on your site.
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 Magento Open Source
- Same process as Adobe Commerce above — locate your custom theme's CSS source files under app/design/frontend/<Vendor>/<theme>/web/css/ and override the failing color rule.
- Deploy static content and clear cache to see changes.
/* BEFORE (fails WCAG 1.4.3 — ratio 4.47:1 on white) */
.secondary-text {
color: #777777;
}
/* AFTER (passes WCAG 1.4.3 — ratio ~7.0:1 on white) */
.secondary-text {
color: #595959;
}Replace the failing color with one that achieves 4.5:1+ contrast on white (#ffffff). #595959 achieves ~7.0:1 on white.
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 Magento Open Source
- Follow the same steps as Adobe Commerce above — the template file paths and cache-flush command are identical for Magento Open Source.
<!-- ✅ CORRECT: only <dt> and <dd> as direct children -->
<dl>
<dt>Material</dt>
<dd>100% Organic Cotton</dd>
<dt>Weight</dt>
<dd>180 GSM</dd>
<dt>Care Instructions</dt>
<dd>Machine wash cold</dd>
<dd>Tumble dry low</dd>
</dl>
<!-- ✅ ALSO CORRECT: <div> wrappers for layout/styling -->
<dl>
<div>
<dt>Material</dt>
<dd>100% Organic Cotton</dd>
</div>
<div>
<dt>Weight</dt>
<dd>180 GSM</dd>
</div>
</dl>
<!-- ❌ WRONG: stray <p>, <span>, or plain text directly inside <dl> -->
<!-- <dl>
<p>Product Details</p>
<dt>Material</dt>
<dd>Cotton</dd>
</dl> -->Correct `<dl>` structure — use this pattern for all product specs, FAQs, and paired term/value content.
Document titleQuick win
Add a descriptive, non-empty `<title>` element to every page so browsers, screen readers, and search engines can identify it.
On Magento Open Source
- Follow the same admin paths as Adobe Commerce (Magento) above.
- For theme-level fixes, edit `app/design/frontend/<Vendor>/<theme>/Magento_Theme/layout/default_head_blocks.xml`.
- Run `bin/magento cache:clean` from the command line after making theme changes.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Women's Running Shoes – Acme Sports</title>
</head>
<body>
<!-- page content -->
</body>
</html>Place this inside the <head> section of every HTML page. Replace the text with the actual page and store name.
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 Magento Open Source
- Follow the same steps as Adobe Commerce (Magento) above — the template structure, CLI commands, and admin panel paths are identical for the open-source edition.
<!-- ❌ WRONG: empty heading — announced as 'heading' with no label -->
<h2></h2>
<!-- ✅ CORRECT option 1: add visible, descriptive text -->
<h2>Featured Products</h2>
<!-- ✅ CORRECT option 2: visually-hidden text (visible to screen readers, hidden on screen) -->
<!-- Add this CSS once in your stylesheet: -->
<!--
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
-->
<h2><span class="sr-only">Featured Products</span></h2>
<!-- ✅ CORRECT option 3: aria-label when removing the tag is not possible -->
<h2 aria-label="Featured Products"></h2>
<!-- ✅ CORRECT option 4: not a heading at all — use a div instead -->
<div class="decorative-divider"></div>The wrong pattern and the two correct alternatives — visible text, or visually-hidden text for screen readers only.
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 Magento Open Source
- Follow the same steps as Adobe Commerce above — the template file paths and cache-flush command are identical for Magento Open Source.
<!-- OPTION 1: Add visible text (best for most cases) -->
<table>
<thead>
<tr>
<th>Feature</th>
<th>Basic Plan</th>
<th>Pro Plan</th>
</tr>
</thead>
<tbody>
<tr>
<td>Storage</td>
<td>5 GB</td>
<td>50 GB</td>
</tr>
</tbody>
</table>
<!-- OPTION 2: Visually hidden text (for top-left corner cells) -->
<style>
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
</style>
<table>
<thead>
<tr>
<th><span class="sr-only">Feature name</span></th>
<th>Basic Plan</th>
<th>Pro Plan</th>
</tr>
</thead>
</table>
<!-- OPTION 3: aria-label directly on the th element -->
<table>
<thead>
<tr>
<th aria-label="Feature name"></th>
<th>Basic Plan</th>
<th>Pro Plan</th>
</tr>
</thead>
</table>Option 1 — visible text in the header cell (preferred). Option 2 — visually hidden text via a span. Option 3 — aria-label on the th element.
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 Magento Open Source
- Follow the same steps as Adobe Commerce above. If you are using the default Luma theme, create a child theme override rather than editing vendor files directly.
/* =====================================================
Universal visible focus indicator — WCAG 2.4.7
Add to your global/main stylesheet.
Adjust the color values to match your brand palette
while keeping contrast ≥ 3:1 against the background.
===================================================== */
/* Modern browsers: :focus-visible only shows the ring
for keyboard navigation, not mouse clicks. */
*:focus-visible {
outline: 3px solid #005FCC; /* high-contrast blue */
outline-offset: 2px; /* gap between element and ring */
border-radius: 2px; /* optional: soften corners */
}
/* Fallback for older browsers that don't support
:focus-visible — applies to all focus events. */
*:focus {
outline: 3px solid #005FCC;
outline-offset: 2px;
}
/* If you must remove the outline for mouse users in
older browsers, do it ONLY when :focus-visible is
NOT matched — never remove it globally. */
*:focus:not(:focus-visible) {
outline: none;
}Add to your main global stylesheet. This is the safest, most universal fix.
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 Magento Open Source
- Follow the same steps as Adobe Commerce (Magento) above — the template structure and CMS block editing workflow are identical in Magento Open Source.
<!-- 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 Magento Open Source
- Follow the same template-file approach as Adobe Commerce (Magento) above — locate the relevant PHTML file in your custom theme under app/design/frontend/<Vendor>/<theme>/.
- Edit the heading tag to the correct semantic level and flush the cache with bin/magento cache:flush.
- For CMS content, use the Admin WYSIWYG editor (Content → Pages / Blocks) to reassign heading levels via the paragraph-style dropdown.
- Verify the fix with the axe DevTools browser extension.
<!-- 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 Magento Open Source
- Follow the same steps as Adobe Commerce (Magento) above.
- The root template is located at app/design/frontend/[Vendor]/[theme]/Magento_Theme/templates/root.phtml.
- Set the locale in Stores → Configuration → General → General → Locale Options.
- Flush cache after saving changes.
<!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 Magento Open Source
- Follow the same steps as Adobe Commerce (Magento) above — the template structure and locale configuration paths are identical in Magento Open Source.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Page Title</title>
</head>
<body>
<!-- page content -->
</body>
</html>Place this as the very first tag in your HTML document. Replace 'en' with your actual BCP 47 language code.
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 Magento Open Source
- Product images: Admin → Catalog → Products → select a product → Images and Videos section → hover an image → gear icon → fill 'Alt Text' → Save.
- Bulk via CSV import: Admin → System → Import → Products → ensure `image_label` column is populated for each row → import.
- Template files: Edit `app/design/frontend/<Vendor>/<theme>/Magento_Catalog/templates/product/image.phtml` and confirm the `alt` attribute outputs the product's image label or name.
<!-- Meaningful image: describe the content -->
<img src="red-leather-bag.jpg" alt="Red leather crossbody bag with gold clasp" width="600" height="400">
<!-- Image used as a link: describe the destination/action -->
<a href="/sale">
<img src="summer-sale-banner.jpg" alt="Shop our summer sale — up to 40% off">
</a>
<!-- Decorative image: empty alt, screen readers skip it -->
<img src="decorative-swoosh.svg" alt="" role="presentation">
<!-- WRONG — never omit alt entirely on an <img> -->
<!-- <img src="product.jpg"> -->
<!-- WRONG — never use filename or generic words -->
<!-- <img src="IMG_4821.jpg" alt="image"> -->Correct patterns for meaningful, linked, and decorative images.
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 Magento Open Source
- Follow the same steps as Adobe Commerce (Magento) above; the admin panel paths and template locations are identical in the open-source edition.
- After editing theme .phtml files, run: php bin/magento cache:flush and php bin/magento setup:static-content:deploy to apply changes.
<!-- 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 Magento Open Source
- Follow the same steps as Adobe Commerce (Magento) above — template structure and CLI commands are identical.
<!-- CORRECT: Submit button with descriptive value -->
<input type="submit" value="Place Order">
<!-- CORRECT: Button with descriptive value -->
<input type="button" value="Add to Cart">
<!-- CORRECT: Reset button with descriptive value -->
<input type="reset" value="Clear Form">
<!-- CORRECT: Icon-only button — use aria-label when no visible text is possible -->
<input type="submit" value="" aria-label="Search products">
<!-- WRONG: Empty value — screen readers cannot announce purpose -->
<!-- <input type="submit" value=""> -->Use a descriptive value attribute on every input button type. Never leave value empty.
LabelQuick win
Add a visible or programmatic label to every form input so assistive technologies can identify it for users.
On Magento Open Source
- Follow the same steps as Adobe Commerce above — the template locations and cache-flush commands are identical.
- If you are not using a custom theme, create one first (never edit core files directly) so your changes survive upgrades.
<!-- Method 1: Explicit label (recommended) -->
<label for="user-email">Email address</label>
<input type="email" id="user-email" name="email" autocomplete="email">
<!-- Method 2: Implicit (wrapped) label -->
<label>
Email address
<input type="email" name="email" autocomplete="email">
</label>
<!-- Method 3: aria-label (use when no visible label is desired, e.g. icon-only search) -->
<input type="search" name="q" aria-label="Search products">
<!-- Method 4: aria-labelledby (point to existing visible text on the page) -->
<h2 id="newsletter-heading">Subscribe to our newsletter</h2>
<input type="email" name="subscribe" aria-labelledby="newsletter-heading">
<!-- Visually hidden label (visible to screen readers, hidden on screen) -->
<style>
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
</style>
<label for="search-input" class="visually-hidden">Search products</label>
<input type="search" id="search-input" name="q" placeholder="Search...">Preferred method: explicit label linked by for/id. Place immediately before the input.
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 Magento Open Source
- Follow the same process as Adobe Commerce (Magento) above — override the relevant PHTML template in your custom theme directory.
- After adding visible `<label>` elements or `aria-label` attributes, clear the Magento cache: Admin → System → Cache Management → Flush Magento Cache.
- Test on staging before deploying to production.
<!-- CORRECT: Visible label paired to input -->
<div class="form-field">
<label for="customer-email">Email address</label>
<input
type="email"
id="customer-email"
name="email"
autocomplete="email"
/>
</div>
<!-- CORRECT: Visually-hidden label (readable by screen readers, invisible to sighted users) -->
<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>
<div class="form-field">
<label for="site-search" class="visually-hidden">Search the store</label>
<input
type="search"
id="site-search"
name="q"
placeholder="Search..."
/>
</div>
<!-- CORRECT: aria-label as alternative when no <label> element is possible -->
<input
type="search"
name="q"
aria-label="Search the store"
placeholder="Search..."
/>
<!-- WRONG: title only — do NOT rely on this -->
<!-- <input type="search" name="q" title="Search the store" /> -->
<!-- WRONG: placeholder only — do NOT rely on this -->
<!-- <input type="email" name="email" placeholder="Email address" /> -->The correct pattern: a visible <label> paired to an input via matching for/id attributes.
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 Magento Open Source
- Same file path as Adobe Commerce: app/design/frontend/<Vendor>/<theme>/Magento_Theme/templates/root.phtml.
- Ensure `<header class="page-header">` is a direct child of `<body>`, not wrapped in `<main>` or any ARIA landmark div.
- Clear cache (bin/magento cache:flush) and verify.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>My Store</title>
</head>
<body>
<!-- CORRECT: <header> is a direct child of <body> -->
<header>
<a href="/"><img src="/logo.svg" alt="My Store home"></a>
<nav aria-label="Primary">
<ul>
<li><a href="/shop">Shop</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
</header>
<main id="main-content">
<!-- page content here -->
</main>
<footer>
<!-- footer content here -->
</footer>
</body>
</html>Correct top-level structure: <header> is a direct child of <body>, not nested in any landmark.
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 Magento Open Source
- Follow the same steps as Adobe Commerce (Magento) above, applying changes to your custom theme files under app/design/frontend/.
- After editing layout XML and/or .phtml templates, run bin/magento cache:flush and verify the landmark structure with the axe DevTools browser extension.
<!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 Magento Open Source
- Follow the same steps as Adobe Commerce (Magento) above — the theme file structure is identical.
- If you are using a third-party theme (e.g., Hyvä), check the theme's main layout template (typically resources/views/layouts/default.phtml or similar) and apply the same landmark reordering logic.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>My Store</title>
</head>
<body>
<header>
<nav aria-label="Primary navigation">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/shop">Shop</a></li>
</ul>
</nav>
</header>
<!-- CORRECT: <main> is a direct child of <body>, NOT inside <header> or any other landmark -->
<main id="main-content">
<h1>Welcome to My Store</h1>
<p>Browse our latest products below.</p>
</main>
<footer>
<p>© 2024 My Store</p>
</footer>
</body>
</html>Correct top-level landmark structure — <main> is a direct child of <body>, sibling to <header> and <footer>.
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 Magento Open Source
- Follow the same steps as Adobe Commerce (Magento) above — template paths and CLI commands are identical for Magento Open Source.
- If using a third-party theme from Marketplace, create a theme override in app/design/frontend/<YourVendor>/<YourTheme>/ rather than editing the vendor files directly, then apply the same <header>-to-<div> change in your override template.
<!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 mainQuick win
Find and remove every duplicate `<main>` element or `role="main"` attribute so that exactly one main landmark exists on each page.
On Magento Open Source
- Follow the same steps as Adobe Commerce above — the file paths and CLI commands are identical for Magento Open Source.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Page Title</title>
</head>
<body>
<header>
<nav aria-label="Primary navigation">
<!-- site-wide navigation -->
</nav>
</header>
<!-- ✅ Only ONE <main> element per page -->
<main id="main-content">
<h1>Page Heading</h1>
<!-- primary page content goes here -->
</main>
<footer>
<!-- footer content -->
</footer>
</body>
</html>Correct page structure: exactly one <main> element wrapping the primary content, with header and footer outside it.
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 Magento Open Source
- Same process as Adobe Commerce: edit your theme's layout XML (`default.xml`) to set `htmlTag="main"` on the primary content container, and update the corresponding `.phtml` template.
- Run `bin/magento cache:clean` after changes.
- Verify with axe DevTools.
<!-- Skip link at the very top of <body> -->
<a href="#main-content" class="skip-link">Skip to main content</a>
<header><!-- site header / nav here --></header>
<main id="main-content">
<!-- ALL primary page content goes here:
product details, blog post, category grid, etc. -->
</main>
<footer><!-- site footer here --></footer>Place this landmark element around all unique page content, after the <header> and <nav> and before the <footer>. Use exactly one per page.
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 Magento Open Source
- Follow the same steps as Adobe Commerce (Magento) above — the template file structure and caching commands are identical for Magento Open Source.
<!-- BEFORE: two <nav> elements with no label — screen readers can't tell them apart -->
<nav>
<ul><!-- main menu items --></ul>
</nav>
<nav>
<ul><!-- footer links --></ul>
</nav>
<!-- AFTER: each <nav> has a unique aria-label -->
<nav aria-label="Main navigation">
<ul><!-- main menu items --></ul>
</nav>
<nav aria-label="Footer navigation">
<ul><!-- footer links --></ul>
</nav>
<!-- Alternative: use aria-labelledby when a visible heading already exists -->
<section aria-labelledby="filters-heading">
<h2 id="filters-heading">Product Filters</h2>
<!-- filter controls -->
</section>
<section aria-labelledby="recommendations-heading">
<h2 id="recommendations-heading">You May Also Like</h2>
<!-- recommended products -->
</section>Apply unique aria-label attributes to every repeated landmark element. Use aria-labelledby when a visible heading already labels the region.
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 Magento Open Source
- Follow the same Adobe Commerce steps above — the theme file structure and deployment command are identical.
- Theme link styles are typically in app/design/frontend/[Vendor]/[theme]/web/css/source/_theme.less.
- After editing, run: bin/magento setup:static-content:deploy && bin/magento cache:flush
/* Option A: Change link color to one with ≥3:1 contrast against body text
AND ≥4.5:1 contrast against the page background.
Example: body text #3f3f3f on white background — #0057d8 passes both thresholds. */
a {
color: #0057d8;
}
/* Option B: Keep your existing link color but add a non-color visual cue.
An underline satisfies WCAG 1.4.1 without requiring the 3:1 color contrast. */
a {
text-decoration: underline;
}
/* Best practice: combine both for maximum clarity */
a {
color: #0057d8;
text-decoration: underline;
}
/* If you want to scope this to body/content areas only: */
.content a,
.entry-content a,
article a,
p a {
color: #0057d8;
text-decoration: underline;
}Add to your global stylesheet. Option A changes link color; Option B restores underline. Use both for strongest compliance.
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 Magento Open Source
- Follow the same steps as Adobe Commerce (Magento) above — the template structure and fix approach are identical.
- Create or use a custom theme under app/design/frontend/ and copy only the affected .phtml files into it before editing.
- Flush the cache after changes: bin/magento cache:flush.
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 Magento Open Source
- Follow the same steps as Adobe Commerce above — the file structure and cache-flush command are identical for Magento Open Source.
<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 Magento Open Source
- Follow the same steps as Adobe Commerce (Magento) above.
- The theme file path is the same: app/design/frontend/<Vendor>/<theme>/. Run bin/magento cache:flush after saving.
<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 Magento Open Source
- Locate the PHTML template responsible for the affected UI — check app/design/frontend/<Vendor>/<Theme>/ first, then vendor/magento/module-*/view/frontend/templates/.
- Search the template for nested `<a>` inside `<button>` or `<button>` inside `<a>` patterns.
- Refactor by converting the inner interactive element to a `<span>` or by merging both into a single element. Use CSS to preserve the visual appearance.
- Clear the Magento cache (bin/magento cache:flush) and redeploy static content, then verify with Tab-key navigation.
<!-- ❌ 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.
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 Magento Open Source
- Follow the same steps as Adobe Commerce (Magento) above — the theme and template architecture is identical.
- If you are on an older Magento 2 version without a heavily customized theme, check that the default Luma or Blank theme's 'root.phtml' already includes landmark tags; if you have overridden it without them, add them as described above.
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 Magento Open Source
- Same process as Adobe Commerce: locate the .phtml file in your custom theme using grep -r 'role="img"' app/design/.
- Override the template in your child theme and add aria-label or aria-labelledby as described above.
- Clear cache and redeploy static assets to test.
<!-- Pattern 1: Inline element (icon font, emoji, span) -->
<span role="img" aria-label="Free shipping available">🚚</span>
<!-- Pattern 2: SVG element — use <title> + aria-labelledby (most robust) -->
<svg role="img" aria-labelledby="icon-free-shipping" width="24" height="24" xmlns="http://www.w3.org/2000/svg">
<title id="icon-free-shipping">Free shipping available</title>
<use href="#icon-truck"></use>
</svg>
<!-- Pattern 3: SVG with aria-label only (simpler, slightly less support) -->
<svg role="img" aria-label="4.5 out of 5 stars" width="120" height="24" xmlns="http://www.w3.org/2000/svg">
<!-- star paths here -->
</svg>
<!-- Pattern 4: div/section used as a decorative image — HIDE from screen readers -->
<div role="presentation" aria-hidden="true" class="decorative-banner"></div>
<!-- Pattern 5: referencing an existing visible caption -->
<figure>
<div role="img" aria-labelledby="badge-caption" class="trust-badge"></div>
<figcaption id="badge-caption">Verified Secure Checkout</figcaption>
</figure>Apply to any HTML element using role="img" — choose the pattern that matches your element type.
Select nameQuick win
Add a visible or programmatic label to every `<select>` dropdown so assistive technologies can announce its purpose to users.
On Magento Open Source
- Follow the same steps as Adobe Commerce (Magento) above — the template structure and cache-clearing commands are identical.
<!-- Method 1: Visible label (preferred) -->
<label for="sort-order">Sort by</label>
<select id="sort-order" name="sort">
<option value="price-asc">Price: Low to High</option>
<option value="price-desc">Price: High to Low</option>
<option value="newest">Newest</option>
</select>
<!-- Method 2: Visually hidden label (when visible label breaks design) -->
<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>
<label for="sort-order" class="visually-hidden">Sort by</label>
<select id="sort-order" name="sort">
<option value="price-asc">Price: Low to High</option>
<option value="price-desc">Price: High to Low</option>
<option value="newest">Newest</option>
</select>
<!-- Method 3: aria-label (last resort, no visible label possible) -->
<select aria-label="Sort by" name="sort">
<option value="price-asc">Price: Low to High</option>
<option value="price-desc">Price: High to Low</option>
<option value="newest">Newest</option>
</select>
<!-- Method 4: Wrapping label (implicit association — no for/id needed) -->
<label>
Sort by
<select name="sort">
<option value="price-asc">Price: Low to High</option>
<option value="price-desc">Price: High to Low</option>
<option value="newest">Newest</option>
</select>
</label>The gold-standard approach: a visible <label> linked to the <select> via matching for/id attributes.
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 Magento Open Source
- Follow the same steps as Adobe Commerce (Magento) above — the theme file structure and process are identical for Magento Open Source 2.x.
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 Magento Open Source
- Follow the same template-override approach as Adobe Commerce: locate the .phtml or UI-component .html file, create an override in app/design/frontend/<Vendor>/<theme>/, and correct all positive tabindex values.
- Flush the Magento cache and redeploy static content after saving changes.
- Verify with keyboard-only navigation through the storefront.
<!-- ❌ 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 Magento Open Source
- Follow the same steps as Adobe Commerce (Magento) above — the template path and locale configuration are identical in Magento Open Source.
<!-- Primary page language on the root element -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>My Store</title>
</head>
<body>
<p>Welcome to our store. Free shipping on orders over $50.</p>
<!-- Inline passage in a different language: add lang to the wrapping element -->
<p lang="fr">Bienvenue dans notre boutique. Livraison gratuite dès 50 €.</p>
<!-- Inline span for a short foreign-language phrase within an English sentence -->
<p>Our slogan is <span lang="de">Qualität zuerst</span>, which means "Quality first".</p>
</body>
</html>Set the primary language on the <html> tag and mark inline foreign-language passages with their own lang attribute.
Security (OWASP) · 22 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 Magento Open Source
- Same steps as Adobe Commerce: configure SMTP relay under Admin → Stores → Configuration → Advanced → System → Mail Sending Settings.
- Publish the DKIM key from your relay provider and update SPF at your DNS host.
- Edit the _dmarc TXT record at your DNS provider to change p=none to p=quarantine, then p=reject after confirming alignment.
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 Magento Open Source
- Set the HSTS header at the Apache or Nginx web server level — the steps are identical to Adobe Commerce (Magento) above.
- For Apache .htaccess in your Magento root, add inside <IfModule mod_headers.c>: Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
- For Nginx server block: add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
- Reload your web server and flush full-page cache: php bin/magento cache:flush
- Verify: curl -I https://yourdomain.com | grep -i strict
Strict-Transport-Security: max-age=31536000; includeSubDomainsThis is an HTTP response header — it cannot be set in HTML meta tags. It must be set by your server, CDN, or platform. Shown here for reference only.
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 Magento Open Source
- Same web-server approach as Adobe Commerce — set the header in Nginx or Apache config as described above.
- For Nginx: add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; inside the server block for port 443.
- For Apache: Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains" inside <VirtualHost *:443>.
- Reload the web server after saving: sudo systemctl reload nginx or sudo systemctl reload apache2.
- Flush Magento cache: php bin/magento cache:flush full_page. Verify headers in DevTools.
Strict-Transport-Security: max-age=31536000; includeSubDomainsThis is an HTTP response header — it must be sent by your server, NOT as an HTML meta tag. The meta tag is not supported for HSTS.
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 Magento Open Source
- Follow the same steps as Adobe Commerce (Magento) above — the Admin configuration paths and web-server changes are identical.
- Install your SSL certificate via Let's Encrypt/Certbot or your hosting panel, then configure secure base URLs under Stores → Configuration → General → Web.
- Add HTTP-to-HTTPS redirect rules in .htaccess (Apache) or your server block (Nginx) and flush all caches after saving.
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 Magento Open Source
- Same web-server-level fix as Adobe Commerce: for nginx add `server_tokens off;`, for Apache add `ServerTokens Prod` and `ServerSignature Off` to the server config.
- If hosted on a shared host with cPanel: log in to cPanel → File Manager → edit `.htaccess` in the site root → add `Header unset Server` (requires `mod_headers` to be enabled by your host).
- Alternatively, install a WAF or reverse proxy (e.g., Cloudflare free tier) in front of your Magento store and configure it to strip the `Server` header.
- Verify with `curl -I https://yourstore.com`.
<!-- The Server header cannot be controlled from HTML.
Apply the fix at your web server, CDN, or hosting control panel
using one of the platform-specific configurations shown below. -->There is no HTML tag to set or suppress the Server header — it is purely a server/CDN-level configuration. Use the server-specific snippets below.
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 Magento Open Source
- Follow the same steps as Adobe Commerce (Magento) above — the server stack and file locations are identical.
<!-- X-Powered-By is an HTTP response header set by the server, not in HTML markup.
Use the server-side code snippets below for your specific platform. -->There is no HTML tag for this — it is a server-side HTTP response header. Use the server/framework snippets below.
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 Magento Open Source
- Follow the identical steps as Adobe Commerce above — the Stores → Configuration paths are the same.
- Additionally edit app/etc/env.php to set cookie flags at the PHP session level as a backstop (see code snippet).
- Run bin/magento setup:upgrade && bin/magento cache:flush.
Set-Cookie: session_id=abc123; HttpOnly; Secure; SameSite=StrictCorrect Set-Cookie HTTP response header — all three flags present. Your server must emit this header, not JavaScript.
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 Magento Open Source
- Same as Adobe Commerce above — native CSP configuration is available from version 2.3.5+.
- Go to Stores → Configuration → Security → Content Security Policy in the Admin panel.
- Use Report Only mode first, then switch to Restrict (enforce) mode after reviewing violation logs.
- Add trusted domains per directive in the UI, or use a module-level csp_whitelist.xml for programmatic control.
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 Magento Open Source
- Same DNS and mail-authentication process as Adobe Commerce above — DMARC is purely a DNS TXT record managed at your registrar or DNS host.
- Add TXT record at _dmarc.yourdomain.com with value: v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com
- Configure SMTP authentication in Magento Open Source via Stores → Configuration → Advanced → System → Mail Sending Settings, pointing to an SPF/DKIM-enabled relay.
- Use a community SMTP extension from the Magento Marketplace to enable authenticated outbound mail if the default server configuration does not support it.
_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 Magento Open Source
- Same steps as Adobe Commerce (Magento) above — use .htaccess (Apache), nginx.conf (Nginx), a custom response plugin, or a CDN-level rule.
- Apache .htaccess (Magento web root): add the Header set directive inside <IfModule mod_headers.c> as shown in the code snippets.
- Nginx: add 'add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=(), interest-cohort=()" always;' in the server block.
- Flush Magento cache and 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 Magento Open Source
- Follow the same Apache/Nginx server-block approach as Adobe Commerce (Magento) above.
- For Apache (.htaccess at Magento root): add Header always set Referrer-Policy "strict-origin-when-cross-origin"
- For Nginx (server block): add add_header Referrer-Policy "strict-origin-when-cross-origin" always;
- Flush caches and verify 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 Magento Open Source
- Same as Adobe Commerce: place security.txt in pub/.well-known/security.txt via SFTP.
- Confirm your Nginx or Apache config allows serving that path (see code snippets for Nginx location block).
- Verify by visiting https://yourdomain.com/.well-known/security.txt.
Contact: mailto:security@yourdomain.com
Expires: 2026-12-31T23:59:00.000Z
Preferred-Languages: en
Policy: https://yourdomain.com/security-policy
Acknowledgments: https://yourdomain.com/security-acknowledgmentsSave this as security.txt and place it at /.well-known/security.txt on your web server. Update all placeholder values before publishing.
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 Magento Open Source
- DNS is managed at your registrar or hosting provider — not inside Magento.
- Log in to your DNS provider and add a TXT record at '@' with your full SPF value.
- In Magento admin, go to Stores → Configuration → Advanced → System → Mail Sending Settings to confirm the SMTP host in use, then add that provider's SPF include: directive to your record.
- Common setups include a server-local sendmail (add your server's IP as 'ip4:x.x.x.x') or a third-party SMTP relay like SendGrid or Mailgun (use their published include: tag).
- Verify propagation with a free SPF lookup tool.
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 Magento Open Source
- Follow the same Apache or Nginx server-level steps as Adobe Commerce (Magento) above — the process is identical for the open-source edition.
- Edit your HTTPS VirtualHost (Apache) or server block (Nginx) to add the Strict-Transport-Security header as shown in the code snippets below.
- Ensure mod_headers (Apache) or the ngx_http_headers_module (Nginx, compiled in by default) is active.
- Flush the Magento cache after any configuration change: php bin/magento cache:flush.
<!-- 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 Magento Open Source
- Identical to Adobe Commerce (Magento) above — use the Nginx/Apache server config method or a custom module plugin.
X-Content-Type-Options: nosniffThis is an HTTP response header — it is sent by the server, not placed in HTML markup. The examples below show where and how to set it.
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 Magento Open Source
- Same steps as Adobe Commerce (Magento) above — the Magento 2 CLI command bin/magento config:set web/frame_options SAMEORIGIN applies to both editions.
- Also check your server-level config (Apache .htaccess or Nginx server block) as described above to ensure the header is sent at the web server layer as well.
X-Frame-Options: SAMEORIGINThis is an HTTP response header — it must be sent by the server, not a meta tag. It should appear in the HTTP response headers for every page.
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 Magento Open Source
- Certificate management is identical to self-hosted Adobe Commerce above — the SSL certificate lives at the web server or load balancer level.
- Renew via Let's Encrypt (certbot renew) or your paid CA, reinstall the certificate in your Apache/Nginx virtual host config, and reload the web server.
- Confirm Magento Admin → Stores → Configuration → General → Web → Base URLs (Secure) uses https://.
<!-- 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 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 Magento Open Source
- SSL management is identical to Adobe Commerce self-hosted — handled at the server/hosting level, not inside the Magento admin panel.
- Use your hosting control panel (cPanel/Plesk) or Let's Encrypt/Certbot on the command line to issue a certificate that covers all domain variants.
- Run: `sudo certbot --nginx -d yourstore.com -d www.yourstore.com` (adjust for Apache if needed).
- In Magento Admin → Stores → Configuration → Web, update Secure Base URL to use https:// with the correct domain.
- Flush Magento cache after 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 Magento Open Source
- Follow the same server-level steps as Adobe Commerce above (open port 443, install TLS certificate via Certbot or hosting panel).
- In Admin → Stores → Configuration → General → Web → Base URLs (Secure), update the Secure Base URL to https://.
- Set 'Use Secure URLs on Storefront' and 'Use Secure URLs in Admin' to 'Yes'.
- Add a redirect in your Nginx or Apache config: for Nginx, add `return 301 https://$host$request_uri;` in the port-80 server block; for Apache, add `RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]` in your VirtualHost or .htaccess.
- Flush the Magento cache: `bin/magento cache:flush`.
Vulnerable component axiosModerate effort
Update the Axios JavaScript library to its latest stable release to eliminate known security vulnerabilities in your storefront code.
On Magento Open Source
- Follow the same steps as Adobe Commerce (Magento) above.
- Additionally, run `composer update` if Axios is pulled in via a PHP/Composer package dependency, and check all installed third-party extensions for their own Axios bundles.
<!-- Load the latest Axios from jsDelivr CDN (always points to newest stable release) -->
<script src="https://cdn.jsdelivr.net/npm/axios@latest/dist/axios.min.js" integrity="" crossorigin="anonymous"></script>
<!-- OR pin to a specific safe version (recommended for production stability) -->
<script src="https://cdn.jsdelivr.net/npm/axios@1.7.9/dist/axios.min.js" crossorigin="anonymous"></script>Replace any hardcoded Axios CDN script tag in your HTML templates with this — always uses the latest stable version.
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 Magento Open Source
- Follow the same Nginx/Apache steps as Adobe Commerce above.
- Check for any third-party Magento extension labelled 'Security Headers' in your Admin → System → Extensions — if one exists, disable the server-level directive and manage it only via the extension, or vice versa, to avoid duplication.
- Reload the web server and verify.
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 Magento Open Source
- Follow the same steps as Adobe Commerce (Magento) above — the server-level config and Magento 2 Admin path are identical for Magento Open Source.
- For Apache, add to pub/.htaccess: Header always set X-Frame-Options "DENY"
- For Nginx, add to the server{} block in your vhost config: add_header X-Frame-Options "DENY" always;
- Flush cache: bin/magento cache:flush, then verify with curl -I https://yourdomain.com | grep -i x-frame.
<!-- X-Frame-Options MUST be sent as an HTTP response header, not a meta tag. -->
<!-- Use the server-side or platform snippets below instead. -->
<!-- The modern equivalent you CAN set via meta tag (limited support): -->
<meta http-equiv="Content-Security-Policy" content="frame-ancestors 'none';">If you cannot set server-level headers, use this meta-equivalent only as a last resort — note that X-Frame-Options via meta tag is NOT supported by browsers (it must be an HTTP header). This snippet is shown for illustration only; use server-side headers.
Site Lifecycle · 10 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 Magento Open Source
- Follow the same SSH/Composer-based upgrade process as Adobe Commerce (Magento) above.
- Check current releases at github.com/magento/magento2/releases.
- Run 'composer require magento/product-community-edition <new-version> --no-update && composer update' followed by the standard 'bin/magento setup:upgrade' sequence.
- Use the Upgrade Compatibility Tool and test on staging before touching production.
<!-- REMOVE or suppress this tag from your <head> — it advertises your CMS version to attackers -->
<!-- <meta name="generator" content="WordPress 5.8.1" /> -->
<!-- In WordPress, add this to your theme's functions.php to remove it: -->
<!-- remove_action('wp_head', 'wp_generator'); -->If your site injects a generator meta tag exposing your CMS version publicly, remove it to reduce information exposure to attackers. This does not replace upgrading — it is a secondary hardening step.
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 Magento Open Source
- Same as Adobe Commerce — Magento Open Source has no domain management feature. Log in to your registrar or hosting control panel and enable auto-renew on the domain.
- Contact your hosting provider (e.g. Nexcess, Cloudways, SiteGround) if you are unsure who registered the domain — they can tell you the registrar.
<!-- 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.
Html langQuick win
Add a lang attribute to your site's <html> tag that matches the primary language of your store's content.
On Magento Open Source
- Follow the same steps as Adobe Commerce (Magento): locate root.phtml in your custom theme at app/design/frontend/<Vendor>/<Theme>/Magento_Theme/templates/root.phtml.
- Edit the <html tag to include lang="en" (or your locale code).
- Run bin/magento cache:clean && bin/magento setup:static-content:deploy after saving.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Your Store</title>
</head>
<body>
<!-- page content -->
</body>
</html>Place this as the very first tag in your HTML document, before <head>.
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 Magento Open Source
- Follow the same steps as Adobe Commerce above — the admin UI is identical for catalog and category management.
- Reindex and flush cache via System → Index Management and System → Cache Management after saving.
<!-- 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 Magento Open Source
- Go to Admin → Marketing → SEO & Search → Site Map → Add Sitemap or regenerate the existing one.
- Configure inclusion settings at Admin → Stores → Configuration → Catalog → XML Sitemap → Products; adjust priority and frequency as needed.
- Ensure all target products have Status = 'Enabled' and Visibility set to 'Catalog, Search' or 'Search' (Admin → Catalog → Products → edit each product → Search Engine Optimization tab).
- After saving, click 'Save & Generate' on the sitemap record and verify the .xml file is accessible at your domain.
- 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 Magento Open Source
- Follow the same steps as Adobe Commerce above — the Catalog → Categories tree and CMS widget tools are identical in Magento Open Source.
- If you have a custom theme, also check your navigation template at app/design/frontend/[Vendor]/[theme]/Magento_Theme/templates/html/ and flatten the menu depth there.
- Run bin/magento cache:flush after making category changes.
<!-- 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 Magento Open Source
- Follow the same steps as Adobe Commerce (Magento) above — the admin path and configuration options are identical.
- Additionally check app/design theme template files for any hardcoded 'http://' asset URLs and update to 'https://'.
- Flush the full cache after any changes.
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">Add this meta tag inside <head> to instruct browsers to automatically upgrade HTTP sub-requests to HTTPS. This is a safety net — fix the root URLs too.
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 Magento Open Source
- Follow the identical steps as Adobe Commerce above — the file paths and cache flush commands are the same.
- If you are on an older Magento 2 version, check `vendor/magento/module-theme/view/frontend/layout/default_head_blocks.xml` for the default tag and override it in your custom theme rather than editing core files.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Page Title</title>
</head>
<body>
<!-- page content -->
</body>
</html>Place the viewport meta tag as early as possible in <head>, before any CSS or other meta tags.
Server versionQuick win
Remove or suppress HTTP response headers that advertise your server software name and version to the public internet.
On Magento Open Source
- Follow the same Nginx/Apache/PHP steps as Adobe Commerce (Magento) above — the application layer is identical.
- Add 'expose_php = Off' in 'php.ini' and 'server_tokens off;' in Nginx config, then restart both services.
- Verify with 'curl -I https://yourdomain.com' to confirm headers are suppressed.
<!-- HTTP response headers cannot be set via HTML meta tags.
Use the server-side or CDN configuration snippets below. -->Meta tags cannot suppress HTTP response headers — this must be done at the server/CDN layer. The snippets below cover the most common server/framework approaches.
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 Magento Open Source
- Log into your server via SSH and run `sudo certbot certificates` to see your current certificate expiry.
- Run `sudo certbot renew --dry-run` to verify auto-renewal will work without errors.
- Confirm the renewal timer: `sudo systemctl status certbot.timer` (Linux with systemd) or check `/etc/cron.d/certbot`.
- After a successful renewal, clear Magento's cache: `php bin/magento cache:flush`.
- Ensure Base URLs in Admin → Stores → Configuration → General → Web 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.