Shopify hreflang: what Markets writes in every head, and why the mistakes are never in the tags
Shopify writes the hreflang block itself from markets and languages. What it emits, subfolders or domains, x-default, and a bilingual Swiss store as the worked case.
11 min read
Contents — 8 sections
- hreflang is the one signal that tells Google which translation to show
- Shopify writes the tags itself, and the theme has no say in it
- Subfolders, subdomains and domains each tell Google something different
- x-default lands where the primary market lands, and that is a choice you make once
- The worked case: a bilingual Swiss store
- The mistakes are almost never in the tags
- Check it in five minutes, on one product page in each language
- Translate the catalogue first; the tags are already right
hreflang is the one signal that tells Google which translation to show
Google's documentation on localised versions of a page puts the mechanism in a sentence: if you have multiple versions of a page for different languages or regions, tell Google, and Search will point people to the version that matches their language or region. The tag is a <link rel="alternate"> element carrying an hreflang code and the href of that variant, one per variant, in the <head> of every variant. Four rules from the same page decide whether the set works at all. Each version must list itself and every other version. Every URL must be fully qualified, scheme included. If two pages do not both point at each other, the tags are ignored, so a one-way link is no link. And the value is a language code from ISO 639-1 with an optional region from ISO 3166-1 alpha-2: fr, fr-CH, de-CH. A region on its own is invalid, and reserved codes such as UK and EU are silently dropped.
Two things the tag does not do, from the same page. Google does not use hreflang, or the HTML lang attribute, to detect the language of a page; it reads the visible content. And the tag does not make two pages different. Google's guide to multi-regional and multilingual sites says localised versions are only treated as duplicates if the main content stays untranslated — which is the sentence that decides most of what follows, because on Shopify the tags are never the problem. The content behind them usually is.
Shopify writes the tags itself, and the theme has no say in it
Shopify's own multi-currency and multi-language guide states it plainly: Shopify automatically includes hreflang tags through the content_for_header object, which is required in theme.liquid and which the documentation tells you not to modify or parse. In Dawn it sits on line 66 as of 17 September 2026, inside the head, after the canonical; Horizon has it on line 31. There is no admin field for the tags and no theme setting. What they contain is decided entirely by which languages you publish and which markets you give a web presence to.
We opened three live storefronts on 17 September 2026, two of them Swiss, to read what the object actually emits. The pattern was the same on all three. The block starts with x-default, then one language-only tag per language of the primary market, then one language-REGION pair for every language of every market that has its own subfolder or domain. The href values are absolute, the path matches the page you are on, and the set is identical on every language version of that page — a French collection page carries the same list as its German twin, in the same order. That satisfies all four of Google's rules without anyone touching the theme.
Two observations worth keeping. The tags are in the HTML only. On the stores we opened, the sitemap index lists a separate child sitemap for each locale (/de/sitemap_products_1.xml alongside /sitemap_products_1.xml), but none of them carry xhtml:link alternates; Google's page says the three methods are equivalent and that there is no benefit to using more than one, so nothing is missing. And the block scales with your markets whether or not anyone buys there. One of the Swiss stores serves three languages across roughly thirty country markets and carries 97 hreflang tags in the head of every page. That is not wrong. It is a signal that the market list was never pruned.
Subfolders, subdomains and domains each tell Google something different
A market's web presence, in Shopify's Admin API definition, is one of three shapes: a different domain (example.ca), a subdomain (ca.example.com), or subfolders of the primary domain (example.com/en-ca). A market with no web presence is reachable only through the country selector, on the primary domain's URLs, and gets no hreflang of its own. The Markets overview gives the naming rule: a language published on a domain gets a language subfolder (example.com/fr), and a market with a subfolder presence gets a language-and-market subfolder (example.com/fr-ca), or ca.example.com/fr or example.ca/fr if the market has its own host.
Google's URL structure table is the other half of the decision. A country-code domain gives clear geotargeting and can only ever target one country. A subdomain and a subfolder are both easy to set up, and both leave a reader unsure whether de is the language or the country. A URL parameter is not recommended. The same page lists the country-code domains Google treats as generic — .co, .io, .tv, .me among them — and .ch is not on that list. A .ch domain tells Google Switzerland, on every URL under it, and there is no tag that changes that.
That gives one rule for a Shopify store. Choose subfolders on a generic domain if you sell, or intend to sell, in more than one country; choose a country-code domain only if that country is the whole business, because the hreflang pairs Shopify writes for a Germany market under example.ch are asking Google to geotarget Germany from a URL whose domain says Switzerland. Google's page on duplicate handling says pick a preferred version and use both rel="canonical" and hreflang when example.de/ and example.com/de/ show the same German content; Shopify handles the canonical side on its own, and on a bilingual store we opened the canonical_url on a /de/ page was the /de/ page itself, not the primary.
x-default lands where the primary market lands, and that is a choice you make once
Google's page describes x-default as the page for people whose browser language matches nothing you publish, designed for language selector pages but valid on any page. Shopify has no selector page to point it at. On every storefront we opened, x-default carried the same URL as the language-only tag of the primary language: the German root on a German-first store, the English root on an English-first one. There is no setting that moves it. It moves when the primary market or the primary language moves, which is not a change to make for the sake of a tag.
What it means in practice: the fallback is the page in your primary language, on your primary market's URLs. On a German-first Swiss store, an Italian-speaking searcher in Ticino, an English-speaking searcher in Geneva, and anyone with an unmatched browser language all land on the German page. If that is not where you want them, the fix is a third locale, not an edit to the tag.
The worked case: a bilingual Swiss store
A brand in Zurich sells on example.ch. German is the primary language. French is added through Translate & Adapt, which the Markets theme documentation names as the tool that stores translations of theme content, and those translations render on every plan. One market, Switzerland, at the root of the domain. Shopify creates example.ch/fr/ the moment French is published, per the multi-language guide, and the head of example.ch/products/tasche now carries three tags:
| hreflang | href |
|---|---|
x-default | the German product URL, at the domain root |
de | the same German URL |
fr | the same path under /fr/ |
The same three lines, in the same order, appear on example.ch/fr/products/tasche. The <html> element's lang attribute, which Dawn writes from request.locale.iso_code, reads de on one and fr on the other. Neither tag carries -CH, because a market at the domain root has no subfolder suffix and Shopify writes language-only codes for it. That is correct: the tag says German and French, the domain says Switzerland, and a French speaker in Lausanne searching for the product gets the /fr/ URL. Everything hangs on the French page being French. If the product title, description and collection copy were left in German with only the navigation translated, Google's duplicate rule applies, the /fr/ URL is a duplicate of the German one, and the tag has told Google about a French page that does not exist.
Now the brand adds an EU market to reach France and Germany, with a subfolder presence. Shopify creates example.ch/de-eu/ and example.ch/fr-eu/, and the block grows to five tags — but EU is on Google's list of reserved codes that are ignored, so the two new pairs carry no region for Google at all, and the .ch domain has already declared the country. Two separate markets, Germany at /de-de and France at /fr-fr, would produce valid pairs; they would still sit under a domain that says Switzerland. The honest version of that expansion is a generic domain with example.com/de-ch, example.com/fr-ch, example.com/de-de and example.com/fr-fr, and it is the structure one of the two Swiss stores we opened has already moved to. It is a migration, with redirects and a Search Console change of address, and it is far cheaper before the French catalogue exists than after.
The mistakes are almost never in the tags
We read the head of a multilingual store in the same order every time, and the tags are the last thing that turns out to be wrong.
- A locale published before its content was translated. Google's page describes the scenario exactly: the main content in one language, only the template translated. On Shopify it looks like a French locale with the navigation and footer done and the product fields not, and Google's duplicate rule means every untranslated product URL under
/fr/is a duplicate with a tag pointing at it. Publish the locale when the catalogue is translated, or unpublish it until it is. - Links that fall out of the locale. The multi-language guide is explicit: a hard-coded
/cartor/collections/allin a theme, an app block or a script sends a visitor on/fr/back to the German store. Useroutes.cart_urland theurlproperty of objects in Liquid, andwindow.Shopify.routes.rootin JavaScript, which the guide says always returns the current root with a trailing slash —/fr/on the French pages. - A second set of tags. An SEO app or an older theme that writes its own hreflang block produces two sets in one head, and if either names a URL the other does not, the return-link rule is broken for that pair. Count the
hreflangattributes on a product page; the number should be languages times markets-with-a-presence, plus one. - A switcher that is not a link. Dawn's language selector is a form: each option is an anchor with
href="#"that submitslanguage_code. Google's multilingual guide asks for hyperlinks to the other language versions so crawlers and readers can reach them. The alternates in the head cover discovery, but a plain list of links to each locale'sroot_urlin the footer costs six lines and removes the dependency on JavaScript. - Redirecting by location. Shopify Plus uses geolocation to pre-select the most relevant enabled market, per the Markets documentation. Google's guide says its crawler usually originates from the USA, sends no
Accept-Languageheader, and may not find every variation if content is routed by location; it asks that you avoid automatic redirects between language versions. Pre-selection in a selector is not a redirect. A geolocation app that bounces every visitor to a locale is, and it will show Googlebot exactly one store. - Markets nobody sells to. Every market with a subfolder adds a language's worth of tags to every page and a locale's worth of URLs to the sitemap. Ninety-seven tags is the head of a store with a web presence in roughly thirty markets. Give a web presence to the markets you actually stock and ship to, and leave the rest on the country selector.
Check it in five minutes, on one product page in each language
Pick your best-selling product. In a terminal, with URL set first to the German product page and then to the French one:
curl -s "$URL" | grep -o '<link rel="alternate" hreflang="[^"]*" href="[^"]*">'
You want the same lines, in the same order, from both runs. Count them, and account for every one: which market, which language, and whether you sell there. Any tag that is not Shopify's — a different quote style, a different position, a code such as UK, EU or a bare region — came from an app or the theme and needs an owner. Then open the two pages in a browser and read the French one as a French customer: the title, the first paragraph of the description, the collection names in the breadcrumb and the shipping line. If any of it is German, that page is a duplicate, whatever the tag says. Finally, click the language switcher and watch the address bar: the path should gain or lose /fr/ and nothing else, and the product should be the same product. Google's page lists two third-party hreflang checkers at the bottom; either will confirm the return links across the whole set, and neither is needed to find the problems above.
Translate the catalogue first; the tags are already right
In this order, because each step is what makes the next one testable:
- The catalogue. Every product title and description in every published locale, then collections, then pages and policies. Days, not hours, and it is the only step Google can see the result of. A locale whose products are still in the primary language should be unpublished until this is done.
- The domain decision. One hour to decide, before the second market is created: a country-code domain if one country is the business, subfolders on a generic domain if it is not. Half a day to migrate if the decision comes late.
- Links and switcher. An hour to search the theme and app blocks for hard-coded paths and replace them with
routesand object URLs, and to add the footer list. - Prune the markets. Thirty minutes in Markets to remove the web presence from countries you do not ship to. The head gets shorter on every page.
- Recount. Run the command on both pages again and keep the output; it is the reference the next app install gets compared against.
It belongs in the AI visibility section of a store audit, beside the product schema check, where the same priceCurrency rule applies: Shopify's guide says the price node must carry the cart currency, not the shop's, or a French-market page marks up the wrong money. Our own site runs the smallest version of all this — one French page paired with its English original, four tags each, and no French URL that does not exist. If you would rather have the whole head read, the duplicates found and the markets ranked against everything else the store is leaving on the table, that is the Store Teardown: €750, five days, and the document is yours whatever you decide afterwards.