Theme SEO and accessibility
On this page
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
... {% render "meta_data" %}```
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
{% 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 %}
{% 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
alttext on all images with fallback:alt="{{ image.alt_text | default: product.name }}" - [ ] Store-level
meta_descriptionset 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
```
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
```
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
{{ theme_bar }} {{ "accessibility.skip_to_content" | t | default: "Skip to main content" }} {% render "header" %}```
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, witharia-labelto 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
<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 %} {{ form.errors.first }} {% endif %} ```
Accessible navigation
```liquid
```
Live regions
Use aria-live for content that updates dynamically without a page reload:
```liquid
{%- comment -%} Cart count badge {%- endcomment -%} {{ current_cart.item_count }} {{ “cart.items” | t }}
{%- comment -%} Flash messages {%- endcomment -%}
```
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> ```
Keyboard navigation
- All interactive elements must be keyboard-focusable.
- Focus indicators must be visible — add
:focus-visiblestyles. - Dropdowns and modals must close on Escape and return focus to the trigger element.
- Use
tabindexonly 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
altattributes 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) androle="alert"(alerts) - [ ] Icon-only buttons and links have
aria-label - [ ] Dropdowns and accordions use
aria-expanded - [ ] Modals use
role="dialog"witharia-modal="true"andaria-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
Was this article helpful?
Thanks for your feedback! It helps us improve our docs.