# Theme SEO and accessibility

Source: https://support.storeconnect.com/articles/theme-seo-accessibility · Last modified 21 August 2026

Building a theme that ranks well in search engines and works for all users is not optional — it is part of shipping quality work. This article covers StoreConnect's built-in SEO features, responsive image best practices, and the WCAG 2.1 Level AA accessibility requirements that apply to storefront themes.

## SEO

### The `meta_data` snippet

Include `{% render "meta_data" %}` in your layout's `<head>`. This single snippet auto-generates the title, meta description, meta keywords, Open Graph tags, and a canonical URL for every page type.


```liquid

<head>
  ...
  {% render "meta_data" %}
</head>
```


When `meta_data` is included, do **not** manually add a `<title>` tag — the snippet provides it.

**What it generates:**

| Tag | Source |
|-----|--------|
| `<title>` | The `title` variable from the platform (auto-generated per page type) |
| `<meta name="description">` | The `meta_description` variable |
| `<meta name="keywords">` | The `meta_keywords` variable |
| `<link rel="canonical">` | `current_request.canonical_url` (auto-normalized) |
| `<meta property="og:type">` | Auto-detected: `"website"`, `"article"`, or `"page"` |
| `<meta property="og:title">` | Same as `<title>` |
| `<meta property="og:description">` | Same as meta description |
| `<meta property="og:url">` | Canonical URL |
| `<meta property="og:locale">` | `current_store.locale` |
| `<meta property="og:image">` | Social image or primary image |
| `<meta property="og:price:amount">` | Product price (product pages only) |
| `<meta property="og:price:currency">` | Store currency (product pages only) |

On paginated pages, the title automatically appends " - Page N".

### SEO properties on drops

Most content drops expose SEO fields that merchants can set in the CMS:

| Drop | SEO properties |
|------|---------------|
| ProductDrop | `meta_title`, `meta_description`, `meta_keywords`, `search_description`, `social_image`, `url`, `condition`, `upc` |
| ArticleDrop | `meta_title`, `meta_description`, `meta_keywords`, `search_keywords`, `canonical_url` |
| ProductCategoryDrop | `meta_title`, `meta_description`, `meta_keywords`, `social_image`, `google_product_category` |
| ArticleCategoryDrop | `meta_title`, `meta_description`, `meta_keywords`, `social_image` |
| PageDrop | `meta_title`, `meta_description` |
| StoreDrop | `meta_title`, `meta_description`, `meta_keywords`, `social_image` |

### Meta description defaults

CMS meta description fields are often blank. Override the `meta_data` snippet to auto-generate descriptions from content fields when the CMS field is empty:


```liquid

{%- if meta_description == blank -%}
  {%- if current_product != blank -%}
    {%- assign meta_description = current_product.search_description
        | default: current_product.summary_content | strip_html | truncate: 160 -%}
  {%- elsif current_product_category != blank -%}
    {%- assign meta_description = current_product_category.introduction_content
        | strip_html | truncate: 160 -%}
  {%- elsif current_article != blank -%}
    {%- assign meta_description = current_article.summary_content
        | default: current_article.introduction_content | strip_html | truncate: 160 -%}
  {%- elsif current_page != blank -%}
    {%- assign meta_description = current_page.body_content | strip_html | truncate: 160 -%}
  {%- endif -%}
  {%- if meta_description == blank -%}
    {%- assign meta_description = current_store.meta_description -%}
  {%- endif -%}
{%- endif -%}
```


This ensures every page has a meta description — Lighthouse flags missing descriptions as a critical SEO issue.

### Canonical URLs

The platform auto-generates canonical URLs via `current_request.canonical_url`. These are normalized to remove tracking parameters, normalize casing and trailing slashes, and include the full domain. The `meta_data` snippet renders the `<link rel="canonical">` tag automatically.

### Structured data (JSON-LD)

#### Breadcrumbs

The `breadcrumbs` snippet renders both visible breadcrumb HTML and a `BreadcrumbList` JSON-LD script:


```liquid

{% render "breadcrumbs" %}
```


Include this on product, category, article, and content pages.

#### Product rich data

The `products/product/rich_data_json` snippet generates Product or ProductGroup JSON-LD markup:


```liquid

{% render "products/product/rich_data_json", product: current_product %}
```


Include this on the product page. It includes name, SKU, GTIN, brand, description, images, price range, availability, condition, and URL.

### Sitemaps and robots.txt

The platform auto-generates `/sitemap.xml` and `/robots.txt`. These require no theme configuration. Custom content pages are automatically included in the sitemap.

### Merchant feeds

Automated product feeds are available at `/merchant_feeds/google.xml` and `/merchant_feeds/facebook.xml`. These are generated from product data with no theme work required.

### Twitter/X cards

Twitter card tags are not auto-generated. Add them to your layout if needed:


```liquid

<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="{{ title }}">
<meta name="twitter:description" content="{{ meta_description }}">
{% if current_product != blank %}
  {% assign tw_image = current_product.social_image | default: current_product.image %}
{% elsif current_article != blank %}
  {% assign tw_image = current_article.social_image | default: current_article.hero_image %}
{% else %}
  {% assign tw_image = current_store.social_image | default: current_store.logo %}
{% endif %}
{% if tw_image != blank %}
  <meta name="twitter:image" content="{{ tw_image.huge_url }}">
{% endif %}
```


### SEO checklist

- [ ] `{% render "meta_data" %}` in layout `<head>`
- [ ] Meta description defaults added for when CMS fields are blank
- [ ] `{% render "breadcrumbs" %}` on product, category, article, and content pages
- [ ] `{% render "products/product/rich_data_json", product: current_product %}` on product page
- [ ] `<html lang="{{ current_store.locale }}">` on the `<html>` element
- [ ] One `<h1>` per page
- [ ] Logical heading hierarchy (h1 > h2 > h3, no skipped levels)
- [ ] Descriptive `alt` text on all images with fallback: `alt="{{ image.alt_text | default: product.name }}"`
- [ ] Store-level `meta_description` set in the CMS (used as final fallback)

---

## Responsive images

Google uses mobile-first indexing — your site's mobile experience determines its search ranking. Always use `srcset` and `sizes` on content images rather than a fixed `src` alone.

### Image sizes available

The platform generates images at nine named sizes:

| Size | Max dimensions | Property | Typical use |
|------|----------------|----------|-------------|
| `pico` | 16×16 | `pico_url` | LQIP placeholders |
| `icon` | 32×32 | `icon_url` | Tiny icons |
| `tiny` | 50×50 | `tiny_url` | Cart line item thumbnails |
| `small` | 100×100 | `small_url` | Thumbnails |
| `thumb` | 240×240 | `thumb_url` | Product cards (mobile) |
| `medium` | 480×480 | `medium_url` | Product cards (desktop) |
| `large` | 640×640 | `large_url` | Product detail images |
| `huge` | 1024×1024 | `huge_url` | Hero images |
| `massive` | 2048×2048 | `massive_url` | Retina hero images |

### Responsive image pattern


```liquid

<img src="{{ image.medium_url }}"
     srcset="{{ image.thumb_url }} 240w,
             {{ image.medium_url }} 480w,
             {{ image.large_url }} 640w,
             {{ image.huge_url }} 1024w,
             {{ image.massive_url }} 2048w"
     sizes="(max-width: 575px) 100vw,
            (max-width: 767px) 50vw,
            (max-width: 991px) 33vw,
            25vw"
     alt="{{ image.alt_text | default: product.name }}"
     loading="lazy"
     decoding="async">
```


### Loading strategy

| Location | Strategy | Attributes |
|----------|----------|------------|
| Hero / above the fold | Eager load, high priority | `fetchpriority="high"` (no `loading="lazy"`) |
| Product cards in grid | Lazy load | `loading="lazy" decoding="async"` |
| Below-the-fold content | Lazy load | `loading="lazy" decoding="async"` |

---

## Accessibility

Target: **WCAG 2.1 Level AA** conformance. This means all content is perceivable, operable, understandable, and robust for users with disabilities — including those using screen readers, keyboard navigation, and assistive technologies.

### Viewport meta tag

The layout must include:

```html

<meta name="viewport" content="width=device-width, initial-scale=1.0">
```

Never use `maximum-scale=1` or `user-scalable=no` — these prevent pinch-to-zoom, which is a WCAG 1.4.4 violation.

### Skip link

Add a "skip to main content" link as the first focusable element after `{{ theme_bar }}`:


```liquid

<body>
  {{ theme_bar }}
  <a href="#SC-Main" class="skip-link">
    {{ "accessibility.skip_to_content" | t | default: "Skip to main content" }}
  </a>
  {% render "header" %}
  <main id="SC-Main">
    ...
  </main>
</body>
```


Required CSS (the base theme does not include this — add it to your theme):

```css

.skip-link {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.skip-link:focus {
  position: fixed;
  top: 0;
  left: 0;
  width: auto;
  height: auto;
  padding: 0.75rem 1.5rem;
  margin: 0;
  overflow: visible;
  clip: auto;
  white-space: normal;
  background: #fff;
  color: #000;
  z-index: 99999;
  font-size: 1rem;
  text-decoration: underline;
}
```

### Semantic HTML

Use correct HTML elements for their purpose:

- `<header>` — site header, once per page
- `<nav>` — navigation regions, with `aria-label` to distinguish multiple navs
- `<main id="SC-Main">` — primary content, once per page
- `<footer>` — site footer
- `<section>` — thematic grouping with a heading

### Heading hierarchy

- One `<h1>` per page (the main content title).
- Use heading levels sequentially — never skip from `<h1>` to `<h3>`.
- Footer headings should use `<h2>` with a CSS class for visual sizing, not `<h4>`. Lighthouse flags level skips.

### Image alt text

Every `<img>` must have an `alt` attribute:

- Content images — describe what is shown: `alt="{{ image.alt_text | default: product.name }}"`
- Decorative images — use empty alt: `alt=""`
- Decorative SVG icons — use `aria-hidden="true" focusable="false"`

### Form accessibility


```liquid

<label for="email">{{ form.email.label }}</label>
<input type="email"
       id="email"
       name="{{ form.email.name }}"
       value="{{ form.email.value }}"
       {% if form.errors.size > 0 %}
         aria-invalid="true"
         aria-describedby="email-error"
       {% endif %}
       required
       autocomplete="email">
{% if form.errors.size > 0 %}
  <span id="email-error" class="error">{{ form.errors.first }}</span>
{% endif %}
```


### Accessible navigation


```liquid

<nav aria-label="{{ 'navigation.main' | t }}">
  <ul>
    {% for item in all_menus.main-menu.items %}
      <li>
        {% if item.items.size > 0 %}
          <button aria-expanded="false" aria-controls="submenu-{{ forloop.index }}">
            {{ item.title }}
          </button>
          <ul id="submenu-{{ forloop.index }}" hidden>
            {% for child in item.items %}
              <li><a href="{{ child.url }}">{{ child.title }}</a></li>
            {% endfor %}
          </ul>
        {% else %}
          <a href="{{ item.url }}">{{ item.title }}</a>
        {% endif %}
      </li>
    {% endfor %}
  </ul>
</nav>
```


### Live regions

Use `aria-live` for content that updates dynamically without a page reload:


```liquid

{%- comment -%} Cart count badge {%- endcomment -%}
<span aria-live="polite" aria-atomic="true">
  {{ current_cart.item_count }} {{ "cart.items" | t }}
</span>

{%- comment -%} Flash messages {%- endcomment -%}
<div role="alert">
  {{ current_flash.notice }}
</div>
```


### Color contrast

WCAG AA requires:
- 4.5:1 contrast ratio for normal text
- 3:1 contrast ratio for large text (18px+ or 14px+ bold)
- 3:1 for UI components and graphical objects

Create a CSS variable for brand colors on dark surfaces:

```css

:root {
  --color-primary-on-dark: #E0944A; /* lighter tone that passes 4.5:1 on dark backgrounds */
}
```

### Icon-only buttons

Every icon-only button or link must have an accessible label:


```liquid

<button aria-label="{{ 'navigation.open_menu' | t }}">
  <svg aria-hidden="true" focusable="false">...</svg>
</button>
```


### Keyboard navigation

- All interactive elements must be keyboard-focusable.
- Focus indicators must be visible — add `:focus-visible` styles.
- Dropdowns and modals must close on Escape and return focus to the trigger element.
- Use `tabindex` only when necessary; never use values greater than 0.

## Common pitfalls from real theme audits

These recurring issues are found across StoreConnect themes via Lighthouse audits:

**Images without alt text.** Always include `alt` with a fallback: `alt="{{ image.alt_text | default: product.name }}"`. The `alt_text` property comes from the CMS media record — if unset, fall back to the parent object's name.

**Heading hierarchy violations.** Footer headings are commonly `<h4>`, which skips levels from the page's `<h1>`. Use `<h2>` with a CSS class for visual sizing instead. Never choose heading level based on desired font size — use CSS for that.

**SVG icons missing `aria-hidden`.** Decorative SVG icons (hamburger, close, chevron, search, cart) must have `aria-hidden="true" focusable="false"`. If the icon is inside a button with `aria-label`, the icon is decorative.

**Flash messages not announced.** The flash/notice snippet needs `role="status"` on notices and `role="alert"` on alerts for screen readers to announce them.

**Links with no discernible name.** Icon-only links (cart, social media) that hide their text on mobile need `aria-label` so the mobile experience has an accessible name.

**Invalid HTML in menus.** `<ul>` elements must only contain `<li>` children. Close buttons or logos inside a `<ul>` wrapped in a `<div>` produce invalid HTML. Wrap them in `<li>` elements instead.

**`meta_data` snippet limitations.** CMS meta fields are often blank. Override the `meta_data` snippet to generate fallback descriptions from content fields — this single change fixes the most common Lighthouse SEO failure.

**Lighthouse with theme preview.** Testing via `?theme-preview=` adds latency from the redirect and injects a preview bar with its own accessibility issues. These are platform artifacts, not theme issues. Account for them when interpreting scores.

## Accessibility checklist

- [ ] `<html lang="{{ current_store.locale }}">` present
- [ ] Skip-to-content link as first focusable element (with visually-hidden CSS)
- [ ] One `<h1>` per page, logical heading hierarchy (footer uses `<h2>` not `<h4>`)
- [ ] All images have `alt` attributes with fallback (`alt_text | default: name`)
- [ ] Decorative SVGs have `aria-hidden="true" focusable="false"`
- [ ] All form inputs have associated `<label>` elements
- [ ] Flash messages use `role="status"` (notices) and `role="alert"` (alerts)
- [ ] Icon-only buttons and links have `aria-label`
- [ ] Dropdowns and accordions use `aria-expanded`
- [ ] Modals use `role="dialog"` with `aria-modal="true"` and `aria-labelledby`
- [ ] Dynamic content updates use `aria-live`
- [ ] All interactive elements are keyboard accessible
- [ ] Visible focus indicators on all focusable elements
- [ ] Color contrast meets WCAG AA (4.5:1 normal text, 3:1 large text)
- [ ] Dark-surface link colors use an accessible variant of the brand color
- [ ] Menu `<ul>` elements contain only `<li>` children

---

## Follow StoreConnect

- [Email Newsletter](https://getstoreconnect.com/c/lp-newsletter)
- [LinkedIn Newsletter](https://www.linkedin.com/build-relation/newsletter-follow?entityUrn=7444956928444862464)
- [YouTube](https://www.youtube.com/channel/UCngKdP2x8l1wcbAKW3tvU8g)
- [LinkedIn](https://www.linkedin.com/company/storeconnect)
- [X / Twitter](https://x.com/storeconnecthq)

## Popular Links

- [Partners](https://getstoreconnect.com/partners)
- [News](https://getstoreconnect.com/articles/news)
- [Events](https://getstoreconnect.com/articles/events)
- [Feature Comparison](https://getstoreconnect.com/how-we-compare)
- [Download a free trial](https://appexchange.salesforce.com/appxListingDetail?listingId=a0N3A00000FMkeKUAT)
- [Book a Demo](https://getstoreconnect.com/contact)

## Documentation

- [Help documentation](https://support.storeconnect.com/help-documentation)
- [AI agents](https://support.storeconnect.com/ai)
- [Videos & tutorials](https://support.storeconnect.com/videos-tutorials)
- [Developer reference](https://support.storeconnect.com/developer-reference)
- [Release notes](https://support.storeconnect.com/release-notes)
- [Troubleshooting](https://support.storeconnect.com/troubleshooting)
- [Trust Center](https://trust.getstoreconnect.com/)
- [Status Page](https://status.storeconnect.com/)

## Contact

- info@getstoreconnect.com
- US +1 415 745 3230
- AUS +61 2 8365 2308

100 S Ashley Dr, Suite 600-2461
Tampa FL 33602-600 USA

Level 22, Sydney Place
180 George Street
Sydney, NSW, 2000, AUS

---

StoreConnect Support — https://support.storeconnect.com/articles/theme-seo-accessibility