Every Shopify Plus fix we catalog
SEOLZ catalogs 124 fixes for Shopify Plus across 5 areas — SEO, answer-engine readiness, accessibility, security and site-health. Each lists the exact steps for Shopify Plus, with a link to the official docs.
SEO · 42 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 Shopify Plus
- Same steps as Shopify above. Additionally, use the Shopify Plus Launchpad and custom storefronts (Hydrogen/Headless) carefully: Headless builds must generate and serve their own sitemap — confirm it exists and is submitted to Google Search Console.
- In the Shopify admin, check Admin › Markets if you use international markets: each market's locale URL structure should be in the sitemap and not blocked by robots.txt.
- Use Google Search Console's URL Inspection tool on sample products from each collection to confirm they are Indexed and not 'Discovered – currently not indexed'.
<!-- 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 clsModerate effort
Eliminate unexpected layout shifts by reserving space for every image, video, embed, and dynamically injected element so nothing jumps when the page loads.
On Shopify Plus
- All standard Shopify steps above apply. Additionally, use Shopify Scripts or Launchpad to ensure promotional banners and flash-sale bars are pre-rendered in theme sections rather than injected via client-side JavaScript.
- For checkout extensions: in the Shopify Plus Checkout Editor, all blocks are rendered server-side with reserved space — avoid injecting custom checkout content via external scripts that fire after load.
- Use Shopify's CDN image transformations ({{ image | image_url: width: 800 }}) and always pair with width/height attributes in Liquid to prevent CLS on checkout and account pages.
<!-- CORRECT: explicit width and height prevent layout shift -->
<img
src="/images/product-red-sneaker.jpg"
alt="Red canvas sneaker, side view"
width="800"
height="800"
loading="lazy"
>Always declare width and height on every img element. The browser uses these to reserve space before the image file downloads.
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 Shopify Plus
- Follow all Shopify steps above. Additionally, audit scripts injected via Shopify Scripts (Script Editor app) — these run at checkout and can inflate INP on checkout pages.
- In the Checkout Extensibility admin (if enabled), review Checkout UI Extensions for heavy JavaScript in their event callbacks and refactor them to defer work.
- Use the Shopify Checkout Speed report (Analytics → Reports → Checkout funnel) alongside PageSpeed Insights to track INP on checkout pages specifically.
Cwv lcpModerate effort
Reduce Largest Contentful Paint (LCP) to under 2.5 seconds by serving the hero image in a next-gen format (WebP/AVIF), preloading it, and eliminating render-blocking resources.
On Shopify Plus
- Follow all standard Shopify steps above — Shopify Plus uses the same Liquid/theme architecture.
- Additionally, use Shopify Plus's Script Editor (or Shopify Functions) to review checkout scripts — remove or defer any non-critical checkout JS that may be loaded site-wide via theme.liquid.
- Use the Shopify Plus Organization Admin to push the optimized theme across multiple stores simultaneously if the same hero template is used across storefronts.
- Engage Shopify Plus's Launch Engineer or a Shopify Expert for server-side rendering (Hydrogen/Oxygen) migration if LCP cannot be brought under 2.5s through theme-level changes alone.
<!-- Step 1: Preload the LCP hero image -->
<link
rel="preload"
as="image"
href="/images/hero-banner.webp"
imagesrcset="/images/hero-banner-400.webp 400w,
/images/hero-banner-800.webp 800w,
/images/hero-banner-1500.webp 1500w"
imagesizes="100vw"
fetchpriority="high"
>
<!-- Step 2: Hero <img> tag — eager, high priority, no lazy-load -->
<img
src="/images/hero-banner-1500.webp"
srcset="/images/hero-banner-400.webp 400w,
/images/hero-banner-800.webp 800w,
/images/hero-banner-1500.webp 1500w"
sizes="100vw"
alt="[Descriptive alt text for your hero image]"
width="1500"
height="600"
loading="eager"
fetchpriority="high"
>Add inside <head>, immediately after <meta charset>. Replace the href with your actual hero image CDN URL.
Duplicate meta descriptionModerate effort
Write a unique, page-specific meta description for every page on your store so Google can display a compelling, accurate snippet for each one in search results.
On Shopify Plus
- Follow all standard Shopify steps above — Shopify Plus uses the same admin interface for meta descriptions.
- In addition, use Shopify Plus Scripts or the Bulk Edit API to programmatically update meta descriptions across thousands of products at scale.
- Connect a Shopify Plus-compatible SEO app (e.g. SEO Manager, Plug in SEO Enterprise) that supports bulk AI-assisted description writing across your full catalogue.
- For storefronts using custom Liquid themes, ensure your theme's layout files include {{ page_description }} in the <meta name='description'> tag so CMS-entered descriptions are rendered correctly.
- Use Shopify's Metafields or the Online Store 2.0 theme architecture to store and surface unique descriptions for custom page types.
- After bulk updates, use Google Search Console's URL Inspection tool and the Coverage report to confirm descriptions are being picked up.
<head>
<meta charset="UTF-8">
<meta name="description" content="Shop women's running shoes at Acme Store — lightweight, cushioned designs for road and trail. Free shipping on orders over $50.">
<title>Women's Running Shoes | Acme Store</title>
</head>Place inside the <head> element of every page. Replace the content value with a unique 120–160 character description for that specific page.
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 Shopify Plus
- Shopify Plus stores have the same canonical behavior as standard Shopify for collection/filter URLs. Follow the Shopify steps above.
- Additionally, Shopify Plus merchants with custom storefronts (Hydrogen/Headless) must manage canonical tags explicitly in their React/Remix components — set the canonical <link> in each collection page's <head> to the clean collection URL, stripping all filter query params.
- Shopify Plus stores using Launchpad or custom scripts for promotions: ensure promotional filter URLs (e.g., /collections/sale?color=red) also point their canonical to /collections/sale.
- Enterprise-scale Plus stores should audit via a crawl tool (Screaming Frog, Sitebulb) filtered by URLs containing filter parameter patterns to get the full scope of affected URLs before and after the fix.
<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 Shopify Plus
- All steps are identical to standard Shopify (see Shopify above).
- For large catalogs at Plus scale, use the Shopify Admin API (REST or GraphQL) to bulk-update image alt text programmatically — query all productImages, filter where altText is null or empty, then send a productImageUpdate mutation with the correct altText value.
- Alternatively, use a Shopify Plus-approved bulk-edit app such as Matrixify (Excelify) — export products to Excel/CSV, fill the 'Image Alt Text' column, re-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 Shopify Plus
- Follow the same steps as Shopify above — Shopify Plus stores use the same theme code layer.
- If you use a custom storefront (Hydrogen / Headless), locate the React component rendering product images (typically `ProductImage` or similar) and ensure the `<img>` element includes `alt={product.title}` or `alt={image.altText ?? product.title}`.
- For scripts or apps injecting images via Script Tags, update those scripts to set `.alt` on every created `<img>` element.
<!-- 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.
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 Shopify Plus
- Follow the same steps as Shopify (the SEO panel is identical).
- For bulk edits across many products, use the Shopify bulk editor: Products → select multiple products → Edit products → add the 'SEO Description' column.
- Alternatively, use a Shopify Plus-compatible SEO app such as 'SEO Manager' or 'Plug In SEO' for store-wide auditing and bulk updates.
<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 Shopify Plus
- All steps are identical to standard Shopify (see above).
- For large-scale updates across thousands of SKUs, use the Shopify Admin REST API (products endpoint, 'metafields' or direct 'body_html'/'seo_description') or a trusted app such as Yoast SEO for Shopify or SEO Manager to bulk-edit meta descriptions via CSV upload.
<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 Shopify Plus
- Follow identical steps to standard Shopify above — the `canonical_url` Liquid variable is available on all Shopify Plus storefronts.
- If you use Shopify's Headless / Hydrogen framework, the canonical must be injected programmatically in the `<head>` of your React components (see Headless / Custom entry).
- For checkout pages (checkout.liquid, available on Plus), add the canonical tag inside the `<head>` of `layout/checkout.liquid` as `<link rel='canonical' href='{{ canonical_url }}'>`.
- Save and use Google Search Console's URL Inspection tool to verify.
<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 Shopify Plus
- Shopify Plus stores use the same theme system as Shopify. Follow the same Shopify steps above.
- If you use a custom storefront (Hydrogen/Headless), locate the product/collection/page component in your codebase and ensure the title is wrapped in an <h1> element.
- For script-level changes across all templates, use the Theme Code Editor or deploy via GitHub integration.
<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 Shopify Plus
- All standard Shopify paths above apply. Additionally, use the Shopify Plus 'Bulk Editor' (Admin → Products → select multiple products → Bulk edit) to add/edit meta description fields across many products at once.
- For theme-level default templates, edit the relevant Liquid file (e.g., product.liquid) and ensure `{{ page_description }}` is output inside a `<meta name='description'>` tag — Shopify Plus merchants often use a custom theme.
<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 Shopify Plus
- Follow the same Shopify steps above; Shopify Plus uses the same theme architecture.
- If you use a custom storefront (Hydrogen/Headless), add og:description in the <Meta> component of each route's loader — see the Headless / Custom entry below.
<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 Shopify Plus
- Follow the same steps as Shopify above — Shopify Plus uses the same theme and admin system.
- For enterprise control at scale, use Shopify Plus's Script Editor or Metafields to programmatically assign og:image values to products and collections via the Shopify Admin API.
- You can also use a Shopify Plus automation app (e.g. Launchpad or a custom script) to bulk-update product metafields for og:image across large catalogs.
- Validate a representative sample of product, collection, and landing pages using Facebook's Sharing Debugger.
Missing og titleQuick win
Add an og:title meta tag to every page so your store's title appears correctly when shared on social media platforms like Facebook, LinkedIn, and Pinterest.
On Shopify Plus
- Follow the same steps as Shopify (theme code editing applies equally to Shopify Plus).
- Shopify Plus additionally allows editing checkout.liquid: go to Online Store → Themes → Edit code → checkout.liquid and ensure og:title is present if you need social sharing from checkout or post-purchase pages.
- For headless Shopify Plus storefronts, add og:title in your front-end framework's <head> component using the Storefront API's product.title or page.title field.
<head>
<!-- Other head tags -->
<meta property="og:title" content="Amazing Product Name | Your Store Name" />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://www.yourstore.com/page-url" />
<meta property="og:description" content="Short description of this page for social sharing." />
<!-- Other head tags -->
</head>Place inside the <head> section of every HTML page. Replace the content value with your actual page title.
Missing titleQuick win
Add a unique, descriptive title tag (30–60 characters) to every page that is missing one.
On Shopify Plus
- Same admin workflow as Shopify — go to the relevant section (Products, Collections, Pages, or Online Store → Preferences) → 'Search engine listing' → 'Edit' → fill in 'Page title'.
- For bulk updates across many products, use the Shopify Plus bulk editor: Products → select multiple products → Edit products → add the 'SEO Title' column.
- Alternatively, export your product CSV (Products → Export), fill in the 'SEO Title' column in a spreadsheet, and re-import.
<!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 Shopify Plus
- Follow the same Shopify steps above — Shopify Plus stores use the same theme architecture.
- Additionally audit any custom Liquid templates created for specific scripts or checkout pages via the Shopify Plus admin.
- If using a headless or custom storefront (Hydrogen/Oxygen), see the Headless / Custom entry below.
<!-- 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.
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 Shopify Plus
- Follow the same steps as Shopify above. Shopify Plus merchants with large redirect lists can also bulk-upload redirects via Online Store → Navigation → URL Redirects → Import (CSV format: redirect,location).
- For enterprise sites managed via the Storefront API or custom themes, ensure theme developers update any hard-coded URLs in Liquid templates (layout/theme.liquid, sections/.liquid, snippets/.liquid).
- Use the Bulk Redirects app from the Shopify App Store if managing hundreds of 404s at once.
<!-- BEFORE: broken internal link -->
<a href="/old-products/discontinued-item">Shop Now</a>
<!-- AFTER: updated to correct URL -->
<a href="/products/replacement-item">Shop Now</a>Update an internal anchor link to point to the correct URL instead of the broken one.
Page speed warningModerate effort
Reduce your page load time to under 1.5 seconds by optimizing images, eliminating render-blocking resources, and enabling caching and compression.
On Shopify Plus
- All standard Shopify steps apply (see Shopify above).
- Scripts: In Shopify Plus, go to Online Store > Themes > Edit code and audit the checkout.liquid file for any checkout-specific scripts added by an agency or app. Remove unused checkout scripts — they delay the most conversion-critical page in your funnel.
- Launchpad / Flow: Use Shopify Flow to schedule script-heavy promotional apps to activate only during sale periods rather than running 24/7.
- Headless consideration: If your Plus store is on a headless or custom storefront (Hydrogen / Remix), work with your developer to implement streaming SSR, route-based code splitting, and Shopify's built-in CDN for media.
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 Shopify Plus
- Follow the same steps as Shopify above — Shopify Plus gives you identical theme-file access plus the robots.txt.liquid file is always editable.
- If you manage multiple storefronts, apply the change to each theme individually.
- Use the Shopify Plus Script Editor or custom app if you need to programmatically suppress noindex on specific search contexts (e.g. staff previews).
<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 Shopify Plus
- Follow the same steps as Shopify above — the Collections editor is identical in Shopify Plus.
- If you use a custom storefront (Hydrogen/Headless), query the collection.description field via the Storefront API and render it as HTML in your category template.
- For script-heavy customisations, use Shopify Plus's Launchpad or Scripts Editor only for pricing rules — the description field itself is managed in the standard admin Collections editor.
<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 Shopify Plus
- Follow the same steps as Shopify above.
- For stores using a custom storefront or Hydrogen, ensure the collection description field is fetched via the Storefront API and rendered in the category page component.
- Use Shopify Plus Scripts or metafields if you need per-locale or extended descriptions beyond the single description field.
<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 Shopify Plus
- Follow all Shopify steps above — Shopify Plus gives access to the same admin UI plus Launchpad and Scripts.
- For enterprise stores with many empty collections, use the Bulk Editor or the Admin API to query collections with product_count == 0 and batch-apply noindex or deletion.
<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 Shopify Plus
- Follow all Shopify steps above — Plus merchants also have access to the Shopify Admin API and Bulk Editor for reassigning products across collections at scale.
- Use the Bulk Editor (select products → Edit products) to retag multiple products at once, which updates automated collection conditions instantly.
- For headless or custom storefronts, apply noindex meta tags in your layout component conditionally based on collection product count from the Storefront API.
<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 Shopify Plus
- Same as Shopify above. Additionally, if you use a custom storefront or headless setup, ensure your front-end framework outputs the canonical in the <head> using the clean collection URL for all filtered/paginated routes.
- For Shopify Plus stores using Launchpad or scripts, confirm no script overrides the canonical meta at render time.
- Use the Shopify Storefront API's built-in SEO object if rendering canonical tags server-side in a custom theme.
<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 minimalModerate effort
Expand your product descriptions to at least 300 words by adding specifications, use cases, and benefits so search engines have enough content to rank and shoppers have enough information to buy.
On Shopify Plus
- Go to Shopify Admin → Products → All Products (or use the Bulk Editor at Products → Export / Bulk Edit for multi-product updates).
- Click the product and expand the 'Description' rich-text editor.
- Write a structured 300+ word description with specs, use cases, and benefits.
- For programmatic updates across hundreds of products, use the Shopify Admin API (products.json) or a bulk-edit app such as 'Bulk Product Edit' from the App Store.
- Save and request re-indexing via Google Search Console.
<section class="product-description">
<h2>About the [Product Name]</h2>
<p>
The [Product Name] is a [category] designed for [target audience]. Whether you need
[use case 1] or [use case 2], this [product] delivers [core benefit] without
[common pain point].
</p>
<h3>Key Features & Specifications</h3>
<ul>
<li><strong>Material:</strong> [e.g. 100% organic cotton, BPA-free polypropylene]</li>
<li><strong>Dimensions:</strong> [W × H × D] / Weight: [X kg / X lbs]</li>
<li><strong>Colour options:</strong> [Colour 1], [Colour 2], [Colour 3]</li>
<li><strong>Compatibility:</strong> [e.g. Works with iOS 14+ and Android 10+]</li>
<li><strong>Warranty:</strong> [X-year manufacturer warranty]</li>
</ul>
<h3>Why Shoppers Choose [Product Name]</h3>
<p>
Unlike [generic alternatives], the [Product Name] [specific differentiator]. This means
[tangible benefit for the shopper], saving you [time / money / effort].
</p>
<p>
Ideal for [persona / scenario], the [Product Name] has been tested to [standard or
certification] and ships ready to use — no assembly required.
</p>
<h3>How to Use</h3>
<p>
[Step-by-step or scenario-based usage instructions in 2–4 sentences. Include any
important setup, safety, or care notes here.]
</p>
<h3>In the Box</h3>
<ul>
<li>1 × [Product Name]</li>
<li>1 × [Accessory or manual]</li>
<li>1 × [Charging cable / carry bag / etc.]</li>
</ul>
</section>Structural template for a product description section — paste inside your product page's main content area.
Seo product description missingModerate effort
Write and publish a 200–400 word product description covering what the product is, its key specs, who it's for, and how to use it.
On Shopify Plus
- Follow the same steps as Shopify (admin → Products → All products → select product → Description editor).
- If you use a custom storefront or Hydrogen/headless setup, ensure the 'descriptionHtml' field from the Storefront API is rendered in your product page template.
- For bulk updates across many products, use the Shopify bulk editor (Products → select multiple → Bulk edit) or import a CSV via Products → Import.
<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 Shopify Plus
- Same as Shopify above; additionally use the Shopify Plus Bulk Importer (Admin → Products → Import/Export via CSV) to update the 'Body HTML' column in bulk for large catalogues.
- Work with your developer to add Metaobject-powered 'expanded description' sections in your theme if you want descriptions to appear in a separate tab or accordion without affecting the default product template.
<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 Shopify Plus
- Use the same Admin → Products → Media → Edit alt text flow as Shopify.
- For catalog-wide updates, use the Shopify Admin REST or GraphQL API (productImageUpdate mutation, altText field) via a custom script or a Shopify Plus automation.
- Enterprise teams can also use a Metafield-based flow or a third-party PIM integration to push alt text at scale.
<!-- 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 Shopify Plus
- Follow the same steps as Shopify above — Shopify Plus uses the same theme code architecture.
- For enterprise catalogs with complex brand logic, consider using a Shopify App (e.g., 'JSON-LD for SEO' or 'Schema Plus for SEO') which automatically injects brand from `product.vendor` with no code edits required.
- In Shopify Plus scripts or custom storefronts, ensure the Storefront API response includes `vendor` and map it to the `brand.name` field in your schema output.
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 Shopify Plus
- Same as Shopify: populate the Barcode field on each product (Admin → Products → Inventory section).
- For bulk updates, use the Shopify bulk editor (select products → Edit products) or export/import a CSV with the 'Variant Barcode' column filled in.
- For enterprise catalog sync, use the Shopify Admin API (products endpoint) to programmatically set 'barcode' on each variant.
- Use Shopify Plus's B2B or custom storefront? Ensure the JSON-LD snippet in your custom theme references 'variant.barcode' for the 'gtin' property.
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 Shopify Plus
- Follow identical steps to Shopify above — Shopify Plus uses the same theme codebase.
- If you use a custom storefront (Hydrogen/Headless), add the JSON-LD block in your product page component and pull availability from the Storefront API `availableForSale` boolean, mapping `true` → `https://schema.org/InStock` and `false` → `https://schema.org/OutOfStock`.
- For B2B/wholesale storefronts via Shopify Plus, ensure inventory is synced before schema is rendered server-side so the value is always current.
<!-- 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 Shopify Plus
- All standard Shopify steps apply. Additionally, use Shopify Plus's Script Editor or custom theme code to auto-generate spec tables from product metafields across hundreds of SKUs at scale.
- In your theme code, open 'sections/main-product.liquid' (or the equivalent file) and add a Liquid block that iterates over a metafield namespace (e.g. product.metafields.specs) to output a <table> automatically.
- Use the Shopify CLI and GitHub integration available to Plus merchants to deploy and version-control the change across all storefronts.
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 Shopify Plus
- Follow the same steps as Shopify above. Shopify Plus uses the same Liquid theme system.
- If you have a custom Hydrogen (React Server Components) storefront, ensure the ProductPrice component is rendered as a Server Component so the price is included in the server-rendered HTML stream.
- Use the Shopify Rich Results app or manually maintain JSON-LD structured data in your theme's snippets for the 'offers' price property.
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 Shopify Plus
- All steps are identical to Shopify. For large catalogs, use the Shopify Admin Bulk Editor or a metafield/CSV import to set seo.title on every product at scale.
- Consider a Shopify Plus Script or an app such as 'SEO Manager' or 'Yoast SEO for Shopify' to enforce a title template (e.g. '{{ product.title }} – {{ product.type }} – {{ shop.name }}') store-wide.
<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 Shopify Plus
- Same as Shopify above. If you are using a custom Hydrogen/headless storefront, see the Headless / Custom entry.
- For script-generated sitemaps in a custom storefront, filter out any URL that contains a '?' character before writing entries to the sitemap XML.
<?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 Shopify Plus
- Same as Shopify above — edit theme.liquid or the equivalent layout file in the theme code editor.
- Additionally, if you use Shopify's Headless / Hydrogen storefront, you must manually output the canonical tag in your product route component. Set it to the base product URL (without ?variant=) using Remix's <Meta> or Next.js's <Head> component.
- For scripts or apps that rewrite the URL client-side on variant selection, ensure the canonical in the <head> is set server-side to the base product URL and is NOT updated by JavaScript when a variant is selected.
<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.
Slow pageModerate effort
Reduce your page load time to under 3 seconds by compressing images, eliminating render-blocking scripts and styles, and improving server response speed.
On Shopify Plus
- Follow all standard Shopify steps above.
- Use Shopify Plus's Script Editor or Checkout Extensibility to audit and trim any checkout-specific scripts that slow the cart/checkout funnel.
- Work with your Shopify Plus Merchant Success Manager or a Shopify Plus Partner agency to review theme code for custom slowdowns introduced during bespoke development.
- Consider Hydrogen (Shopify's React-based headless framework) for maximum performance control if your catalogue complexity warrants it.
<img
src="product-image.webp"
alt="Descriptive product alt text"
width="800"
height="600"
loading="lazy"
decoding="async"
>Add these attributes to every image below the fold to enable native lazy loading and prevent layout shift.
Thin contentModerate effort
Add meaningful, original written content to pages that contain fewer than 300 words so Google has enough substance to rank and display them confidently.
On Shopify Plus
- All steps are identical to standard Shopify. Additionally, Shopify Plus merchants can use Shopify Scripts or custom Liquid templates (Admin → Online Store → Themes → Edit code) to add structured FAQ blocks or specification tables to product templates at scale.
- For bulk updates across many products, use the Shopify Bulk Editor (Admin → Products → select products → Edit products) or export/import via CSV to update the Body HTML column with expanded descriptions.
<section class="product-description">
<h2>About This Product</h2>
<p>
The Merino Wool Crew-Neck Sweater is crafted from 100% ethically sourced
Grade-A Merino wool, making it naturally temperature-regulating, moisture-
wicking, and soft enough to wear directly against the skin. Perfect for
layering in autumn and winter, or as a standalone piece during mild spring
evenings.
</p>
<h3>Key Features</h3>
<ul>
<li>100% Merino wool — no synthetic blends</li>
<li>Available in sizes XS–3XL with a relaxed, unisex fit</li>
<li>12 colours, each piece individually yarn-dyed for depth</li>
<li>Ribbed cuffs and hem for a tailored finish</li>
<li>Machine washable on a gentle 30°C wool cycle</li>
<li>Certified by the Responsible Wool Standard (RWS)</li>
</ul>
<h3>Sizing & Fit</h3>
<p>
This sweater is cut with a relaxed, slightly oversized silhouette. If you
prefer a closer fit, we recommend sizing down. See our full <a href="/size-guide">size guide</a>
for chest, waist, and sleeve measurements.
</p>
<h3>Care Instructions</h3>
<p>
Machine wash cold on a gentle/wool cycle. Lay flat to dry — do not tumble
dry, iron at high heat, or dry-clean. With proper care, Merino wool retains
its shape and softness for years.
</p>
<h3>Frequently Asked Questions</h3>
<dl>
<dt>Is Merino wool itchy?</dt>
<dd>
No. Merino fibres are finer than standard wool (under 19.5 microns),
which means they bend against the skin rather than prick it, making this
sweater comfortable even for people who normally find wool irritating.
</dd>
<dt>How does Merino wool compare to cashmere?</dt>
<dd>
Merino is more durable and easier to care for than cashmere, while still
offering exceptional softness. It's an ideal everyday choice for those
who want luxury without high maintenance.
</dd>
<dt>Can I return or exchange this item?</dt>
<dd>
Yes. We accept returns and exchanges within 30 days of delivery, provided
the item is unworn and in its original packaging. See our
<a href="/returns">returns policy</a> for full details.
</dd>
</dl>
</section>Example of a well-structured, content-rich product description section with FAQ — paste into any HTML page template.
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 Shopify Plus
- Follow the same path as Shopify (Online Store → Pages / Products / Collections → Search engine listing section).
- For bulk updates across many products or pages, use a CSV export (Products → Export), edit the 'SEO Title' column in a spreadsheet to ≤60 characters, then re-import via Products → Import.
- Alternatively, use the Shopify Admin API or a Shopify Plus automation to update metafields in bulk.
<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.
Title too shortQuick win
Expand your page title to between 30 and 60 characters so Google displays it fully in search results and shoppers can immediately understand what the page is about.
On Shopify Plus
- All steps are identical to standard Shopify (Products, Collections, Online Store → Preferences, Pages, Blog Posts → Search engine listing panel).
- For enterprise stores with thousands of products, use the Shopify bulk editor: Products → All Products → select multiple products → 'Edit products' → add the 'SEO Title' column and update in bulk.
- Alternatively, export products as a CSV (Products → Export), update the 'SEO Title' column in a spreadsheet, and re-import (Products → Import) for large-scale fixes.
<head>
<meta charset="UTF-8">
<title>Men's Merino Wool Beanies – BrandName</title>
</head>Place inside the <head> element of every page. Replace the example with your actual descriptive title (30–60 chars).
Answer Engine Optimization · 9 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 Shopify Plus
- Follow the same steps as Shopify above — Shopify Plus uses the same theme editing interface.
- For large editorial teams, consider using a custom app or Shopify Scripts to programmatically inject author JSON-LD based on article metafields at scale.
- Use the Shopify Admin API to bulk-write author metafields across existing articles rather than editing each one manually.
<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.
Invalid rating valueModerate effort
Fix the AggregateRating.ratingValue in your structured data so it is a valid number between 1 and 5 (or your declared scale's minimum and maximum).
On Shopify Plus
- Follow the same steps as Shopify above.
- For enterprise stores using a custom storefront (Hydrogen/Headless), locate the JSON-LD component in your React/Next.js codebase and apply the same ratingValue bounds fix there.
- If using a third-party review platform (Yotpo, Okendo, etc.) at scale, coordinate with the platform's enterprise support to ensure schema output is valid before re-deploying.
Low review countModerate effort
Collect at least 5 published reviews so Google will display star ratings in search results alongside your product listings.
On Shopify Plus
- Follow the same Shopify steps above — Shopify Plus gives you access to all the same review apps.
- Additionally, use Shopify Flow to build a custom automation: trigger 'Order fulfilled' → wait 10 days → send review request email via your ESP or the review app's webhook.
- For B2B storefronts on Shopify Plus, consider Okendo or Yotpo for enterprise review management with structured data output.
- Verify JSON-LD output at the theme level: in the Shopify admin go to Online Store → Themes → Edit Code and search 'aggregateRating' to confirm no duplicate or conflicting snippets exist.
<!-- Product structured data with AggregateRating -->
<!-- Note: Google will only show review stars when reviewCount >= 5 -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Your Product Name",
"image": [
"https://example.com/images/product-front.jpg",
"https://example.com/images/product-side.jpg"
],
"description": "A clear, accurate description of your product.",
"sku": "SKU-12345",
"mpn": "MPN-12345",
"brand": {
"@type": "Brand",
"name": "Your Brand"
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.5",
"reviewCount": "12",
"bestRating": "5",
"worstRating": "1"
},
"offers": {
"@type": "Offer",
"url": "https://example.com/products/your-product",
"priceCurrency": "USD",
"price": "49.99",
"priceValidUntil": "2026-12-31",
"itemCondition": "https://schema.org/NewCondition",
"availability": "https://schema.org/InStock",
"seller": {
"@type": "Organization",
"name": "Your Store Name"
}
}
}
</script>Drop this complete block into the <head> section of any product page. Swap in your real values for name, image, SKU, price, ratingValue, and reviewCount.
Missing schema articleModerate effort
Add Article schema markup (JSON-LD) to every blog post and editorial page so Google can identify and rich-result-enhance your content in search.
On Shopify Plus
- Follow the same steps as standard Shopify (edit article.liquid or use a structured data app).
- Shopify Plus merchants with a custom storefront or Hydrogen (headless) build should add Article and BreadcrumbList JSON-LD in the React/Next.js component that renders article pages — inject via a <script type='application/ld+json'> tag inside the page's <Head> component, populating values from the Storefront API response (article.title, article.author.name, article.publishedAt).
- Use the Shopify Storefront API fields: articles query returns title, authorV2.name, publishedAt, updatedAt, image.url — map these directly to your JSON-LD template.
- Validate with Google's Rich Results Test after deployment.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>How to Choose the Right Running Shoes</title>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": "How to Choose the Right Running Shoes",
"description": "A complete guide to selecting running shoes based on your gait, terrain, and training goals.",
"image": {
"@type": "ImageObject",
"url": "https://www.example.com/images/running-shoes-guide.jpg",
"width": 1200,
"height": 630
},
"author": {
"@type": "Person",
"name": "Jane Smith",
"url": "https://www.example.com/authors/jane-smith"
},
"publisher": {
"@type": "Organization",
"name": "Example Store",
"logo": {
"@type": "ImageObject",
"url": "https://www.example.com/logo.png",
"width": 250,
"height": 60
}
},
"datePublished": "2024-03-15",
"dateModified": "2024-06-01",
"mainEntityOfPage": {
"@type": "WebPage",
"@id": "https://www.example.com/blog/how-to-choose-running-shoes"
},
"url": "https://www.example.com/blog/how-to-choose-running-shoes"
}
</script>
</head>
<body>
<!-- page content -->
</body>
</html>Place this entire block inside the <head> element of any article or blog-post page. Fill in all values before publishing.
Missing schema howtoModerate effort
Add HowTo structured data (JSON-LD) to any page that walks customers through a multi-step process, so Google can display your instructions as rich results in search.
On Shopify Plus
- Same as Shopify — use Edit Code on the relevant theme. Shopify Plus merchants can additionally use Shopify Scripts or a Storefront API-powered headless layer to inject JSON-LD programmatically.
- For organisation-wide rollout, add the JSON-LD injection to the <head> section of theme.liquid inside a Liquid conditional targeting the page handle (e.g. {% if page.handle contains 'how-to' %}).
- Use a structured data management app from the Shopify App Store for a no-code approach.
- Validate with Google's Rich Results Test.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>How to Complete Your Task | Your Store</title>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "HowTo",
"name": "How to Complete Your Task",
"description": "Follow these steps to complete your task easily.",
"totalTime": "PT10M",
"step": [
{
"@type": "HowToStep",
"position": 1,
"name": "Prepare your materials",
"text": "Gather everything you need before you start: list materials here."
},
{
"@type": "HowToStep",
"position": 2,
"name": "Complete the main action",
"text": "Describe exactly what the user should do in this core step."
},
{
"@type": "HowToStep",
"position": 3,
"name": "Verify the result",
"text": "Explain how the user confirms the task was completed successfully."
}
]
}
</script>
</head>
<body>
<!-- Your how-to page content -->
</body>
</html>Place this block inside the <head> element of any static HTML page that contains step-by-step instructions.
Missing schema localbusinessQuick win
Add LocalBusiness JSON-LD structured data to your store so search engines and AI answer engines can surface your business name, address, phone number, and hours in rich results and direct answers.
On Shopify Plus
- Same as Shopify: Online Store → Themes → Edit code → theme.liquid, paste before </head>.
- On Shopify Plus you can also deploy the script tag globally via the Shopify Scripts editor or a custom Storefront API integration if you manage headless storefronts.
- Enterprise teams often use the 'JSON-LD for SEO' app which supports multi-location and organisation schema with a GUI.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Your Store</title>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Store",
"name": "Your Business Name",
"url": "https://www.yourdomain.com",
"logo": "https://www.yourdomain.com/logo.png",
"address": {
"@type": "PostalAddress",
"streetAddress": "123 Main Street",
"addressLocality": "Springfield",
"addressRegion": "IL",
"postalCode": "62701",
"addressCountry": "US"
},
"telephone": "+1-217-555-0100",
"openingHoursSpecification": [
{
"@type": "OpeningHoursSpecification",
"dayOfWeek": ["Monday","Tuesday","Wednesday","Thursday","Friday"],
"opens": "09:00",
"closes": "18:00"
}
]
}
</script>
</head>
<body>
<!-- page content -->
</body>
</html>Paste this block anywhere inside the <head> element of your HTML page. Replace all placeholder values before deploying.
Missing schema organizationQuick win
Add an Organization schema markup block to your store's homepage (and/or sitewide template) so search engines and AI assistants can definitively identify your brand.
On Shopify Plus
- For Shopify Plus stores, follow the same theme.liquid approach as standard Shopify.
- Additionally, if you use Shopify's Checkout Extensibility, open your checkout.liquid (if still available on legacy) or use a checkout UI extension to inject the schema there too — though homepage coverage is the most important.
- If your store uses a headless front-end, add the JSON-LD block in your root layout component (see Headless instructions).
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "Acme Store",
"url": "https://www.acmestore.com",
"logo": {
"@type": "ImageObject",
"url": "https://www.acmestore.com/images/logo.png",
"width": 250,
"height": 250
},
"sameAs": [
"https://www.facebook.com/acmestore",
"https://www.instagram.com/acmestore",
"https://twitter.com/acmestore",
"https://www.linkedin.com/company/acmestore",
"https://www.youtube.com/c/acmestore"
],
"contactPoint": {
"@type": "ContactPoint",
"contactType": "customer service",
"email": "hello@acmestore.com"
}
}
</script>Drop this complete block inside your <head>…</head> section on every page. Replace all placeholder values.
Missing schema productModerate effort
Add Product schema markup (JSON-LD) to every product page so search engines can display rich results — including price, availability, and ratings — directly in search.
On Shopify Plus
- Follow the same Shopify steps above; in Shopify Plus you may also manage this via your custom storefront or via Shopify Scripts — prefer the theme approach for simplicity.
- If you use a headless or custom Hydrogen storefront, inject the JSON-LD block server-side in the product page component using the product data from the Storefront API response.
- Validate with Google's Rich Results Test after deployment.
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 Shopify Plus
- Follow the same steps as Shopify above — Shopify Plus uses the same theme code architecture.
- If your store uses a custom Hydrogen (headless) storefront, update the date formatting in your React components (see Headless / Custom entry below).
- For script-injected schema via Shopify Scripts or a third-party app (e.g., JSON-LD for SEO), update the date format inside the app's template settings.
<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) · 38 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 Shopify Plus
- Follow the same theme-code editing steps as Shopify above.
- For headless or custom storefronts built on Hydrogen/Remix: open your component files in your code editor, locate the JSX element with the invalid role prop, remove or correct it, then redeploy via your CI/CD pipeline.
- Verify the fix on your staging environment before pushing to production.
<!-- ❌ 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 Shopify Plus
- Follow the same Liquid code-editing steps as Shopify above; Shopify Plus gives you the same theme code editor plus access to checkout.liquid.
- For unlabelled elements specifically inside the checkout (e.g. remove-item buttons), edit checkout.liquid (available only on Shopify Plus) and add the appropriate aria-label attributes.
- Use Shopify's Accessibility app or a third-party accessibility overlay audit to surface remaining issues after editing.
<!-- 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 dialog nameModerate effort
Add a visible or programmatic accessible name to every dialog element so screen readers can announce what the dialog is about when it opens.
On Shopify Plus
- Shopify Plus uses the same theme-editing flow as standard Shopify.
- Go to Online Store → Themes → Edit code, then locate your dialog/modal/cart-drawer Liquid snippets (e.g. snippets/cart-drawer.liquid, snippets/modal.liquid).
- If you use a custom storefront or headless build via Shopify Storefront API, edit the React/Vue/Svelte component that renders the modal and add aria-labelledby or aria-label to the outermost dialog element.
- For third-party apps that inject dialogs (e.g. upsell popups), contact the app developer or use a custom Script Tag / Theme App Extension to patch the ARIA attributes after the element is inserted into the DOM.
- Verify with axe DevTools or a screen reader after each change.
<!-- Preferred: point to the dialog's own heading -->
<div role="dialog" aria-modal="true" aria-labelledby="cart-dialog-title">
<h2 id="cart-dialog-title">Shopping Cart</h2>
<p>You have 3 items in your cart.</p>
<button type="button">Close</button>
</div>
<!-- Alternative: use aria-label when there is no visible heading -->
<div role="dialog" aria-modal="true" aria-label="Shopping Cart">
<p>You have 3 items in your cart.</p>
<button type="button">Close</button>
</div>Preferred method: aria-labelledby points to the dialog's own heading. Use this whenever a visible title exists inside the dialog.
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 Shopify Plus
- Follow the same steps as Shopify above. In Shopify Plus you may also have a custom checkout (checkout.liquid): open it in Online Store → Themes → Edit code → layout/checkout.liquid and apply the same aria-hidden audit to any custom UI injected there.
- If you use Shopify Scripts or a headless storefront, the fix must be applied in your custom front-end code (see Headless / Custom entry below).
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 Shopify Plus
- Same steps as Shopify above. Additionally, if you use Checkout Extensibility (the new checkout editor), go to Settings → Checkout → Customize and inspect any custom checkout UI extensions you have installed.
- For custom checkout UI extensions built with Shopify's checkout UI kit, ensure every <TextField> component includes the required label prop — Shopify's own components handle aria-label automatically when label is supplied.
- For legacy checkout.liquid (if still enabled on your Plus plan), edit the file directly at Online Store → Themes → Edit code → layout/checkout.liquid and apply the same aria-label or <label for> patterns.
<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 Shopify Plus
- Same as Shopify above; Shopify Plus also allows customisation via the Checkout editor — for checkout-page widgets, go to Settings → Checkout → Customize and inspect the checkout liquid files in your theme code editor.
- For headless Shopify Plus storefronts (Hydrogen/custom), locate the React/Vue component responsible for the interactive widget and add the ARIA attributes as props, then wire the state variable to aria-expanded.
<!-- 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 Shopify Plus
- Follow all Shopify steps above — Shopify Plus stores use the same theme code layer.
- If you use a custom storefront (Hydrogen/Headless), locate the relevant React component in your repo (e.g. ProductOptions.tsx, ComboBox.tsx) and apply the role corrections directly in JSX.
- For Hydrogen, prefer Radix UI or Headless UI primitives which ship with correct ARIA role trees out of the box.
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 Shopify Plus
- Follow the same steps as Shopify above; Shopify Plus stores additionally may use custom storefronts — if using the Storefront API with a headless framework, apply the fix in your front-end component (see Headless / Custom entry).
- For checkout customisations using Checkout Extensibility (Checkout UI Extensions), edit the relevant extension component in your local codebase, add the correct parent role wrapper in JSX/TSX, and redeploy via the Shopify CLI (`shopify app deploy`).
<!-- 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 Shopify Plus
- Same as Shopify above; additionally check any custom checkout.liquid (available on Plus) for unnamed icon buttons in the checkout header or upsell sections.
- For headless / Hydrogen storefronts, locate the relevant .jsx/.tsx component (e.g. CartIcon.jsx) and add aria-label to the <button> element in JSX.
<!-- 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 Shopify Plus
- Same steps as Shopify above. Additionally, if you use a custom Checkout UI extension, open the checkout editor (Settings → Checkout → Customize) and update text/label color tokens there.
- For headless Shopify Plus storefronts, update the CSS or design-token file in your frontend repository and redeploy.
/* 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 Shopify Plus
- Follow the same steps as Shopify above; Plus merchants may also have custom checkout pages — check checkout.liquid for any `<dl>` usage and apply the same structural fixes.
- Use the GitHub integration (if connected) to search across all theme files for `<dl` and submit corrections via a pull request for version control.
<!-- ✅ 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 Shopify Plus
- All standard Shopify admin paths apply (see Shopify above).
- For custom storefronts or headless setups, open the relevant layout or page component and ensure `<title>` is populated dynamically from your page data.
- Use Shopify Scripts or custom liquid to enforce a fallback: `<title>{{ page_title | default: shop.name }}</title>` in `layout/theme.liquid`.
<!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 Shopify Plus
- Follow all standard Shopify steps above.
- For checkout customisation, go to Settings → Checkout → Customise and check heading blocks inside checkout sections for empty content.
- In Launchpad or custom scripts, search for dynamically injected heading tags that may render empty based on logic conditions — add a fallback text string or conditional rendering guard.
<!-- ❌ 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 Shopify Plus
- Follow the same steps as Shopify above; additionally check any custom Liquid templates stored in the 'templates' directory (e.g., templates/page.custom-comparison.liquid) and any scripts injected via Shopify Scripts or checkout customizations.
- For B2B price tables built with the Shopify Plus Wholesale Channel or custom apps, inspect the rendered HTML of those pages and patch the source template or app output accordingly.
<!-- 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 Shopify Plus
- Follow the same steps as Shopify above; additionally check any custom checkout CSS under Settings → Checkout → Checkout styling and add the same `:focus` / `:focus-visible` rules there if your checkout suppresses the outline.
/* =====================================================
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 Shopify Plus
- Follow the same steps as Shopify above; additionally audit checkout.liquid (accessible to Shopify Plus merchants) for any iframes used in the checkout flow and add title attributes there.
- For headless or custom storefronts on Shopify Plus, edit the component files in your codebase directly.
<!-- 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 Shopify Plus
- Follow all standard Shopify steps above.
- If using a custom checkout or headless storefront (Hydrogen/Remix), open the relevant React component files and audit JSX heading tags (h1–h6) to ensure sequential order, then redeploy.
<!-- 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 Shopify Plus
- Same as Shopify: Online Store → Themes → Actions → Edit code → Layout/theme.liquid.
- Update the `<html>` tag to include `lang="{{ shop.locale }}"` so it works across all expansion stores and locale-specific storefronts automatically.
- For headless storefronts built on the Storefront API, add the `lang` attribute in your framework's root HTML document component (e.g., `_document.js` in Next.js).
- Click Save and verify via View Source.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Your Page Title</title>
</head>
<body>
<!-- page content -->
</body>
</html>Add the lang attribute to the opening <html> tag of every HTML page. Use a valid BCP 47 language tag.
Html lang validQuick win
Add a valid BCP 47 language code to the `lang` attribute on your site's `<html>` element so browsers and assistive technologies know what language your content is written in.
On Shopify Plus
- Follow the same steps as Shopify above — Shopify Plus themes use the same `theme.liquid` layout file.
- If you use a custom Hydrogen/headless storefront, open your root layout component (e.g. `app/root.jsx` or `app/root.tsx`) and ensure the `<html>` element has `lang={locale}` where `locale` is derived from the Shopify Storefront API locale.
<!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.
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 Shopify Plus
- Follow the same steps as Shopify above; Shopify Plus stores also allow checkout customisation via Checkout Extensibility.
- For checkout UI extensions, open your extension code in the Shopify CLI / partner dashboard and locate any `<Button>` or rendered input components and ensure their label or children prop is non-empty.
- For legacy checkout.liquid (if still active), go to Online Store → Themes → Edit code → layout/checkout.liquid and fix any empty-value input buttons there.
<!-- 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 Shopify Plus
- For storefront templates follow the same steps as Shopify above (Online Store → Themes → Edit code).
- For the checkout, go to Settings → Checkout → (scroll to) Checkout customization → Checkout branding. Shopify Plus allows custom checkout UI extensions; use the Checkout UI Extension SDK to add `aria-label` props to custom input components.
- If you have a custom checkout.liquid (legacy), edit it directly: add `<label for='...'> </label>` / `aria-label` to any custom fields you have added.
<!-- 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 Shopify Plus
- Shopify Plus stores use the same theme code structure. Access Online Store → Themes → Actions → Edit code.
- For checkout-specific fields (checkout.liquid is available on Plus only): go to Online Store → Themes → Actions → Edit code → `layout/checkout.liquid` and inspect any custom input fields for missing `<label>` elements.
- Apply the same label/aria-label fix described in the Shopify steps above, paying particular attention to custom checkout upsell or gift-message input fields added via scripts.
- Use Shopify's Theme Inspector or a browser accessibility tool (axe DevTools extension) to verify after saving.
<!-- 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 Shopify Plus
- Same as Shopify: Online Store → Themes → Edit code → layout/theme.liquid.
- If your Plus store uses a custom checkout, also check checkout.liquid and ensure the checkout header is not nested inside a landmark.
- Save and verify with axe DevTools.
<!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 Shopify Plus
- Follow the same steps as Shopify above. In Shopify Plus you may also have a 'checkout.liquid' layout — open it and apply the same footer restructuring there if a <footer> or role="contentinfo" element exists inside a landmark wrapper in that file.
<!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 Shopify Plus
- Follow the same steps as Shopify above; Shopify Plus uses the same theme architecture.
- If you use a custom Hydrogen (headless) storefront, locate the root layout component (e.g., app/root.jsx or app/components/Layout.jsx) and ensure `<main>` is rendered as a direct child of the document body element, not inside a `<header>` or landmark wrapper component.
<!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 mainQuick win
Find and remove every duplicate `<main>` element or `role="main"` attribute so that exactly one main landmark exists on each page.
On Shopify Plus
- Follow the same steps as Shopify above; also check any custom checkout.liquid if it is enabled on your plan — it has its own layout and may independently contain a duplicate '<main>' tag.
- Review scripts added via Shopify Scripts or custom pixel injections in Settings → Customer events for DOM insertions.
<!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 Shopify Plus
- Same as Shopify: go to Online Store → Themes → Edit code → `layout/theme.liquid`.
- For checkout customisations, go to Settings → Checkout → Edit checkout (if on Checkout Extensibility). The checkout page landmark is managed by Shopify's checkout UI — you cannot directly edit the checkout HTML on Plus unless using a custom checkout.liquid (legacy). Focus on storefront templates.
- Wrap `{{ content_for_layout }}` in `<main id="MainContent">…</main>` in `theme.liquid` as above.
<!-- 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.
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 Shopify Plus
- Follow the same Shopify steps above; Shopify Plus themes use the same theme code editor.
- For enterprise themes (e.g. custom Dawn forks), the link styles may live in a component file such as sections/main-article.liquid or assets/component-rte.css — search across files for the 'a {' rule.
- Use the Theme Customizer → Custom CSS field (Online Store → Themes → Customize → Theme Settings → Custom CSS) for a quick override without touching source files.
/* 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 Shopify Plus
- Follow the same steps as Shopify above — Shopify Plus uses the same theme code architecture.
- If your store uses a custom Hydrogen/headless storefront, apply fixes directly in your React components: add aria-label props to <a> and <Link> elements, and aria-hidden='true' to decorative SVG icons.
- For enterprise themes with a design system, update the Icon component or Button component at the source so all instances inherit the fix.
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 Shopify Plus
- Follow the same steps as Shopify above — edit Layout → theme.liquid in the theme code editor.
- If your store uses a custom checkout or Checkout Extensibility, also check any checkout-specific liquid files (e.g. checkout.liquid if still available on your plan) for a separate viewport tag.
<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 Shopify Plus
- Follow the same steps as Shopify above.
- If you use a custom checkout (Checkout Extensibility), also check any checkout-specific liquid files such as checkout.liquid under the Layout folder and apply the same viewport fix there.
<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 Shopify Plus
- In your Shopify admin, go to Online Store → Themes → your active theme → Actions → Edit code (or use GitHub integration if your theme is version-controlled).
- Search all Liquid section and snippet files for nested interactive patterns (`<a ...><button`, `<button ...><a `, `<a ...><input`, etc.).
- Edit each offending file: restructure so only one interactive element exists per intended action. Convert the outer or inner element to a non-interactive `<span>` or `<div>` as appropriate.
- If using a headless or custom storefront (Hydrogen/Oxygen), edit the React/JSX component directly and apply the same single-interactive-element rule.
- Save, deploy, and verify with axe DevTools browser extension or keyboard-only 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.
Page has heading oneQuick win
Add a single, descriptive `<h1>` heading to every page so assistive technologies and search engines can identify the page's main topic.
On Shopify Plus
- Follow all standard Shopify steps above; Shopify Plus stores use the same Liquid theme architecture.
- For headless or custom storefronts built on the Storefront API, audit each React/Vue component that renders a page title and ensure the outermost title element is an `<h1>`.
- For checkout extensibility (checkout.liquid or checkout UI extensions), add an `<h1>` to any custom checkout page templates where one is missing.
<main>
<h1>Women's Running Shoes</h1>
<!-- rest of page content -->
</main>Place this after your site navigation, as the first element inside <main>. Style it with CSS as needed — the visual size is independent of the semantic tag.
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 Shopify Plus
- Follow the same steps as Shopify above — Shopify Plus uses the same theme architecture.
- If you have a custom checkout (Checkout Extensibility), edit your checkout UI extensions and ensure any custom content blocks are rendered within landmark-appropriate containers provided by Shopify's checkout components.
- For headless storefronts built on Shopify Storefront API, apply the landmark structure directly in your front-end framework templates (see the Headless / Custom entry below).
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 Shopify Plus
- Same as Shopify: Online Store → Themes → Edit code.
- In Shopify Plus you may also have a checkout.liquid file — search it for role="img" and apply the same aria-label fix there.
- Use the Shopify CLI locally for bulk search: run 'grep -r "role=\"img\"" ./templates ./sections ./snippets' to find all occurrences at once, then edit and push.
<!-- 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 Shopify Plus
- Follow identical steps to Shopify above. On Shopify Plus you can also edit checkout.liquid (available to Plus merchants): go to Online Store → Themes → Edit code → layout/checkout.liquid to label any custom checkout dropdowns.
- For headless/custom storefronts using the Storefront API, edit the React/Vue component that renders the <select> and add a <label htmlFor="..."> or aria-label prop.
<!-- 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 Shopify Plus
- Follow identical steps as Shopify above — `layout/theme.liquid` is the correct file for the skip link and main content target.
- If you use a custom checkout layout, also open `layout/checkout.liquid` (Shopify Plus only) and apply the same skip link + target pattern to the checkout page's main content wrapper.
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 Shopify Plus
- Follow the same steps as Shopify above; Shopify Plus uses the same theme.liquid architecture.
- For headless or custom storefronts built on the Storefront API, set the lang attribute server-side based on the locale returned by the API, e.g. in your Next.js or Hydrogen layout component: <html lang={locale.isoCode}>.
- If you use Shopify Plus Scripts or checkout extensions, also ensure any custom checkout HTML includes a valid lang attribute on its root element.
<!-- 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) · 23 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 Shopify Plus
- Same DNS-level change as standard Shopify — DMARC lives at your DNS provider, not in Shopify Plus admin.
- Shopify Plus merchants frequently send from multiple subdomains or use Launchpad/Flows that trigger transactional email — audit all sending sources in your rua reports first.
- Verify custom sender domain SPF/DKIM under Settings → Notifications in each store, and under Settings → Domains for any Shopify-managed domain.
- Edit _dmarc.yourdomain.com TXT record at your DNS provider to advance the p= tag through quarantine to reject.
v=DMARC1; p=quarantine; sp=quarantine; pct=100; rua=mailto:dmarc-reports@yourdomain.com; ruf=mailto:dmarc-failures@yourdomain.com; ri=86400; aspf=r; adkim=r; fo=1;This is the DNS TXT record value for _dmarc.yourdomain.com — paste this as the record content at your DNS provider. Replace the rua/ruf addresses with your own reporting inbox or DMARC reporting service address.
Hsts disabledQuick win
Set a valid HTTP Strict-Transport-Security (HSTS) header with a max-age of at least 31536000 (one year) and includeSubDomains to force browsers to always use HTTPS for your store.
On Shopify Plus
- Shopify Plus manages HSTS at the platform level for storefronts hosted on Shopify infrastructure — no manual header configuration is required or available in the admin.
- For headless/custom storefronts or any reverse-proxy layer you control in front of Shopify Plus, add the Strict-Transport-Security header in your proxy/CDN configuration (e.g., Cloudflare, Fastly, or your own nginx/Node server).
- In Cloudflare: Log in → select your domain → Security → Settings → enable HTTP Strict Transport Security (HSTS) → set Max Age to 12 months, tick includeSubDomains.
- Verify with DevTools: F12 → Network → reload page → click the document request → Response Headers → look for Strict-Transport-Security: max-age=31536000; includeSubDomains.
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 Shopify Plus
- Same as standard Shopify for standard storefronts — HSTS is managed by Shopify's Fastly CDN.
- If your Shopify Plus store uses a headless Hydrogen storefront deployed on Oxygen, add the header in your server/entry.server.tsx (or .jsx) response: response.headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
- If you route traffic through your own reverse proxy or Cloudflare in front of Shopify Plus, set the header at that layer (see Cloudflare steps in the Headless entry).
- Audit any installed Plus apps that manage security headers and update their max-age setting to 31536000.
import type {EntryContext} from '@shopify/remix-oxygen';
import {RemixServer} from '@remix-run/react';
import {renderToReadableStream} from 'react-dom/server';
export default async function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
) {
responseHeaders.set(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains',
);
const body = await renderToReadableStream(
<RemixServer context={remixContext} url={request.url} />,
{signal: request.signal},
);
return new Response(body, {
status: responseStatusCode,
headers: responseHeaders,
});
}In your Hydrogen storefront, set the header in app/entry.server.tsx (Remix-based Hydrogen v2).
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 Shopify Plus
- SSL certificate provisioning is identical to standard Shopify — fully automatic.
- Go to Admin → Online Store → Domains and confirm 'SSL certificate active' for every custom domain, including any expansion stores.
- Enable 'Redirect all traffic to this domain' on the primary domain to enforce HTTPS site-wide.
- For headless or custom storefronts using the Storefront API, ensure your CDN/edge layer (e.g. Cloudflare, Fastly) is configured with a valid certificate and HTTPS-only routing.
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 Shopify Plus
- Same as Shopify — server infrastructure is managed by Shopify.
- For compliance requirements, use Cloudflare as a reverse-proxy in front of your Shopify Plus storefront: point your domain's DNS to Cloudflare (orange-cloud proxy enabled), then add a Cloudflare Transform Rule to remove the `Server` response header (see Cloudflare steps).
- Contact your Shopify Plus launch engineer or support team to document that the header is platform-controlled for any PCI/compliance evidence package.
<!-- 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 Shopify Plus
- Same as Shopify above — the core Shopify platform controls server headers.
- For custom Hydrogen/Oxygen storefronts, edit server.ts and add response.headers.delete('X-Powered-By'); before returning any response.
- For third-party middleware or app proxies (common with Shopify Plus custom integrations), work with your developer to add the suppression at the proxy layer.
<!-- 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 Shopify Plus
- Follow all standard Shopify steps above.
- For Shopify Plus storefronts using custom checkout (checkout.liquid or Checkout Extensibility), any server-side cookies set in checkout scripts must be configured in your custom app or checkout extension backend with httpOnly: true, secure: true, sameSite: 'Strict'.
- If you use Shopify Plus Scripts or a third-party headless front-end, audit cookies set by that layer and add flags at the server/CDN edge (e.g. via Cloudflare Workers or Vercel Edge Middleware).
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 Shopify Plus
- Same options as Shopify above apply.
- Shopify Plus merchants using Cloudflare for Enterprise or a custom reverse proxy have full control: configure the CSP header at the CDN/proxy layer.
- For Hydrogen/Oxygen storefronts: open your server.ts (or entry.server.tsx) file, locate the handleRequest function, and add the Content-Security-Policy header to the Response object before returning it.
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 Shopify Plus
- Same DNS process as Shopify above — DMARC is a DNS-level record independent of Shopify Plus features.
- Shopify Plus merchants often use custom transactional email platforms (Klaviyo, Bronto, etc.): before enforcing DMARC, check each sending platform's documentation to ensure SPF and DKIM are configured for your domain in that platform.
- If you use Shopify's built-in email (Shopify Email): authenticate your sender domain under Settings → Email Marketing → Sender Domain and follow the SPF/DKIM setup steps so Shopify Email passes DMARC alignment before you set p=reject.
_dmarc.yourdomain.com. 3600 IN TXT "v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com; ruf=mailto:dmarc-forensic@yourdomain.com; sp=none; adkim=r; aspf=r"Add this as a DNS TXT record at your domain registrar or DNS provider. Replace yourdomain.com and the rua address with your actual domain.
Missing permissions policyQuick win
Add a Permissions-Policy HTTP response header to your store to explicitly restrict which browser features (camera, microphone, geolocation, etc.) pages and embedded third-party scripts are allowed to access.
On Shopify Plus
- Same options as standard Shopify apply. Shopify Plus merchants who have a dedicated Shopify-managed infrastructure can also contact Shopify Plus support to inquire about server-level header configuration, but the Cloudflare Transform Rule approach is faster and fully self-serve.
- In Cloudflare: domain → Rules → Transform Rules → Modify Response Header → Add 'Permissions-Policy' header with value 'camera=(), microphone=(), geolocation=(), payment=(), usb=(), interest-cohort=()'.
- Alternatively use a Shopify Plus Script or a compatible security-header app from the App Store.
<!-- 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 Shopify Plus
- Follow the same theme.liquid approach as standard Shopify (Online Store → Themes → Edit code → theme.liquid → <head>).
- Additionally, if you use Cloudflare (common on Plus plans), create a Transform Rule: Security → Transform Rules → Modify Response Header → Add header Referrer-Policy with value strict-origin-when-cross-origin, matching all requests.
- Verify in DevTools → Network → Response Headers that the header appears.
<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 Shopify Plus
- Same limitation as standard Shopify — the /.well-known/ path cannot be served directly from Shopify's asset pipeline.
- Use a Cloudflare Worker (see Shopify steps above) or ask your agency to handle this at the reverse-proxy layer.
- Verify by visiting https://yourdomain.com/.well-known/security.txt in a browser.
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 Shopify Plus
- Follow the same steps as Shopify above — DNS management location and Shopify-managed vs. external DNS apply identically to Plus merchants.
- Shopify Plus merchants using custom transactional email routing (e.g. via SendGrid or Postmark configured in the Plus admin) must also add the include: directive for that provider.
- For enterprise setups with a dedicated IP, confirm the exact SPF include string with your email delivery provider and add it to the record.
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 Shopify Plus
- For standard Shopify Plus storefronts, the platform manages HSTS automatically — verify the header is present using browser DevTools or securityheaders.com.
- For Hydrogen / Oxygen (headless) storefronts, open your server entry file (e.g. server.ts) and add the Strict-Transport-Security header to every response object before it is returned to the client — see the Node/Hydrogen snippet below.
- If you front your storefront with Cloudflare, enable 'HTTP Strict Transport Security (HSTS)' under SSL/TLS → Edge Certificates in your Cloudflare dashboard and set max-age to 31536000 with includeSubDomains enabled.
// server.ts (Hydrogen / Oxygen)
export default {
async fetch(request, env, executionContext) {
const response = await handleRequest(request, env, executionContext);
// Add HSTS header to every HTTPS response
response.headers.set(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains'
);
return response;
},
};In your Hydrogen server entry file (server.ts or server.js), add the header to the response before returning it.
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 Shopify Plus
- Same as Shopify above. Additionally, if you run a headless storefront (Next.js, Hydrogen, etc.) on your own hosting, add the header in your server/framework config — see the Headless / Custom entry below.
- For Shopify Plus checkout extensibility, the Shopify-controlled checkout domain handles its own headers; focus your effort on your main storefront domain via Cloudflare or your hosting layer.
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 Shopify Plus
- Same as Shopify above. Shopify Plus merchants using Checkout Extensibility or custom checkout domains should confirm the header is set on those domains via Cloudflare or their CDN in front of the custom domain.
- In Shopify Plus admin, go to Settings → Domains and verify any custom domain is proxied through Cloudflare or another CDN where you can add custom headers (see Cloudflare steps below).
- For the Launchpad or Scripts editor, these do not control HTTP headers — header changes must be made at the server/CDN layer.
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 Shopify Plus
- SSL management is identical to standard Shopify — certificates are auto-provisioned per domain.
- For Shopify Plus stores using a custom storefront or headless setup, confirm the domain used by your headless front end is also added under Settings → Domains so Shopify provisions its certificate.
- Enterprise plans with dedicated IP or third-party CDN (e.g. Fastly custom config) should contact their Shopify Plus Merchant Success Manager to confirm certificate renewal procedures.
<!-- Place this in your maintenance/holding page only if HTTPS is temporarily unavailable -->
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">Meta-refresh redirect for maintenance: not applicable to SSL renewal. Instead, use this HTML to display a temporary notice if your cert has lapsed and you need to redirect HTTP traffic while fixing HTTPS.
Ssl cert invalidModerate effort
Install a valid SSL/TLS certificate that exactly matches your store's domain name, so browsers trust your site and customer data is encrypted in transit.
On Shopify Plus
- Same automatic provisioning as Shopify applies. Go to Admin → Settings → Domains and verify SSL status.
- Shopify Plus merchants using Shopify's CDN follow the same DNS + re-add-domain steps as standard Shopify.
- If you use a third-party CDN (e.g. Fastly or Cloudflare) in front of your Shopify Plus store, the certificate must be installed at the CDN layer. Log in to your CDN dashboard and upload or provision a certificate that covers your storefront domain, then ensure your CDN's origin is set to your myshopify.com address.
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 Shopify Plus
- The process is identical to standard Shopify — SSL is managed automatically by the platform.
- Go to: Shopify Admin → Settings → Domains and verify DNS records match Shopify's required values.
- For expansion stores or custom checkout domains, ensure each domain/subdomain is added individually under Settings → Domains and has correct DNS pointing to Shopify.
- Shopify Plus merchants with a dedicated Merchant Success Manager can escalate SSL provisioning issues directly to that contact.
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 Shopify Plus
- Same as Shopify above; Shopify Plus merchants also have access to a dedicated Merchant Success Manager who can escalate SSL/port issues to Shopify infrastructure teams faster.
- For headless/custom storefronts on your own servers, ensure your server's firewall opens port 443 and your web server is configured with a valid TLS certificate (see generic steps).
Vulnerable component axiosModerate effort
Update the Axios JavaScript library to its latest stable release to eliminate known security vulnerabilities in your storefront code.
On Shopify Plus
- Follow the same steps as Shopify above; additionally check any scripts added via Shopify Scripts, Checkout Extensions, or the Script Editor app, as these may load their own Axios version.
- For headless Shopify Plus setups, update axios in your package.json via `npm install axios@latest`, rebuild, and redeploy through your hosting pipeline (Vercel, Netlify, etc.).
<!-- 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 Shopify Plus
- Same as Shopify above. Shopify Plus merchants should additionally check any custom Cloudflare Enterprise rules or edge middleware configured by their agency.
- Shopify Admin → Apps — audit all installed apps for header-manipulation behaviour.
- Cloudflare Dashboard → Rules → Transform Rules → Modify Response Headers — ensure no duplicate rule exists for `X-Content-Type-Options`.
- Use a staging/development store to verify headers before pushing changes to production.
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 Shopify Plus
- Shopify Plus storefronts have the same built-in X-Frame-Options: DENY header as standard Shopify — no admin setting is needed or available for the core storefront.
- For custom checkout extensions or headless storefronts deployed on external infrastructure (e.g. Vercel, AWS), set the header at the hosting/server layer as described in the Headless/Custom entry.
- If using a Contentstack or similar CMS preview integration that sends a conflicting ALLOW-FROM header, contact that vendor and request they update their preview configuration to use Content-Security-Policy frame-ancestors instead.
- Verify headers via DevTools → Network tab on your storefront pages.
<!-- 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 · 12 fixes
Cms versionModerate effort
Update your CMS and ecommerce platform to the latest stable version immediately to close known security vulnerabilities and maintain vendor support.
On Shopify Plus
- Like standard Shopify, the core platform is maintained by Shopify and updated automatically.
- Theme updates: Online Store > Themes — follow the same theme-update process as Shopify (above).
- Shopify Plus merchants with custom Hydrogen/headless storefronts must manually update Hydrogen and Remix package versions in their codebase — check package.json and run 'npm outdated' or 'yarn outdated', then update and re-deploy.
- Work with your Shopify Plus Merchant Success Manager if you are on a legacy platform version or have custom checkout scripts that may break after platform changes.
<!-- 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 Shopify Plus
- Same as Shopify: Admin → Settings → Domains. For enterprise stores the domain is almost always registered externally — log in to your registrar (often managed by an agency or IT team) and confirm auto-renew is active.
- Check with your account team or IT department to confirm who the registrar is, since Plus stores commonly transfer DNS to Cloudflare or another provider — the DNS host and the registrar are different things; auto-renew must be set at 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.
Gtm auditModerate effort
Install Google Tag Manager on your store and configure it to fire GA4 with full ecommerce event tracking (view_item, add_to_cart, purchase, and more).
On Shopify Plus
- Follow the same theme.liquid steps as Shopify above.
- Additionally, add the GTM noscript snippet to your checkout.liquid (available only on Shopify Plus) under Online Store › Themes › Edit code › checkout.liquid, just after the opening <body> tag — this enables purchase event tracking on the order confirmation page.
- Use Elevar or a custom data layer script to push checkout and purchase events from the checkout pages.
- Publish the GTM container and verify purchase events in GA4 DebugView.
<!-- Google Tag Manager (paste in <head>) -->
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-XXXXXXX');</script>
<!-- End Google Tag Manager -->
<!-- Google Tag Manager (noscript) (paste immediately after <body>) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXXXXX"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<!-- End Google Tag Manager (noscript) -->Replace GTM-XXXXXXX with your real Container ID. First snippet goes in <head>, second immediately after <body>.
Html langQuick win
Add a lang attribute to your site's <html> tag that matches the primary language of your store's content.
On Shopify Plus
- Follow the same steps as Shopify: Online Store → Themes → Edit code → Layout/theme.liquid.
- For Shopify Plus stores using Hydrogen (headless), open your root layout file (typically app/root.jsx or app/root.tsx) and set the lang prop on the <html> element: <html lang="en">.
- For multilingual Plus stores using Shopify Markets, use <html lang="{{ request.locale.iso_code }}"> to output the correct locale automatically.
<!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 oos schema not updatedModerate effort
Update the `offers.availability` value in your Product structured data from `InStock` to `OutOfStock` (or `PreOrder`) whenever a product sells out, so Google's records always match your actual inventory.
On Shopify Plus
- Follow the same theme-file steps as Shopify above; Shopify Plus gives you the same Online Store → Themes → Edit Code access.
- If you use a custom storefront (Hydrogen / Headless), update the component that renders JSON-LD for products to derive availability from the Storefront API's `availableForSale` boolean and `quantityAvailable` field.
- For large catalogs, use the Shopify Admin API to bulk-query out-of-stock variants and confirm the theme logic resolves them to `OutOfStock` in the schema output.
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Your Product Name",
"image": "https://example.com/images/product.jpg",
"description": "Short product description.",
"sku": "YOUR-SKU-123",
"brand": {
"@type": "Brand",
"name": "Your Brand"
},
"offers": {
"@type": "Offer",
"url": "https://example.com/product-page",
"priceCurrency": "USD",
"price": "29.99",
"availability": "https://schema.org/OutOfStock",
"itemCondition": "https://schema.org/NewCondition"
}
}
</script>Place this inside a <script type='application/ld+json'> tag in the <head> or <body> of your product page. Replace placeholder values with real data. Use OutOfStock when sold out.
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 Shopify Plus
- Follow all standard Shopify steps above.
- Additionally, use Shopify Plus Scripts or the Storefront API to ensure orphaned products appear in personalized recommendation carousels on the homepage and PDP pages.
- Use Shopify Plus's B2B Catalogs feature (if applicable) to confirm orphaned products are included in the correct price list and therefore discoverable by wholesale buyers.
<!-- 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 Shopify Plus
- Same auto-generation as Shopify; visit yourdomain.com/sitemap.xml to verify all active products are listed.
- For enterprise catalogs with many products, confirm no script or app is selectively setting products to 'Draft'; use Bulk Edit (Admin → Products → select all → Edit products) to check Status across the full catalog.
- If using Hydrogen/headless, implement a custom sitemap route in your Hydrogen app that queries all products via the Storefront API and outputs valid XML.
- Submit in Google Search Console → Indexing → Sitemaps → 'sitemap.xml' → Submit.
<?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 Shopify Plus
- Follow all standard Shopify steps above.
- In Shopify Plus you can also edit theme code directly: open Online Store → Themes → Actions → Edit Code and modify your navigation Liquid templates (typically navigation.liquid or header.liquid) to flatten menu nesting.
- Use Shopify Plus's Launchpad or Scripts to surface seasonal deep-linked products on the homepage during key campaigns, reducing temporary click depth spikes.
<!-- 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 Shopify Plus
- Follow the same theme-code and page-content steps as Shopify above.
- Additionally audit any Launchpad scripts, Shopify Scripts, or custom checkout.liquid modifications: go to Online Store → Themes → Edit code → checkout.liquid and search for 'http://'.
- Check any custom third-party integrations or headless front-end configurations for hardcoded HTTP asset URLs.
- Re-test with DevTools Console.
<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 Shopify Plus
- Follow the identical steps as Shopify above — the master template is still Online Store → Themes → Actions → Edit code → Layout/theme.liquid.
- If your store uses a custom storefront (Hydrogen/Headless), edit the root HTML shell file (e.g. root.tsx or _app.tsx) and add the meta tag inside the `<head>` component.
- For checkout extensibility, Shopify Plus injects the viewport tag automatically; no change needed there.
<!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 Shopify Plus
- Same as Shopify above — the core Liquid storefront headers are managed by Shopify's CDN and cannot be changed from the merchant admin.
- For custom Hydrogen/headless deployments on Oxygen or an external host, follow the Headless / Custom steps below.
- Engage your Shopify Plus Merchant Success Manager if a PCI or security audit specifically requires header suppression evidence — they can provide platform attestations.
<!-- 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 Shopify Plus
- Same as Shopify: certificates are managed automatically by Shopify's infrastructure.
- For Plus merchants using Shopify's advanced domain features or custom storefronts, confirm each domain in Admin → Settings → Domains shows an active SSL status.
- If you use a third-party reverse proxy or Cloudflare in front of your Shopify Plus store, also check that the certificate on the proxy layer is current.
<!-- 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.