Every Shopify fix we catalog

SEOLZ catalogs 182 fixes for Shopify across 6 areas — SEO, answer-engine readiness, accessibility, security and site-health. Each lists the exact steps for Shopify, with a link to the official docs.

SEO · 52 fixes

Catalog coverageModerate effort

Audit your XML sitemap and internal linking so that every product and category page is discoverable and crawlable by Google — not hidden behind JavaScript, pagination, or missing links.

On Shopify

  1. Admin › Online Store › Preferences: ensure your store is password-protected only while in development. On live stores, remove the password so crawlers can access the site.
  2. Shopify auto-generates a sitemap at yourstore.com/sitemap.xml that includes products and collections. Verify it in Admin › Online Store › Themes › Actions › Edit code — check theme.liquid for any accidental noindex tags on collection or product templates.
  3. In Google Search Console, go to Indexing › Sitemaps, enter yourstore.com/sitemap.xml and click Submit. Monitor the 'Discovered URLs' count against your actual product count.
  4. Check navigation: Admin › Online Store › Navigation. Ensure every collection (category) is reachable from a menu. Products without a collection link may only be reachable via the sitemap.
  5. For large catalogs using Shopify's built-in pagination, confirm each paginated collection page URL (e.g., /collections/shoes?page=2) is linked via the 'Next' button rendered as a standard <a> tag — Shopify's default themes do this correctly, but custom themes may break it.
  6. Install a sitemap or SEO app (e.g., Plug In SEO or SEO Manager) to audit which products are missing from the sitemap or carry noindex tags.
Code · Shopify (liquid)
{% comment %}
  CORRECT: Only noindex pages that should be hidden (e.g., search, cart).
  Do NOT noindex product or collection templates.
{% endcomment %}
{% if template == 'search' or template == 'cart' %}
  <meta name="robots" content="noindex, follow">
{% else %}
  <meta name="robots" content="index, follow">
{% endif %}

In theme.liquid (Online Store › Themes › Edit code › Layout › theme.liquid), check the <head> for any conditional noindex logic and remove it for product/collection templates.

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

  1. Images: In your theme editor (Online Store → Themes → Edit code), open the Liquid snippet that renders product/collection images — typically sections/product-template.liquid or snippets/product-image.liquid. Find every <img> tag and add width and height attributes matching the rendered dimensions, e.g. width='800' height='800'. Alternatively install the free 'Booster SEO' or 'TinyIMG' app which patches image dimensions automatically across your theme.
  2. Lazy-loading: Shopify's Dawn and most modern themes already add loading='lazy' and explicit dimensions via Liquid's | image_url filter with width parameter. If yours does not, update image_url calls to use: {{ image | image_url: width: 800 }} and render the img tag with width and height set.
  3. Announcement bar / cookie banner: In the theme editor (Customize → Header), move any announcement bar to a server-rendered position rather than injecting it via JavaScript after load. For third-party cookie apps, choose ones that render as fixed overlays (bottom of viewport), not top-push banners.
  4. Fonts: In your theme's layout/theme.liquid, add font-display: optional to any @font-face declarations. If using Shopify's font picker, edit the generated CSS in assets/base.css to add the descriptor.
  5. Ads / embeds: Wrap any review widgets (e.g. Judge.me, Yotpo) or social feed apps in a <div style='min-height:200px'> container in the relevant section file so space is reserved before the widget JS loads.
  6. Verify: Use PageSpeed Insights on your homepage and top product pages; confirm CLS < 0.1 on both mobile and desktop.
Code · Shopify (liquid)
{%- liquid
  assign img_width  = 800
  assign img_height = 800
  assign img_src    = product.featured_image | image_url: width: img_width
-%}
<img
  src="{{ img_src }}"
  alt="{{ product.featured_image.alt | escape }}"
  width="{{ img_width }}"
  height="{{ img_height }}"
  loading="lazy"
  decoding="async"
>

In your theme's image snippet (e.g. snippets/responsive-image.liquid or inline in a section file), use Shopify's image_url filter with explicit width and height to output a properly sized img tag.

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

  1. Go to Online Store → Themes → (your theme) → Edit code.
  2. Open theme.liquid (or layout/theme.liquid). Find any render-blocking `<script>` tags for third-party apps (chat, reviews, pop-ups) and add the `defer` or `async` attribute, or move them just before `</body>`.
  3. In the Shopify admin, go to Online Store → Preferences and disable any built-in scripts you don't need (e.g., welcome pop-up overlays).
  4. Go to Apps and review every installed app. Remove apps whose scripts are not actively driving revenue — each app injects JavaScript that can inflate INP. Check the App Blocks in the theme editor and remove unused ones.
  5. For custom JavaScript in assets/theme.js or assets/global.js, open those files and refactor heavy click/submit handlers: extract expensive work into a `setTimeout` or use `scheduler.yield()` so the browser can paint between steps.
  6. Use Shopify's Speed Score (Online Store → Themes) and PageSpeed Insights to verify improvement after each change.
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

  1. IDENTIFY LCP IMAGE: Run PageSpeed Insights on your storefront URL. Note the filename of the LCP image (usually the first banner/hero in your theme's index section).
  2. CONVERT TO WEBP: Shopify automatically serves WebP when you upload a standard JPEG/PNG — verify by right-clicking your live hero image and checking the URL ends in .webp or contains 'format=webp'. If not, ensure you are using a modern theme (Dawn or equivalent).
  3. PRELOAD: Go to Online Store › Themes › your active theme › Edit code. Open 'layout/theme.liquid'. Inside the <head> tag, add: <link rel='preload' as='image' href='{{ section.settings.image | image_url: width: 1500 }}' fetchpriority='high'>. For a hardcoded URL use your CDN path directly.
  4. NO LAZY-LOAD: In your hero section's Liquid file (e.g., sections/image-banner.liquid), find the <img> tag for the hero and remove loading='lazy' or change it to loading='eager'. Also add fetchpriority='high'.
  5. DEFER JS: In theme.liquid, add 'defer' to non-critical <script src='...'> tags that appear in <head>.
  6. APPS: Install a performance app such as 'Crush.pics' or 'TinyIMG' for bulk WebP conversion, or 'Booster: Page Speed Optimizer' for automatic render-blocking JS deferral.
  7. VERIFY: Re-run PageSpeed Insights and check Google Search Console › Experience › Core Web Vitals.
Code · Shopify (liquid)
{%- comment -%} Preload LCP hero image — place high in <head> in layout/theme.liquid {%- endcomment -%}
{%- if section.settings.image -%}
  {%- assign hero_url_1x = section.settings.image | image_url: width: 1500 -%}
  {%- assign hero_url_2x = section.settings.image | image_url: width: 3000 -%}
  <link
    rel="preload"
    as="image"
    href="{{ hero_url_1x }}"
    imagesrcset="{{ hero_url_1x }} 1500w, {{ hero_url_2x }} 3000w"
    imagesizes="100vw"
    fetchpriority="high"
  >
{%- endif -%}

Paste inside <head> in layout/theme.liquid, as early as possible. Adjust the section/setting variable to match your theme's hero image setting.

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

  1. For a single page: go to the relevant page in your Shopify admin (Online Store → Pages, Products, or Collections), scroll to the bottom of the editor to the 'Search engine listing' section, click 'Edit', and fill in the 'Meta description' field with your unique text.
  2. For products in bulk: go to Products → All Products, select the products to update, and use the 'Edit products' bulk editor — or export your product CSV (Products → Export), fill in the 'Body HTML' / 'SEO Description' column, then re-import.
  3. For collections: go to Online Store → Collections, open each collection, scroll to 'Search engine listing', and update the Meta description field.
  4. For the homepage: go to Online Store → Preferences and update the 'Meta description' field at the top.
  5. Consider installing a dedicated SEO app (e.g. Plug in SEO, SEO Manager, or TinyIMG) to audit and bulk-edit all meta descriptions from one dashboard.
  6. After updating, use the 'View' link on the page to confirm the description is live, then submit the URL for re-indexing in Google Search Console.
Code · Shopify (liquid)
{% comment %}
  In layout/theme.liquid, inside <head>:
  This outputs the meta description entered in the Shopify admin
  for every page type (product, collection, page, blog, article).
{% endcomment %}
<meta name="description" content="{{ page_description | escape }}">

In your theme's layout/theme.liquid file, ensure the meta description tag uses the page_description variable so the description you enter in the admin is output correctly.

Duplicate titleModerate effort

Give every page a unique, descriptive title tag that accurately reflects that page's specific content.

On Shopify

  1. For individual pages/products: Online Store → Pages (or Products / Collections) → click the item → scroll to 'Search engine listing' section at the bottom → edit 'Page title'.
  2. For theme-level templates (so every product page auto-generates a unique title): Online Store → Themes → Edit code → open the relevant layout/template file (e.g. 'layout/theme.liquid') and confirm the <title> tag uses dynamic Liquid variables such as {{ page_title }} — Shopify populates this from the product name automatically.
  3. For bulk updates across many products, use a bulk-edit CSV: Products → Export → edit the 'SEO Title' column in the CSV → import back.
  4. Install a dedicated SEO app (e.g. Plug In SEO, SEO Manager, or TinyIMG) to audit and bulk-edit title tags store-wide from one dashboard.
Code · Shopify (liquid)
<title>
  {%- if page_title == shop.name -%}
    {{ shop.name }} – {{ shop.description | truncate: 60 }}
  {%- else -%}
    {{ page_title }} | {{ shop.name }}
  {%- endif -%}
</title>

In layout/theme.liquid — Shopify's {{ page_title }} variable is already unique per page. Confirm this pattern is present; do not hard-code a static string.

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

  1. Shopify's native collection URLs (e.g., /collections/womens-pants) are clean by default. Faceted filter URLs are typically generated by apps (e.g., Boost Commerce, Searchanise, Instant Search+) or theme-level filter code using query strings like ?filter.p.m.color=Navy.
  2. Go to Online Store → Themes → click 'Edit code' on your active theme.
  3. Open theme/layout/theme.liquid (or the equivalent head partial). Locate where <link rel='canonical'> is already output — in most themes this is a Liquid line like <link rel='canonical' href='{{ canonical_url }}'>.
  4. Shopify automatically sets canonical_url to the clean collection URL even when filter params are present for standard collection pages — but verify this by using 'View Source' on a filtered URL and checking the canonical tag. If it already points to /collections/[handle] without query params, you may only need to ensure your filter app is not overriding it.
  5. If a filter app is adding its own canonical or overriding the theme's, go into that app's settings and look for a 'Canonical URL', 'SEO', or 'Indexing' section and set it to always point to the parent collection URL.
  6. To explicitly force canonical regardless of params, replace the canonical line in theme.liquid with: <link rel='canonical' href='{{ collection.url | prepend: shop.url }}'> — this always outputs the clean collection URL.
  7. Save the file. Use Google Search Console's URL Inspection to confirm a filtered URL (e.g., /collections/womens-pants?filter.p.m.color=Navy) now reports the canonical as /collections/womens-pants.
Code · Shopify (liquid)
{% comment %} Canonical tag — always points to clean collection/page URL, never filtered variants {% endcomment %}
{% if template contains 'collection' %}
  <link rel="canonical" href="{{ shop.url }}{{ collection.url }}">
{% elsif template contains 'product' %}
  <link rel="canonical" href="{{ shop.url }}{{ product.url }}">
{% else %}
  <link rel="canonical" href="{{ canonical_url }}">
{% endif %}

In theme/layout/theme.liquid — replaces the default canonical output to always use the clean collection URL, stripping filter params.

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

  1. Product images: Go to Admin → Products → [select product] → scroll to the Media section → click the image thumbnail → in the panel that slides in, type your description in the 'Alt text' field → click Save.
  2. Collection/banner images: Admin → Products → Collections → [select collection] → click the collection image → add alt text in the pop-up field → Save.
  3. Theme section images (hero banners, etc.): Admin → Online Store → Themes → Customize → click the section containing the image → look for an 'Image alt text' or 'Alt text' input field in the left panel → fill in the description → Save.
  4. Blog post / page images: In the rich-text editor, click the image → click the image icon or 'Edit image' → fill in the Alt text field.
  5. Bulk update for many products: Use the Shopify bulk editor (Admin → Products → select multiple → Edit) or export your product CSV, fill the 'Image Alt Text' column for every row, then re-import via Admin → Products → Import.
Code · Shopify (liquid)
{% comment %} Preferred: use Liquid image_tag filter with alt attribute {% endcomment %}
{{ product.featured_image | image_tag: alt: product.featured_image.alt | default: product.title }}

{% comment %} Or manually in an img tag — always output the alt attribute {% endcomment %}
<img
  src="{{ product.featured_image | img_url: '800x' }}"
  alt="{{ product.featured_image.alt | default: product.title | escape }}"
  width="800"
  height="800"
  loading="lazy">

In a Shopify Liquid theme file (e.g. product-media-gallery.liquid or product.liquid), replace img tags with the image_tag filter and pass alt text dynamically.

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

  1. Go to Online Store → Themes → click 'Customize' on your active theme, OR click the three-dot menu → 'Edit code'.
  2. In Edit Code, open the template most responsible for the issue: typically `sections/product-template.liquid` (or `sections/main-product.liquid` in Dawn-based themes) for product pages, or `sections/collection-template.liquid` / `sections/main-collection-product-grid.liquid` for collection grids.
  3. Find every `<img>` tag and any `{% render 'image' %}` or `image_tag` Liquid filter call. For product images, ensure the alt attribute is set to `{{ image.alt | escape }}` with a fallback: `{{ image.alt | default: product.title | escape }}`.
  4. For collection grids, the card snippet (e.g. `snippets/card-product.liquid`) is usually the single file to edit — update the `<img>` tag there to use `{{ card_product.featured_image.alt | default: card_product.title | escape }}`.
  5. To set alt text on individual images without editing code: go to Content → Files (or Settings → Files) and click on an image to add alt text, OR on the Product page, click each product image thumbnail and enter alt text in the field that appears.
  6. Save the file and preview a product/collection page to confirm `alt` attributes are populated.
Code · Shopify (liquid)
{% comment %} Product card image with alt fallback {% endcomment %}
{%- assign alt_text = card_product.featured_image.alt | default: card_product.title -%}
<img
  src="{{ card_product.featured_image | image_url: width: 600 }}"
  alt="{{ alt_text | escape }}"
  width="600"
  height="600"
  loading="lazy">

{% comment %} Product gallery thumbnail — main-product.liquid {% endcomment %}
{%- for media in product.media -%}
  {%- assign media_alt = media.alt | default: product.title -%}
  <img
    src="{{ media | image_url: width: 1200 }}"
    alt="{{ media_alt | escape }}"
    width="1200"
    height="1200"
    loading="{% if forloop.first %}eager{% else %}lazy{% endif %}">
{%- endfor -%}

Place in product image snippets such as snippets/card-product.liquid or sections/main-product.liquid. Uses image.alt with a fallback to product.title.

Low performance scoreLarger project

Improve your Lighthouse/PageSpeed performance score by reducing page weight, eliminating render-blocking resources, optimising images, and addressing Core Web Vitals so Google ranks your store higher in search results.

On Shopify

  1. Audit apps: Admin → Apps → remove every app you no longer use actively; each installed app injects JS on every storefront page.
  2. Switch to a performance-optimised theme: Online Store → Themes → Explore free themes → choose Dawn (Shopify's reference theme, built for Core Web Vitals). Dawn scores 90+ on mobile out of the box.
  3. In your theme's Liquid, ensure all <img> tags use loading='lazy' for below-the-fold images and include explicit width and height attributes to prevent CLS. Dawn does this by default.
  4. Use Shopify's built-in image CDN: when you reference images via {{ image | image_url: width: X }}, Shopify automatically serves WebP via its global CDN — never hotlink external image sources.
  5. Install a trusted performance app such as Crush.pics or TinyIMG to bulk-compress existing product images, and enable lazy loading store-wide.
  6. Defer third-party scripts: in theme.liquid, move non-essential <script> tags to just before </body> and add the defer attribute.
  7. Enable Shopify's built-in speed optimisations: they are on by default (CDN, HTTP/2, GZIP) — verify nothing has been overridden in Settings → Files or a custom app.
  8. Re-check: use PageSpeed Insights on your live store URL to confirm improvement.
Code · Shopify (liquid)
{%- comment -%}
  Performance-optimised image tag for Shopify.
  Usage: {% render 'perf-image', image: product.featured_image, priority: false %}
{%- endcomment -%}

{%- assign loading = 'lazy' -%}
{%- assign fetchpriority = 'auto' -%}
{%- if priority == true -%}
  {%- assign loading = 'eager' -%}
  {%- assign fetchpriority = 'high' -%}
{%- endif -%}

<img
  src="{{ image | image_url: width: 800 }}"
  srcset="
    {{ image | image_url: width: 400 }} 400w,
    {{ image | image_url: width: 800 }} 800w,
    {{ image | image_url: width: 1200 }} 1200w
  "
  sizes="(max-width: 600px) 100vw, 50vw"
  alt="{{ image.alt | escape }}"
  width="{{ image.width }}"
  height="{{ image.height }}"
  loading="{{ loading }}"
  fetchpriority="{{ fetchpriority }}"
  decoding="async"
>

Add to theme.liquid (or snippets/image.liquid) in Online Store → Themes → Edit code. Replaces a standard product image tag.

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

  1. Go to Online Store → Pages (for content pages) or Products / Collections (for product/collection pages).
  2. Click the page, product, or collection you want to edit.
  3. Scroll to the bottom of the editor to the 'Search engine listing' section and click 'Edit website SEO'.
  4. Edit the 'Description' field — Shopify shows a live character counter. Keep it under 160 characters (under 80 for Japanese/CJK).
  5. Click Save. No theme code changes are needed.
Code · Shopify (liquid)
<meta name="description" content="{% if page_description %}{{ page_description | escape | truncate: 155 }}{% else %}{{ shop.description | escape | truncate: 155 }}{% endif %}">

In your theme's layout/theme.liquid, ensure you output the page-level SEO description before falling back to the shop description.

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

  1. For a product page: Admin → Products → select the product → scroll to the 'Search engine listing' section at the bottom → click 'Edit' → update the 'Meta description' field (target 120–160 characters) → Save.
  2. For a collection page: Admin → Products → Collections → select the collection → same 'Search engine listing' section → Edit → update description → Save.
  3. For the homepage: Admin → Online Store → Preferences → 'Homepage meta description' field → Save.
  4. For blog posts or pages: Admin → Online Store → Blog Posts (or Pages) → select entry → 'Search engine listing' section → Edit → Save.
  5. To bulk-update many products at once, use a CSV export (Admin → Products → Export), edit the 'SEO Description' column in a spreadsheet, then re-import.
Code · Shopify (liquid)
<meta name="description" content="{% if page_description != blank %}{{ page_description | escape }}{% else %}{{ shop.description | escape }}{% endif %}">

In your theme's layout/theme.liquid file, replace or extend the existing meta description tag inside <head>.

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

  1. Shopify automatically outputs a canonical tag for product, collection, blog, and article pages via the `canonical_url` Liquid variable in most themes — but only if the theme template includes it.
  2. In your Shopify Admin, go to Online Store → Themes → click the '...' menu next to your active theme → Edit code.
  3. Open the file `layout/theme.liquid` (this is your global HTML wrapper).
  4. Inside the `<head>` section, confirm this line exists: `<link rel='canonical' href='{{ canonical_url }}'>`. If it is missing, add it directly after the `<title>` tag.
  5. For custom landing pages or pages built with a page template, open the relevant template file (e.g. `templates/page.liquid`) and check that it uses the main layout that already includes the canonical, OR add the tag manually.
  6. Save and verify by visiting a product page, right-clicking → View Page Source, and searching for 'canonical'.
Code · Shopify (liquid)
<link rel="canonical" href="{{ canonical_url }}">

Add inside the <head> section of layout/theme.liquid. Shopify's canonical_url variable resolves to the correct clean URL automatically.

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

  1. For product pages: Online Store → Products → open the product → the 'Title' field at the very top automatically renders as the H1 in most themes. Ensure this field is filled in.
  2. For collection pages: Online Store → Products → Collections → open the collection → the 'Title' field renders as the H1. Fill it in if blank.
  3. For the homepage: Online Store → Customize (Theme Editor) → click the section at the top of your homepage → locate a 'Heading' or 'Title' block and add your H1 text there.
  4. For blog posts/pages: Online Store → Blog Posts (or Pages) → open the post → the 'Title' field renders as the H1.
  5. If your theme does NOT output the title as an H1 (verify in DevTools), go to Online Store → Edit Code → open the relevant template (e.g. templates/product.liquid or sections/main-product.liquid) and wrap the title output in an <h1> tag: <h1>{{ product.title }}</h1>.
  6. Save changes and verify with View Page Source, searching for '<h1>'.
Code · Shopify (liquid)
<h1 class="product__title">{{ product.title }}</h1>

In sections/main-product.liquid (or product.liquid), replace any non-H1 title wrapper with this.

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

  1. For a product page: Admin → Products → click the product → scroll to the 'Search engine listing' section at the bottom → click 'Edit' → fill in the 'Meta description' field (Shopify shows a live character counter).
  2. For a collection page: Admin → Products → Collections → click the collection → same 'Search engine listing' section at the bottom.
  3. For the homepage: Admin → Online Store → Preferences → fill in the 'Meta description' field under 'Homepage meta tags'.
  4. For blog posts: Admin → Online Store → Blog Posts → click the post → 'Search engine listing' section at the bottom.
  5. For other pages (About, Contact, etc.): Admin → Online Store → Pages → click the page → 'Search engine listing' section.
  6. To bulk-update products at scale, use a third-party app such as 'SEO Manager' or 'Plug In SEO', or export products via Admin → Products → Export, fill the 'Body HTML' / SEO columns in the CSV, then re-import.
Code · Shopify (liquid)
{%- if page_description -%}
  <meta name="description" content="{{ page_description | escape }}">
{%- else -%}
  <meta name="description" content="{{ shop.description | escape }}">
{%- endif -%}

Add this to the <head> section of your theme.liquid (or layout/theme.liquid) as a fallback for pages where no manual description has been set.

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

  1. In your Shopify Admin, go to Online Store → Themes → click Actions → Edit code.
  2. Open theme/layout/theme.liquid (or your active layout file).
  3. Locate the <head> section and find any existing Open Graph block (search for 'og:').
  4. If your theme already has an OG block, ensure it includes: <meta property="og:description" content="{{ page_description | escape }}">. Shopify's page_description variable pulls from the 'Description' field of each page, product, or collection.
  5. If there is no OG block, add the full block (see code snippet below) just before </head>.
  6. For product pages specifically, open sections/product-template.liquid (or product-form.liquid in Dawn) and verify the same og:description tag is present in any schema/head overrides.
  7. Save the file, then test a product URL in Facebook's Sharing Debugger.
Code · Shopify (liquid)
{% comment %} Open Graph Tags {% endcomment %}
<meta property="og:type"        content="{% if template == 'product' %}product{% elsif template == 'article' %}article{% else %}website{% endif %}" />
<meta property="og:url"         content="{{ canonical_url | escape }}" />
<meta property="og:title"       content="{{ page_title | escape }}" />
<meta property="og:description" content="{% if page_description != blank %}{{ page_description | strip_html | truncate: 160 | escape }}{% else %}{{ shop.description | strip_html | truncate: 160 | escape }}{% endif %}" />
{% if product.featured_image %}
  <meta property="og:image"     content="{{ product.featured_image | img_url: '1200x630' }}" />
{% elsif page.image %}
  <meta property="og:image"     content="{{ page.image | img_url: '1200x630' }}" />
{% endif %}

Add this block inside the <head> tag in layout/theme.liquid. Shopify's page_description variable auto-populates per page/product/collection.

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

  1. In your Shopify admin, go to Online Store → Themes → click 'Customize' on your active theme.
  2. For a site-wide default og:image (homepage and pages without a specific image), go to Online Store → Preferences → scroll to 'Social sharing image' → upload your 1200×630 px brand image and save.
  3. For product pages: each product's featured image is automatically used as og:image by most modern Shopify themes. Ensure every product has a high-quality featured image set.
  4. To customize the og:image tag in code, go to Online Store → Themes → Actions → Edit code → open 'theme.liquid' (or your theme's layout file) and locate the <head> section. Most themes already include {% if page_image %} logic — verify it outputs a full og:image tag.
  5. For finer control or to add og:image to collection and blog pages, install a free SEO app such as 'SEO King' or 'Yoast SEO for Shopify' which provide per-page og:image fields in the page editor.
  6. Validate using Facebook's Sharing Debugger after saving changes.
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

  1. In your Shopify admin, go to Online Store → Themes → click 'Edit code' on your active theme.
  2. Open the file theme.liquid (or layout/theme.liquid). Locate the closing </head> tag.
  3. Add the og:title snippet just before </head>. Most modern Shopify themes (Dawn, Debut, etc.) already include Open Graph tags in a snippets/social-meta-tags.liquid or snippets/head-social.liquid file — search for 'og:title' across your theme files first.
  4. If using a snippets file (e.g. snippets/social-meta-tags.liquid), ensure it contains: <meta property="og:title" content="{{ page_title | escape }} | {{ shop.name | escape }}"> — and that this snippet is rendered via {% render 'social-meta-tags' %} inside theme.liquid.
  5. For per-page SEO titles: go to the individual product, page, or blog post editor in the admin → scroll to the 'Search engine listing preview' section → click 'Edit website SEO' → fill in the 'Page title' field. Shopify uses this value for og:title if your theme is set up correctly.
  6. Verify using Facebook Sharing Debugger: paste your store URL and confirm the og:title is shown.
Code · Shopify (liquid)
{% comment %} snippets/social-meta-tags.liquid {% endcomment %}
{% assign og_title = page_title | escape %}
{% if og_title == blank %}
  {% assign og_title = shop.name | escape %}
{% endif %}
<meta property="og:title" content="{{ og_title }}" />
<meta property="og:type" content="{% if template == 'product' %}product{% else %}website{% endif %}" />
<meta property="og:url" content="{{ canonical_url }}" />
<meta property="og:site_name" content="{{ shop.name | escape }}" />

Place in snippets/social-meta-tags.liquid and render it with {% render 'social-meta-tags' %} inside the <head> in layout/theme.liquid.

Missing titleQuick win

Add a unique, descriptive title tag (30–60 characters) to every page that is missing one.

On Shopify

  1. For a product: go to Products → select the product → scroll to the 'Search engine listing' section at the bottom → click 'Edit' → fill in the 'Page title' field.
  2. For a collection: go to Products → Collections → select the collection → scroll to 'Search engine listing' → click 'Edit' → fill in 'Page title'.
  3. For the homepage or a regular page: go to Online Store → Pages (or Preferences for the homepage) → select the page → scroll to 'Search engine listing' → click 'Edit' → fill in 'Page title'.
  4. For the homepage specifically: go to Online Store → Preferences → fill in the 'Homepage title' field at the top.
  5. Keep the title 30–60 characters and click Save.
Code · Shopify (liquid)
<title>
  {%- if page_title == blank -%}
    {{ shop.name }}
  {%- elsif template == 'index' -%}
    {{ shop.name }}{% if current_tags %} &ndash; {{ current_tags | join: ', ' }}{% endif %}
  {%- else -%}
    {{ page_title }} | {{ shop.name }}
  {%- endif -%}
</title>

In layout/theme.liquid, replace or ensure the <title> block looks like this. This is the theme-wide template; individual page titles set in the admin override page_title automatically.

Multiple h1Moderate effort

Remove all but one H1 tag from each page so every page has exactly one clear, descriptive primary heading.

On Shopify

  1. Go to Online Store → Themes → click Actions → Edit code on your active theme.
  2. Open the template file for the affected page type: product.liquid (or sections/main-product.liquid in Dawn-based themes) for product pages, collection.liquid or sections/main-collection-product-grid.liquid for collections, index.liquid or sections/image-banner.liquid for the homepage.
  3. Search (Ctrl+F) for '<h1' in each file. There should be exactly one. Change any extras to <h2> or the correct semantic level.
  4. In product card snippets (e.g. snippets/card-product.liquid), product titles are often wrapped in H3 — confirm they are NOT H1.
  5. Check any installed apps that inject HTML (reviews, upsell widgets) by previewing the page and using browser DevTools → Elements → search for h1.
  6. Save each file, preview on a live page, and confirm with DevTools or an SEO checker.
Code · Shopify (liquid)
{%- comment -%}
  Correct: product title as the one and only H1 on the product page.
  All other headings in this template should use h2, h3, etc.
{%- endcomment -%}

<h1 class="product__title">{{ product.title | escape }}</h1>

<!-- Section headings below the title use h2 -->
<h2>Product Description</h2>
<div class="product__description rte">
  {{ product.description }}
</div>

<h2>You May Also Like</h2>
{%- comment -%}
  In card-product.liquid, product names in the grid should be h3, NOT h1:
{%- endcomment -%}
{%- for related in recommendations.products -%}
  <h3><a href="{{ related.url }}">{{ related.title | escape }}</a></h3>
{%- endfor -%}

Place in sections/main-product.liquid (Dawn theme) or product.liquid — ensures product name is the sole H1.

Noindex detectedQuick win

Remove or replace the noindex directive on any page you want to appear in Google search results.

On Shopify

  1. Global toggle: In the Shopify admin, there is no single 'noindex all pages' toggle for live stores — but check your theme's theme.liquid file for any hardcoded <meta name="robots"> tag and remove or correct it.
  2. Per-page (Pages & Blogs): Shopify does not expose a native per-page noindex toggle in admin; if a noindex tag exists it is almost certainly in your theme code or an SEO app.
  3. Theme file check: Go to Online Store → Themes → your active theme → Edit code. Open theme.liquid and search for 'noindex'. Remove or change any <meta name="robots" content="noindex"> you find.
  4. SEO app check: If you use an SEO app (e.g., Plug In SEO, SEO Manager, Smart SEO), open the app and check both global settings and per-page/template overrides for noindex checkboxes and uncheck them.
  5. Password-protected stores: Online Store → Preferences → Password protection — if 'Enable password' is on, the entire store is noindexed. Disable it for a live store.
  6. After fixing, submit the URL in Google Search Console (Search Console → URL Inspection → Request Indexing).
Code · Shopify (liquid)
{% comment %} Robots meta tag — allows indexing for all public pages {% endcomment %}
{% unless template == 'cart' or template == 'checkout' or template == '404' %}
  <meta name="robots" content="index,follow">
{% endunless %}

In your theme's theme.liquid, find and replace any noindex robots meta tag with this block inside <head>.

Non self canonicalQuick win

Audit every page whose canonical tag points to a different URL, then either correct it to self-reference or confirm the cross-reference is intentional and correct.

On Shopify

  1. Shopify auto-generates self-canonical tags for standard product, collection, blog, and page templates — so a non-self-canonical usually means a theme customization or app override is present.
  2. In your Shopify admin go to Online Store → Themes → click 'Edit code' on your active theme.
  3. Open the layout file (typically 'theme.liquid') and search for 'canonical'. If you see a hardcoded or overridden canonical tag (not the Liquid variable {{ canonical_url }}), replace it with the standard tag: <link rel="canonical" href="{{ canonical_url }}">.
  4. Check any installed SEO apps (e.g., SEO Manager, Plug In SEO, Smart SEO) — open the app and look for a canonical override setting for the affected page type; disable or correct it.
  5. For individual pages/products, some SEO apps let you set a custom canonical per-URL — find that setting and either remove the override or correct the destination URL.
  6. Save changes, then use the Shopify storefront preview to view-source the affected page and confirm the canonical tag now matches the page URL.
Code · Shopify (liquid)
<link rel="canonical" href="{{ canonical_url }}">

In theme.liquid (or your layout file), inside <head> — uses Shopify's built-in canonical_url variable.

Og url mismatchModerate effort

Set a unique, page-specific og:url meta tag on every page that exactly matches that page's canonical URL.

On Shopify

  1. In your Shopify Admin, go to Online Store → Themes → click 'Edit code' on your active theme.
  2. Open 'snippets/' and look for a file named 'social-meta-tags.liquid', 'meta-tags.liquid', or similar (theme names vary; search for 'og:url' across all files using the search box in the code editor).
  3. Find the og:url meta tag line. If it reads something like content='{{ shop.url }}', replace it with the correct dynamic value per page type (see code snippets below).
  4. If your theme uses 'theme.liquid', locate the og:url line in the <head> section and apply the same fix.
  5. Save the file, then preview a product page and a collection page and use 'View Source' to confirm the og:url matches each page's actual URL.
  6. If you use a third-party SEO app (e.g. Yoast for Shopify, SEO King, Plug In SEO), check the app's settings — it may override theme og:url output and have its own field for this.
Code · Shopify (liquid)
{%- assign og_url = canonical_url | default: shop.url -%}
<meta property="og:url" content="{{ og_url }}" />

In snippets/social-meta-tags.liquid or theme.liquid inside the <head>. Replaces any hardcoded og:url.

Page not foundModerate effort

Fix broken internal links pointing to pages that return a 404 error by either removing those links or setting up a 301 redirect from the missing URL to the correct replacement page.

On Shopify

  1. Log in to your Shopify admin and go to Online Store → Navigation → URL Redirects.
  2. Click 'Create URL redirect'. Enter the old broken path (e.g. /products/old-product-handle) in 'Redirect from' and the new correct URL in 'Redirect to'. Save.
  3. To fix the internal links, go to Online Store → Pages (for content pages), Products, or Blog Posts and edit each page that contains the broken link. Update or remove the anchor tag in the page content editor.
  4. For links inside your theme (navigation menus, footers, featured-product sections), go to Online Store → Navigation and edit the offending menu item, or use the theme editor (Online Store → Themes → Customize) to update hard-coded links in sections.
  5. Verify by visiting the old URL in your browser — you should be automatically sent to the new page. Confirm the HTTP status is 301 using a tool like httpstatus.io.
Code · Shopify (liquid)
<!-- BEFORE: hard-coded broken path in a Liquid template -->
<a href="/collections/old-collection-handle">Old Collection</a>

<!-- AFTER: corrected path -->
<a href="/collections/new-collection-handle">New Collection</a>

<!-- BEST PRACTICE: use Liquid link_to or route helpers when available -->
<a href="{{ routes.collections_url }}/new-collection-handle">New Collection</a>

Update a hard-coded broken link inside a Liquid theme file (e.g. sections/footer.liquid or snippets/nav.liquid) to point to the correct URL.

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

  1. Images: In the Shopify admin go to Content > Files and re-upload key images as WebP before adding them to your theme. In your theme's Liquid templates use the built-in image_url filter with 'width' and 'format: 'webp'' parameters so Shopify's CDN serves correctly sized, modern-format images automatically.
  2. Theme speed: Go to Online Store > Themes > Customize, and in the theme settings disable any built-in features you do not use (e.g. predictive search animations, sticky header effects, video autoplay) — each one loads extra JS/CSS.
  3. Apps: In the Shopify admin go to Apps and uninstall any app you are not actively using — every installed app injects its own JavaScript into every storefront page even when idle.
  4. Scripts: In Online Store > Themes > Edit code, open theme.liquid and move non-critical <script> tags to load with defer or async attributes, and place them just before </body> rather than in <head>.
  5. Speed app: Install a reputable performance app such as 'Booster: Page Speed Optimizer' from the Shopify App Store for automated script deferral and image lazy loading without code edits.
  6. Fonts: In your theme settings, choose a system font stack instead of a Google Font, or self-host the font files and add font-display: swap to reduce layout shift.
Redirect chainModerate effort

Update every internal link that passes through a redirect chain to point directly at the final destination URL, eliminating all intermediate hops.

On Shopify

  1. Run a crawl (e.g. Screaming Frog) on your store and export all internal links that pass through redirects. Note each chained URL and its final destination.
  2. For links inside page or blog content: go to Online Store → Pages (or Blog Posts), open each affected page in the rich-text editor, select the hyperlinked text, click the link icon, and replace the URL with the final destination URL.
  3. For navigation menu links: go to Online Store → Navigation, open the relevant menu (e.g. Main Menu or Footer), click the item whose URL is a redirect, and update it to the final URL. Save.
  4. For links hard-coded in theme templates (e.g. header.liquid, footer.liquid, collection pages): go to Online Store → Themes → Actions → Edit Code, locate the template file containing the link, and update the href to the final URL.
  5. If you installed an SEO or redirect app (e.g. Redirectify, SEO Manager), open it and review the redirect list — remove or update any redirect that is itself pointing to another redirect rather than to a live 200 page.
  6. Re-crawl the store after changes to confirm zero-hop chains remain.
Code · Shopify (liquid)
{% comment %} BEFORE: href resolves through one or more redirects {% endcomment %}
<a href="/old-collection">Shop Now</a>

{% comment %} AFTER: href points directly to the final URL slug {% endcomment %}
<a href="/collections/all-products">Shop Now</a>

In a Liquid theme template (e.g. header.liquid or footer.liquid), replace the chained href with the final URL. Use the direct absolute or root-relative path — do not rely on a redirect to resolve it.

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

  1. In your Shopify Admin, go to Online Store → Themes → click 'Edit code' on your active theme.
  2. Open the 'templates/search.liquid' (or 'sections/main-search.liquid') file — this is the template rendered for every search results page.
  3. In 'layout/theme.liquid', find the <head> section and add a conditional block: {% if template == 'search' %}<meta name="robots" content="noindex,follow">{% endif %}. This scopes the noindex tag only to search pages.
  4. Save the file.
  5. Go to Online Store → Preferences, scroll to the 'Robots.txt' section and click 'Edit robots.txt'. Add a new rule: Disallow: /search (note: Shopify's robots.txt is editable via a robots.txt.liquid template — go to Edit Code, open robots.txt.liquid, and add the Disallow line inside the existing ruleset).
  6. Verify by typing your-store.com/search?q=test in Google Search Console's URL Inspection tool and confirming the page is 'noindex'.
Code · Shopify (liquid)
{% if template == 'search' %}
  <meta name="robots" content="noindex,follow">
{% endif %}

Add this block inside the <head> tag in layout/theme.liquid.

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

  1. In your Shopify admin, go to Products → Collections.
  2. Click the collection you want to edit.
  3. In the 'Description' field (a rich-text editor below the collection title), type or paste your 100–200 word category description.
  4. Use the toolbar to bold key phrases or add bullet points if appropriate — the formatting is rendered as HTML on the storefront.
  5. Click Save. The description is output by your theme using the {{ collection.description }} Liquid variable; most themes display it above or below the product grid automatically.
  6. If your theme does not display {{ collection.description }}, go to Online Store → Themes → Edit code → find your collection template (usually sections/collection-template.liquid or templates/collection.liquid) and add {{ collection.description }} in the desired location.
  7. Optionally, use a 'read more / show less' JavaScript toggle if you prefer the description to be collapsed on mobile — search the Shopify Theme Store for 'collapsible content' sections.
Code · Shopify (liquid)
{%- if collection.description != blank -%}
  <div class="category-description rte">
    {{ collection.description }}
  </div>
{%- endif -%}

Add to your theme's collection template (sections/main-collection-product-grid.liquid or templates/collection.liquid) where you want the description to appear.

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

  1. In your Shopify admin, go to Products > Collections.
  2. Click the collection you want to improve.
  3. Scroll to the 'Description' rich-text field and type or paste your expanded description (100+ words).
  4. Use the toolbar to add bold headings, bullet lists, or internal links to related collections.
  5. Scroll down to the 'Search engine listing preview' section and confirm the meta description also reflects your content.
  6. Click 'Save'. The description renders where your theme places the collection.description Liquid object — if it doesn't appear on the front end, you may need to add {{ collection.description }} to your collection template in Online Store > Themes > Edit code > sections/collection-template.liquid (or equivalent).
Code · Shopify (liquid)
{% if collection.description != blank %}
  <div class="collection-description rte">
    {{ collection.description }}
  </div>
{% endif %}

Add this to your theme's sections/collection-template.liquid (or equivalent) to render the collection description.

Seo category emptyQuick win

Either populate the empty category with products, or add a noindex tag and remove it from your navigation so Google does not waste crawl budget on a page with no content.

On Shopify

  1. Log in to your Shopify admin and go to Products → Collections.
  2. Click the empty collection to open it.
  3. Option A — Add products: Use the 'Add products' button or switch Conditions to 'Automated' and set rules that will pull in matching products automatically.
  4. Option B — Noindex: In the collection editor, scroll to the 'Search engine listing preview' section, click 'Edit website SEO', and in the URL/handle area note the URL. Then go to Online Store → Themes → Edit code → open 'layout/theme.liquid'. Wrap a conditional noindex tag: {% if collection.handle == 'your-handle' %}<meta name='robots' content='noindex, follow'>{% endif %} inside the <head> block. Save.
  5. Option B (no-code alternative): Install the 'Yoast SEO' or 'SEOAnt' app from the Shopify App Store, find the collection, and toggle 'noindex' on within the app.
  6. Option C — Delete & redirect: Click 'Delete collection' on the collection page, then go to Online Store → Navigation and remove the link. Go to Online Store → URL Redirects → Add URL redirect, enter the old collection path as the source and the parent category as the destination.
  7. Remove the collection from any Navigation menus under Online Store → Navigation.
Code · Shopify (liquid)
{% if collection and collection.products_count == 0 %}
  <meta name="robots" content="noindex, follow">
{% endif %}

Add inside the <head> block in layout/theme.liquid. Replace 'your-empty-collection-handle' with the actual collection handle (visible in the collection URL).

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

  1. Go to Products → Collections in your Shopify admin.
  2. Click a sparse collection. In the 'Products' section, click 'Browse' or adjust the collection's conditions to include more matching products.
  3. To add a description/intro text: scroll to the collection page editor and add copy in the Description field (supports rich text and HTML).
  4. To merge collections: move all products from the sparse collection into the surviving collection, then in Online Store → Navigation remove the old collection from menus. Use a third-party redirect app (e.g., Shopify's built-in URL Redirects: Online Store → Navigation → URL Redirects) to 301 redirect the old /collections/old-slug to /collections/new-slug.
  5. To noindex a sparse collection: in Online Store → Themes, edit your theme's collection.liquid (or collection.json sections). Add <meta name="robots" content="noindex, follow"> inside the <head> conditionally for that collection, or install an SEO app (e.g., SEO King, Smart SEO) that provides per-collection noindex toggles.
Code · Shopify (liquid)
{% if collection.products_count < 6 %}
  <meta name="robots" content="noindex, follow">
{% endif %}

In your theme's layout/theme.liquid or sections/main-collection-banner.liquid, conditionally noindex collections with fewer than a set number of products.

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

  1. Shopify automatically outputs a self-referencing canonical on most pages, but it does NOT automatically set a canonical on facet/filter URLs generated by third-party filter apps or ?sort_by= and ?page= parameters.
  2. Open your theme code: Online Store → Themes → Actions → Edit Code.
  3. Open the file 'layout/theme.liquid'. Locate the existing <link rel="canonical"> tag (search for 'canonical').
  4. Replace or wrap the canonical logic so that for collection pages, the canonical always outputs the clean collection URL without query parameters. Use Liquid: {% assign canonical_url = canonical_url | split: '?' | first %} then <link rel="canonical" href="{{ canonical_url }}">.
  5. If you use a faceted-search app (e.g. Boost Commerce, Searchpie, Filterify), check the app's settings for a 'Canonical URL' or 'SEO' section — many have a built-in toggle to enforce canonicals. Enable it.
  6. Save changes and use the URL Inspection tool in Google Search Console to verify the canonical on a filtered URL (e.g. ?sort_by=price-ascending) now points to the clean collection URL.
Code · Shopify (liquid)
{%- if template.name == 'collection' -%}
  <link rel="canonical" href="{{ shop.url }}{{ collection.url }}">
{%- elsif template.name == 'product' -%}
  <link rel="canonical" href="{{ shop.url }}{{ product.url }}">
{%- else -%}
  <link rel="canonical" href="{{ canonical_url }}">
{%- endif -%}

In layout/theme.liquid, replace the existing canonical tag (or add if missing) inside <head>. This strips all query parameters from the URL for collection pages.

Seo product content client renderedLarger project

Server-render your product descriptions in the initial HTML so that all crawlers — including Google, AI search bots, and social media scrapers — can read your content without executing JavaScript.

On Shopify

  1. Shopify's Liquid templating engine is server-rendered by default — if your product descriptions are missing from raw HTML, a third-party app or custom JavaScript in your theme is the culprit.
  2. Go to Online Store → Themes → Actions → Edit Code. Open templates/product.liquid (or sections/product-template.liquid / sections/main-product.liquid depending on your theme).
  3. Locate where the description is output. It should read {{ product.description }} inside the Liquid file — NOT loaded via a fetch() call in a .js file. If it is loaded via JS, move the output back to the Liquid template.
  4. To add Product JSON-LD, open or create snippets/product-jsonld.liquid, paste the JSON-LD snippet (see code_snippets below), and include it in your product template with {% render 'product-jsonld', product: product %}. This renders the JSON-LD on the server inside the <head>.
  5. Never place structured data inside a script tag that is itself injected by JavaScript — it must be a static Liquid-rendered <script type='application/ld+json'> block.
  6. Use the Shopify Theme Inspector Chrome extension or Google Search Console's URL Inspection → View Tested Page → HTML tab to confirm the description and JSON-LD appear before JS runs.
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

  1. Go to Shopify Admin → Products → All Products.
  2. Click the product you want to update.
  3. Scroll to the 'Description' rich-text editor field.
  4. Replace or expand the existing description using the formatting toolbar to add headings (H2/H3), bullet lists for specs, and body paragraphs for benefits and use cases.
  5. Aim for 300+ words of original, structured content.
  6. Click 'Save' and then use Google Search Console's URL Inspection to request re-indexing.
Code · Shopify (liquid)
{%- if product.description != blank -%}
  <div class="product__description rte" itemprop="description">
    {{ product.description }}
  </div>
{%- else -%}
  <div class="product__description rte">
    <p>No description available for this product yet.</p>
  </div>
{%- endif -%}

If your theme's product.liquid or main-product.liquid truncates the description, replace or augment it with this block. Edit via Online Store → Themes → Edit code → Sections → main-product.liquid (or product-template.liquid).

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

  1. In your Shopify admin, go to Products → All products.
  2. Click the product that is missing a description.
  3. In the 'Description' rich-text editor (the large text area directly below the product title), type or paste your 200–400 word description. Use the toolbar to add bold text, bullet lists, or headings for readability.
  4. Scroll down to the 'Search engine listing preview' section and click 'Edit website SEO' to verify the meta description (first 155–160 characters of the description, or write a custom one).
  5. Click 'Save' and visit the live product URL to confirm the description appears.
Code · Shopify (liquid)
{% if product.description != blank %}
  <div class="product-description rte">
    {{ product.description }}
  </div>
{% else %}
  <p class="product-description--empty">
    Description coming soon.
  </p>
{% endif %}

In your product template (sections/main-product.liquid or product.liquid), render the description field with this block — Shopify outputs the rich-text HTML stored in the description editor.

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

  1. Go to Admin → Products → [select the product].
  2. Scroll to the 'Description' rich-text editor (HTML editor available via the '<>' icon in the toolbar).
  3. Replace or expand the existing description with your new 300+ word copy; use the formatting toolbar to add bullet lists and bold key specs.
  4. To batch-edit many products efficiently, use the 'Edit products' bulk editor (Admin → Products → select multiple → Edit products) — note this shows a plain-text field; for rich HTML use individual product pages or a third-party app like Metafields Guru or Bulk Product Edit.
  5. Click 'Save' and then submit the URL for re-indexing in Google Search Console.
Code · Shopify (liquid)
{% if product.description != blank %}
  <div class="product__description rte" itemprop="description">
    {{ product.description }}
  </div>
{% else %}
  <div class="product__description rte">
    <p>No description available for this product.</p>
  </div>
{% endif %}

In your product template (e.g. sections/main-product.liquid or templates/product.liquid), ensure the description is output with this block. This is already present in most themes — confirm it renders and is not hidden by CSS.

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

  1. Go to Admin → Products and open a product.
  2. In the Media section, hover over a product image and click the three-dot menu (⋯) or the image itself.
  3. Click 'Edit alt text' in the pop-up dialog.
  4. Type your descriptive alt text (e.g. 'Blue cotton crew-neck sweater, front view') and click Save.
  5. Repeat for every image on every product. For bulk updates across a large catalog, use the Shopify Bulk Editor (Admin → Products → select multiple → Edit products) or a third-party app such as 'Alt Text' by Booster Apps or 'SEO Image Optimizer' by Hextom.
Code · Shopify (liquid)
{%- for media in product.media -%}
  {%- if media.media_type == 'image' -%}
    {%- assign alt_text = media.alt | default: product.title -%}
    <img
      src="{{ media | image_url: width: 800 }}"
      alt="{{ alt_text | escape }}"
      width="800"
      height="{{ 800 | divided_by: media.aspect_ratio | round }}"
      loading="lazy"
    >
  {%- endif -%}
{%- endfor -%}

In a Shopify theme (e.g. sections/main-product.liquid), use the image's alt attribute which is set in the admin. Output it with a fallback to the product title.

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

  1. In your Shopify admin, go to Online Store → Themes → click 'Edit code' on your active theme.
  2. Open the product template file: either `templates/product.json` (Dawn and newer OS 2.0 themes) or `templates/product.liquid` (older themes).
  3. Find or search for `@type.*Product` to locate the existing Product schema block — in most themes it lives in a `<script type="application/ld+json">` tag inside `snippets/structured-data.liquid` or `sections/main-product.liquid`.
  4. Inside the Product JSON-LD object, add the brand property using Liquid to pull the vendor: `"brand": { "@type": "Brand", "name": {{ product.vendor | json }} }`.
  5. Click 'Save'. Use Shopify's Preview to open any product URL, then test it with Google's Rich Results Test to confirm the brand field is present.
Code · Shopify (liquid)
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": {{ product.title | json }},
  "image": {{ product.featured_image | img_url: 'grande' | prepend: 'https:' | json }},
  "description": {{ product.description | strip_html | json }},
  "sku": {{ product.selected_or_first_available_variant.sku | json }},
  "brand": {
    "@type": "Brand",
    "name": {{ product.vendor | json }}
  },
  "offers": {
    "@type": "Offer",
    "url": {{ shop.url | append: product.url | json }},
    "priceCurrency": {{ cart.currency.iso_code | json }},
    "price": {{ product.selected_or_first_available_variant.price | divided_by: 100.0 | json }},
    "availability": {% if product.available %}"https://schema.org/InStock"{% else %}"https://schema.org/OutOfStock"{% endif %}
  }
}
</script>

Add or replace the brand block inside your Product JSON-LD snippet (snippets/structured-data.liquid or sections/main-product.liquid). The `product.vendor` Liquid variable holds the brand/vendor value.

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

  1. Go to Admin → Products → [select a product].
  2. In the 'Inventory' section, find the 'Barcode (ISBN, UPC, GTIN, etc.)' field and enter the product's GTIN (UPC/EAN digits only, no dashes).
  3. Shopify's default theme (Dawn and most modern themes) automatically outputs this field as the 'gtin' property in the Product JSON-LD that is generated on every product page — no code change needed.
  4. To verify: view the product page source and search for '"gtin"' — you should see your barcode value.
  5. If your theme does NOT output gtin automatically, open the theme code editor (Online Store → Themes → Edit code) and locate 'product.json' or the schema script in 'snippets/structured-data.liquid'. Add: '"gtin": {{ product.variants.first.barcode | json }}' inside the Product object.
  6. For MPN, add a product metafield: Admin → Settings → Custom data → Products → Add definition. Key: 'mpn', Namespace: 'custom'. Then reference it in the schema snippet as '"mpn": {{ product.metafields.custom.mpn | json }}'.
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

  1. Shopify automatically outputs `availability` in its built-in Product schema IF your theme uses the Online Store 2.0 schema — verify by going to Online Store → Themes → Edit Code, opening `sections/product-template.liquid` or `sections/main-product.liquid`, and searching for `schema.org`.
  2. If your theme injects Product JSON-LD, find the script block (commonly in `snippets/product-schema.liquid` or inside the product section file) and add: `"availability": "{{ product.available | ternary: 'https://schema.org/InStock', 'https://schema.org/OutOfStock' }}"` inside the JSON-LD object.
  3. For the visible label, in `sections/main-product.liquid` (or your product form snippet), add a Liquid conditional near the Add-to-Cart button: `{% if product.available %}<span class="stock-label">In Stock</span>{% else %}<span class="stock-label">Out of Stock</span>{% endif %}`.
  4. Save, preview a product, and validate at the Rich Results Test.
Code · Shopify (liquid)
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": {{ product.title | json }},
  "description": {{ product.description | strip_html | truncatewords: 50 | json }},
  "sku": {{ product.selected_or_first_available_variant.sku | json }},
  "image": {{ product.featured_image | img_url: 'grande' | prepend: 'https:' | json }},
  "brand": {
    "@type": "Brand",
    "name": {{ product.vendor | json }}
  },
  "offers": {
    "@type": "Offer",
    "url": {{ shop.url | append: product.url | json }},
    "priceCurrency": {{ cart.currency.iso_code | json }},
    "price": {{ product.selected_or_first_available_variant.price | money_without_currency | json }},
    "availability": {% if product.available %}"https://schema.org/InStock"{% else %}"https://schema.org/OutOfStock"{% endif %},
    "itemCondition": "https://schema.org/NewCondition"
  }
}
</script>

{%- comment -%}Visible stock label — place near the Add-to-Cart button{%- endcomment -%}
{% if product.available %}
  <p class="stock-label in-stock">&#10003; In Stock</p>
{% else %}
  <p class="stock-label out-of-stock">&#10005; Out of Stock</p>
{% endif %}

Add inside your Product JSON-LD script block in snippets/product-schema.liquid or sections/main-product.liquid. This replaces or supplements the existing offers object.

Seo product no imagesModerate effort

Add at least one high-quality image to every product page so shoppers can see what they're buying and Google can surface your products in Image Search.

On Shopify

  1. Log in to your Shopify admin and go to Products → All products.
  2. Click the name of the product that is missing images.
  3. In the 'Media' section at the top of the product edit page, click 'Add media' (or drag and drop files).
  4. Upload one or more image files (JPG, PNG, or WebP recommended).
  5. Click on each uploaded image thumbnail and fill in the 'Alt text' field with a descriptive phrase.
  6. Click 'Save' and then visit the product's storefront URL to confirm the images appear.
Code · Shopify (liquid)
{% if product.media.size > 0 %}
  <div class="product-gallery">
    {% for media in product.media %}
      {% if media.media_type == 'image' %}
        <img
          src="{{ media | img_url: '800x800', crop: 'center' }}"
          alt="{{ media.alt | escape | default: product.title | escape }}"
          width="800"
          height="800"
          {% if forloop.first %}loading="eager"{% else %}loading="lazy"{% endif %}
        />
      {% endif %}
    {% endfor %}
  </div>
{% else %}
  <p class="no-image-notice">No product images available.</p>
{% endif %}

Paste this into your product template (e.g. sections/product-template.liquid or sections/main-product.liquid) where you want the gallery to render. This loops over all product media.

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

  1. Go to Online Store → Themes → click 'Customize' on your live theme.
  2. Navigate to a product page and look for a content section called 'Product information', 'Product details', or 'Collapsible tabs' (most modern themes like Dawn include a Collapsible Tab block).
  3. Add a new 'Collapsible tab' block, label it 'Specifications', and paste your HTML table or list into the content field using the 'Show HTML' (<>) toggle.
  4. Alternatively, edit the product description directly: Products → (select product) → in the Description editor click the '<>' source icon and paste the spec table HTML.
  5. For a scalable approach, install a free/paid metafield app (e.g. Metafields Guru) or use Shopify's native Metafields (Settings → Custom data → Products) to store spec values, then render them via your theme's Liquid template at 'sections/main-product.liquid'.
  6. Save and preview on both desktop and mobile before publishing.
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

  1. In your Shopify Admin go to Online Store → Themes → current theme → Edit code.
  2. Open sections/main-product.liquid (or templates/product.liquid on older themes).
  3. Locate the price block. Shopify's Dawn theme uses a snippet: snippets/price.liquid. Ensure the price is output via Liquid, e.g. {{ product.selected_or_first_available_variant.price | money }}.
  4. If your theme uses a JavaScript framework (e.g. a custom React/Vue storefront) that fetches price via the Storefront API, you must move that fetch to a server component or use Hydrogen's server-rendering. For standard Liquid themes this is already server-rendered — confirm no JS is overwriting the element before the initial HTML paint.
  5. For structured data, open or create snippets/structured-data.liquid and add a JSON-LD Product schema block that includes the 'offers' property with 'price' populated by Liquid variables (see code snippet below). Include this snippet in theme.liquid inside <head>.
  6. Preview the page, view source (Cmd+U), and search for the price value to confirm it is present in raw HTML.
Seo product single imageModerate effort

Add at least 4 product images (multiple angles, detail shots, and lifestyle/in-use photos) to every product page to improve click-through rates, conversions, and Google Images visibility.

On Shopify

  1. Go to Admin → Products → [select your product].
  2. In the 'Media' section at the top of the product page, click 'Add media' to upload additional images, videos, or 3D models.
  3. Drag and drop images to reorder them — the first image becomes the main gallery image.
  4. Click each image thumbnail, then click 'Edit alt text' in the panel that appears and type a descriptive alt text for that image.
  5. Save the product. For bulk image alt-text editing across many products, use the free 'Bulk Image Edit' app from the Shopify App Store.
Code · Shopify (liquid)
<div class="product-gallery">
  {% for media in product.media %}
    {% if media.media_type == 'image' %}
      <img
        src="{{ media | img_url: '2000x2000' }}"
        alt="{{ media.alt | escape }}"
        width="2000"
        height="2000"
        {% if forloop.first %}loading="eager" fetchpriority="high"{% else %}loading="lazy"{% endif %}
      />
    {% endif %}
  {% endfor %}
</div>

Place inside your product template file (e.g., sections/product-template.liquid or sections/main-product.liquid). Renders all product media with alt text.

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

  1. Go to Products → All products and click the product you want to fix.
  2. Scroll to the bottom of the product editor to the 'Search engine listing' section and click 'Edit'.
  3. Update the 'Page title' field with your descriptive title (e.g. 'Magnetic Card Holder Mount – MagSafe – BrandName'). This field directly writes the HTML <title> tag.
  4. Leave the on-page product heading (the 'Title' field at the top) as-is or update it to match — it controls the visible H1, not the <title>.
  5. Click Save. To bulk-update many products, use the Shopify bulk editor (select products → Edit products) or export/import via CSV, editing the 'SEO Title' column.
  6. For theme-level title templates, edit the product.liquid (or product JSON template) and look for {{ page_title }} — ensure it outputs the SEO title field, not just the product title.
Code · Shopify (liquid)
<title>
  {%- if page_title == blank -%}
    {{ shop.name }}
  {%- elsif template.name == 'product' -%}
    {{ product.metafields.global.title_tag | default: product.title }} – {{ product.type }} – {{ shop.name }}
  {%- else -%}
    {{ page_title }} – {{ shop.name }}
  {%- endif -%}
</title>

In your theme's layout/theme.liquid (or sections/head-content.liquid), ensure the <title> tag uses the SEO title field with a meaningful fallback. Place inside <head>.

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

  1. Shopify's built-in sitemap.xml automatically includes only canonical product URLs and excludes variant query strings — if you are seeing variant URLs, a third-party sitemap app is the likely culprit.
  2. Go to Apps → check for any installed sitemap, SEO, or schema apps (e.g. 'SEO Manager', 'Sitemap & SEO', 'Smart SEO').
  3. Open the app's settings and look for a 'Sitemap' or 'URL Filters' section; disable variant URL inclusion or enable 'exclude query parameters'.
  4. If you have no sitemap app, verify the native sitemap at yourdomain.com/sitemap.xml — it should list only /products/[handle] URLs with no query strings.
  5. Re-submit the sitemap in Google Search Console under Indexing → Sitemaps.
Code · Generic HTML (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

  1. Shopify automatically adds a canonical tag to product pages via its theme. The tag is output by the Liquid snippet {{ canonical_url | tag: 'canonical' }} (or similar) in your theme's <head>.
  2. Go to Online Store → Themes → your active theme → Edit code.
  3. Open theme.liquid (or the layout file such as base.liquid in Dawn). Search for 'canonical'.
  4. Shopify sets canonical_url to the base product URL by default. HOWEVER, if a variant is selected via a query parameter (?variant=XXXXXXXX), some themes or apps may override this — verify on a live variant URL using View Source or the URL Inspection tool in Google Search Console.
  5. If the canonical is correct (base product URL, no ?variant= parameter), no change is needed. If it incorrectly includes variant parameters, replace the canonical line with: <link rel="canonical" href="{{ product.url | prepend: shop.url }}"> — this always resolves to the base product URL regardless of the selected variant.
  6. For collection-scoped URLs (e.g. /collections/X/products/Y), ensure the canonical points to the canonical product URL (/products/Y), not the collection-scoped version: use {{ product.url | prepend: shop.url }} which Shopify resolves to the /products/ path.
Code · Shopify (liquid)
<head>
  {%- comment -%}
    Canonical always points to base product URL, not the variant or collection-scoped URL.
    product.url resolves to /products/handle — prepend shop.url for absolute URL.
  {%- endcomment -%}
  {%- if template.name == 'product' -%}
    <link rel="canonical" href="{{ shop.url }}{{ product.url }}">
  {%- else -%}
    <link rel="canonical" href="{{ canonical_url }}">
  {%- endif -%}
</head>

In theme.liquid (or base.liquid), replace any existing canonical line inside <head> with this. This always resolves to the base /products/ URL regardless of selected variant.

Server errorModerate effort

Identify and fix every page returning an HTTP 5xx server error so search engines can crawl and index those URLs again.

On Shopify

  1. In your Shopify Admin go to Settings > Apps and sales channels and disable any recently added apps one at a time to isolate an app causing the error.
  2. Go to Online Store > Themes > … (Actions) > Edit code and check your theme's Liquid templates for syntax errors — Shopify displays a Liquid error message inline when a template is broken.
  3. Check the Shopify Admin API activity log at Settings > Apps > [App name] > API logs if the error is app-related.
  4. For persistent errors, open a support ticket at help.shopify.com; Shopify's infrastructure team can pull server-side logs you cannot access yourself.
  5. Once fixed, go to Google Search Console > URL Inspection, paste the affected URL, and click 'Request Indexing'.
Code · Shopify (liquid)
{% comment %} Example of a safe Liquid block that won't cause a 500 {% endcomment %}
{% if product != blank %}
  <h1>{{ product.title | escape }}</h1>
  <p>{{ product.description }}</p>
{% else %}
  <p>Product not found.</p>
{% endif %}

If a Liquid template syntax error is causing the 500, ensure all tags are properly closed. This example shows correct Liquid error-handling for a product section.

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

  1. Run your storefront URL through Google PageSpeed Insights to get a prioritised list of issues before touching anything.
  2. Images: In the Shopify admin go to Content → Files and replace large images with compressed WebP versions. In your theme's Liquid templates use the | image_url and | image_tag filters with 'width' parameters and loading='lazy' to serve correctly sized images automatically.
  3. Theme JS/CSS: Go to Online Store → Themes → Actions → Edit Code. In theme.liquid, move non-critical <script> tags to just before </body> and add the 'defer' or 'async' attribute.
  4. Apps: Go to Apps and uninstall any app you are not actively using — every installed app can inject scripts. Use the Shopify App Store's performance-rated apps where possible.
  5. Speed apps: Install a dedicated performance app such as 'Crush.pics' (image compression), 'TinyIMG', or 'Swift' to automate image and code optimisation.
  6. Shopify's CDN is built-in for hosted assets, but ensure your theme uses the Shopify CDN URLs ({{ 'style.css' | asset_url }}) rather than external CDN links for theme files.
Code · Shopify (liquid)
{%- assign img_url = product.featured_image | image_url: width: 800 -%}
<img
  src="{{ img_url }}"
  srcset="
    {{ product.featured_image | image_url: width: 400 }} 400w,
    {{ product.featured_image | image_url: width: 800 }} 800w,
    {{ product.featured_image | image_url: width: 1200 }} 1200w
  "
  sizes="(max-width: 600px) 100vw, 800px"
  alt="{{ product.featured_image.alt | escape }}"
  width="800"
  height="{{ 800 | divided_by: product.featured_image.aspect_ratio | round }}"
  loading="lazy"
  decoding="async"
>

In your Shopify theme's Liquid templates (e.g. sections/product-template.liquid), use this pattern for responsive, WebP-served, lazy-loaded product images.

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

  1. Go to your Shopify Admin → Products → click the product with thin content.
  2. Scroll to the 'Description' rich-text editor. Write or paste your expanded product description here — aim for at least 300 words covering features, benefits, specifications, use-cases, and FAQs.
  3. For collection pages: Admin → Products → Collections → click the collection → add introductory text in the 'Description' field at the top of the collection editor.
  4. For standard pages or landing pages: Admin → Online Store → Pages → click the page → edit the Content field.
  5. To edit the theme template itself (e.g. to add a FAQ accordion or review section below the description): Admin → Online Store → Themes → Customize, then select the relevant template and add a 'Rich text' or 'Custom HTML' section.
  6. Save changes and use the Preview function to confirm the text is visible on the live page.
Code · Shopify (liquid)
<!-- Inside product.liquid or main-product.liquid, after {{ product.description }} -->
<!-- The structured content below is best added directly via the Admin rich-text editor,
     but here is the Liquid/HTML equivalent for theme developers. -->

<div class="product-details">
  <h2>About {{ product.title }}</h2>
  <div class="rte">
    {{ product.description }}
  </div>

  {% if product.metafields.custom.faq_block != blank %}
    <div class="product-faq">
      <h3>Frequently Asked Questions</h3>
      {{ product.metafields.custom.faq_block | metafield_tag }}
    </div>
  {% endif %}

  {% if product.metafields.custom.size_guide != blank %}
    <div class="product-size-guide">
      <h3>Sizing &amp; Fit</h3>
      {{ product.metafields.custom.size_guide | metafield_tag }}
    </div>
  {% endif %}
</div>

Paste this into your product description rich-text editor in the Shopify Admin — or use it as a starting template inside the product.liquid / main-product.liquid section if building a custom description block.

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

  1. For a page: go to Online Store → Pages, open the page, scroll to the 'Search engine listing' section at the bottom, and edit the 'Page title' field.
  2. For a product: go to Products → All products, open the product, scroll to 'Search engine listing', and edit the 'Page title' field.
  3. For a collection: go to Products → Collections, open the collection, scroll to 'Search engine listing', and edit the 'Page title' field.
  4. For the homepage: go to Online Store → Preferences and edit the 'Homepage title' field.
  5. Keep every title at or under 60 characters, then click Save.
Code · Shopify (liquid)
<title>
  {%- if page_title -%}
    {{ page_title }}
    {%- unless page_title contains shop.name -%} | {{ shop.name }}{%- endunless -%}
  {%- else -%}
    {{ shop.name }}
  {%- endif -%}
</title>

In your theme's layout/theme.liquid file, replace or verify the <title> block looks like this. The SEO title entered in the admin will populate {{ page_title }}.

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

  1. For a product page: go to Products → select the product → scroll to the 'Search engine listing' section at the bottom → click 'Edit' → update the 'Page title' field.
  2. For a collection page: go to Products → Collections → select the collection → scroll to 'Search engine listing' → click 'Edit' → update 'Page title'.
  3. For the homepage: go to Online Store → Preferences → find the 'Title and meta description' section → update the 'Title' field.
  4. For other pages (About, Contact, etc.): go to Online Store → Pages → select the page → scroll to 'Search engine listing' → click 'Edit' → update 'Page title'.
  5. For blog posts: go to Online Store → Blog posts → select the post → scroll to 'Search engine listing' → update 'Page title'.
  6. Titles are saved immediately when you click 'Save'. No theme editing required.
Code · Shopify (liquid)
{% comment %}
  In layout/theme.liquid, inside <head>:
  Shopify automatically outputs the correct title using page_title.
  Ensure this line exists and is not overridden.
{% endcomment %}
<title>
  {{ page_title }}
  {%- if current_tags %} &ndash; {{ current_tags | join: ', ' }}{% endif %}
  {%- unless page_title contains shop.name %} &ndash; {{ shop.name }}{% endunless %}
</title>

In your theme's layout/theme.liquid, ensure the title tag renders the SEO title field. This is the default pattern — verify yours matches.

Answer Engine Optimization · 17 fixes

Aeo schema client renderedModerate effort

Move your JSON-LD structured data from client-side JavaScript injection to server-rendered HTML so that AI crawlers and search engines can read it in the raw page source.

On Shopify

  1. In your Shopify admin go to Online Store → Themes → Actions → Edit Code.
  2. Open the relevant template file: product.liquid (or sections/main-product.liquid in Dawn and most modern themes) for product schema, or the equivalent section for other page types.
  3. Locate any existing JSON-LD block — if it is added by a JavaScript app or a <script> that builds schema dynamically in the browser, remove or disable it.
  4. Paste a server-rendered JSON-LD block directly into the template file using Liquid variables (e.g., {{ product.title | json }}, {{ product.price | money_without_currency }}) so the HTML is fully formed before it leaves Shopify's servers.
  5. Save the file, then view the live page source (Ctrl+U) and confirm the <script type="application/ld+json"> block is present in the raw HTML.
  6. If you use a schema app (e.g., JSON-LD for SEO, Schema Plus), check its settings — reputable apps write schema server-side via Shopify's ScriptTag API or theme injection; confirm 'server-side rendering' or 'theme injection' is enabled rather than a browser-side script.
Code · Shopify (liquid)
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": {{ product.title | json }},
  "description": {{ product.description | strip_html | truncate: 5000 | json }},
  "sku": {{ product.selected_or_first_available_variant.sku | json }},
  "brand": {
    "@type": "Brand",
    "name": {{ product.vendor | json }}
  },
  "image": [
    {{ product.featured_image | img_url: 'master' | prepend: 'https:' | json }}
  ],
  "offers": {
    "@type": "Offer",
    "url": {{ shop.url | append: product.url | json }},
    "priceCurrency": {{ cart.currency.iso_code | json }},
    "price": {{ product.selected_or_first_available_variant.price | divided_by: 100.0 | json }},
    "availability": {% if product.available %}"https://schema.org/InStock"{% else %}"https://schema.org/OutOfStock"{% endif %},
    "itemCondition": "https://schema.org/NewCondition"
  }
}
</script>

Add this block inside sections/main-product.liquid (or product.liquid) — all Liquid tags are rendered server-side by Shopify before the HTML reaches the browser.

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

  1. In your Shopify Admin, go to Online Store → Themes → Actions → Edit Code.
  2. Open the template file that renders blog articles — typically templates/article.liquid or sections/article-template.liquid (theme-dependent).
  3. Paste the JSON-LD snippet (see code examples) inside the {% block head %} or directly before the closing </article> tag in that file.
  4. To make the author dynamic, use Liquid variables: {{ article.author }} for the name, and add a metafield (Settings → Custom data → Articles) called 'author_url' and 'author_same_as' to store the bio URL and social profile URLs per article.
  5. Reference those metafields in the JSON-LD: {{ article.metafields.custom.author_url | json }} etc.
  6. Save and preview an article; validate with the Rich Results Test (search.google.com/test/rich-results).
Code · Shopify (liquid)
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": {{ article.title | json }},
  "datePublished": {{ article.published_at | date: '%Y-%m-%dT%H:%M:%S%z' | json }},
  "dateModified": {{ article.updated_at | date: '%Y-%m-%dT%H:%M:%S%z' | json }},
  "author": {
    "@type": "Person",
    "name": {{ article.author | json }},
    "url": {{ article.metafields.custom.author_url | default: shop.url | json }},
    "sameAs": [
      {{ article.metafields.custom.author_same_as | default: '' | json }}
    ]
  },
  "publisher": {
    "@type": "Organization",
    "name": {{ shop.name | json }},
    "logo": {
      "@type": "ImageObject",
      "url": {{ shop.url | append: '/path/to/logo.png' | json }}
    }
  },
  "mainEntityOfPage": {
    "@type": "WebPage",
    "@id": {{ article.url | prepend: shop.url | json }}
  }
}
</script>

Paste this inside sections/article-template.liquid or templates/article.liquid, just before the closing </article> tag. Add 'author_url' and 'author_same_as' metafields to the Articles resource in Settings → Custom data first.

Faq schema not visibleModerate effort

Render all FAQ answers in the server-delivered HTML so that both Google and your FAQ structured data can see every question and answer — not just the one currently open in a JavaScript accordion.

On Shopify

  1. In your Shopify Admin, go to Online Store → Themes → Actions → Edit code.
  2. Open the template file for the page with the FAQ (e.g. templates/page.faq.liquid or sections/faq.liquid).
  3. Replace any JavaScript-only accordion with native <details>/<summary> HTML elements — each <details> block contains a <summary> (the question) and a <p> (the answer). All answers are in the HTML at all times.
  4. In the same file (or in theme.liquid inside <head>), add a FAQPage JSON-LD <script> block listing every question and answer. Use Liquid loops to generate it dynamically from your metafields or section blocks so it stays in sync.
  5. Save, then test the live URL in Google's Rich Results Test (search.google.com/test/rich-results).
Code · Shopify (liquid)
{%- comment -%} FAQ section — all answers rendered in HTML; JSON-LD generated from same data {%- endcomment -%}

<section class="faq-section">
  {%- for block in section.blocks -%}
    {%- if block.type == 'faq_item' -%}
      <details id="faq-{{ forloop.index }}">
        <summary>{{ block.settings.question }}</summary>
        <p>{{ block.settings.answer }}</p>
      </details>
    {%- endif -%}
  {%- endfor -%}
</section>

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [
    {%- for block in section.blocks -%}
      {%- if block.type == 'faq_item' -%}
        {
          "@type": "Question",
          "name": {{ block.settings.question | json }},
          "acceptedAnswer": {
            "@type": "Answer",
            "text": {{ block.settings.answer | json }}
          }
        }{%- unless forloop.last -%},{%- endunless -%}
      {%- endif -%}
    {%- endfor -%}
  ]
}
</script>

{% schema %}
{
  "name": "FAQ",
  "blocks": [
    {
      "type": "faq_item",
      "name": "FAQ Item",
      "settings": [
        { "type": "text", "id": "question", "label": "Question", "default": "What is your question?" },
        { "type": "textarea", "id": "answer", "label": "Answer", "default": "Your answer here." }
      ]
    }
  ],
  "presets": [{ "name": "FAQ", "blocks": [] }]
}
{% endschema %}

Drop into your sections/faq.liquid or templates/page.faq.liquid file. Uses section blocks to keep content and schema in sync.

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

  1. Go to Online Store → Themes → click 'Customize' on your active theme, then exit back to the theme editor and choose 'Edit code' (three-dot menu next to the theme name).
  2. Search for 'ratingValue' across all theme files using the search box in the code editor. The most common locations are product.json, product-template.liquid, main-product.liquid, or a snippets/structured-data.liquid file.
  3. Find the line outputting ratingValue and confirm it references a real Liquid variable such as {{ product.metafields.reviews.rating.value }} or a reviews app variable — NOT a hardcoded 0 or an empty string.
  4. If you use a reviews app (Judge.me, Yotpo, Okendo, Stamped, etc.), check that app's settings for 'Rich Snippets' or 'SEO structured data' — most apps have a toggle to inject valid AggregateRating automatically. Disable any manual duplicate markup.
  5. Add a Liquid conditional to suppress the entire AggregateRating block when no reviews exist: wrap the block in {% if product.metafields.reviews.rating_count > 0 %} … {% endif %} (exact metafield key depends on your reviews app).
  6. Save, then test a product URL in the Google Rich Results Test (search.google.com/test/rich-results) to confirm stars appear with no errors.
Code · Shopify (liquid)
{% assign review_count = product.metafields.reviews.rating_count | plus: 0 %}
{% assign rating_value = product.metafields.reviews.rating.value | plus: 0 %}

{% if review_count > 0 and rating_value >= 1 %}
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": {{ product.title | json }},
  "aggregateRating": {
    "@type": "AggregateRating",
    "ratingValue": {{ rating_value | json }},
    "bestRating": "5",
    "worstRating": "1",
    "reviewCount": {{ review_count | json }}
  }
}
</script>
{% endif %}

Add to snippets/structured-data.liquid (or your product structured data snippet) and render it from product.liquid. Adjust the metafield namespace/key to match your reviews app.

Low answer first scoreModerate effort

Restructure page content to lead with a direct, concise answer, use question-format headings, and add FAQ structured data so search engines and AI assistants can surface your answers prominently.

On Shopify

  1. Online Store → Themes → Actions → Edit code.
  2. For product pages open templates/product.liquid (or sections/main-product.liquid in Dawn-based themes); add your 160-character summary paragraph as the first element inside the product description block.
  3. For blog/landing pages go to Online Store → Pages or Blog Posts; in the rich-text editor place the answer summary as the first paragraph and rewrite subheadings as questions using the Heading 2/3 toolbar options.
  4. To add FAQPage JSON-LD: in Edit code, open theme.liquid and paste the FAQ JSON-LD script just before </body>, OR install a free app such as 'JSON-LD for SEO' or 'Schema Plus for SEO' which generates FAQ schema from a dedicated page section.
  5. Alternatively, add a FAQ section to a page using a Shopify page builder app (e.g., PageFly, Shogun) — these apps have built-in FAQ blocks that also output schema markup automatically.
Code · Shopify (liquid)
{%- if product.metafields.custom.answer_summary != blank -%}
  <p class="product-answer-summary">
    {{ product.metafields.custom.answer_summary | escape }}
  </p>
{%- endif -%}

{{ product.description }}

{%- if product.metafields.custom.faq_schema != blank -%}
  <script type="application/ld+json">
    {{ product.metafields.custom.faq_schema }}
  </script>
{%- endif -%}

Add to sections/main-product.liquid (Dawn) or templates/product.liquid just before the product description output to prepend the answer summary.

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

  1. Install a review app from the Shopify App Store — the most common are 'Product Reviews' (free, by Shopify), 'Judge.me', or 'Okendo'.
  2. In the app's settings, enable automated post-purchase review request emails and set the send delay to 7–14 days after fulfilment.
  3. Enable the app's built-in structured data / JSON-LD output for AggregateRating — most major apps do this automatically. In Judge.me: Settings → SEO → enable 'Rich Snippets'. In Okendo: structured data is on by default.
  4. To import existing reviews: in Judge.me go to Reviews → Import; in Okendo use the CSV import under Settings → Imports.
  5. Once products reach 5 reviews, paste a product URL into the Google Rich Results Test (search.google.com/test/rich-results) and confirm 'AggregateRating' appears in the detected structured data.
Code · Shopify (liquid)
{% if product.metafields.reviews.rating.value != blank %}
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": {{ product.title | json }},
  "image": {{ product.featured_image | img_url: 'grande' | prepend: 'https:' | json }},
  "description": {{ product.description | strip_html | truncatewords: 50 | json }},
  "sku": {{ product.selected_or_first_available_variant.sku | json }},
  "brand": {
    "@type": "Brand",
    "name": {{ product.vendor | json }}
  },
  "aggregateRating": {
    "@type": "AggregateRating",
    "ratingValue": {{ product.metafields.reviews.rating.value.rating | json }},
    "reviewCount": {{ product.metafields.reviews.rating_count | json }},
    "bestRating": "5",
    "worstRating": "1"
  },
  "offers": {
    "@type": "Offer",
    "url": {{ shop.url | append: product.url | json }},
    "priceCurrency": {{ cart.currency.iso_code | json }},
    "price": {{ product.price | money_without_currency | json }},
    "availability": {% if product.available %}"https://schema.org/InStock"{% else %}"https://schema.org/OutOfStock"{% endif %},
    "itemCondition": "https://schema.org/NewCondition"
  }
}
</script>
{% endif %}

Add this snippet to your product.liquid or sections/main-product.liquid template inside the <head> or immediately before </body>. It dynamically outputs live review data if you use Shopify's native Product Reviews app. For Judge.me or Okendo, their apps inject this automatically — do not duplicate.

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

  1. Go to Online Store → Themes → click 'Customize' on your active theme.
  2. In the theme editor, navigate to a blog post template. Alternatively, click 'Edit code' from the Themes page.
  3. Open the file 'templates/article.liquid' (or 'sections/main-article.liquid' in newer themes).
  4. Paste the JSON-LD <script> block (see code snippet) just before the closing </article> tag or inside the <head> via 'layout/theme.liquid'. Use Liquid variables ({{ article.title }}, {{ article.author }}, {{ article.published_at | date: '%Y-%m-%d' }}, {{ article.metafields... }}) to populate fields dynamically.
  5. Save the file. Then open any live blog post and use Google's Rich Results Test to verify the schema is detected.
Code · Shopify (liquid)
{% if template == 'article' %}
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": {{ article.title | json }},
  "description": {{ article.excerpt_or_content | strip_html | truncate: 200 | json }},
  "image": {
    "@type": "ImageObject",
    "url": {{ article.image.src | img_url: 'master' | prepend: 'https:' | json }},
    "width": 1200,
    "height": 630
  },
  "author": {
    "@type": "Person",
    "name": {{ article.author | json }}
  },
  "publisher": {
    "@type": "Organization",
    "name": {{ shop.name | json }},
    "logo": {
      "@type": "ImageObject",
      "url": {{ settings.logo | img_url: 'master' | prepend: 'https:' | json }}
    }
  },
  "datePublished": {{ article.published_at | date: '%Y-%m-%d' | json }},
  "dateModified": {{ article.updated_at | date: '%Y-%m-%d' | json }},
  "mainEntityOfPage": {
    "@type": "WebPage",
    "@id": {{ canonical_url | json }}
  },
  "url": {{ canonical_url | json }}
}
</script>
{% endif %}

Add this block to templates/article.liquid (or sections/main-article.liquid) inside the <head> via layout/theme.liquid conditional, or directly in the article template. Outputs dynamic Article schema for every blog post.

Missing schema breadcrumblistModerate effort

Add BreadcrumbList structured data (JSON-LD) to every page so Google can display your site's navigation path directly in search results.

On Shopify

  1. In your Shopify admin, go to Online Store › Themes › your active theme › Actions › Edit code.
  2. Open theme.liquid (for site-wide injection) or the specific template files: product.liquid / product-template.liquid for product pages, collection.liquid for category pages, article.liquid for blog posts.
  3. Paste the JSON-LD script block (see code snippets) just before the closing </head> tag in theme.liquid, or inside the relevant template for page-specific markup.
  4. Use Liquid variables ({{ product.title }}, {{ collection.title }}, {{ shop.url }}, {{ product.url }}, {{ collection.url }}) to make the name and item values dynamic.
  5. Save the file, then validate a representative product URL and collection URL using Google's Rich Results Test.
Code · Shopify (liquid)
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": "Home",
      "item": {{ shop.url | append: '/' | json }}
    },
    {
      "@type": "ListItem",
      "position": 2,
      "name": {{ collection.title | json }},
      "item": {{ shop.url | append: collection.url | json }}
    },
    {
      "@type": "ListItem",
      "position": 3,
      "name": {{ product.title | json }},
      "item": {{ shop.url | append: product.url | json }}
    }
  ]
}
</script>

Add to product.liquid (or product-template.liquid) inside the <head> section of theme.liquid. Adjust for collection/article templates similarly.

Missing schema faqpageQuick win

Add FAQPage schema markup to any page that contains a Frequently Asked Questions section so Google can display your answers as rich results directly in search.

On Shopify

  1. In your Shopify Admin, go to Online Store → Themes → click 'Customize' on your active theme.
  2. For a standalone FAQ page: go to Online Store → Pages, open the FAQ page, switch the content editor to HTML view, and paste the JSON-LD <script> block at the very top or bottom of the page HTML field.
  3. For product pages: go to Online Store → Themes → Edit Code → open 'templates/product.liquid' (or 'sections/product-template.liquid' / 'sections/main-product.liquid' depending on your theme) and paste the JSON-LD block inside the file, just before the closing </section> or at the top of the template.
  4. Alternatively, install a free app such as 'Schema Plus for SEO' or 'SEO King' from the Shopify App Store — these apps auto-generate FAQPage schema from your page content with no code edits.
  5. Verify by visiting your live page URL in Google's Rich Results Test.
Code · Shopify (liquid)
{% comment %} FAQPage Schema — place near the top of your FAQ page template {% endcomment %}
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [
    {
      "@type": "Question",
      "name": "What is your return policy?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "We accept returns within 30 days of purchase. Items must be unused and in their original packaging. Contact support@yourstore.com to begin a return."
      }
    },
    {
      "@type": "Question",
      "name": "How long does shipping take?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Standard shipping takes 5–7 business days. Expedited shipping (2–3 business days) is available at checkout."
      }
    },
    {
      "@type": "Question",
      "name": "Do you offer free shipping?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Yes, we offer free standard shipping on all orders over $50 within the contiguous United States."
      }
    }
  ]
}
</script>

Add this block to your FAQ page template (e.g. templates/page.faq.liquid or sections/main-page.liquid). The schema renders alongside your visible FAQ content.

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

  1. In your Shopify admin, go to Online Store > Themes > (your active theme) > Edit code.
  2. For a blog post or page: open the template file that renders that content type — typically 'templates/article.liquid' or 'templates/page.liquid'.
  3. Scroll to the bottom of the file (or find the closing </article> tag) and paste your complete <script type='application/ld+json'> HowTo block there. Use Liquid variables like {{ article.title | json }} to pull dynamic values if desired.
  4. Alternatively, for a specific page only, go to Online Store > Pages, open the page, switch the content editor to HTML mode, and paste the JSON-LD script block at the very bottom of the HTML content.
  5. Save the file, then validate the live URL with Google's Rich Results Test.
Code · Shopify (liquid)
{% if article %}
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "HowTo",
  "name": {{ article.title | json }},
  "description": {{ article.excerpt_or_content | strip_html | truncate: 200 | json }},
  "step": [
    {
      "@type": "HowToStep",
      "position": 1,
      "name": "Step One Title",
      "text": "Describe the first step your customer must take."
    },
    {
      "@type": "HowToStep",
      "position": 2,
      "name": "Step Two Title",
      "text": "Describe the second step your customer must take."
    },
    {
      "@type": "HowToStep",
      "position": 3,
      "name": "Step Three Title",
      "text": "Describe the third step your customer must take."
    }
  ]
}
</script>
{% endif %}

Add to the bottom of templates/article.liquid or templates/page.liquid in your Shopify theme. Uses Liquid to pull the article title dynamically.

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

  1. From your Shopify admin, go to Online Store → Themes.
  2. Click the '...' (Actions) button next to your live theme, then select 'Edit code'.
  3. In the Layout folder, open 'theme.liquid'.
  4. Paste your completed JSON-LD <script> block just before the closing </head> tag.
  5. Click 'Save'. The schema will now appear on every page of your store.
  6. For more control (e.g. homepage only), open 'index.liquid' under Templates and paste the block there instead.
Code · Shopify (liquid)
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Store",
  "name": {{ shop.name | json }},
  "url": {{ shop.url | append: '/' | json }},
  "logo": {{ shop.metafields.global.logo_url | default: '' | json }},
  "description": {{ shop.description | json }},
  "address": {
    "@type": "PostalAddress",
    "streetAddress": "123 Main Street",
    "addressLocality": "Springfield",
    "addressRegion": "IL",
    "postalCode": "62701",
    "addressCountry": "US"
  },
  "telephone": "+1-217-555-0100",
  "email": "hello@yourdomain.com",
  "openingHoursSpecification": [
    {
      "@type": "OpeningHoursSpecification",
      "dayOfWeek": ["Monday","Tuesday","Wednesday","Thursday","Friday"],
      "opens": "09:00",
      "closes": "18:00"
    }
  ],
  "sameAs": [
    "https://www.facebook.com/yourbusiness",
    "https://www.instagram.com/yourbusiness"
  ]
}
</script>

Paste this block in theme.liquid just before </head>. Liquid variables auto-fill shop name and URL; replace static address/phone fields with your real data.

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

  1. In your Shopify admin, go to Online Store → Themes → click the three-dot menu next to your active theme → Edit code.
  2. Open the file theme.liquid (under Layout). This file wraps every page on your store.
  3. Locate the closing </head> tag. Paste your complete <script type='application/ld+json'>…</script> Organization block immediately before it.
  4. Click Save. The schema now appears on every storefront page.
  5. Optionally, if you only want it on the homepage, open templates/index.json (or index.liquid) instead and add the script block there.
  6. Verify at https://search.google.com/test/rich-results by entering your store URL.
Code · Shopify (liquid)
{% assign org_name = 'Acme Store' %}
{% assign org_url = 'https://www.acmestore.com' %}
{% assign org_logo = 'https://www.acmestore.com/images/logo.png' %}

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Organization",
  "name": "{{ org_name }}",
  "url": "{{ org_url }}",
  "logo": {
    "@type": "ImageObject",
    "url": "{{ org_logo }}",
    "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"
  ],
  "contactPoint": {
    "@type": "ContactPoint",
    "contactType": "customer service",
    "email": "hello@acmestore.com"
  }
}
</script>

Paste inside theme.liquid, immediately before the closing </head> tag. Edit values in the assign block at the top.

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

  1. In your Shopify admin, go to Online Store → Themes → click 'Customize' next to your active theme.
  2. Exit the visual editor and instead click 'Edit code' (under the three-dot menu or Actions menu on the theme).
  3. Open the file 'templates/product.liquid' (or 'sections/product-template.liquid' / 'sections/main-product.liquid' depending on your theme generation).
  4. Paste the JSON-LD Product schema block (see code snippet below) at the very top or bottom of that file, using Liquid variables like {{ product.title }}, {{ product.price | money_without_currency }}, {{ product.featured_image | img_url: 'master' }}, and {{ product.selected_or_first_available_variant.sku }} to populate it dynamically.
  5. Save the file, then visit any live product page and use Google's Rich Results Test to validate the output.
Missing schema webpageModerate effort

Add WebPage schema markup to every page so search engines and AI answer engines can precisely understand and describe your content.

On Shopify

  1. In your Shopify admin, go to Online Store → Themes → click the three-dot menu next to your active theme → Edit code.
  2. Open 'theme.liquid' (the global layout file) inside the Layout folder — this file wraps every page.
  3. Paste your WebPage JSON-LD <script> block inside the <head>...</head> section, ideally just before the closing </head> tag.
  4. Because Shopify uses Liquid templating, use Liquid variables to make the schema dynamic: {{ page_title | json }} for the name, {{ page_description | json }} for description, and {{ canonical_url | json }} for the url property.
  5. Save the file. Verify with Google's Rich Results Test on several page types (homepage, an About page, a policy page).
  6. For Product pages, Shopify already injects some Product schema; add WebPage/ItemPage schema via a separate product.liquid or product.json template section if needed.
Code · Shopify (liquid)
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "WebPage",
  "name": {{ page_title | json }},
  "description": {{ page_description | default: shop.description | json }},
  "url": {{ canonical_url | json }},
  "inLanguage": {{ shop.locale | json }},
  "isPartOf": {
    "@type": "WebSite",
    "name": {{ shop.name | json }},
    "url": {{ shop.url | append: '/' | json }}
  }
}
</script>

Add this block inside the <head> section of your theme.liquid layout file. Liquid variables are resolved dynamically per page.

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

  1. In your Shopify admin, go to Online Store → Themes → click 'Edit code' on your active theme.
  2. Search for 'datePublished' using the search bar in the code editor — it is typically found in theme.liquid, article.liquid, page.liquid, or a snippets/ file such as snippets/structured-data.liquid.
  3. Locate the Liquid output tag that renders the date, e.g. {{ article.published_at }} or a hardcoded string.
  4. Replace it with the Liquid date filter to enforce ISO-8601: {{ article.published_at | date: '%Y-%m-%dT%H:%M:%S+00:00' }}
  5. Do the same for dateModified, using {{ article.updated_at | date: '%Y-%m-%dT%H:%M:%S+00:00' }}
  6. Save the file, then test a live article URL in Google's Rich Results Test to confirm the dates parse correctly.
Code · Shopify (liquid)
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": {{ article.title | json }},
  "url": {{ shop.url | append: article.url | json }},
  "datePublished": {{ article.published_at | date: '%Y-%m-%dT%H:%M:%S+00:00' | json }},
  "dateModified": {{ article.updated_at | date: '%Y-%m-%dT%H:%M:%S+00:00' | json }},
  "author": {
    "@type": "Person",
    "name": {{ article.author | json }}
  }
}
</script>

Place inside article.liquid or snippets/structured-data.liquid, replacing the existing date output.

Org missing sameasQuick win

Add a `sameAs` array to your Organization schema, linking to your verified social profiles, Wikipedia page, LinkedIn, and other authoritative sources so search engines can confidently identify your brand as a known entity.

On Shopify

  1. From your Shopify Admin, go to Online Store → Themes → click the '…' (Actions) menu next to your live theme → Edit code.
  2. Open theme.liquid (found under Layout). Locate any existing JSON-LD <script type='application/ld+json'> block, or place your cursor just before the closing </head> tag.
  3. Paste or update the Organization JSON-LD block with the sameAs array (see code snippet below).
  4. Click Save. To verify, visit your storefront, right-click → View Page Source, and search for 'sameAs' to confirm it is present.
  5. Paste the live URL into Google's Rich Results Test to validate.
Code · Shopify (liquid)
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Organization",
  "name": {{ shop.name | json }},
  "url": {{ shop.url | append: '/' | json }},
  "logo": {{ settings.logo | img_url: 'master' | prepend: 'https:' | json }},
  "sameAs": [
    "https://www.linkedin.com/company/your-company",
    "https://www.facebook.com/yourpage",
    "https://twitter.com/yourhandle",
    "https://www.instagram.com/yourhandle",
    "https://www.youtube.com/@yourchannel",
    "https://en.wikipedia.org/wiki/Your_Brand",
    "https://www.wikidata.org/wiki/QXXXXXXX"
  ]
}
</script>

Paste this block in theme.liquid, just before the closing </head> tag.

Page unreachableModerate effort

Ensure every page that should appear in AI-powered answers and rich results is actually reachable by crawlers, then add structured data so search engines and AI systems can read and surface its content.

On Shopify

  1. Go to Online Store → Pages (or Products / Collections) and confirm the page exists and is set to 'Visible' (not password-protected or unpublished).
  2. If a page was deleted, recreate it or add a URL redirect: Online Store → Navigation → URL Redirects → Add redirect. Map the old broken URL to the correct live page.
  3. Check robots.txt: your Shopify robots.txt is auto-generated. If you need to allow a previously disallowed path, go to Online Store → Themes → Edit Code → config/robots.txt.liquid (if customized) and remove any Disallow rules blocking the page.
  4. To add Product structured data, install a schema app (e.g., 'JSON-LD for SEO' or 'Schema Plus for SEO') from the Shopify App Store, or edit your theme's product.liquid / product-template.liquid file to include a JSON-LD script block.
  5. For FAQPage schema on a blog post or page, edit the page template in Online Store → Themes → Edit Code and add a <script type='application/ld+json'> block with FAQPage markup.
  6. After fixing, submit the URL in Google Search Console (URL Inspection → Request Indexing) and test with the Rich Results Test.

Accessibility (WCAG) · 66 fixes

Aria allowed attrModerate effort

Audit every element that uses ARIA attributes and ensure each attribute is permitted for that element's role — removing or correcting any mismatched aria-* attributes.

On Shopify

  1. Go to Online Store → Themes → click 'Customize' on your active theme.
  2. For most ARIA issues, the source is in theme Liquid files. Click 'Edit code' (three-dot menu next to your active theme) to open the code editor.
  3. Use the search box (Ctrl+F / Cmd+F in the editor) to find aria- attributes across files — common culprits are sections/header.liquid, snippets/navigation.liquid, snippets/product-card.liquid, and assets/*.js.
  4. Locate the flagged element and correct the aria-* attribute (remove it, replace it, or change the role) as determined in your audit.
  5. If the violation is inside a third-party app's widget (e.g. a review app or chat widget), contact that app's support team with the specific element and attribute flagged.
  6. Save and preview the theme, then re-run axe DevTools in your browser to verify the fix.
Code · Shopify (liquid)
{% comment %}
  Corrected accordion / collapsible section — aria-expanded belongs on the
  button that controls the collapsible panel, not on a wrapper div.
{% endcomment %}

<div class="accordion">
  <h3>
    <button
      type="button"
      aria-expanded="false"
      aria-controls="accordion-panel-1"
      id="accordion-btn-1"
      class="accordion__trigger"
    >
      Shipping Information
    </button>
  </h3>
  <div
    id="accordion-panel-1"
    role="region"
    aria-labelledby="accordion-btn-1"
    hidden
    class="accordion__panel"
  >
    <p>Your shipping details go here.</p>
  </div>
</div>

Place corrected ARIA markup in the relevant section or snippet Liquid file (e.g. snippets/navigation.liquid). This example shows a corrected accessible accordion trigger.

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

  1. In your Shopify Admin, go to Online Store → Themes → (your active theme) → Actions → Edit code.
  2. Search across Liquid/HTML files for the offending `role` attribute value (e.g. `role="textbox"`) using the search bar at the top of the code editor.
  3. Open the relevant file (commonly a section file under `sections/`, a snippet under `snippets/`, or `layout/theme.liquid`).
  4. Apply the fix: remove the invalid `role` attribute, replace it with a permitted role, or swap the element for the correct native HTML tag (e.g. `<input type="text">`).
  5. Click Save, then re-audit with axe DevTools in Chrome to confirm the violation is gone.
  6. If the element is inside a third-party app block, contact the app developer — you may not be able to edit that markup directly.
Code · Shopify (liquid)
{%- comment -%}
  Before: invalid role on a <select>
  <select role="textbox" name="options[{{ option.name | escape }}]">
{%- endcomment -%}

{%- comment -%}
  After: role removed; <select> already communicates its semantics natively
{%- endcomment -%}
<label for="option-{{ option.name | handleize }}">{{ option.name }}</label>
<select id="option-{{ option.name | handleize }}"
        name="options[{{ option.name | escape }}]"
        class="product-form__select">
  {%- for value in option.values -%}
    <option value="{{ value | escape }}"
            {%- if option.selected_value == value %} selected{% endif %}>
      {{ value }}
    </option>
  {%- endfor -%}
</select>

Example in a Liquid snippet (snippets/custom-input.liquid) — remove or fix the invalid role attribute.

Aria command nameModerate effort

Give every button, link, and menuitem a clear, accessible name so screen readers can announce what it does.

On Shopify

  1. In your Shopify admin, go to Online Store → Themes → click 'Customize' on your active theme.
  2. To edit theme code directly: Online Store → Themes → Actions → Edit code.
  3. Locate the Liquid template or snippet that renders the offending element. Common culprits: `snippets/header.liquid`, `sections/header.liquid`, `snippets/cart-icon.liquid`, or `snippets/icon-*.liquid`.
  4. Find the `<button>`, `<a>`, or element with `role="button"` / `role="link"` / `role="menuitem"` that has no visible text.
  5. Add `aria-label="Descriptive action name"` directly to the HTML tag (e.g., `<button aria-label="Open cart">{% render 'icon-cart' %}</button>`).
  6. If the theme uses a visually-hidden helper class (common in Dawn and other Shopify themes), wrap a `<span class="visually-hidden">Open cart</span>` inside the button instead — this is even better as it is visible to all assistive technologies.
  7. Click Save, then re-test with the axe browser extension.
Code · Shopify (liquid)
{%- comment -%}
  Icon-only cart button with accessible name — Shopify / Dawn pattern.
  Replace 'icon-cart' with your theme's cart icon snippet name.
{%- endcomment -%}
<button
  type="button"
  class="header__icon header__icon--cart"
  aria-label="{{ 'sections.header.cart_count' | t: count: cart.item_count }}"
  aria-expanded="false"
  aria-controls="cart-notification"
>
  {%- render 'icon-cart' -%}
  <span class="visually-hidden">{{ 'sections.header.cart' | t }}</span>
</button>

Place in the relevant section or snippet Liquid file (e.g., sections/header.liquid). Dawn theme uses a 'visually-hidden' class instead of 'sr-only'.

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

  1. In your Shopify admin, go to Online Store → Themes → click the (…) menu next to your active theme → Edit code.
  2. Search across all .liquid files for 'role="dialog"' or 'role="alertdialog"' using the search box in the code editor (or look in sections/cart-drawer.liquid, sections/header.liquid, assets/theme.js for modal templates).
  3. For each match, find the visible heading inside the modal (e.g., <h2>Shopping cart</h2>), add a unique id to it (e.g., id="cart-dialog-title"), then add aria-labelledby="cart-dialog-title" to the element with role="dialog".
  4. If there is no heading, add aria-label="Cart" (or an appropriate name) directly to the role="dialog" element.
  5. Save the file and preview the theme. Use a screen reader or the axe DevTools browser extension to verify the dialog announces its name correctly.
Code · Shopify (liquid)
<div
  id="CartDrawer"
  class="cart-drawer"
  role="dialog"
  aria-modal="true"
  aria-labelledby="CartDrawer-Title"
  tabindex="-1"
>
  <div class="cart-drawer__header">
    <h2 id="CartDrawer-Title" class="cart-drawer__title">Your Cart</h2>
    <button
      class="cart-drawer__close"
      type="button"
      aria-label="Close cart"
    >
      &times;
    </button>
  </div>
  <div class="cart-drawer__body">
    {%- if cart.item_count > 0 -%}
      {%- for item in cart.items -%}
        <p>{{ item.product.title }}</p>
      {%- endfor -%}
    {%- else -%}
      <p>Your cart is empty.</p>
    {%- endif -%}
  </div>
</div>

Place in sections/cart-drawer.liquid or whichever .liquid file renders your modal. Replace IDs and text to match your theme.

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

  1. From your Shopify admin go to Online Store → Themes → click the '…' menu next to your active theme → Edit code.
  2. The most common sources of this issue in Shopify themes are: off-canvas mobile menus (typically sections/header.liquid or snippets/header-drawer.liquid), announcement bars, cookie consent apps, and product image carousels.
  3. In the theme code editor, search (Ctrl+F / Cmd+F in the editor) for 'aria-hidden' to find every occurrence.
  4. For each match, check whether the element also contains <a>, <button>, or <input> tags. If it does, either: (A) remove aria-hidden='true' from the container, or (B) add tabindex='-1' to every focusable child inside it when that section is hidden.
  5. For drawer/off-canvas menus, the pattern should be: when drawer is closed → aria-hidden='true' on the drawer + tabindex='-1' on inner links; when drawer opens → aria-hidden='false' (or remove attribute) + remove tabindex='-1'. This is often controlled in assets/theme.js or assets/global.js — look for the function that toggles the drawer open/closed.
  6. Save changes, preview the theme, and re-run an axe or Lighthouse accessibility scan on the live preview URL to confirm the issue is resolved.
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

  1. From your Shopify Admin, go to Online Store → Themes → click the three-dot menu next to your active theme → Edit code.
  2. Locate the relevant template or snippet file — common offenders are sections/header.liquid (search bar), templates/cart.liquid or snippets/cart-drawer.liquid (quantity inputs), and templates/contact.liquid (contact form).
  3. For a search input, find the <input type="search" ...> tag and add aria-label="Search products" if it has no associated <label>.
  4. For quantity fields in the cart, add aria-label="Quantity for [product name]" — you can dynamically include the product title using Liquid: aria-label="Quantity for {{ item.product.title }}".
  5. Save the file. Preview your store, open browser DevTools → Accessibility tab, focus the field, and confirm the Accessible Name is now set.
  6. If your theme uses a JavaScript-rendered component (e.g. a React-based cart drawer from a theme app), contact the app developer or add the aria-label via a JavaScript snippet in theme.liquid after the component mounts.
Code · Shopify (liquid)
<label for="Search-In-Modal" class="sr-only">{{ 'general.search.search' | t }}</label>
<input
  type="search"
  id="Search-In-Modal"
  class="search__input field__input"
  name="q"
  value="{{ search.terms | escape }}"
  placeholder="{{ 'general.search.search' | t }}"
  aria-label="{{ 'general.search.search' | t }}"
>

Add to your search input in sections/header.liquid or snippets/search-form.liquid.

Aria meter nameQuick win

Add a descriptive accessible name (via aria-label, aria-labelledby, or a visible <label>) to every element with role="meter" so screen readers can announce what the meter represents.

On Shopify

  1. In the Shopify admin, go to Online Store → Themes → (your active theme) → Edit code.
  2. Open the template or snippet file that renders the widget containing the meter (common files: snippets/product-rating.liquid, snippets/stock-level.liquid, sections/product-template.liquid).
  3. Locate the element that has role="meter" or is a <meter> tag.
  4. Add aria-label="[Descriptive name, e.g. Average customer rating]" directly on that element, or add aria-labelledby="[id-of-nearby-text]" if a visible label already exists in the markup.
  5. Ensure aria-valuenow, aria-valuemin, and aria-valuemax are present with correct values.
  6. Save the file and preview the theme. Use the axe DevTools browser extension to confirm the violation is gone before publishing.
Code · Shopify (liquid)
{%- assign rating_value = product.metafields.reviews.rating.value | default: 0 -%}
{%- assign rating_max = product.metafields.reviews.rating_count.value | default: 5 -%}

<meter
  role="meter"
  aria-label="Average customer rating"
  aria-valuenow="{{ rating_value }}"
  aria-valuemin="0"
  aria-valuemax="{{ rating_max }}"
  value="{{ rating_value }}"
  min="0"
  max="{{ rating_max }}"
  class="product-rating__meter">
  {{ rating_value }} out of {{ rating_max }}
</meter>

Paste into the relevant .liquid snippet (e.g. snippets/product-rating.liquid). Adjust the Liquid variables to match your theme's data.

Aria progressbar nameQuick win

Add an accessible name (aria-label or aria-labelledby) to every progress bar element so screen readers can announce its purpose.

On Shopify

  1. In your Shopify Admin, go to Online Store → Themes → (your active theme) → Actions → Edit code.
  2. Search across theme files (Ctrl+F in the editor, or use the search icon) for 'progressbar', '<progress', 'role="progressbar"' to locate all instances.
  3. Common locations: sections/cart.liquid or sections/main-cart-footer.liquid (cart progress bars), snippets/product-reviews.liquid or an app-injected snippet (star-rating bars), checkout.liquid (if on Shopify Plus — for checkout step progress).
  4. Add aria-label='Descriptive label here' to the opening tag of each progress bar element found. Example: change <div role='progressbar' aria-valuenow='60' aria-valuemin='0' aria-valuemax='100'> to <div role='progressbar' aria-label='Checkout step progress' aria-valuenow='60' aria-valuemin='0' aria-valuemax='100'>.
  5. If the bar is injected by a third-party app (e.g. a review app or loyalty app), check that app's settings panel for accessibility/label options, or contact the app developer.
  6. Save the file and verify with the axe DevTools browser extension.
Code · Shopify (liquid)
{%- assign progress_pct = cart.total_price | times: 100 | divided_by: free_shipping_threshold -%}
{%- if progress_pct > 100 -%}{%- assign progress_pct = 100 -%}{%- endif -%}

<p id="free-shipping-label">
  {%- if progress_pct < 100 -%}
    Spend {{ free_shipping_threshold | minus: cart.total_price | money }} more for free shipping
  {%- else -%}
    You qualify for free shipping!
  {%- endif -%}
</p>

<div
  role="progressbar"
  aria-labelledby="free-shipping-label"
  aria-valuenow="{{ progress_pct }}"
  aria-valuemin="0"
  aria-valuemax="100"
  style="width: {{ progress_pct }}%; height: 6px; background-color: #4caf50; transition: width 0.3s ease;"
></div>

Add to the relevant section or snippet file in Online Store → Themes → Edit code. Example shows a free-shipping progress bar snippet.

Aria prohibited attrModerate effort

Remove or replace any ARIA attributes that are prohibited for the role assigned to each HTML element, so assistive technologies receive accurate, unambiguous information.

On Shopify

  1. From your Shopify Admin, go to Online Store → Themes → click the '…' menu next to your active theme → Edit code.
  2. In the 'Layout' and 'Sections' folders, open the template file containing the flagged element (common culprits: header.liquid, product-card.liquid, slideshow.liquid).
  3. Use Ctrl+F / Cmd+F to search for the prohibited ARIA attribute name (e.g. 'aria-checked', 'aria-expanded') within that file.
  4. Remove or replace the prohibited attribute according to the correct fix for that role (see steps above).
  5. Click Save, then preview the live storefront and re-test with the axe browser extension.
Code · Shopify (liquid)
{%- comment -%}
  Before: prohibited aria-checked on a heading role
  <h2 class="product__title" aria-checked="false">{{ product.title }}</h2>

  After: remove the prohibited attribute entirely
{%- endcomment -%}

<h2 class="product__title">{{ product.title }}</h2>

{%- comment -%}
  Before: aria-expanded on a non-interactive <span>
  <span class="nav__label" aria-expanded="false">{{ section.settings.menu_label }}</span>

  After: move aria-expanded to the parent <button> that controls the menu
{%- endcomment -%}

<button
  class="nav__toggle"
  aria-expanded="false"
  aria-controls="nav-dropdown-{{ section.id }}"
>
  {{ section.settings.menu_label }}
</button>
<ul id="nav-dropdown-{{ section.id }}" hidden>
  {%- for link in linklists[section.settings.menu].links -%}
    <li><a href="{{ link.url }}">{{ link.title }}</a></li>
  {%- endfor -%}
</ul>

Example correction inside a Shopify Liquid section file (e.g. sections/header.liquid or snippets/product-card.liquid).

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

  1. From your Shopify admin, go to Online Store → Themes → (your active theme) → Actions → Edit code.
  2. In the 'Sections' or 'Snippets' folder, open the file that renders the component with the missing attribute — common culprits are header.liquid (navigation dropdowns), predictive-search.liquid (search combobox), or any accordion/FAQ section.
  3. Find the HTML element with the ARIA role (e.g. <button role='button'> or <div role='combobox'>) and add the missing attributes: aria-expanded='false' and/or aria-controls='target-element-id'.
  4. Open the corresponding JavaScript file (usually assets/theme.js or assets/[component-name].js) and find the toggle function. Add a line that updates the attribute on state change: element.setAttribute('aria-expanded', isOpen ? 'true' : 'false').
  5. Save both files, then preview the theme and test with a screen reader or the axe browser extension.
Code · Shopify (liquid)
<button
  class="nav-toggle"
  id="nav-toggle-{{ section.id }}"
  aria-expanded="false"
  aria-controls="nav-menu-{{ section.id }}"
>
  {{ block.settings.label | default: 'Menu' }}
</button>

<ul
  id="nav-menu-{{ section.id }}"
  class="nav-menu"
  hidden
>
  {% for link in linklists[section.settings.menu].links %}
    <li><a href="{{ link.url }}">{{ link.title }}</a></li>
  {% endfor %}
</ul>

Add to the header or navigation section Liquid file (e.g. sections/header.liquid) for a nav dropdown toggle button.

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

  1. In your Shopify Admin go to Online Store → Themes → click the '...' (Actions) menu next to your active theme → Edit code.
  2. Identify which template or section renders the broken widget. Common culprits: sections/product-form.liquid (variant selectors), sections/header.liquid (nav dropdowns), snippets/facets.liquid (collection filters).
  3. Open that file and locate the container element that has the incorrect or missing ARIA roles (e.g. a <ul> acting as a listbox for a custom dropdown).
  4. Add role="listbox" (or the appropriate container role) to the wrapper element and role="option" (or appropriate child role) to each child <li> or <div>.
  5. Also add aria-selected="true/false" to each option child if required by the role (listbox options require aria-selected).
  6. Save the file, then preview the theme and test with a screen reader or the axe browser extension to confirm the violation is resolved.
  7. If the widget is rendered by a third-party app (e.g. a custom search or filter app), contact the app developer or replace the app with one that passes ARIA validation.
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

  1. In your Shopify Admin, go to Online Store → Themes → click 'Customize', then open the theme code editor via Actions → Edit code.
  2. Locate the template or section file that renders the affected component — common culprits are 'header.liquid' (navigation menus), 'predictive-search.liquid' (search dropdown), or a custom section file.
  3. In the code editor, find the element with the orphaned child role (e.g. `role='menuitem'`) and wrap it — along with all its siblings — in a parent element carrying the correct role (e.g. `<ul role='menu'>...</ul>`).
  4. If the nav is built in a JavaScript component (e.g. a Shopify 'component-*.js' file in Assets), locate the `render()` or template literal that produces the HTML and add the parent wrapper there.
  5. Save the file, preview the theme, and run the axe browser extension to confirm the violation is resolved before publishing.
Code · Shopify (liquid)
{% comment %} CORRECT Shopify nav with proper ARIA role hierarchy {% endcomment %}
<nav aria-label="Main navigation">
  <ul role="menubar">
    {% for link in linklists.main-menu.links %}
      <li role="none">
        {% if link.links.size > 0 %}
          <button
            role="menuitem"
            aria-haspopup="true"
            aria-expanded="false"
          >
            {{ link.title }}
          </button>
          <ul role="menu" aria-label="{{ link.title }} submenu">
            {% for child_link in link.links %}
              <li role="none">
                <a href="{{ child_link.url }}" role="menuitem">{{ child_link.title }}</a>
              </li>
            {% endfor %}
          </ul>
        {% else %}
          <a href="{{ link.url }}" role="menuitem">{{ link.title }}</a>
        {% endif %}
      </li>
    {% endfor %}
  </ul>
</nav>

Place in header.liquid or your navigation section file inside the theme code editor (Online Store → Themes → Actions → Edit code).

Aria rolesQuick win

Audit every element that has a `role` attribute and replace any invalid or misspelled role value with a valid WAI-ARIA role from the official specification.

On Shopify

  1. In your Shopify Admin, go to Online Store → Themes → click Actions → Edit code on your active theme.
  2. Use the search box (top-left of the code editor) to search for `role=` across all theme files.
  3. Click each result — most will be in `.liquid` files under `sections/`, `snippets/`, or `layout/theme.liquid`.
  4. In the file editor, locate the invalid role attribute (the scanner will have identified the specific value), correct or remove it, and click Save.
  5. For roles added by third-party Shopify apps, check the app's settings or contact the app developer — you cannot edit app-injected code directly.
  6. Verify the fix with a browser-based axe DevTools scan on the live storefront.
Code · Shopify (liquid)
{%- comment -%}
  BEFORE (invalid role — fictional example):
  <div role="prodcard" class="product-card">
{%- endcomment -%}

{%- comment -%}
  AFTER: remove the invalid role; use a valid landmark or no role if none fits.
  If interactivity is needed, switch to a native element.
{%- endcomment -%}

<article class="product-card" aria-label="{{ product.title }}">
  <a href="{{ product.url }}" class="product-card__link">
    <img
      src="{{ product.featured_image | img_url: '400x' }}"
      alt="{{ product.featured_image.alt | escape }}"
      width="400"
      height="400"
      loading="lazy"
    >
    <span class="product-card__title">{{ product.title }}</span>
  </a>
  <button type="button" class="btn btn--primary">
    Add to cart
  </button>
</article>

Example correction inside a Shopify Liquid snippet (e.g. snippets/product-card.liquid or sections/header.liquid):

Aria toggle field nameQuick win

Add a visible or programmatic accessible name to every toggle control (checkbox, switch, or button with role="switch") so screen readers can announce what the control does.

On Shopify

  1. In your Shopify Admin, go to Online Store → Themes → and click Actions → Edit code on your active theme.
  2. Search across your Liquid files (especially sections/ and snippets/) for <input type="checkbox">, role="switch", or toggle-related component files.
  3. For each checkbox without a <label>, either wrap both the input and its visible text in a <label> tag, or add a <label for="INPUT_ID"> element.
  4. For icon-only toggle buttons (common in header nav, theme switchers), open the relevant snippet file and add aria-label="Description" directly on the <button> element.
  5. If your theme uses a JS component for custom toggles (common in Dawn and similar themes), locate the component template in snippets/ or assets/ and add the aria-label or <label> there.
  6. Save changes, preview the theme, and verify with the axe DevTools Chrome extension by right-clicking the toggle and running 'Inspect Accessibility'.
Code · Shopify (liquid)
{%- comment -%}
  Accessible checkbox toggle — Shopify Liquid
  Replace 'newsletter' and the label text with your actual field name and description.
{%- endcomment -%}

<div class="toggle-field">
  <input
    type="checkbox"
    id="newsletter-opt-in"
    name="newsletter"
    class="toggle-field__input"
  >
  <label for="newsletter-opt-in" class="toggle-field__label">
    Subscribe to our newsletter
  </label>
</div>

Liquid snippet for a labelled toggle checkbox — place in the relevant sections/*.liquid or snippets/*.liquid file.

Aria tooltip nameModerate effort

Give every tooltip element an accessible name so screen readers can announce its content to users who cannot see the screen.

On Shopify

  1. In your Shopify admin, go to Online Store → Themes → (your active theme) → Actions → Edit code.
  2. Search all .liquid files (Sections, Snippets, Templates) for role="tooltip" using the search bar in the code editor.
  3. For each match, ensure the element contains descriptive inner text, or add aria-label="[description]" directly on that element.
  4. If a tooltip is rendered by a third-party app (e.g., a size-guide or product-options app), open the app's settings or contact its support to request accessible tooltip markup.
  5. Save your changes, then preview the theme and use the Chrome axe DevTools browser extension to confirm no aria-tooltip-name violations remain.
Code · Shopify (liquid)
{%- comment -%}
  Usage: {% render 'tooltip', trigger_label: 'Shipping info', tooltip_text: 'Ships in 2–3 business days.', tooltip_id: 'ship-tip' %}
{%- endcomment -%}

<span class="tooltip-wrapper">
  <button
    type="button"
    class="tooltip-trigger"
    aria-describedby="{{ tooltip_id }}"
    aria-label="{{ trigger_label }}"
  >
    {%- render 'icon-info' -%}
  </button>
  <span
    id="{{ tooltip_id }}"
    role="tooltip"
    class="tooltip-content"
  >
    {{- tooltip_text -}}
  </span>
</span>

Add this snippet inside a Snippets file (e.g., snippets/tooltip.liquid) and render it wherever a tooltip is needed.

Aria valid attrQuick win

Find and replace every misspelled or non-existent ARIA attribute name (e.g. `aria-labeledby`) with the correct, spec-defined name (e.g. `aria-labelledby`) across your entire site's HTML and templates.

On Shopify

  1. In your Shopify admin, go to Online Store → Themes → (your active theme) → Actions → Edit code.
  2. Use the search icon (magnifying glass) at the top of the file list to search across all theme files for the misspelled attribute, e.g. `aria-labeledby`.
  3. Open each file that contains the misspelling (commonly in Sections, Snippets, or Layout files like `theme.liquid`).
  4. Correct the attribute name in each file (e.g. change `aria-labeledby` to `aria-labelledby`) and click Save.
  5. If the issue is inside a Shopify App embed or App Block, contact the app developer — you cannot edit third-party app output directly.
  6. Preview your theme and use the axe DevTools browser extension to verify the violation is resolved before publishing.
Code · Shopify (liquid)
{%- comment -%}
  WRONG — aria-labeledby is a typo and will be silently ignored:
  <div class="product-tabs" aria-labeledby="tabs-heading">

  CORRECT — use aria-labelledby (double 'l'):
{%- endcomment -%}

<h2 id="tabs-heading" class="visually-hidden">Product details tabs</h2>
<div class="product-tabs" aria-labelledby="tabs-heading" role="tablist">
  <button
    id="tab-description"
    class="tab-btn"
    role="tab"
    aria-selected="true"
    aria-controls="panel-description"
  >
    Description
  </button>
  <button
    id="tab-shipping"
    class="tab-btn"
    role="tab"
    aria-selected="false"
    aria-controls="panel-shipping"
  >
    Shipping
  </button>
</div>
<div id="panel-description" role="tabpanel" aria-labelledby="tab-description" tabindex="0">
  {{ product.description }}
</div>
<div id="panel-shipping" role="tabpanel" aria-labelledby="tab-shipping" tabindex="0" hidden>
  {{ pages.shipping-info.content }}
</div>

Example correction inside a Shopify Liquid snippet (e.g. snippets/product-form.liquid or sections/header.liquid). Search for the misspelling and replace as shown.

Aria valid attr valueModerate effort

Audit every ARIA attribute on your pages and correct any that have invalid or mismatched values so assistive technologies can interpret them correctly.

On Shopify

  1. In your Shopify admin, go to Online Store → Themes → click 'Customize' on your active theme.
  2. To edit code directly: Online Store → Themes → Actions → Edit code.
  3. Locate the Liquid template or section file containing the flagged element (e.g. `sections/header.liquid`, `snippets/product-form.liquid`).
  4. Search (Ctrl+F / Cmd+F) for the invalid aria attribute name (e.g. `aria-expanded`, `aria-describedby`) and correct the value.
  5. If the invalid value is injected by a third-party Shopify app, check the app's settings or contact the app developer — you cannot edit app-injected code directly.
  6. Save changes, then verify with the axe DevTools browser extension or Lighthouse on a live preview.
Code · Shopify (liquid)
{% comment %}
  Example: a collapsible navigation menu item in a Shopify Liquid section.
  aria-expanded must be a string 'true' or 'false', not a number or variable
  that resolves to something else.
{% endcomment %}

<button
  class="nav-toggle"
  aria-expanded="false"
  aria-controls="main-nav-list"
>
  Menu
</button>

<ul id="main-nav-list" hidden>
  <li><a href="/">Home</a></li>
  <li><a href="/collections/all">Shop</a></li>
</ul>

{% comment %}
  Example: a product form field with a valid aria-describedby reference.
  The id on the <p> tag MUST match the aria-describedby value exactly.
{% endcomment %}

<label for="customer-email">Email</label>
<input
  type="email"
  id="customer-email"
  name="email"
  aria-describedby="email-format-hint"
/>
<p id="email-format-hint">Enter the email address on your account.</p>

Place in the relevant section or snippet Liquid file (e.g. sections/header.liquid). Ensure dynamic IDs used in aria-describedby/aria-labelledby are rendered into the DOM.

Autocomplete validModerate effort

Add valid `autocomplete` attribute values to every form field that collects personal information, using the exact tokens specified by the HTML standard.

On Shopify

  1. Go to Online Store → Themes → Actions → Edit code.
  2. For the checkout form, Shopify's native checkout is hosted and rendered by Shopify — you cannot edit its HTML directly on standard plans. Shopify already injects correct autocomplete tokens on its default checkout fields.
  3. For any custom forms in theme files (contact forms, newsletter sign-ups, login/register pages), open the relevant template: `templates/customers/login.liquid`, `templates/customers/register.liquid`, `templates/customers/addresses.liquid`, or `sections/contact-form.liquid`.
  4. Locate each `<input>` tag and add or correct the `autocomplete` attribute using the approved HTML token (see code snippet).
  5. On Shopify Plus, if you have checkout.liquid customisation, open `layout/checkout.liquid` and apply the same edits to any custom input fields you have added.
  6. Save the file, then validate with an axe browser extension on the live page.
Code · Shopify (liquid)
<!-- Registration form example in Shopify Liquid -->
<form method="post" action="/account">
  <input type="hidden" name="form_type" value="create_customer" />
  <input type="hidden" name="utf8" value="✓" />

  <label for="FirstName">First name</label>
  <input type="text"
         id="FirstName"
         name="customer[first_name]"
         autocomplete="given-name"
         value="{{ form.first_name }}" />

  <label for="LastName">Last name</label>
  <input type="text"
         id="LastName"
         name="customer[last_name]"
         autocomplete="family-name"
         value="{{ form.last_name }}" />

  <label for="Email">Email</label>
  <input type="email"
         id="Email"
         name="customer[email]"
         autocomplete="email"
         value="{{ form.email }}" />

  <label for="CreatePassword">Password</label>
  <input type="password"
         id="CreatePassword"
         name="customer[password]"
         autocomplete="new-password" />

  <button type="submit">Create account</button>
</form>

Place in templates/customers/register.liquid or the relevant customer template in your theme.

Avoid inline spacingModerate effort

Remove hard-coded text-spacing CSS values from inline `style` attributes so users can override them with their own stylesheets or browser settings.

On Shopify

  1. Go to Online Store → Themes → click the three-dot menu next to your active theme → Edit code.
  2. Open the relevant template file (e.g. `templates/product.liquid`, `sections/featured-product.liquid`) and search for `style=` to locate inline spacing properties.
  3. Open `assets/base.css` (or your theme's main CSS file, often `assets/theme.css`) and add a new CSS class with the spacing values.
  4. Back in the template file, replace the `style="..."` attribute with `class="your-new-class"` on each affected element.
  5. Save both files. Preview the theme and use the axe browser extension to confirm the rule is resolved.
  6. If a Shopify app (page builder, reviews widget, etc.) injects the inline styles, use the app's own style settings, or add a corrective rule in Online Store → Themes → Customize → Custom CSS (or `assets/custom.css`).
Code · Shopify (liquid)
{% comment %} BEFORE: inline spacing in a Liquid template {% endcomment %}
<p style="line-height: 1.6; letter-spacing: 0.04em;">
  {{ product.description }}
</p>

{% comment %} AFTER: use a CSS class instead {% endcomment %}
<p class="product-desc">
  {{ product.description }}
</p>

{% comment %} Add this to assets/base.css (or assets/theme.css): {% endcomment %}
{% comment %}
.product-desc {
  line-height: 1.6;
  letter-spacing: 0.04em;
}
{% endcomment %}

In your Liquid template (e.g. sections/main-product.liquid): replace inline style with a class. Add the CSS class to assets/base.css.

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

  1. In your Shopify Admin, go to Online Store → Themes → click the '…' menu next to your active theme → Edit code.
  2. Locate the template or snippet file containing the offending button. Common files: sections/header.liquid (hamburger/search/cart icons), snippets/product-form.liquid or sections/main-product.liquid (Add to Cart, quantity buttons), snippets/icon-*.liquid (reusable icon includes).
  3. For icon-only buttons, add aria-label directly to the <button> tag. Example: change <button class="search-toggle"> to <button class="search-toggle" aria-label="Open search">.
  4. If your theme uses SVG icons inline, add aria-hidden="true" to the <svg> element and rely on the button's aria-label for the name, preventing double-reading.
  5. For quantity +/– buttons, find the input stepper markup and add aria-label="Increase quantity" / aria-label="Decrease quantity" to each button.
  6. Save the file, preview the theme, and test with VoiceOver (Mac) or NVDA (Windows) to confirm each button announces correctly. You can also install the free 'Accessibility Checker' app from the Shopify App Store for ongoing monitoring.
Code · Shopify (liquid)
{% comment %} Search toggle button — add aria-label {% endcomment %}
<button
  type="button"
  class="header__icon header__icon--search"
  aria-label="{{ 'general.search.search' | t }}"
  aria-expanded="false"
  aria-controls="Search-In-Modal"
>
  {% render 'icon-search' %}
</button>

{% comment %} Cart button — add aria-label with item count for context {% endcomment %}
<a
  href="{{ routes.cart_url }}"
  class="header__icon header__icon--cart"
  aria-label="{{ 'sections.header.cart_count' | t: count: cart.item_count }}"
>
  {% render 'icon-cart' %}
</a>

{% comment %} Mobile menu toggle — add aria-label {% endcomment %}
<button
  type="button"
  class="header__icon header__icon--menu"
  aria-label="{{ 'sections.header.menu' | t }}"
  aria-expanded="false"
  aria-controls="menu-drawer"
>
  {% render 'icon-hamburger' %}
</button>

{% comment %} Quantity stepper buttons — add aria-label {% endcomment %}
<button
  class="quantity__button"
  name="minus"
  type="button"
  aria-label="{{ 'products.product.quantity.decrease' | t: product: product.title }}"
>
  {% render 'icon-minus' %}
</button>
<button
  class="quantity__button"
  name="plus"
  type="button"
  aria-label="{{ 'products.product.quantity.increase' | t: product: product.title }}"
>
  {% render 'icon-plus' %}
</button>

Place in the relevant section or snippet file (e.g. sections/header.liquid or snippets/product-form.liquid) — edit via Online Store → Themes → Edit code.

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

  1. Go to Online Store → Themes → click Customize on your active theme.
  2. Open Theme Settings (bottom-left gear icon) → Typography or Colors section — many themes expose body text color as a color picker here. Change it to an accessible value (e.g. #595959 instead of #777777) and Save.
  3. If the failing color is not exposed in the visual editor, go back to Themes → click the '…' menu → Edit code.
  4. Open assets/base.css (or theme.css / global.css — the name varies by theme). Search for the failing hex value (e.g. #777777) and replace it with your accessible color.
  5. Click Save and verify with a contrast checker.
Code · Shopify (css)
/* Fix insufficient color contrast — add to assets/base.css */
.rte,
.product__description,
.caption,
.light-text {
  color: #595959; /* replaces #777777 — contrast ratio 7.0:1 on white */
}

Add to assets/base.css (or your theme's main CSS file) via Online Store → Themes → Edit code.

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

  1. In your Shopify admin, go to Online Store → Themes → click Actions → Edit Code on your active theme.
  2. Search across theme files (sections/, snippets/, templates/) for `<dl` using the search box at the top of the file list.
  3. Common culprits: product.liquid or product-template.liquid (product specs), faq.liquid or collapsible-content.liquid (FAQs), cart.liquid (order summaries).
  4. Open each file containing a `<dl>` and edit the HTML directly in the code editor to ensure only `<dt>`, `<dd>`, or wrapping `<div>` elements appear as direct children.
  5. If a third-party app renders the `<dl>` (e.g. a product specs app), check that app's settings or contact its support — you may not be able to edit its output directly.
  6. Click Save, preview the page, and re-run an axe browser extension scan to verify the fix.
Code · Shopify (liquid)
{% comment %} ✅ Correct Liquid definition list for product specifications {% endcomment %}
<dl class="product-specs">
  {% for feature in product.metafields.custom.specifications.value %}
    <div class="product-specs__row">
      <dt class="product-specs__term">{{ feature.name }}</dt>
      <dd class="product-specs__detail">{{ feature.value }}</dd>
    </div>
  {% endfor %}
</dl>

Place in the relevant section/snippet file (e.g. sections/main-product.liquid). Replace the loop with your actual metafield/variant data.

DlitemModerate effort

Wrap every `<dt>` (term) and `<dd>` (description) element inside a parent `<dl>` element so description list items have their required container.

On Shopify

  1. In your Shopify admin, go to Online Store → Themes → click 'Customize' on your active theme.
  2. To edit template files directly: from the theme editor click the three-dot menu → 'Edit code'.
  3. Search across all Liquid files (sections/, templates/, snippets/) for `<dt` or `<dd` using the search box at the top of the code editor.
  4. For each occurrence, check whether the nearest ancestor element in the file is a `<dl>`. If it is a `<div>`, `<ul>`, or anything else, add `<dl>` immediately before the first `<dt>` and `</dl>` immediately after the last `<dd>` in that group.
  5. Common locations: product description sections (sections/main-product.liquid or product.liquid), FAQ sections, and custom metafield templates.
  6. Save each file and preview the page; use browser DevTools Elements panel to confirm the DOM structure.
Code · Shopify (liquid)
{% comment %} Correct Liquid template for a description list of product metafields {% endcomment %}
<dl class="product-specs">
  {% if product.metafields.custom.material != blank %}
  <div class="product-specs__item">
    <dt class="product-specs__term">Material</dt>
    <dd class="product-specs__detail">{{ product.metafields.custom.material }}</dd>
  </div>
  {% endif %}

  {% if product.metafields.custom.dimensions != blank %}
  <div class="product-specs__item">
    <dt class="product-specs__term">Dimensions</dt>
    <dd class="product-specs__detail">{{ product.metafields.custom.dimensions }}</dd>
  </div>
  {% endif %}

  {% if product.metafields.custom.shipping_time != blank %}
  <div class="product-specs__item">
    <dt class="product-specs__term">Shipping time</dt>
    <dd class="product-specs__detail">{{ product.metafields.custom.shipping_time }}</dd>
  </div>
  {% endif %}
</dl>

Place in the relevant Liquid section or snippet file (e.g. sections/main-product.liquid) where product specs or key-value pairs are rendered.

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

  1. For the homepage: Admin → Online Store → Preferences → 'Homepage title' field → save.
  2. For product pages: Admin → Products → select a product → scroll to 'Search engine listing' section → edit 'Page title' field.
  3. For collection pages: Admin → Products → Collections → select a collection → 'Search engine listing' → 'Page title'.
  4. For blog posts: Admin → Online Store → Blog Posts → select a post → 'Search engine listing' → 'Page title'.
  5. For other pages (About, Contact, etc.): Admin → Online Store → Pages → select page → 'Search engine listing' → 'Page title'.
  6. To fix all product/collection titles at the template level: Online Store → Themes → Actions → Edit code → open `layout/theme.liquid` → ensure `<title>{{ page_title }}</title>` is present inside `<head>`.
Code · Shopify (liquid)
<title>
  {{ page_title }}
  {%- if page_title != shop.name %} &ndash; {{ shop.name }}{% endif %}
</title>

In layout/theme.liquid, inside <head>. This is the standard Shopify pattern; most themes already include it — verify it is present and not empty.

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

  1. In your Shopify Admin, go to Online Store → Themes → (your active theme) → Actions → Edit code.
  2. Search across template files (Ctrl+F in the code editor, or use the 'Search files' box) for `<h1`, `<h2`, `<h3`, `<h4`, `<h5`, `<h6` to find heading tags.
  3. Identify any that have no text between the opening and closing tag (e.g. `<h2></h2>` or `<h2> </h2>`).
  4. Add meaningful visible text inside the tag, or replace the tag with a non-heading element (`<div>`, `<p>`) if no heading is needed.
  5. If the heading wraps a Liquid variable that may render empty (e.g. `{{ section.settings.title }}`), add a conditional: `{% if section.settings.title != blank %}<h2>{{ section.settings.title }}</h2>{% endif %}`.
  6. For sections built in the Theme Customizer (Online Store → Themes → Customize), check each section's settings panel — many themes expose a 'Heading' text field; ensure it is filled in or the heading is hidden when blank.
  7. Save, then verify with the axe browser extension or Shopify's built-in Lighthouse audit (Customize → … → Check performance).
Code · Shopify (liquid)
{% comment %}
  Only render the heading tag when the section setting contains text.
  Replace 'section.settings.heading' with your actual setting key.
{% endcomment %}
{% if section.settings.heading != blank %}
  <h2 class="section-heading">{{ section.settings.heading | escape }}</h2>
{% endif %}

Place in any section `.liquid` file where a heading is rendered from a theme setting. Prevents the heading from rendering when the setting is blank.

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

  1. In your Shopify Admin, go to Online Store → Themes → click Actions → Edit code on your active theme.
  2. Search for table-related Liquid files: common locations are sections/product-template.liquid, sections/collection-template.liquid, snippets/product-comparison.liquid, or any custom section containing `<table>` markup.
  3. Use Ctrl+F / Cmd+F to search for `<th>` within those files and locate any that are empty (e.g., `<th></th>` or `<th> </th>`).
  4. Add meaningful text inside the empty `<th>`, or add an `aria-label` attribute: `<th aria-label="Row category"></th>`.
  5. If your theme uses a page builder (e.g., Dawn's sections schema), also check any custom HTML blocks added via the theme editor under Online Store → Customize → the relevant section.
  6. Save the file and verify with axe DevTools or the WAVE browser extension.
Code · Shopify (liquid)
{%- comment -%}
  Example: A product comparison table snippet with all <th> cells labeled.
  Replace column names with your actual attribute names.
{%- endcomment -%}

<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 class="product-comparison">
  <thead>
    <tr>
      {%- comment -%} Top-left corner cell — no visible label needed, but must have accessible text {%- endcomment -%}
      <th scope="col"><span class="sr-only">Feature</span></th>
      {%- for product in section.settings.products -%}
        <th scope="col">{{ product.title }}</th>
      {%- endfor -%}
    </tr>
  </thead>
  <tbody>
    {%- for feature in section.settings.features -%}
      <tr>
        <th scope="row">{{ feature.name }}</th>
        {%- for product in section.settings.products -%}
          <td>{{ product.metafields.comparison[feature.key] | default: '—' }}</td>
        {%- endfor -%}
      </tr>
    {%- endfor -%}
  </tbody>
</table>

Paste into the relevant section or snippet Liquid file (e.g. snippets/product-comparison.liquid) inside your theme code editor.

Error not associatedModerate effort

Add `aria-invalid="true"` and a linked `aria-describedby` error message to every form field that fails validation, so screen reader users are told exactly what went wrong.

On Shopify

  1. In your Shopify admin, go to Online Store → Themes → click 'Edit code' on your active theme.
  2. Locate the form templates: checkout is controlled by Shopify's native checkout (customisable only on Shopify Plus via checkout.liquid or Checkout Extensibility); contact forms are in templates/page.contact.liquid or sections/contact-form.liquid.
  3. Open the relevant Liquid file and find each <input> or <textarea> element inside the form.
  4. Directly below each input, add a hidden error span: <span id="FIELDNAME-error" class="form-error" hidden></span>
  5. Add aria-describedby="FIELDNAME-error" to the matching input element.
  6. In your theme's main JavaScript file (assets/theme.js or assets/application.js), hook into the form's submit/validation logic to: set input.setAttribute('aria-invalid','true'), set errorSpan.textContent = 'Your descriptive error text', and errorSpan.removeAttribute('hidden') when invalid; reverse these on correction.
  7. For Shopify Plus stores, use Checkout Extensibility (checkout UI extensions) or the checkout.liquid theme section to add the same ARIA attributes to checkout fields.
  8. Save all files and test with a screen reader.
Code · Shopify (liquid)
<div class="field">
  <label for="ContactForm-email">
    {{ 'templates.contact.form.email' | t }}
    <span aria-hidden="true">*</span>
  </label>
  <input
    class="field__input"
    autocomplete="email"
    type="email"
    id="ContactForm-email"
    name="contact[email]"
    spellcheck="false"
    autocapitalize="off"
    value="{{ contact.email }}"
    aria-required="true"
    aria-describedby="ContactForm-email-error"
    {%- if form.errors contains 'email' -%}aria-invalid="true"{%- endif -%}
  >
  <span
    id="ContactForm-email-error"
    class="form__message"
    role="alert"
    {%- unless form.errors contains 'email' -%}hidden{%- endunless -%}
  >
    {%- if form.errors contains 'email' -%}
      {{- 'templates.contact.form.error_html' | t: error: form.errors.messages['email'] -}}
    {%- endif -%}
  </span>
</div>

Add to your contact form section (sections/contact-form.liquid or templates/page.contact.liquid) replacing the existing email input block.

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

  1. In your Shopify Admin, go to Online Store → Themes → click the three-dot menu next to your active theme → Edit code.
  2. Open Assets → base.css (or theme.css / global.css — whichever is the main stylesheet; search for 'outline' to locate it quickly).
  3. Find any `outline: none` or `outline: 0` rules on links, buttons, or inputs and either delete them or replace them with your custom focus style.
  4. At the bottom of the file (or in a dedicated accessibility section), add the universal focus rule from the code snippet below.
  5. Click Save. Open a preview of your store and press Tab to verify a visible ring appears on every interactive element.
Code · Shopify (css)
/* --- Visible focus indicator fix (WCAG 2.4.7) ---
   Paste this at the END of base.css / theme.css
   to override any earlier outline: none rules.   */

a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
[tabindex]:focus-visible {
  outline: 3px solid #005FCC !important;
  outline-offset: 2px !important;
  border-radius: 2px;
}

/* Older browser fallback */
a:focus,
button:focus,
input:focus,
select:focus,
textarea:focus,
[tabindex]:focus {
  outline: 3px solid #005FCC !important;
  outline-offset: 2px !important;
}

Paste into Online Store → Themes → Edit Code → Assets → base.css (or theme.css). Add after any existing outline rules.

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

  1. From your Shopify admin, go to Online Store → Themes → click the three-dot menu next to your active theme → Edit code.
  2. Search for '<iframe' across all template files using the search box at the top-left of the code editor. Check layout/theme.liquid, all templates/.liquid files, and sections/.liquid files.
  3. For iframes you control directly, add title="…" inside the tag and save.
  4. For iframes injected by Shopify apps (e.g. chat, reviews, maps), open the app's settings page inside Shopify admin — look for an 'accessibility label' or 'title' field. If none exists, add a JavaScript snippet to Online Store → Themes → Edit code → assets/theme.js (or a new <script> block in theme.liquid) that sets the attribute after page load (see code snippet below).
  5. Use the Shopify Theme Inspector or a browser extension like axe DevTools to verify the fix on your live store.
Code · Shopify (liquid)
{%- assign map_title = section.settings.map_title | default: 'Map showing our store location' -%}
<iframe
  src="https://www.google.com/maps/embed?pb={{ section.settings.map_embed_url | url_encode }}"
  title="{{ map_title | escape }}"
  width="100%"
  height="400"
  loading="lazy"
  style="border:0;"
  referrerpolicy="no-referrer-when-downgrade"
></iframe>

In a Liquid template file (e.g. sections/map.liquid or snippets/video.liquid), add the title attribute to your iframe tag.

Heading orderModerate effort

Restructure your page headings so they follow a logical, sequential order (H1 → H2 → H3) with no levels skipped.

On Shopify

  1. Go to Online Store → Themes → click 'Customize' on your active theme.
  2. For each section type (e.g., Featured Collection, Rich Text, Image with Text), click the section and look for a 'Heading' text field and a 'Heading size' or 'Heading tag' dropdown — some themes let you choose H1/H2/H3 directly here.
  3. For full control, go to Online Store → Themes → Actions → Edit Code, then open the relevant section Liquid file (e.g., sections/featured-collection.liquid) and change the hard-coded tag, e.g. replace <h3> with <h2> where the hierarchy demands it.
  4. For blog posts and pages, edit the content in Online Store → Blog Posts or Online Store → Pages using the rich-text editor — highlight text and select the correct heading level from the paragraph-style dropdown.
  5. Install the free axe DevTools browser extension, visit each key page, and run a scan to confirm no heading-order violations remain.
Code · Shopify (liquid)
{%- comment -%}
  Use h2 for a section title (the page's H1 is typically the product/page name).
  Adjust the level to match the actual heading hierarchy of the page.
{%- endcomment -%}

<section class="featured-collection">
  <h2 class="featured-collection__title">{{ section.settings.title }}</h2>

  <ul class="product-grid">
    {%- for product in collection.products limit: section.settings.products_to_show -%}
      <li class="product-grid__item">
        {%- comment -%} H3 for individual product titles nested within the H2 section {%- endcomment -%}
        <h3 class="product-card__title">
          <a href="{{ product.url }}">{{ product.title }}</a>
        </h3>
        <p class="product-card__price">{{ product.price | money }}</p>
      </li>
    {%- endfor -%}
  </ul>
</section>

In a section Liquid file, use the correct heading tag for the context. Example: sections/featured-collection.liquid

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

  1. In your Shopify admin, go to Online Store → Themes.
  2. Next to your active theme, click Actions → Edit code.
  3. In the left file list, under Layout, click theme.liquid (this is your site's master template).
  4. Find the opening `<html` tag near the top of the file.
  5. If it already contains `xml:lang`, replace it with `lang`. If neither exists, add `lang="{{ shop.locale }}"` — Shopify's Liquid variable that outputs the store's active locale code automatically.
  6. The result should look like: `<html lang="{{ shop.locale }}">` (or a hard-coded code like `lang="en"` if you only serve one language).
  7. Click Save. Verify by viewing your storefront source.
Code · Shopify (liquid)
<!DOCTYPE html>
<html lang="{{ shop.locale }}">
<head>
  <meta charset="UTF-8">
  <title>{{ page_title }}</title>
  {{ content_for_header }}
</head>
<body>
  {{ content_for_layout }}
</body>
</html>

Place this in Layout/theme.liquid, replacing any existing <html> tag. The Liquid variable outputs the active store locale automatically.

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

  1. In your Shopify admin, go to Online Store → Themes.
  2. Click the '...' (Actions) button next to your active theme and select 'Edit code'.
  3. In the Layout folder, open 'theme.liquid' (this is your global HTML template).
  4. Find the opening `<html` tag near the top of the file — it often already has `lang` set to a Liquid variable like `lang="{{ request.locale.iso_code }}"`. If missing or incorrect, update it to `<html lang="{{ request.locale.iso_code }}">` — Shopify's Liquid will automatically output the correct BCP 47 code for each storefront language.
  5. Click 'Save'. Preview your store and view page source to confirm the rendered `<html>` tag contains a valid language code.
Code · Shopify (liquid)
<!DOCTYPE html>
<html lang="{{ request.locale.iso_code }}">
  <head>
    {{ content_for_header }}
  </head>
  <body>
    {{ content_for_layout }}
  </body>
</html>

In theme.liquid (Layout folder), replace or update the opening <html> tag with this Liquid snippet.

Image altModerate effort

Add descriptive alt text to every meaningful image, and mark purely decorative images with an empty alt attribute or role="presentation".

On Shopify

  1. Product images: Go to Admin → Products → [Product name] → click on any product image thumbnail → an 'Alt text' field appears in the right panel — type your description and click 'Save'.
  2. Collection images: Admin → Products → Collections → [Collection name] → click the collection image → edit the 'Image alt text' field.
  3. Theme images (banners, slideshows): In Online Store → Themes → Customize, click any image block — look for an 'Alt text' or 'Image description' field in the left sidebar (exact name depends on theme).
  4. Blog/page images: In the rich-text editor, click an inserted image → click the 'Edit' (pencil) icon → fill in the 'Alt text' field.
  5. For bulk updates or to audit many products, use the Shopify bulk editor (Admin → Products → select products → Edit) or export your product CSV, fill in the 'Image Alt Text' column, and re-import.
Code · Shopify (liquid)
{% comment %} Shopify image objects expose .alt — fall back to product title if blank {% endcomment %}
<img
  src="{{ product.featured_image | img_url: '600x' }}"
  alt="{{ product.featured_image.alt | default: product.title | escape }}"
  width="600"
  height="{{ 600 | divided_by: product.featured_image.aspect_ratio | round }}"
  loading="lazy"
>

{% comment %} Gallery images loop {% endcomment %}
{% for image in product.images %}
  <img
    src="{{ image | img_url: '600x' }}"
    alt="{{ image.alt | default: product.title | escape }}"
    width="600"
    height="{{ 600 | divided_by: image.aspect_ratio | round }}"
    loading="lazy"
  >
{% endfor %}

Use in product/collection templates (e.g., snippets/product-media.liquid or sections/main-product.liquid) to output alt text from the image object.

Image redundant altQuick win

Remove or clear the alt text on images whose description is already provided by visible text immediately surrounding them, so screen readers don't read the same information twice.

On Shopify

  1. Go to Online Store → Themes → click 'Customize' on your active theme.
  2. For product images on product pages: in the theme editor, these are rendered by Liquid; go to Online Store → Themes → Actions → Edit Code.
  3. Open the relevant section file (commonly sections/product-template.liquid, sections/main-product.liquid, or snippets/product-media.liquid depending on your theme).
  4. Find the <img> tag for the product image. If it is inside an anchor tag that also outputs the product title as visible text, change the alt attribute value to an empty string: alt="".
  5. For collection page product cards, check sections/collection-template.liquid or snippets/product-card.liquid and apply the same empty alt fix to any image whose parent link already contains the product title as visible text.
  6. Save the file, then preview the store and verify with the axe browser extension.
Code · Shopify (liquid)
{% comment %}
  Product card: image is inside an anchor that also shows product.title as text.
  Use alt="" so screen readers skip the image and read the title only once.
{% endcomment %}
<a href="{{ product.url }}" class="product-card__link">
  <img
    src="{{ product.featured_image | img_url: '400x400' }}"
    alt=""
    width="400"
    height="400"
    loading="lazy"
  >
  <span class="product-card__title">{{ product.title }}</span>
</a>

Place in sections/main-product.liquid or snippets/product-card.liquid — set alt to empty string when product title is already rendered as visible text in the same link/card.

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

  1. In your Shopify Admin go to Online Store → Themes → click Actions → Edit code.
  2. Search across theme files (use the search icon in the file sidebar) for `input type="submit"`, `input type="button"`, or `input type="reset"`.
  3. Most commonly affected files are `sections/main-cart-footer.liquid`, `sections/header.liquid`, `snippets/product-form.liquid`, and any custom snippet files.
  4. Find any `<input ... value="">` or `<input ... value>` without text and add a descriptive value, e.g. `value="Add to Cart"`.
  5. If a button is intentionally icon-only, add `aria-label="Add to Cart"` to the input tag.
  6. Click Save, then preview the page and test with a screen reader or the axe browser extension.
Code · Shopify (liquid)
{{- /* BEFORE (broken): empty value attribute */ -}}
{{- /* <input type="submit" value=""> */ -}}

{{- /* AFTER (fixed): descriptive value attribute */ -}}
<input
  type="submit"
  value="{{ 'products.product.add_to_cart' | t }}"
  class="btn product-form__cart-submit"
>

In any .liquid template file, replace empty-value input buttons with labelled ones.

Input image altQuick win

Add a descriptive `alt` attribute to every `<input type="image">` element so screen readers can announce its purpose to visually impaired shoppers.

On Shopify

  1. In your Shopify admin, go to Online Store → Themes → click the three-dot menu beside your active theme → Edit code.
  2. Use the search box (top-left of the code editor) to search for `type="image"` across all theme files.
  3. For each match found (commonly in `sections/header.liquid`, `snippets/search-form.liquid`, or `templates/cart.liquid`), add or update the `alt` attribute: `<input type="image" src="..." alt="Submit search">`.
  4. If the button is rendered by an app (e.g. a PayPal app), open the app's settings — most reputable payment apps include their own alt text; if not, contact the app developer.
  5. Click Save, then preview the theme and verify with the axe browser extension.
Code · Shopify (liquid)
{%- comment -%} Search form image button with required alt text {%- endcomment -%}
<form action="{{ routes.search_url }}" method="get" role="search">
  <input type="hidden" name="type" value="product">
  <input
    type="text"
    name="q"
    placeholder="Search..."
    aria-label="Search products"
  >
  <input
    type="image"
    src="{{ 'search-icon.png' | asset_url }}"
    alt="Search products"
    width="20"
    height="20"
  >
</form>

Place in the relevant Liquid snippet or section file (e.g. snippets/search-form.liquid). Edit via Online Store → Themes → Edit code.

LabelQuick win

Add a visible or programmatic label to every form input so assistive technologies can identify it for users.

On Shopify

  1. Go to Online Store → Themes → Actions → Edit code.
  2. Locate the relevant template file: for the newsletter form it is typically `sections/footer.liquid` or `sections/newsletter.liquid`; for the search box try `sections/header.liquid` or `snippets/search-form.liquid`; for checkout fields these are controlled by Shopify's native checkout (Plus only).
  3. Find the `<input>` element that is missing a label. Add a `<label for='field-id'>Description</label>` immediately before it, and add `id='field-id'` to the input. For inputs where you want a visually hidden label, add the CSS class `visually-hidden` (already defined in most Shopify themes) to the label element.
  4. For the search input specifically, add `aria-label='Search'` directly on the `<input type='search'>` tag if a visible label is not desired.
  5. Click Save, then preview the theme and verify with the axe browser extension.
Code · Shopify (liquid)
{%- comment -%} Search input with visually-hidden label — sections/header.liquid {%- endcomment -%}
<form action="{{ routes.search_url }}" method="get" role="search">
  <label for="Search-In-Modal" class="visually-hidden">{{ 'general.search.placeholder' | t }}</label>
  <input
    id="Search-In-Modal"
    type="search"
    name="q"
    value="{{ search.terms | escape }}"
    placeholder="{{ 'general.search.placeholder' | t }}"
    autocomplete="off"
  >
  <button type="submit" aria-label="{{ 'general.search.search' | t }}">
    {%- render 'icon-search' -%}
  </button>
</form>

{%- comment -%} Newsletter input with visible label — sections/newsletter.liquid {%- endcomment -%}
<label for="NewsletterForm-email">{{ section.settings.label | default: 'Email address' }}</label>
<input
  id="NewsletterForm-email"
  type="email"
  name="contact[email]"
  autocomplete="email"
  placeholder="{{ 'newsletter.label' | t }}"
>

Add to the relevant section .liquid file (e.g. sections/header.liquid for the search input, sections/newsletter.liquid for email sign-up).

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

  1. In your Shopify admin go to Online Store → Themes → (your active theme) → Actions → Edit code.
  2. Common problem files: sections/header.liquid (search input), sections/main-cart-items.liquid (quantity input), templates/customers/login.liquid, and any newsletter section.
  3. Open the relevant .liquid file, find the <input> element, and check whether a <label for='...'> with matching id exists above it.
  4. If missing, add a <label for='search'>Search</label> immediately before the <input id='search' ...>. For visually-hidden labels you still want read aloud, wrap the label text in a <span class='visually-hidden'> using Shopify's built-in utility class instead of relying on title.
  5. For search bars where a visible label is unwanted by the design, replace the title attribute with aria-label='Search' directly on the <input> element.
  6. Save and preview. Use the axe browser extension to verify no label-title-only violations remain.
Code · Shopify (liquid)
{%- comment -%}
  Accessible search form for Shopify — replaces title-only input.
  Add this in your header section or search drawer snippet.
{%- endcomment -%}

<form action="{{ routes.search_url }}" method="get" role="search">
  <label for="Search-In-Header" class="visually-hidden">
    {{ 'general.search.search' | t }}
  </label>
  <input
    id="Search-In-Header"
    type="search"
    name="q"
    value="{{ search.terms | escape }}"
    placeholder="{{ 'general.search.placeholder' | t }}"
    autocomplete="off"
    autocorrect="off"
    autocapitalize="off"
    spellcheck="false"
  />
  <button type="submit" aria-label="{{ 'general.search.search' | t }}">
    {% render 'icon-search' %}
  </button>
</form>

Place in sections/header.liquid (or your search drawer snippet). Replaces a title-only search input.

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

  1. Go to Online Store → Themes → click the three-dot menu beside your active theme → Edit code.
  2. Open layout/theme.liquid — this is the root HTML shell for every page.
  3. Locate the `<header>` element (or search for 'role="banner"'). It should appear as a direct child of `<body>`, before `{% section 'main' %}` or `{{ content_for_layout }}`.
  4. If the `<header>` is inside a `<main>` or any wrapping `<div role="main">`, cut the entire header block and paste it above the opening `<main>` tag.
  5. Click Save, then preview the store and verify with the axe browser extension.
Code · Shopify (liquid)
<!doctype html>
<html lang="{{ request.locale.iso_code }}">
<head>
  {{ content_for_head }}
</head>
<body>

  {%- if settings.enable_skip_link -%}
    <a class="skip-to-content-link" href="#MainContent">Skip to content</a>
  {%- endif -%}

  <!-- CORRECT: header is a direct child of body -->
  {% section 'header' %}

  <main id="MainContent" class="content-for-layout" role="main">
    {{ content_for_layout }}
  </main>

  {% section 'footer' %}

</body>
</html>

In layout/theme.liquid, ensure <header> appears before <main> as a direct body child.

Landmark complementary is top levelModerate effort

Move your <aside> element (or role="complementary") to the top level of the page so it is not nested inside another landmark region.

On Shopify

  1. In your Shopify admin, go to Online Store → Themes → click 'Customize', then exit to code via Actions → Edit code.
  2. Open the relevant layout or section file: for sitewide sidebars check `layout/theme.liquid`; for page-specific sidebars check the section file under `sections/` (e.g., `sections/product-template.liquid` or `sections/collection-template.liquid`).
  3. Find the `<aside>` or `role="complementary"` element. If it is wrapped inside `<main>…</main>` or another landmark tag, cut the entire `<aside>` block out of that container.
  4. Paste the `<aside>` block as a sibling of `<main>` — both should be direct children of a wrapping `<div>` that is itself a direct child of `<body>`, or directly inside `<body>`.
  5. Use CSS in `assets/theme.css` (or your custom stylesheet) with Flexbox or Grid on the wrapper `<div>` to restore the two-column layout: `display: flex;` with `<main>` and `<aside>` as flex children.
  6. Add `aria-label="[Descriptive name]"` to the `<aside>` tag, save all files, preview the theme, and verify with axe DevTools browser extension.
Code · Shopify (liquid)
<div class="page-layout">
  <main id="main-content" role="main">
    {{ content_for_layout }}
  </main>

  <aside aria-label="Store sidebar">
    {% section 'sidebar' %}
  </aside>
</div>

Place in layout/theme.liquid — shows <main> and <aside> as siblings inside a flex wrapper.

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

  1. In your Shopify admin, go to Online Store → Themes.
  2. Click 'Customize' on your active theme, then exit to the code editor: Online Store → Themes → Actions → Edit code.
  3. In the 'Layout' folder, open 'theme.liquid' (this is the global page wrapper for most themes).
  4. Search for '<footer' or 'role="contentinfo"'. Check what element wraps it — if it is inside a <main>, <div role="main">, or any other landmark, identify the closing tag of that wrapper.
  5. Cut the entire <footer>…</footer> block and paste it outside and after the closing </main> tag (or whichever wrapper contained it), so it is a direct sibling of <header> and <main> under <body>.
  6. Save the file, then preview the theme and run axe DevTools in your browser to confirm the issue is resolved.
Code · Shopify (liquid)
<!doctype html>
<html lang="{{ request.locale.iso_code }}">
  <head>
    <meta charset="utf-8">
    <title>{{ page_title }}</title>
    {{ content_for_header }}
  </head>
  <body>

    {% section 'header' %}

    <main id="MainContent" role="main">
      {{ content_for_layout }}
    </main>

    {%- comment -%}
      CORRECT: footer section is rendered here, after </main>,
      as a direct child of <body>.
    {%- endcomment -%}
    {% section 'footer' %}

  </body>
</html>

In theme.liquid, ensure the footer snippet/section is rendered after </main>, directly inside <body>. This is the correct structure.

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

  1. In your Shopify Admin, go to Online Store → Themes → click 'Customize' next to your active theme, then exit Customize and instead click the '…' menu → Edit code.
  2. In the Layout folder, open 'theme.liquid' (the master HTML shell for almost every Shopify theme).
  3. Search (Ctrl/Cmd+F) for '<main' or 'role="main"'.
  4. Check what HTML tag directly wraps the `<main>` element. Common culprits: a `<div role='banner'>`, a `<header>`, or a wrapper `<section>`.
  5. Cut the entire `<main>…</main>` block (and its closing tag) and paste it as a direct child of `<body>`, typically right after the closing `</header>` tag.
  6. Click Save, then visit a storefront page and run axe DevTools or WAVE in your browser to confirm the fix.
Code · Shopify (liquid)
<!DOCTYPE html>
<html lang="{{ request.locale.iso_code }}">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  {{ content_for_header }}
  <title>{{ page_title }}</title>
</head>
<body>

  {% section 'header' %}

  <!-- CORRECT: <main> is a direct child of <body>, outside the header section -->
  <main id="MainContent" class="content-for-layout" role="main" tabindex="-1">
    {{ content_for_layout }}
  </main>

  {% section 'footer' %}

  {{ content_for_layout }}
</body>
</html>

Place this structure in Layout/theme.liquid. Ensure <main> is between </header> and <footer> at the body's top level.

Landmark no duplicate bannerModerate effort

Ensure your page has only one banner landmark (one `<header>` or `role="banner"` element that is not nested inside another landmark), so screen reader users can navigate your site reliably.

On Shopify

  1. Go to Online Store → Themes → click 'Customize' on your active theme.
  2. To edit HTML directly: click 'Edit code' (theme.liquid and section files).
  3. Open 'layout/theme.liquid' — look for the top-level `<header>` tag; this should be the only banner-level header.
  4. Search all files (use the search bar in code editor) for additional `<header` tags or `role="banner"`. Common culprits: announcement bar sections, sticky-header sections, or app-injected blocks.
  5. For any secondary `<header>` inside a content section (e.g. inside a `.page-content` div), change the tag to `<div>` and add an appropriate class for styling.
  6. Save changes and re-test with an accessibility browser extension.
Code · Shopify (liquid)
{%- comment -%}
  layout/theme.liquid
  ONE top-level <header> for the page banner.
  The announcement bar uses <div>, not <header>.
{%- endcomment -%}

<!doctype html>
<html lang="{{ request.locale.iso_code }}">
<head>
  <meta charset="utf-8">
  {{ content_for_header }}
</head>
<body>

  {%- section 'announcement-bar' -%}  {#- renders a <div>, see section file below -#}

  <header class="site-header" role="banner">
    <a href="{{ routes.root_url }}"
       aria-label="{{ shop.name }} — home">
      {%- if settings.logo != blank -%}
        {{ settings.logo | image_url: width: 200 | image_tag: alt: shop.name }}
      {%- else -%}
        {{ shop.name }}
      {%- endif -%}
    </a>
    {%- section 'main-menu' -%}
  </header>

  <main id="MainContent" role="main" tabindex="-1">
    {{ content_for_layout }}
  </main>

  {%- section 'footer' -%}

</body>
</html>

In layout/theme.liquid — keep ONE top-level <header>; convert announcement-bar sections to <div>.

Landmark no duplicate contentinfoModerate effort

Ensure your page has only one footer/contentinfo landmark by removing or re-marking any duplicate `<footer>` elements or `role="contentinfo"` attributes.

On Shopify

  1. Go to Online Store → Themes → click 'Edit code' on your active theme.
  2. Open the layout file: Layout/theme.liquid. Look for any <footer> tag near the bottom of the file — this is your main footer landmark.
  3. Open Sections/footer-group.liquid or Sections/footer.liquid (name varies by theme) and confirm it contains exactly one top-level <footer> element.
  4. Search all section and snippet files (Snippets/) for additional <footer> tags or role="contentinfo" by using the theme editor's search (magnifying glass icon). Change any secondary ones to <div> with an appropriate aria-label.
  5. If a third-party app (e.g. a chat widget installed via the App Embed block) is causing the duplicate, go to Online Store → Themes → Customize → App Embeds, open the app's settings, and look for an HTML/accessibility option to disable its own footer wrapper.
  6. Save all files and re-test with the axe DevTools browser extension.
Code · Shopify (liquid)
{%- comment -%}
  Layout/theme.liquid — correct structure
  Only ONE <footer> tag should exist as a top-level child of <body>.
{%- endcomment -%}
<!DOCTYPE html>
<html lang="{{ request.locale.iso_code }}">
<head>
  {{ content_for_header }}
</head>
<body>
  {% section 'header' %}

  <main id="MainContent" role="main" tabindex="-1">
    {{ content_for_layout }}
  </main>

  {%- comment -%}
    If a promotional bar or app embed renders a secondary <footer>,
    wrap it in a <div> here instead:
  {%- endcomment -%}
  <div class="promo-strip" aria-label="Site-wide promotion">
    {% section 'announcement-bar' %}
  </div>

  {%- comment -%}
    The ONE footer landmark:
  {%- endcomment -%}
  <footer class="site-footer" role="contentinfo">
    {% section 'footer' %}
  </footer>
</body>
</html>

In Layout/theme.liquid — ensure only one <footer> element exists at the document root level. Secondary sections use <div>.

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

  1. Go to Online Store → Themes → Actions → Edit code.
  2. Open layout/theme.liquid — this is the master page shell. Search for '<main' and 'role="main"'. There should be exactly one; it typically wraps '{{ content_for_layout }}'.
  3. Open sections/ and snippets/ files (especially header.liquid, footer.liquid, sidebar snippets) and search each for '<main' or 'role="main"'. Remove or replace any extras with '<div>' or '<section>'.
  4. If you use a third-party app that injects a chat or overlay widget, inspect the live page DOM in DevTools after the app loads to check whether it also adds a landmark.
  5. Save changes and verify with the axe browser extension on your storefront.
Code · Shopify (liquid)
{% comment %} layout/theme.liquid — simplified structure {% endcomment %}
<!DOCTYPE html>
<html lang="{{ request.locale.iso_code }}">
<head>
  {{ content_for_header }}
</head>
<body>

  {% section 'header' %}

  <main id="MainContent" class="content-for-layout" role="main" tabindex="-1">
    {{ content_for_layout }}
  </main>

  {% section 'footer' %}

</body>
</html>

Place this pattern in layout/theme.liquid — one <main> wrapping {{ content_for_layout }}. Remove any <main> from header.liquid, footer.liquid, and snippets.

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

  1. In your Shopify Admin, go to Online Store → Themes → click the three-dot menu on your active theme → Edit code.
  2. Open `layout/theme.liquid` — this is the master layout that wraps every page.
  3. Find the `<div>` (often something like `<div class="main-content" id="MainContent">`) that wraps `{{ content_for_layout }}`.
  4. Change that opening tag to `<main id="MainContent" role="main">` and its closing tag to `</main>` (or simply `<main id="MainContent">` — the semantic element implies the role).
  5. If a 'Skip to main content' link already exists in the header, confirm its `href` matches `#MainContent`.
  6. Save, then verify with the axe browser extension on a live preview.
Code · Shopify (liquid)
{%- comment -%}
  layout/theme.liquid — replace your existing main content <div> with this
{%- endcomment -%}

<a class="skip-to-content-link button visually-hidden" href="#MainContent">
  {{ 'accessibility.skip_to_text' | t }}
</a>

<main id="MainContent" class="content-for-layout" role="main">
  {{ content_for_layout }}
</main>

Replaces the main content wrapper in layout/theme.liquid — wrap {{ content_for_layout }} in <main>.

Landmark uniqueModerate effort

Add a unique aria-label (or aria-labelledby) to every repeated landmark role on the page so screen-reader users can tell them apart.

On Shopify

  1. In your Shopify admin, go to Online Store → Themes → (your active theme) → Actions → Edit code.
  2. Open the theme layout file: Layout → theme.liquid. This file wraps every page and typically contains the main <header>, <nav>, and <footer> landmarks.
  3. Search for every <nav>, <header>, <aside>, <section>, and <footer> tag in theme.liquid, and in any section/snippet files (Sections folder and Snippets folder) included in the template.
  4. For each duplicated landmark type, add a descriptive aria-label attribute. Example: change <nav> to <nav aria-label='Main navigation'> and a second one to <nav aria-label='Footer navigation'>.
  5. For section files (e.g. header.liquid, footer.liquid, predictive-search.liquid, breadcrumbs.liquid), open each file individually in the code editor and apply the same aria-label additions.
  6. Save each file, preview the live theme, and verify with the axe DevTools browser extension that landmark-unique violations are cleared.
Code · Shopify (liquid)
{% comment %}
  In theme.liquid or header.liquid — label the main site navigation
{% endcomment %}
<nav aria-label="Main navigation" role="navigation">
  {% render 'header-nav' %}
</nav>

{% comment %}
  In footer.liquid — label the footer navigation distinctly
{% endcomment %}
<nav aria-label="Footer navigation">
  <ul>
    {% for link in linklists.footer.links %}
      <li><a href="{{ link.url }}">{{ link.title }}</a></li>
    {% endfor %}
  </ul>
</nav>

{% comment %}
  In breadcrumbs.liquid — label the breadcrumb nav
{% endcomment %}
<nav aria-label="Breadcrumb">
  <ol>
    <li><a href="{{ routes.root_url }}">Home</a></li>
    <li>{{ product.title }}</li>
  </ol>
</nav>

Edit Layout/theme.liquid and relevant Sections/Snippets files in the Shopify code editor.

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

  1. In your Shopify admin, go to Online Store → Themes.
  2. Next to your active theme, click the three-dot menu (⋯) → Edit code.
  3. Open Assets → base.css (or theme.css / global.css depending on your theme).
  4. Search for the existing link rule (e.g. 'a {' or 'a:link'). Add or update: color: #YOUR_NEW_COLOR; text-decoration: underline;
  5. If you want to scope it only to content/body text blocks, use a selector like '.rte a { }' (Shopify themes wrap rich-text content in the .rte class).
  6. Click Save, then preview your store and verify links in paragraphs are clearly distinguishable.
Code · Shopify (css)
/* Shopify: target all inline links inside rich-text content */
.rte a {
  color: #0057d8; /* update to your chosen compliant color */
  text-decoration: underline;
}

/* Also cover standalone paragraph links outside .rte */
a {
  color: #0057d8;
  text-decoration: underline;
}

Paste into Assets/base.css, or use Online Store → Themes → Customize → Theme Settings → Custom CSS. The .rte class wraps all rich-text content (product descriptions, pages, blog posts).

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

  1. In your Shopify Admin, go to Online Store → Themes → and click 'Edit code' (or Customize → Edit code for theme 2.0 themes).
  2. The most common culprits are in 'sections/header.liquid' (nav/cart icon links), 'sections/footer.liquid' (social media icon links), and 'snippets/card-product.liquid' (product card image links).
  3. For icon-only links (e.g. cart, search, social icons), add an aria-label attribute: <a href='/cart' aria-label='Shopping cart'>…icon…</a>.
  4. For image links in product cards, locate the wrapping <a> tag around the product image and add aria-label='View [product title]' or ensure the inner <img> has a descriptive alt='{{ product.title }}' attribute.
  5. For social media icon links in the footer, add aria-label matching the network name: <a href='{{ settings.social_instagram_link }}' aria-label='Follow us on Instagram' target='_blank'>.
  6. Save each file, then use the Shopify Theme Preview to tab through the page and confirm announced link names make sense.
ListModerate effort

Fix all HTML lists so that only valid child elements appear directly inside `<ul>` and `<ol>` tags, and only `<ul>` or `<ol>` elements wrap `<li>` items.

On Shopify

  1. In your Shopify Admin, go to Online Store → Themes → click the '...' (Actions) menu next to your active theme → Edit code.
  2. The most common list errors live in navigation and footer snippets. Open sections/header.liquid (or snippets/navigation.liquid) and locate any `<ul>` used for the nav menu — ensure every direct child is `<li>`.
  3. Open sections/footer.liquid and repeat the check for footer link lists.
  4. For product feature bullets, open sections/product-template.liquid (or the relevant product section file) and confirm `<ul>`/`<ol>` contains only `<li>` children.
  5. If your theme uses a mega-menu or third-party navigation app, check that app's own settings or custom HTML blocks for the same pattern.
  6. Save each file, then use the Shopify Theme Preview to verify appearance, and re-run the axe browser extension to confirm no remaining list violations.
Code · Shopify (liquid)
<nav aria-label="Main navigation">
  <ul class="nav-list">
    {% for link in linklists.main-menu.links %}
      <li class="nav-item">
        <a href="{{ link.url }}">{{ link.title }}</a>
        {% if link.links.size > 0 %}
          <ul class="nav-dropdown">
            {% for child_link in link.links %}
              <li class="nav-dropdown__item">
                <a href="{{ child_link.url }}">{{ child_link.title }}</a>
              </li>
            {% endfor %}
          </ul>
        {% endif %}
      </li>
    {% endfor %}
  </ul>
</nav>

Place in sections/header.liquid or snippets/navigation.liquid — typical nav menu pattern with correct list structure.

ListitemModerate effort

Ensure every `<li>` element is a direct child of a `<ul>` or `<ol>` element (or an element explicitly given `role="list"`), so that screen readers can correctly announce list structure to users.

On Shopify

  1. In your Shopify admin go to Online Store → Themes → click the three-dot menu beside your active theme → Edit code.
  2. Use the search box (or browse Sections / Templates / Snippets folders) to locate the Liquid file that renders the broken list — common culprits are `header.liquid`, `footer.liquid`, `navigation.liquid`, `collection-template.liquid`, or a snippet like `product-features.liquid`.
  3. Find the `<li>` element and look at its parent tag. If it is a `<div>`, `<nav>`, or any non-list element, wrap the block of `<li>` tags in `<ul>...</ul>` (or `<ol>...</ol>` for ordered lists).
  4. If the parent element must remain (e.g. a `<nav>` wrapping a menu), nest a `<ul>` inside it and move the `<li>` elements inside that `<ul>`: `<nav><ul><li>...</li></ul></nav>`.
  5. Save the file and preview the theme. Use Chrome DevTools → Elements → Accessibility panel to confirm the list is now announced correctly.
  6. If you use a third-party app that injects list markup (e.g. a product-tabs or mega-menu app), contact the app developer to fix the generated HTML or replace the app.
Code · Shopify (liquid)
{%- comment -%}
  Example: a product features list in a Liquid section/snippet.
  The <ul> is the direct parent of every <li>.
{%- endcomment -%}

<section class="product-features">
  <h2>Product Highlights</h2>
  <ul class="product-features__list">
    {%- for feature in product.metafields.custom.features.value -%}
      <li class="product-features__item">{{ feature }}</li>
    {%- endfor -%}
  </ul>
</section>

{%- comment -%}
  Example: navigation — <ul> nested inside <nav>, NOT <li> inside <nav> directly.
{%- endcomment -%}

<nav class="site-nav" aria-label="Main navigation">
  <ul class="site-nav__list">
    {%- for link in linklists.main-menu.links -%}
      <li class="site-nav__item">
        <a href="{{ link.url }}">{{ link.title }}</a>
      </li>
    {%- endfor -%}
  </ul>
</nav>

In a Liquid snippet or section file — wrap <li> tags in <ul> inside any wrapper element.

Meta refreshQuick win

Remove or disable the `<meta http-equiv="refresh">` tag that forces an automatic page reload, or replace any redirect use-case with a server-side redirect instead.

On Shopify

  1. Go to Online Store → Themes → click the three-dot menu beside your active theme → Edit code.
  2. Open the file `layout/theme.liquid` (this wraps every storefront page).
  3. Use Cmd/Ctrl+F to search for `http-equiv` or `meta.*refresh`. Remove the entire `<meta http-equiv="refresh" ...>` line.
  4. If the tag appears in a section or snippet file (check snippets/ and sections/), search each file the same way.
  5. If you need a redirect, use Shopify's built-in URL redirect tool: Online Store → Navigation → URL Redirects → Add URL Redirect. This creates a proper 301.
  6. Save the theme file, then preview and re-scan to confirm the tag is gone.
Code · Shopify (liquid)
{% comment %}
  REMOVE any line like this from layout/theme.liquid:
  <meta http-equiv="refresh" content="30">
  or
  <meta http-equiv="refresh" content="10; url={{ shop.url }}/new-page">

  For redirects, use Shopify's URL Redirect tool:
  Online Store → Navigation → URL Redirects → Add URL Redirect
{% endcomment %}

Search for and remove this pattern in layout/theme.liquid. Example of what to delete:

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

  1. In your Shopify admin, go to Online Store → Themes.
  2. Next to your active theme, click Actions → Edit code.
  3. In the left file tree, open Layout → theme.liquid (this is the master HTML template).
  4. Use Ctrl+F / Cmd+F to search for 'viewport'.
  5. Find the <meta name="viewport" …> tag and remove `user-scalable=no` and/or change `maximum-scale` to 5 (or delete that parameter).
  6. Click Save. The fix is live immediately — no republishing needed.
Code · Shopify (liquid)
<meta name="viewport" content="width=device-width, initial-scale=1">

In Layout/theme.liquid, replace the existing viewport tag with this line.

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

  1. In your Shopify Admin, go to Online Store → Themes.
  2. Click the '…' (Actions) menu next to your active theme and choose 'Edit code'.
  3. In the Layout folder on the left, open 'theme.liquid' (or 'layout/theme.liquid').
  4. Use Ctrl+F / Cmd+F to search for 'viewport'.
  5. Find the <meta name="viewport"> tag and remove maximum-scale=1 and/or user-scalable=no, or set maximum-scale=5.
  6. Click 'Save'. No app or re-publish needed — the change is live immediately.
Code · Shopify (liquid)
<meta name="viewport" content="width=device-width, initial-scale=1">

In layout/theme.liquid, replace the existing viewport meta tag (usually near the top of <head>) with this line.

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

  1. In your Shopify admin, go to Online Store → Themes → your active theme → Actions → Edit code.
  2. Open the Liquid template or section file where the nested element appears (common locations: sections/header.liquid, sections/featured-collection.liquid, snippets/product-card.liquid, snippets/cart-drawer.liquid).
  3. Search for patterns like `<a ...><button` or `<button ...><a ` in the code editor (use Cmd/Ctrl+F).
  4. Refactor the markup: keep only one interactive element and replace the other with a styled `<span>` or `<div>`, or merge both into a single `<a>` or `<button>` element with equivalent styling.
  5. Click Save, then preview your storefront. Use browser DevTools → Accessibility tab to confirm no nesting warning appears, and test keyboard navigation with the Tab key.
Code · Shopify (liquid)
{%- comment -%}
  ❌ WRONG — button nested inside link
  <a href="{{ product.url }}">
    <button type="button" class="btn">{{ product.title }}</button>
  </a>
{%- endcomment -%}

{%- comment -%} ✅ CORRECT — single <a> element styled as a button {%- endcomment -%}
<a href="{{ product.url }}" class="btn product-card__title-link">
  {{ product.title }}
</a>

In snippets/product-card.liquid (or equivalent): replace a link wrapping a button with a single styled link.

Object altQuick win

Add a meaningful text alternative (via `title` attribute or `aria-label`) to every `<object>` element so screen readers can describe its content to users who cannot see it.

On Shopify

  1. In the Shopify Admin, go to Online Store → Themes → and click 'Customize' (or 'Edit code' for direct file edits).
  2. If the <object> is in a theme section or snippet, click 'Edit code', then open the relevant .liquid file under Sections/, Snippets/, or Templates/.
  3. Locate the <object> tag in the Liquid/HTML source.
  4. Add a `title` attribute: e.g., change `<object data='...' type='...'>` to `<object data='...' type='...' title='Descriptive label here'>`.
  5. If the <object> is embedded via a Custom HTML block in the page editor (Online Store → Pages → Edit), switch to the HTML view of the rich-text editor and add the `title` attribute there.
  6. Save the file or block, then preview the page and validate with the axe DevTools browser extension.
Code · Shopify (liquid)
{%- assign catalog_title = 'Winter 2024 Product Catalog (PDF)' -%}
<object
  data="{{ 'product-catalog.pdf' | asset_url }}"
  type="application/pdf"
  width="100%"
  height="500"
  title="{{ catalog_title | escape }}">
  <p>
    <a href="{{ 'product-catalog.pdf' | asset_url }}">Download {{ catalog_title }}</a>
  </p>
</object>

Place this in the relevant .liquid file under Sections/, Snippets/, or Templates/ in your theme's code editor. Replace the title value with your actual descriptive label.

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

  1. Go to Online Store → Themes → click 'Customize' on your active theme.
  2. For product pages: the product title is usually already rendered as an <h1> in the theme's product template (e.g. sections/main-product.liquid). Open the theme code via Online Store → Themes → '…' → Edit code and search for 'product.title' — wrap it in <h1> if it isn't already.
  3. For custom pages (e.g. About, Contact): go to Online Store → Pages, open the page, and in the content editor type the page title, highlight it, and choose 'Heading 1' from the paragraph-style dropdown.
  4. For the homepage: in Theme Customize, click on the hero or banner section. If it outputs the text as a <div> or <p>, edit the section's Liquid file (sections/slideshow.liquid or similar) and change the element to <h1>.
  5. After saving, use the axe DevTools browser extension to re-check the live page.
Code · Shopify (liquid)
<h1 class="product__title">{{ product.title }}</h1>

In your product template (sections/main-product.liquid or templates/product.liquid), ensure the product title is an <h1>.

Presentation role conflictModerate effort

Remove conflicting ARIA attributes (global ARIA properties and tabindex) from any element that has been marked as purely presentational with role="presentation" or role="none".

On Shopify

  1. In your Shopify admin, go to Online Store → Themes → Actions → Edit code.
  2. Search across all Liquid/HTML files for occurrences of role="presentation" or role="none" using the search bar at the top of the code editor (search for 'role="presentation"' and 'role="none"').
  3. Open each flagged file (commonly layout/theme.liquid, sections/.liquid, snippets/.liquid) and inspect the element for conflicting ARIA attributes or tabindex.
  4. Remove the conflicting attribute(s) following the decision logic in the generic steps above.
  5. Save the file and preview your storefront. Use Chrome's axe DevTools extension to re-scan and confirm the issue is resolved.
  6. If your theme is from the Theme Store and you cannot safely edit it, consider reaching out to the theme developer or installing an accessibility-focused app (e.g. UserWay or accessiBe) as a temporary measure — but note these cannot fully substitute for clean code.
Code · Shopify (liquid)
{%- comment -%}
  Decorative hero background image — no alt text, no ARIA needed.
  Do NOT add aria-label, aria-describedby, or tabindex here.
{%- endcomment -%}
<img
  src="{{ section.settings.image | img_url: '1200x' }}"
  alt=""
  loading="lazy"
  width="1200"
  height="600"
/>

Example corrected snippet for a decorative image in a Liquid template (e.g. snippets/hero-image.liquid).

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

  1. From your Shopify admin go to Online Store → Themes → (your active theme) → Actions → Edit code.
  2. Open 'layout/theme.liquid' — this is the master shell for every page.
  3. Locate the opening and closing of your page structure. Ensure the site header is wrapped in `<header role='banner'>` (or just `<header>`), the main content area uses `<main id='MainContent'>`, the footer uses `<footer role='contentinfo'>`, and any navigation uses `<nav aria-label='Main'>` (or similar).
  4. Open any section files under 'sections/' (e.g. 'sections/announcement-bar.liquid', 'sections/header.liquid') and confirm their rendered output lands inside the correct landmark in theme.liquid.
  5. For promotional banners or custom sections that render outside `<main>` or `<header>`, wrap their content with `<section aria-label='Promotion'>` inside the appropriate landmark.
  6. Save changes and verify with the axe DevTools browser extension on your storefront.
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

  1. In Shopify Admin, go to Online Store → Themes → click 'Customize' on your active theme.
  2. For SVG icons or icon-font spans in your theme code: click 'Edit code' (three-dot menu next to the theme name) to open the theme code editor.
  3. Search for 'role="img"' across all Liquid files (use the search box at the top of the file tree).
  4. On each matching element, add aria-label="[description]" as an attribute, e.g. <span role="img" aria-label="Free shipping"> or on an SVG: add a <title id="icon-title">Free shipping</title> as the first child and set aria-labelledby="icon-title" on the <svg role="img"> element.
  5. For third-party sections or apps that inject HTML you cannot edit, contact the app developer or use a theme.liquid script injection to patch the rendered DOM after page load as a last resort.
  6. Save and preview with a screen reader (VoiceOver on Mac, NVDA on Windows) or re-run an axe browser extension audit to verify.
Code · Shopify (liquid)
{%- comment -%}
  Example: a star-rating icon rendered with an icon font in a product card snippet.
  File: snippets/product-rating.liquid
{%- endcomment -%}

{%- assign rating_label = 'Rating: ' | append: product.metafields.reviews.rating.value | append: ' out of 5 stars' -%}

<span role="img" aria-label="{{ rating_label | escape }}" class="product-rating__stars">
  {%- for i in (1..5) -%}
    <span aria-hidden="true" class="icon-star{% if i <= product.metafields.reviews.rating.value %} icon-star--filled{% endif %}"></span>
  {%- endfor -%}
</span>

{%- comment -%}
  Example: inline SVG in a section file.
  File: sections/announcement-bar.liquid
{%- endcomment -%}
<svg role="img" aria-labelledby="announcement-icon-title" width="20" height="20" xmlns="http://www.w3.org/2000/svg">
  <title id="announcement-icon-title">Free shipping on orders over $50</title>
  <use href="#icon-truck"></use>
</svg>

Place in the relevant .liquid snippet or section file where the icon or SVG is rendered (Online Store → Themes → Edit code).

Scrollable region focusableQuick win

Make every scrollable region on your site reachable by keyboard by adding tabindex="0" (or placing focusable content inside it) so keyboard and assistive-technology users can scroll it.

On Shopify

  1. In your Shopify Admin, go to Online Store → Themes → click the three-dot menu next to your active theme → Edit code.
  2. Identify the template or section file that contains the scrollable element (e.g. sections/product-template.liquid for a product description, or snippets/reviews.liquid for a reviews panel).
  3. Locate the HTML tag that has overflow: scroll / overflow: auto in its inline style or in a CSS class, and add tabindex="0" and an aria-label attribute to that tag.
  4. Open Assets → theme.css (or your main stylesheet) and add a :focus rule for that element: .your-scrollable-class:focus { outline: 2px solid #005fcc; }
  5. Save the file, preview the theme, and verify with Tab-key navigation.
Code · Shopify (liquid)
<div
  class="product__description rte"
  tabindex="0"
  aria-label="Product description"
  role="region"
  style="max-height: 300px; overflow-y: auto;"
>
  {{ product.description }}
</div>

Edit the relevant section file in Online Store → Themes → Edit code (e.g. sections/main-product.liquid). Replace the scrollable div with this pattern.

Select nameQuick win

Add a visible or programmatic label to every `<select>` dropdown so assistive technologies can announce its purpose to users.

On Shopify

  1. In your Shopify admin go to Online Store → Themes → click 'Customize' on your active theme.
  2. For product option selects (size, color, etc.), click 'Edit code' (Actions → Edit code) and open the relevant Liquid file — typically `snippets/product-variant-picker.liquid`, `sections/main-product.liquid`, or `snippets/product-form.liquid` depending on your theme.
  3. Locate the `<select>` tag for each option. Check whether it already has a `<label for="…">` above it with a matching `id` on the select. If not, add `id="product-option-{{ option.name | handleize }}"` to the `<select>` and add `<label for="product-option-{{ option.name | handleize }}">{{ option.name }}</label>` immediately before it.
  4. For sort-by dropdowns, open `sections/main-collection-product-grid.liquid` (or `collection.liquid`) and find the sort `<select>`. Add `id="SortBy"` to the select and `<label for="SortBy" class="visually-hidden">Sort by</label>` before it. Most themes already include a `.visually-hidden` utility class in their base CSS.
  5. For checkout fields (country, province), these are controlled by Shopify's checkout — you cannot directly edit them on standard Shopify plans. On Shopify Plus you can customize checkout.liquid: add matching `for`/`id` pairs to any custom selects you add.
  6. Save the file, then preview the theme and test with a screen reader or the axe browser extension to verify the label is announced.
Code · Shopify (liquid)
{%- for option in product.options_with_values -%}
  {%- assign select_id = 'option-' | append: section.id | append: '-' | append: forloop.index0 -%}
  <div class="product-option">
    <label for="{{ select_id }}">
      {{ option.name }}
    </label>
    <select
      id="{{ select_id }}"
      name="options[{{ option.name | escape }}]"
      class="product-option__select"
    >
      {%- for value in option.values -%}
        <option
          value="{{ value | escape }}"
          {%- if option.selected_value == value %} selected="selected"{% endif %}
        >
          {{ value }}
        </option>
      {%- endfor -%}
    </select>
  </div>
{%- endfor -%}

<!-- Sort-by dropdown (e.g. in sections/main-collection-product-grid.liquid) -->
<label for="SortBy" class="visually-hidden">Sort by</label>
<select name="sort_by" id="SortBy" class="collection-sort__select">
  {%- for option in collection.sort_options -%}
    <option value="{{ option.value }}" {%- if option.value == collection.sort_by %} selected{% endif %}>
      {{ option.name }}
    </option>
  {%- endfor -%}
</select>

Place in your product form Liquid snippet (e.g. snippets/product-variant-picker.liquid or sections/main-product.liquid). Adjust variable names to match your theme.

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

  1. In your Shopify admin, go to Online Store → Themes → click the three-dot menu beside your active theme → Edit code.
  2. Open `layout/theme.liquid` (the root layout file that wraps every page).
  3. Near the very top of the `<body>`, locate or add a skip link: `<a class="skip-to-content-link" href="#MainContent">Skip to main content</a>`.
  4. Search for your main content wrapper — in most Shopify themes it is a `<main>` tag. Add or confirm it has `id="MainContent"` and `tabindex="-1"`: `<main id="MainContent" class="main-content" tabindex="-1">`.
  5. Open `assets/base.css` (or your theme's primary CSS file) and add the visually-hidden-until-focused style for `.skip-to-content-link` (see code snippet below).
  6. Click Save, then preview the theme and press Tab once to confirm the skip link appears and, on Enter, moves focus to the `<main>` element. Most modern Shopify themes (Dawn, Sense, Craft) already include this pattern — verify the `id` values match.
Svg img altModerate effort

Add a meaningful text alternative to every SVG image so screen readers can describe it to visually impaired shoppers.

On Shopify

  1. Go to Online Store > Themes > (your active theme) > Actions > Edit code.
  2. SVGs appear in multiple places: theme layout files (layout/theme.liquid), section files (sections/.liquid), snippet files (snippets/.liquid), and asset files (assets/*.svg). Use the search icon (🔍) in the code editor to search for '<svg' across files.
  3. For each meaningful SVG, open the file, locate the `<svg>` opening tag, add `role="img"` and `aria-label="Descriptive text"` — e.g. `<svg role="img" aria-label="Store logo" …>`.
  4. If the SVG is loaded from an asset file (e.g. via `{{ 'icon-cart.svg' | asset_url }}`), download the SVG asset, add `<title id="svg-title">Descriptive text</title>` as the first child of `<svg>`, re-upload it to Assets, and add `role="img" aria-labelledby="svg-title"` to the `<svg>` tag.
  5. For decorative SVGs (dividers, background shapes), add `aria-hidden="true"` to the `<svg>` tag.
  6. Save each file and preview the storefront; use Chrome DevTools > Accessibility tab to verify the accessible name appears.
Code · Shopify (liquid)
{%- comment -%}
  Meaningful SVG icon — add role and aria-label to the <svg> tag.
  Replace the aria-label value and path data with your actual content.
{%- endcomment -%}
<svg
  role="img"
  aria-label="{{ 'icon.cart' | t }}"
  xmlns="http://www.w3.org/2000/svg"
  viewBox="0 0 24 24"
  focusable="false"
>
  <title>{{ 'icon.cart' | t }}</title>
  <path d="M6 2L3 6v14a2 2 0 002 2h14a2 2 0 002-2V6l-3-4z" fill="currentColor"/>
</svg>

{%- comment -%}
  Decorative SVG — hidden from screen readers.
{%- endcomment -%}
<svg
  aria-hidden="true"
  focusable="false"
  xmlns="http://www.w3.org/2000/svg"
  viewBox="0 0 200 4"
>
  <rect width="200" height="4" fill="#e0e0e0"/>
</svg>

Place in the relevant .liquid snippet or section file where the SVG is rendered (e.g. snippets/icon-cart.liquid).

TabindexModerate effort

Remove or replace all positive tabindex values (tabindex="1" or higher) on your page elements, using tabindex="0" for focusable elements and tabindex="-1" only when programmatic focus is needed.

On Shopify

  1. In your Shopify admin, go to Online Store → Themes → click the three-dot menu next to your active theme → Edit code.
  2. Use the search icon (🔍) at the top of the file list to search across all theme files for 'tabindex'.
  3. For each result, open the file (common locations: layout/theme.liquid, sections/.liquid, snippets/.liquid) and locate the tabindex attribute.
  4. Change any tabindex value greater than 0 to '0' (or remove the attribute if the element is a native interactive element like a button or a link).
  5. Click Save on each file you modify.
  6. If you use a third-party theme, check the theme's support documentation or contact the theme developer, as some themes have accessibility options in the Theme Settings (Online Store → Themes → Customize → Theme settings).
Code · Shopify (liquid)
{% comment %}
  ❌ WRONG — positive tabindex in a Liquid template:
  <button tabindex="2">Add to Cart</button>

  ✅ CORRECT — remove the tabindex attribute from native interactive elements:
{% endcomment %}
<button type="submit" name="add" class="btn btn--primary">
  Add to Cart
</button>

{% comment %}
  ✅ CORRECT — non-native focusable element (e.g., a custom dropdown trigger):
{% endcomment %}
<div
  tabindex="0"
  role="button"
  aria-haspopup="listbox"
  aria-label="Select a size"
  class="custom-select-trigger">
  Select Size
</div>

Place in the relevant .liquid file under layout/, sections/, or snippets/ in Online Store → Themes → Edit code.

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

  1. Go to Online Store → Themes → click 'Edit code' on your active theme.
  2. Open layout/theme.liquid (this file contains your <html> tag).
  3. Find the opening <html> tag — it usually looks like <html class="..."> — and add or correct the lang attribute: <html lang="{{ request.locale.iso_code }}">. Shopify's request.locale.iso_code outputs a valid BCP 47 code automatically for each active locale.
  4. Click Save.
  5. For inline foreign-language passages in product descriptions or page content, edit the HTML directly in the rich-text editor (click the </> source button) and add lang="fr" (or the correct code) to the wrapping <span> or <p> tag.
  6. If you use Shopify Markets with multiple languages, verify the above Liquid variable is present — it ensures every translated storefront gets the correct lang value without any further changes.
Code · Shopify (liquid)
<!doctype html>
<html lang="{{ request.locale.iso_code }}" dir="{{ request.locale.direction | default: 'ltr' }}">
  <head>
    <meta charset="utf-8">
    {{ content_for_header }}
  </head>
  <body>
    {{ content_for_layout }}
  </body>
</html>

In layout/theme.liquid — dynamically outputs the correct BCP 47 lang code for every active locale.

Security (OWASP) · 26 fixes

Dmarc policy noneModerate effort

Strengthen your DMARC policy from p=none (monitor-only) to p=quarantine, then p=reject, so that emails fraudulently sent from your domain are blocked rather than silently allowed through.

On Shopify

  1. DMARC is a DNS record — it lives with your domain registrar or DNS host (e.g. GoDaddy, Namecheap, Cloudflare), not inside Shopify's admin.
  2. First, verify all Shopify-originated mail passes DMARC: in Shopify Admin → Settings → Notifications, ensure you have set up a custom sender domain and completed the SPF/DKIM verification Shopify requests.
  3. Then go to your DNS provider, find the TXT record for _dmarc.yourdomain.com, and edit the p= value from none to quarantine (then later reject).
  4. If you use third-party email apps (Klaviyo, Mailchimp, Omnisend, Gorgias, etc.), complete their DKIM/SPF setup steps inside each app before tightening the policy.
Code · Generic HTML (dns)
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

  1. Shopify automatically enforces HTTPS and sets HSTS for all stores on the shopify.com infrastructure — you cannot and do not need to set it manually in the Shopify admin.
  2. If you use a custom domain, go to Admin → Settings → Domains and ensure your domain shows a green 'Connected and redirecting to HTTPS' status; Shopify then manages the HSTS header for you.
  3. If your store sits behind a custom reverse proxy or a third-party CDN (e.g., Cloudflare in front of Shopify), set the HSTS header in that proxy layer — see the Cloudflare steps below.
  4. Verify the live header at your domain using browser DevTools (F12 → Network → your domain request → Response Headers) and confirm Strict-Transport-Security is present with max-age=31536000.
Code · Generic HTML (html)
Strict-Transport-Security: max-age=31536000; includeSubDomains

This 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

  1. Shopify automatically handles HTTPS and HSTS for all storefronts — you cannot directly edit server response headers from the Shopify admin.
  2. Shopify's CDN (Fastly) sets HSTS on your behalf. If a scanner reports a short max-age, it may be reading a header injected by a third-party app or a custom Shopify Storefront/Hydrogen deployment.
  3. Check your installed apps (Admin → Apps) for any 'Security Headers', 'HTTP Headers', or 'CSP Manager' apps that may be overriding the HSTS header with a short max-age, and update or remove them.
  4. If you use Shopify Plus with a custom domain proxy or a headless front-end (Hydrogen/Oxygen), set the header at the Oxygen edge or your own CDN layer — see the Headless / Custom entry below.
  5. For Shopify-managed stores with no custom proxy, no action is required; Shopify's platform-level header is already compliant. Re-run the scan to confirm.
Code · Shopify Plus (Hydrogen / Oxygen) (typescript)
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

  1. Shopify automatically provisions and renews a free SSL certificate for every store — no manual installation is required.
  2. In your Shopify Admin, go to Online Store → Domains.
  3. Find your custom domain and confirm the SSL certificate status shows 'SSL certificate active' (green checkmark). If it shows 'Unavailable', click the domain and follow the on-screen prompts; Shopify will attempt to re-provision the certificate.
  4. Scroll down on the same Domains page and enable the toggle 'Redirect all traffic to this domain' — this enforces HTTPS and the canonical domain simultaneously.
  5. HSTS is managed by Shopify's infrastructure; no additional action is needed.
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

  1. Shopify's infrastructure is fully managed — the `Server` header is controlled by Shopify, not by theme code or admin settings.
  2. As a merchant you cannot modify server-level headers directly; Shopify already suppresses detailed version information.
  3. If a scanner still flags a `Server` header value on your Shopify storefront, open a support ticket with Shopify Plus support (for Plus merchants) referencing the specific header value, or route all traffic through Cloudflare (see Cloudflare steps below) to strip the header at the CDN edge before it reaches end users.
Code · Generic HTML (html)
<!-- 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

  1. Shopify's infrastructure manages all server-level response headers; store owners cannot directly modify X-Powered-By headers.
  2. Shopify does not expose an X-Powered-By header by default on its standard storefronts — if a scanner flagged one, it likely originated from a third-party app proxy or a custom Hydrogen/headless deployment.
  3. For Hydrogen (headless) deployments on Oxygen: open your Hydrogen app's server entry file (e.g. server.ts), find the Response object, and explicitly delete the header: response.headers.delete('X-Powered-By');
  4. For third-party app embeds adding the header, contact that app's support to request they suppress it on their proxy responses.
Code · Shopify / Hydrogen (Oxygen) (typescript)
// server.ts (Hydrogen / Oxygen)
import {createRequestHandler} from '@remix-run/server-runtime';

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const response = await createRequestHandler(/* ... */)(request);

    // Remove X-Powered-By header before sending response
    response.headers.delete('X-Powered-By');

    return response;
  },
};

In your Hydrogen app's server.ts (Remix-based), remove the header before returning any response.

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

  1. Shopify manages its own session cookies (e.g. _shopify_y, _shopify_s) internally and applies Secure and SameSite=Lax by default — you cannot modify these directly.
  2. For the XSRF-TOKEN cookie specifically: Shopify sets this cookie for its own CSRF protection. If a scanner flags it as missing HttpOnly, note that XSRF tokens are intentionally readable by JavaScript (that is how CSRF protection works). The real concern is the Secure and SameSite flags. Open your theme's Shopify admin: Online Store → Themes → Edit code.
  3. If your theme or a custom app sets any cookies via JavaScript (e.g. in a .js asset file or a <script> tag in a .liquid file), locate that code and switch from document.cookie = 'name=value' to a server-side Set-Cookie header (via a Shopify App Proxy or a custom app's backend) so you can add the flags properly.
  4. For any custom Shopify app backend (Node/Ruby/Python), set cookies using your framework's cookie helper with { httpOnly: true, secure: true, sameSite: 'Strict' } (see code snippets).
  5. Verify in Chrome DevTools → Application → Cookies → your store domain that all auth/session cookies show the correct flags.
Code · Generic HTML (http)
Set-Cookie: session_id=abc123; HttpOnly; Secure; SameSite=Strict

Correct 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

  1. Shopify does not allow arbitrary HTTP response headers to be set from the admin UI for storefront pages.
  2. Install a Shopify app such as 'EasyCSP' or 'CSP Manager' from the Shopify App Store, which injects the CSP header via a Shopify Proxy or middleware layer.
  3. Alternatively, if you use Shopify with a custom Oxygen / Hydrogen (headless) front-end, set the CSP header inside your Remix/Next.js server response headers in the loader or server entry file.
  4. For theme-level partial mitigation (not a true HTTP header), you can add a <meta http-equiv='Content-Security-Policy'> tag inside the <head> of your theme.liquid file (Online Store → Themes → Edit Code → layout/theme.liquid), but note that meta-tag CSPs do not cover all directives (e.g., frame-ancestors is ignored) and are weaker than HTTP headers.
  5. The most robust Shopify approach is to route traffic through Cloudflare (free plan available) and set the CSP as a Cloudflare Transform Rule (see Cloudflare entry below).
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

  1. DMARC is a DNS record — it is not set inside Shopify's admin. You must go to wherever your domain's DNS is managed.
  2. If you bought your domain through Shopify: go to Shopify Admin → Settings → Domains → click your domain → click 'Manage DNS settings'. Add a TXT record: Host = _dmarc, Value = v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com.
  3. If your domain DNS is managed elsewhere (Cloudflare, GoDaddy, Namecheap, etc.), log in to that provider's DNS dashboard and add the TXT record there.
  4. Shopify's built-in SPF and DKIM records for Shopify Email are added automatically when you verify your sender domain under Settings → Email → Sender email address — complete that step first so DMARC has valid SPF/DKIM to align against.
Code · Shopify (dns)
v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com; ruf=mailto:dmarc-forensic@yourdomain.com; sp=none; adkim=r; aspf=r

In Shopify Admin → Settings → Domains → Manage DNS settings. Add a TXT record with Host=_dmarc and the value below.

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

  1. Shopify's shared infrastructure does not let you set arbitrary HTTP response headers directly from the admin panel — you must use a Cloudflare proxy or a Shopify app.
  2. Option A (Cloudflare — recommended): Point your store's domain to Cloudflare (free plan works). In the Cloudflare dashboard go to your domain → Rules → Transform Rules → Modify Response Header → Create a rule. Set 'Header name' to 'Permissions-Policy' and 'Value' to 'camera=(), microphone=(), geolocation=(), payment=(), usb=(), interest-cohort=()'. Set the rule to apply to all requests (hostname matches your domain). Save and deploy.
  3. Option B (Shopify app): Search the Shopify App Store for 'security headers' apps (e.g. 'HTTP Headers' or 'Security & Compliance' apps). Install, then add the Permissions-Policy header and value through the app's UI.
  4. Verify by opening your storefront in a browser, DevTools → Network → select the HTML document → Response Headers — confirm 'permissions-policy' appears.
Code · Generic HTML (meta tag — limited support) (html)
<!-- 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

  1. Shopify does not expose direct server configuration, so the most reliable method is injecting a <meta> tag into every page via your theme.
  2. In your Shopify Admin, go to Online Store → Themes → click the '...' menu next to your live theme → Edit code.
  3. Open Layout → theme.liquid (the master layout file).
  4. Inside the <head> tag, add: <meta name="referrer" content="strict-origin-when-cross-origin">
  5. Click Save.
  6. For a header-level solution (stronger), use a Cloudflare Worker or a CDN/proxy in front of your Shopify store to inject the HTTP response header automatically.
Code · Shopify (liquid)
<!doctype html>
<html lang="{{ request.locale.iso_code }}">
<head>
  <meta name="referrer" content="strict-origin-when-cross-origin">
  <!-- rest of your existing <head> content -->
  {{ content_for_header }}
</head>
<body>
  {{ content_for_layout }}
</body>
</html>

Add inside the <head> tag in Layout → theme.liquid via Online Store → Themes → Edit code.

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

  1. Shopify does not allow arbitrary files to be placed at /.well-known/ via the file manager for live storefronts.
  2. The recommended workaround is to use a Shopify app that serves a custom route, OR to handle it at the CDN/proxy layer (e.g. Cloudflare Worker or an edge redirect) that sits in front of your store.
  3. Alternatively, if you use a custom domain proxied through Cloudflare: in your Cloudflare dashboard go to Workers & Pages → Create Worker, paste the worker script (see code snippets) that returns your security.txt content for requests to /.well-known/security.txt, then add a Worker Route for yourdomain.com/.well-known/security.txt.
  4. Verify by visiting https://yourdomain.com/.well-known/security.txt in a browser.
Code · Shopify (javascript)
addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request))
})

async function handleRequest(request) {
  const url = new URL(request.url)
  if (url.pathname === '/.well-known/security.txt') {
    const body = [
      'Contact: mailto:security@yourdomain.com',
      'Expires: 2026-12-31T23:59:00.000Z',
      'Preferred-Languages: en',
      'Policy: https://yourdomain.com/security-policy'
    ].join('\n') + '\n'
    return new Response(body, {
      status: 200,
      headers: {
        'Content-Type': 'text/plain; charset=utf-8',
        'Cache-Control': 'public, max-age=86400'
      }
    })
  }
  return fetch(request)
}

Cloudflare Worker script — paste into Workers & Pages → Create Worker. Add a Worker Route for yourdomain.com/.well-known/security.txt.

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

  1. Shopify itself does not manage your domain's DNS records — DNS is controlled by wherever you registered or pointed your domain.
  2. If your domain was purchased through Shopify: go to Shopify Admin → Settings → Domains → click your domain → click 'Manage DNS' (or 'DNS Settings').
  3. Click 'Add record', choose Type = TXT, set Name/Host to '@' (the domain apex), and paste your SPF value (e.g. v=spf1 include:_spf.google.com include:sendgrid.net -all) into the Value field. Save.
  4. If your domain is registered elsewhere (GoDaddy, Namecheap, Cloudflare, etc.), log in to that registrar's DNS panel and add the TXT record there instead.
  5. Shopify's transactional emails are sent via Shopify's own infrastructure. If you use Shopify Email or Shopify's default notifications, add 'include:shops.shopify.com' to your SPF record. Confirm the exact include string in Shopify's Help Center under 'Set up email authentication'.
  6. Verify with MXToolbox SPF Lookup.
Code · Generic HTML (dns)
v=spf1 include:_spf.google.com include:sendgrid.net include:spf.mailgun.org -all

Add 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

  1. Shopify's cloud infrastructure automatically enforces HTTPS for all storefronts on myshopify.com and custom domains added through the Shopify admin.
  2. Go to Admin → Settings → Domains. Ensure your custom domain shows a green padlock / 'SSL certificate available' status.
  3. Shopify injects the Strict-Transport-Security header server-side for all storefronts. If it is missing, the most common cause is a third-party proxy (Cloudflare, Fastly, etc.) stripping the header — fix it at the proxy level (see CDN note below).
  4. If you use Shopify Plus with custom server-side middleware (Oxygen / Hydrogen), add the header in your server response handler — see the Headless / Custom code snippet below.
Code · Shopify Plus (Hydrogen / Oxygen) (javascript)
// 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

  1. Shopify's core storefront servers set most security headers automatically; however, `X-Content-Type-Options` may be absent on certain responses or custom domains.
  2. The most reliable approach for Shopify is to route traffic through Cloudflare (free plan works) and add the header at the edge — see the Cloudflare instructions below.
  3. If you use a Shopify app that injects a custom server-side proxy (e.g. a custom app built with Node/Express), add `res.setHeader('X-Content-Type-Options', 'nosniff')` in your middleware.
  4. Verify by visiting your storefront, opening DevTools → Network → select the document → Response Headers, and checking for `x-content-type-options: nosniff`.
Code · Generic HTML (http)
X-Content-Type-Options: nosniff

This 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

  1. Shopify's core storefront automatically sends X-Frame-Options: DENY on all theme pages — if the scanner flagged this on a standard Shopify storefront, it may relate to a custom app proxy, external landing page, or headless front end, not the Shopify-hosted store itself.
  2. For custom Shopify apps or app proxies: in your app's server code (Node/Ruby/PHP), add the header in your HTTP response middleware. Example for Express.js: app.use((req, res, next) => { res.setHeader('X-Frame-Options', 'SAMEORIGIN'); next(); });
  3. If you use a Shopify Hydrogen (headless) storefront, add the header in your Remix/Next.js server config or via your hosting provider (Vercel, Oxygen) — see the Headless / Custom entry below.
  4. For pages hosted outside Shopify (e.g., a custom checkout page), apply the header at the web server or CDN level as described in the Headless / Custom steps.
Code · Generic HTML (http)
X-Frame-Options: SAMEORIGIN

This 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.

Passive scan onlyModerate effort

Set up regular active security testing (DAST) on a staging copy of your store to catch vulnerabilities that passive header/cookie/TLS scans cannot find.

On Shopify

  1. Shopify hosts the production environment; you cannot self-host a true staging server, but you can use a separate Shopify development store (free via Partner account) as your staging target.
  2. In your Shopify Partner Dashboard → Stores → 'Add store' → 'Development store' — populate it with your current theme and a realistic product/order dataset.
  3. Password-protect the development store: Online Store → Preferences → Password protection → Enable password.
  4. Point OWASP ZAP at the development store URL. In ZAP: Quick Start → Automated Scan → enter the development store URL, add your test-account credentials under 'Context → Authentication'.
  5. Run the active scan, review findings, then apply fixes in the development store theme or via app settings before pushing theme changes to production with 'Publish'.
  6. Repeat before every theme update, new app install, or custom Liquid/JavaScript change.
Code · Generic HTML (html)
<meta name="robots" content="noindex, nofollow">

Add this meta tag to your staging site's <head> to prevent search engines from indexing it while DAST scanning is in progress.

Ssl cert expiring soonQuick win

Renew your SSL/TLS certificate before it expires to keep your store secure, trusted, and visible in search results.

On Shopify

  1. Shopify provisions and auto-renews SSL certificates for all stores on shopify.com subdomains and connected custom domains — you do not manage this manually.
  2. To verify: go to Shopify Admin → Settings → Domains. Each domain shows a green 'SSL certificate active' badge when the certificate is valid.
  3. If a domain shows 'SSL unavailable' or 'SSL pending', click the domain name, then click 'Enable SSL' or 'Renew SSL'. Shopify will re-provision it automatically within 48 hours.
  4. Ensure your custom domain's DNS A-record or CNAME points correctly to Shopify's servers (as shown in Settings → Domains), because incorrect DNS prevents auto-provisioning.
  5. If the issue persists after 48 hours, contact Shopify Support from Admin → Help.
Code · Generic HTML (html)
<!-- 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

  1. Shopify provisions and manages SSL certificates automatically for all stores on the myshopify.com subdomain and for connected custom domains.
  2. Go to Admin → Settings → Domains. Verify your custom domain is listed and shows a green padlock / 'SSL available' status.
  3. If it shows 'SSL unavailable' or a pending state, remove the domain and re-add it: click the domain name → Remove domain, then re-add it using 'Connect existing domain'. Shopify will re-provision the certificate within minutes to hours.
  4. Ensure your domain's DNS A record points to Shopify's IP (23.227.38.65) and the CNAME for www points to shops.myshopify.com — a misconfigured DNS record is the most common cause of Shopify SSL failures.
  5. If the certificate is still not provisioning after 48 hours, contact Shopify Support. Shopify does not allow you to upload a custom certificate on standard plans.
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

  1. Shopify provisions and renews SSL certificates automatically for all stores on the myshopify.com subdomain and for custom domains added correctly through Shopify.
  2. Go to: Shopify Admin → Settings → Domains.
  3. If your custom domain shows a warning or 'SSL unavailable', click the domain name and then click 'Re-verify connection' or remove and re-add the domain.
  4. Ensure your DNS A record points to Shopify's IP (23.227.38.65) and your CNAME for www points to shops.myshopify.com — a misconfigured DNS record is the most common cause of certificate mismatch on Shopify.
  5. Allow up to 48 hours after correcting DNS for Shopify to automatically re-provision a valid certificate. If it persists beyond 48 hours, contact Shopify Support.
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

  1. Shopify manages SSL certificates and port 443 automatically for all stores on the shopify.com infrastructure — you do not control firewall rules directly.
  2. Go to Admin → Online Store → Domains.
  3. If you have a custom domain, ensure it is correctly pointed to Shopify's servers (CNAME to shops.myshopify.com or Shopify's IP). Shopify will auto-provision and renew a Let's Encrypt certificate within 48 hours of correct DNS propagation.
  4. Enable the 'Redirect all traffic to HTTPS' toggle found at Admin → Online Store → Domains (scroll to the SSL certificate section).
  5. If port 443 remains unreachable after DNS propagates, contact Shopify Support — the issue is on their infrastructure, not yours.
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

  1. In your Shopify admin go to Online Store → Themes → click Actions → Edit code on your active theme.
  2. Search all .liquid files and assets/ folder for 'axios' to find where it is loaded (script tag or bundled JS file).
  3. If loaded via a CDN <script> tag, change the src URL to the latest version, e.g. https://cdn.jsdelivr.net/npm/axios@latest/dist/axios.min.js, then save the file.
  4. If Axios is bundled inside a custom JS file (e.g. theme.js or a compiled bundle uploaded to assets/), you must rebuild the bundle locally with the updated Axios version using npm/yarn, then re-upload the compiled file to the assets/ folder.
  5. If the vulnerable version is injected by a Shopify App, go to Apps → identify the offending app → check whether an update is available in the Shopify App Store, or contact the app developer to request a patch.
  6. After saving, use your browser's DevTools Network tab to confirm the new Axios version is loading on your storefront.
Code · Shopify (liquid)
{{ 'https://cdn.jsdelivr.net/npm/axios@1.7.9/dist/axios.min.js' | script_tag }}

Add this to your theme.liquid <head> section (or replace an existing Axios script tag). Use the specific version number rather than @latest in production.

Vulnerable component jqueryModerate effort

Update jQuery to the latest stable version (3.7+) to eliminate known cross-site scripting (XSS) and prototype-pollution vulnerabilities that attackers actively exploit.

On Shopify

  1. In your Shopify Admin, go to Online Store → Themes → click '...' next to your active theme → Edit code.
  2. Open 'Layout' → 'theme.liquid' (or your main layout file) and search for any <script> tags loading jQuery.
  3. If jQuery is loaded via a CDN URL like '//code.jquery.com/jquery-2.x.x.min.js', update the URL to 'https://code.jquery.com/jquery-3.7.1.min.js' (the current stable release).
  4. If jQuery is a local file in Assets (e.g. 'assets/jquery.min.js'), download the latest jQuery from jquery.com, rename it 'jquery.min.js', and upload it under Assets → Add a new asset → upload, overwriting the old file.
  5. Also open every file under Sections/ and Snippets/ and search for additional jQuery script tags that may load an old version.
  6. Check all installed Shopify Apps — outdated jQuery bundled inside an app cannot be fixed from your theme; contact the app developer and request an update or remove the app.
  7. Click Save and preview your storefront; open DevTools → Network, filter by 'jquery', and confirm only the new version loads.
Code · Shopify (liquid)
{%- comment -%}
  Replace your old jQuery script tag with this in Layout/theme.liquid
  inside the <head> section.
{%- endcomment -%}
<script
  src="https://code.jquery.com/jquery-3.7.1.min.js"
  integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo="
  crossorigin="anonymous"></script>

In theme.liquid (Layout folder), replace your existing jQuery <script> tag with this snippet inside <head>.

Weak spfQuick win

Add a hard-fail (-all) or soft-fail (~all) terminator to your SPF DNS record so that mail servers reject or flag email sent from servers not explicitly authorized by your domain.

On Shopify

  1. Shopify does not host your domain's DNS — it only registers the domain via Shopify Domains or points to an external registrar.
  2. If your domain is managed through Shopify Domains: go to Shopify Admin → Settings → Domains → click your domain → 'DNS Settings'. Find the TXT record starting with `v=spf1` and click Edit.
  3. Append `-all` (or `~all`) to the end of the record value if it is missing, e.g. change `v=spf1 redirect=cf3962es._spf._d.mim.ec` to `v=spf1 redirect=cf3962es._spf._d.mim.ec -all` (only if the redirect target itself doesn't end with `-all`; otherwise fix the target record instead).
  4. If your DNS is managed externally (GoDaddy, Cloudflare, Namecheap, etc.), log in to that provider's DNS dashboard and edit the TXT record there — Shopify's admin cannot manage it.
Code · Shopify (dns)
v=spf1 include:shops.shopify.com include:send.klaviyo.com -all

Typical SPF record for a Shopify store using Shopify Email and an external transactional email provider (e.g., Klaviyo). Paste as TXT record value for @:

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

  1. Shopify's core storefront automatically sends `X-Content-Type-Options: nosniff` — if your scanner shows a duplicate or malformed value, the extra copy is almost certainly coming from a third-party app or a Cloudflare/CDN layer you've added.
  2. Check installed apps: Shopify Admin → Apps — look for any 'security headers' or 'HTTP headers' app and review its settings. Disable or remove any app that is independently setting this header.
  3. If you use Cloudflare in front of Shopify: Cloudflare Dashboard → your domain → Rules → Transform Rules → Modify Response Headers — delete any rule that sets `X-Content-Type-Options`, since Shopify already handles it.
  4. Verify the fix: open Chrome DevTools on your storefront, go to Network → select any page request → Headers, and confirm `X-Content-Type-Options: nosniff` appears exactly once.
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

  1. Shopify's core storefront automatically sends X-Frame-Options: DENY — you cannot override server-level response headers directly from the Shopify admin for the storefront.
  2. If your finding comes from an embedded app, custom storefront (Hydrogen/Headless), or a third-party integration (e.g. Contentstack), the header must be set in that app's server configuration or middleware, not in Shopify admin.
  3. For Hydrogen (custom storefronts): open your server.ts (or entry.server.tsx) file and add the header in the handleRequest function using: response.headers.set('X-Frame-Options', 'DENY') and response.headers.set('Content-Security-Policy', "frame-ancestors 'none';").
  4. For third-party embedded apps served from external servers: set the header in that server's configuration (Node.js, Vercel, Netlify, etc.) — see the Headless/Custom entry below.
  5. Verify by loading your storefront URL in a browser, opening DevTools → Network → select the document request → Response Headers.
Code · Shopify (javascript)
// In server.ts (Hydrogen / Remix-based Shopify storefront)
export default {
  async fetch(request, env, executionContext) {
    const response = await handleRequest(request, env, executionContext);

    // Enforce framing policy
    response.headers.set('X-Frame-Options', 'DENY');
    response.headers.set('Content-Security-Policy', "frame-ancestors 'none';");

    return response;
  },
};

For Hydrogen (Shopify headless) storefronts: add to your server.ts request handler.

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

  1. Shopify's core platform is SaaS (software-as-a-service) and is updated automatically by Shopify — you do not manage core version updates yourself.
  2. What you DO control: your theme version. In the Shopify admin go to Online Store > Themes. If your theme is a free Shopify theme, an 'Update available' badge appears; click 'Update theme' (Shopify creates a backup automatically).
  3. For paid/third-party themes, check the theme developer's changelog and download the latest version from your original purchase source, then upload it via Online Store > Themes > Add theme > Upload zip.
  4. Audit installed apps: go to Apps and remove any apps you no longer use, as abandoned apps can introduce vulnerabilities even on Shopify.
  5. Review Shopify's changelog at shopify.dev/changelog to understand what platform changes may affect your storefront and stay informed.
Code · Generic HTML (html)
<!-- 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

  1. If your domain was purchased through Shopify: go to Shopify Admin → Settings → Domains. Click the domain, then click 'Manage' next to it. You will see the expiry date and a toggle for auto-renew — turn it ON.
  2. If your domain was purchased elsewhere and only connected to Shopify: log in to your external registrar directly (e.g. GoDaddy, Namecheap) and manage auto-renew there. Shopify only manages domains it sold.
  3. To confirm which registrar owns the domain, go to Settings → Domains and look at the domain row — Shopify-managed domains show a 'Purchased' badge; third-party domains show 'Connected'.
Code · Generic HTML (html)
<!-- 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

  1. Go to Online Store › Themes › and click 'Edit code' on your active theme.
  2. Open theme.liquid. Paste the GTM <script> snippet just before the closing </head> tag, and the <noscript> snippet just after the opening <body> tag.
  3. For GA4 ecommerce events, install the free 'GA4 & GTM Kit' app (by House of Analytics) or 'Elevar' from the Shopify App Store — both push a complete ecommerce data layer so GTM can pick up view_item, add_to_cart, and purchase events automatically.
  4. In GTM, create GA4 Event tags that read from the data layer variables the app creates (e.g. ecommerce.items, event = 'add_to_cart') and publish the container.
  5. Verify by opening your storefront in GTM Preview mode and walking through a product page and checkout.
Code · Shopify (liquid)
{% comment %} Google Tag Manager — paste before </head> {% endcomment %}
<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>

{% comment %} Google Tag Manager noscript — paste after <body> opening tag {% endcomment %}
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXXXXX"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>

Paste in theme.liquid — first block before </head>, second block after opening <body> tag.

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

  1. In your Shopify admin, go to Online Store → Themes.
  2. Next to your active theme, click the '...' (Actions) menu → Edit code.
  3. In the Layout folder on the left, open theme.liquid.
  4. Find the opening <html tag (usually line 1 or 2) and add lang="en" (or your language code), e.g. <html lang="en" class="..." ...>.
  5. Click Save. Shopify's Liquid variable {{ shop.locale }} can also be used for multilingual stores: <html lang="{{ request.locale.iso_code }}">.
Code · Shopify (liquid)
<!DOCTYPE html>
<html lang="{{ request.locale.iso_code }}">
<head>
  <meta charset="utf-8">
  <title>{{ page_title }}</title>
  {{ content_for_header }}
</head>
<body>
  {{ content_for_layout }}
</body>
</html>

In Layout/theme.liquid — uses Shopify's request.locale.iso_code for automatic multilingual support.

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

  1. Shopify's default themes (Dawn and most modern themes) generate Product schema dynamically from the `product` Liquid object, so availability should update automatically when inventory hits zero — but only if the theme's schema template is using `product.available` correctly.
  2. In your Shopify admin, go to Online Store → Themes → (your active theme) → Actions → Edit Code.
  3. Open `sections/main-product.liquid` (or `templates/product.json` / `templates/product.liquid` depending on your theme). Search for `schema.org/InStock` or `availability`.
  4. The availability line should read: `"availability" : "{{ product.available | json }}"` — but the correct pattern is a conditional: if `product.available` output `https://schema.org/InStock`, else `https://schema.org/OutOfStock`.
  5. Replace any hardcoded `https://schema.org/InStock` string with the Liquid conditional shown in the code snippet below.
  6. Save the file, then verify with Google's Rich Results Test on a sold-out product URL.
Code · Shopify (liquid)
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": {{ product.title | json }},
  "image": {{ product.featured_image | img_url: 'master' | prepend: 'https:' | json }},
  "description": {{ product.description | strip_html | truncate: 200 | json }},
  "sku": {{ product.selected_or_first_available_variant.sku | json }},
  "brand": {
    "@type": "Brand",
    "name": {{ product.vendor | json }}
  },
  "offers": {
    "@type": "Offer",
    "url": {{ shop.url | append: product.url | json }},
    "priceCurrency": {{ cart.currency.iso_code | json }},
    "price": {{ product.selected_or_first_available_variant.price | divided_by: 100.0 | json }},
    "availability": {% if product.available %}"https://schema.org/InStock"{% else %}"https://schema.org/OutOfStock"{% endif %},
    "itemCondition": "https://schema.org/NewCondition"
  }
}
</script>

Paste this block into sections/main-product.liquid (or your theme's product schema section) inside the <script type='application/ld+json'> block, replacing any hardcoded InStock string.

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

  1. Go to Shopify Admin → Products → Collections.
  2. Open the relevant collection (or click 'Create collection' to make a new one for products that share a theme).
  3. In the 'Products' section of the collection editor, search for the orphaned product by name and click 'Add' — repeat for each orphaned product and each applicable collection.
  4. For automated collections, update the collection conditions (e.g., add a product tag like 'featured' to the orphaned product via Products → [product name] → Tags, then set the collection to include products with that tag).
  5. To surface orphaned products in 'Related products' or 'You may also like' blocks, install a free app such as 'Also Bought' or 'LimeSpot Personalizer,' or edit your theme's product.liquid template to include a recommendations section using Shopify's product recommendations API.
  6. Navigate to Online Store → Navigation to add the collection to your main menu if the entire category was missing from the nav.
Code · Shopify (liquid)
{% comment %}
  Featured products section — tag orphaned products with 'featured'
  then surface them here so they receive at least one internal link.
{% endcomment %}
<section class="featured-products">
  <h2>Featured Products</h2>
  <ul>
    {% for product in collections['featured'].products %}
      <li>
        <a href="{{ product.url }}">
          <img src="{{ product.featured_image | img_url: '300x300' }}" alt="{{ product.title | escape }}">
          <span>{{ product.title }}</span>
        </a>
      </li>
    {% endfor %}
  </ul>
</section>

Add to your theme's collection.liquid or index.liquid to render a 'Featured Products' section that includes orphaned products by tag. Place in the appropriate section file under Online Store → Themes → Edit Code.

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

  1. Shopify auto-generates /sitemap.xml — visit yourdomain.com/sitemap.xml to confirm it lists your products under the <loc> entries in the products sitemap section.
  2. If products are missing, go to Admin → Products and check each missing product's Status (top-right of the product edit page). Only 'Active' products appear in the sitemap; change any 'Draft' products to 'Active'.
  3. Check that no product has a 'noindex' tag added by a third-party SEO app: go to Admin → Apps → your SEO app (e.g., SEO Manager, TrueProfit SEO, Plug In SEO) and look for any URL exclusion or 'hide from search engines' settings on those products.
  4. If you use a custom storefront or headless setup, you must generate and host a sitemap yourself; use the Shopify Storefront API to query all product handles and build the XML.
  5. Submit the sitemap in Google Search Console: go to GSC → your property → Indexing → Sitemaps → Add a new sitemap → enter 'sitemap.xml' → Submit.
Code · Generic HTML (xml)
<?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

  1. Go to Online Store → Navigation in your Shopify admin.
  2. Open your Main Menu. Each nested 'sub-item' under a menu item adds one click; items more than two levels deep push products past the 3-click threshold.
  3. Drag deeply nested menu items up to the top level, or restructure so products are reachable: Main Menu item → Collection page → Product (3 clicks total).
  4. In Online Store → Themes → Customize, add a 'Featured Collection' or 'Product List' section directly on the homepage and populate it with the previously buried products — this makes those products 1 click from the homepage.
  5. Alternatively, go to Products → Collections, open or create a top-level collection (e.g. 'Best Sellers'), and add the buried products to it; then link that collection in your Main Menu.
  6. Use an app such as Sitemap & SEO by Plug in SEO or Screaming Frog to re-crawl and confirm click depth is reduced.
Code · Shopify (liquid)
{% comment %} Add to a homepage section to surface previously buried products {% endcomment %}
{% assign featured = collections['best-sellers'] %}
{% if featured and featured.products.size > 0 %}
  <section class="featured-products">
    <h2>{{ featured.title }}</h2>
    <ul class="product-grid">
      {% for product in featured.products limit: 8 %}
        <li class="product-grid__item">
          <a href="{{ product.url }}">
            <img
              src="{{ product.featured_image | img_url: '400x400' }}"
              alt="{{ product.featured_image.alt | escape }}"
              width="400"
              height="400"
              loading="lazy"
            />
            <span>{{ product.title }}</span>
            <span>{{ product.price | money }}</span>
          </a>
        </li>
      {% endfor %}
    </ul>
  </section>
{% endif %}

Add this snippet to your homepage template (sections/home-page.liquid or index.json section) to surface a curated collection's products directly on the homepage, creating a 1-click path.

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

  1. Shopify forces HTTPS for all hosted assets automatically, so mixed content almost always comes from hardcoded HTTP URLs in theme code or page content.
  2. Go to Online Store → Themes → your active theme → Actions → Edit code.
  3. Use the search box (magnifying-glass icon) to search for 'http://' across all theme files. Change any found instances to 'https://' or '//'.
  4. Also check Online Store → Pages, Blog posts, and any Custom content sections — edit each in the rich-text editor, switch to HTML view, and replace 'http://' URLs.
  5. For third-party app embeds, revisit the app's settings page and copy the latest embed snippet, which should already use HTTPS.
  6. Re-test with DevTools Console on your live storefront.
Code · Shopify (liquid)
{% comment %} WRONG — never hardcode http:// in Liquid templates {% endcomment %}
<img src="http://cdn.example.com/image.jpg" alt="Product image">

{% comment %} CORRECT — use Shopify's built-in Liquid filters for hosted assets {% endcomment %}
<img src="{{ product.featured_image | img_url: '800x' }}" alt="{{ product.title | escape }}">

{% comment %} CORRECT — for external resources, always use https:// {% endcomment %}
<script src="https://cdn.example.com/widget.js"></script>

In any Liquid template, always use the | img_url or | asset_url filters rather than hardcoding URLs. When referencing external CDN resources, always prefix with https://.

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

  1. In your Shopify admin, go to Online Store → Themes.
  2. Next to your active theme, click Actions → Edit code.
  3. In the file tree on the left, open Layout → theme.liquid (this is the master template wrapping every page).
  4. Search for `<meta name="viewport"`. If it exists, verify it reads exactly `<meta name="viewport" content="width=device-width, initial-scale=1">` and remove any `user-scalable=no` or `maximum-scale` restrictions.
  5. If the tag is missing, paste `<meta name="viewport" content="width=device-width, initial-scale=1">` immediately after the opening `<head>` tag.
  6. Click Save. All pages inherit this template, so the fix applies site-wide instantly.
Code · Shopify (liquid)
{% comment %} In Layout/theme.liquid, immediately after <head> {% endcomment %}
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  {{ content_for_header }}
  {{ 'theme.css' | asset_url | stylesheet_tag }}
</head>

Edit Online Store → Themes → Actions → Edit code → Layout/theme.liquid.

Server versionQuick win

Remove or suppress HTTP response headers that advertise your server software name and version to the public internet.

On Shopify

  1. Shopify's infrastructure manages the 'Server' header automatically and does not expose a configurable value — you cannot change it from the admin.
  2. If you have a custom storefront or middleware (e.g. a Node/Express proxy), add 'app.disable("x-powered-by")' and 'app.use((req, res, next) => { res.removeHeader("X-Powered-By"); next(); })' in your server entry file.
  3. For Shopify Hydrogen (Remix/Next.js) storefronts deployed to Oxygen, add custom headers in your 'server.ts' entry: use Remix's 'headers()' export or a middleware to delete the header before responding.
  4. Verify in DevTools (Network tab → Response Headers) that no version string appears.
Code · Generic HTML (html)
<!-- 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

  1. Shopify provisions and auto-renews TLS certificates for all custom domains automatically — no action is required from you.
  2. To verify: go to Admin → Settings → Domains, click your custom domain, and confirm 'SSL certificate' shows a green checkmark and an active status.
  3. If the status shows 'SSL unavailable', click 'Enable SSL' or remove and re-add the domain to trigger re-provisioning.
  4. Ensure your DNS A/CNAME records still point to Shopify's servers, as a broken DNS record prevents certificate issuance.
Code · Generic HTML (html)
<!-- 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.

Generative Engine Optimization · 9 fixes

Ai crawler blockedQuick win

Your robots.txt explicitly blocks one or more AI assistants from crawling your store, so those engines can't discover, index, or cite your products when shoppers ask for recommendations.

On Shopify

  1. From Shopify admin go to Online Store > Themes > (the three-dot menu on your live theme) > Edit code.
  2. Under the 'templates' folder click 'Add a new template' and choose 'robots.txt' (this creates robots.txt.liquid).
  3. Shopify's default template loops over rules; to allow an AI bot remove any block that adds 'Disallow: /' for it, or append an explicit allow group such as 'User-agent: GPTBot' then 'Allow: /'.
  4. Save. Visit https://yourstore.com/robots.txt and confirm the bot no longer has 'Disallow: /'.
Ai crawler not specifiedQuick win

Your robots.txt doesn't explicitly allow several AI crawlers. They aren't blocked, but a number of AI engines act on explicit permission, so naming them removes any ambiguity.

On Shopify

  1. From Shopify admin go to Online Store > Themes > (the three-dot menu on your live theme) > Edit code.
  2. Under the 'templates' folder click 'Add a new template' and choose 'robots.txt' (this creates robots.txt.liquid).
  3. Shopify's default template loops over rules; to allow an AI bot remove any block that adds 'Disallow: /' for it, or append an explicit allow group such as 'User-agent: GPTBot' then 'Allow: /'.
  4. Save. Visit https://yourstore.com/robots.txt and confirm the bot no longer has 'Disallow: /'.
Geo brand name inconsistentModerate effort

Your brand name appears in multiple different formats across your site (schema, og:site_name, footer, logo). AI engines may treat the variants as separate entities.

On Shopify

  1. Edit your theme (Online Store > Themes > Edit code). Product schema usually lives in 'sections/main-product.liquid' or a JSON-LD snippet; Organization schema in 'theme.liquid' / a global snippet.
  2. Add the missing properties using Liquid variables ({{ product.title }}, {{ product.price | money_without_currency }}, {{ product.featured_image | image_url }}, {{ shop.name }}).
  3. Or install a structured-data app (Schema Plus, JSON-LD for SEO) that fills required Product/Organization fields automatically.
  4. Validate the page in Google's Rich Results Test.
Geo eeat trust signals weakModerate effort

Your store is missing trust signals (E-E-A-T) that AI engines and shoppers look for before recommending a store: clear policies, real contact details, and a credible business identity.

On Shopify

  1. Edit your theme (Online Store > Themes > Edit code). Product schema usually lives in 'sections/main-product.liquid' or a JSON-LD snippet; Organization schema in 'theme.liquid' / a global snippet.
  2. Add the missing properties using Liquid variables ({{ product.title }}, {{ product.price | money_without_currency }}, {{ product.featured_image | image_url }}, {{ shop.name }}).
  3. Or install a structured-data app (Schema Plus, JSON-LD for SEO) that fills required Product/Organization fields automatically.
  4. Validate the page in Google's Rich Results Test.
Geo faqpage missing productsModerate effort

Your product pages don't use FAQPage schema, so AI assistants can't pull direct answers to common buyer questions (sizing, materials, shipping) about your products.

On Shopify

  1. Edit your theme (Online Store > Themes > Edit code). Product schema usually lives in 'sections/main-product.liquid' or a JSON-LD snippet; Organization schema in 'theme.liquid' / a global snippet.
  2. Add the missing properties using Liquid variables ({{ product.title }}, {{ product.price | money_without_currency }}, {{ product.featured_image | image_url }}, {{ shop.name }}).
  3. Or install a structured-data app (Schema Plus, JSON-LD for SEO) that fills required Product/Organization fields automatically.
  4. Validate the page in Google's Rich Results Test.
Geo price not crawlableModerate effort

Your product prices appear to be rendered by JavaScript and may not be present in the static HTML, so AI crawlers (which often don't run JS) can't read or cite them.

On Shopify

  1. Edit your theme (Online Store > Themes > Edit code). Product schema usually lives in 'sections/main-product.liquid' or a JSON-LD snippet; Organization schema in 'theme.liquid' / a global snippet.
  2. Add the missing properties using Liquid variables ({{ product.title }}, {{ product.price | money_without_currency }}, {{ product.featured_image | image_url }}, {{ shop.name }}).
  3. Or install a structured-data app (Schema Plus, JSON-LD for SEO) that fills required Product/Organization fields automatically.
  4. Validate the page in Google's Rich Results Test.
Geo schema org missing sameasModerate effort

Your Organization schema has no sameAs links to your social profiles, Wikipedia, or other authoritative sources - so AI engines can't confidently connect the dots about your brand.

On Shopify

  1. Edit your theme (Online Store > Themes > Edit code). Product schema usually lives in 'sections/main-product.liquid' or a JSON-LD snippet; Organization schema in 'theme.liquid' / a global snippet.
  2. Add the missing properties using Liquid variables ({{ product.title }}, {{ product.price | money_without_currency }}, {{ product.featured_image | image_url }}, {{ shop.name }}).
  3. Or install a structured-data app (Schema Plus, JSON-LD for SEO) that fills required Product/Organization fields automatically.
  4. Validate the page in Google's Rich Results Test.
Geo schema product incompleteModerate effort

Your Product structured data is missing fields AI engines rely on to cite your products accurately (price, availability, currency, brand, ratings).

On Shopify

  1. Edit your theme (Online Store > Themes > Edit code). Product schema usually lives in 'sections/main-product.liquid' or a JSON-LD snippet; Organization schema in 'theme.liquid' / a global snippet.
  2. Add the missing properties using Liquid variables ({{ product.title }}, {{ product.price | money_without_currency }}, {{ product.featured_image | image_url }}, {{ shop.name }}).
  3. Or install a structured-data app (Schema Plus, JSON-LD for SEO) that fills required Product/Organization fields automatically.
  4. Validate the page in Google's Rich Results Test.
Llms txt missingQuick win

Your store has no llms.txt - the emerging standard (like robots.txt, but for AI) that tells language models which content to read and cite. SEOLZ has generated a ready-to-upload file from your own pages.

On Shopify

  1. In Shopify a file at the domain root needs a small workaround: create the content in admin under Content > Files (upload a .txt), then add a redirect/route, OR use a theme app/proxy that serves /llms.txt.
  2. Simplest reliable route: use an app such as 'Easy Robots.txt' style file managers, or host llms.txt via a Shopify app proxy that maps /llms.txt to your generated content.
  3. Paste the generated llms.txt content (SEOLZ pre-filled it from your store).
  4. Verify at https://yourstore.com/llms.txt.