Markus Michalski FAQ

Sylius SEO Suite Pro: hreflang

hreflang in Sylius SEO Suite Pro: which pages belong together, channel groups, rules for listed languages, x-default, admin settings and output in the storefront and Shop API.

Deutsche Version

Back to overview

Sylius SEO Suite Pro: hreflang

The plugin renders <link rel="alternate" hreflang="..." href="..."> in the <head> of product pages, category listings and the home page. The same list is available in the Shop API in the field alternates. A page with fewer than two alternates gets nothing, so a shop with one language per channel renders exactly what it rendered before.

<link rel="alternate" hreflang="de-DE" href="https://shop.example.de/de_DE/products/blaues-sofa">
<link rel="alternate" hreflang="en-US" href="https://shop.example.com/en_US/products/blue-sofa">
<link rel="alternate" hreflang="x-default" href="https://shop.example.de/de_DE/products/blaues-sofa">

Which pages belong together

Channels are often separate shops with an assortment of their own, not translations of each other. The plugin therefore never guesses across channels:

Products and categories are matched by their code. For the home page, the home page of every channel in the group counts.

The set is always built from the group, never from the page that is being rendered. Every page of a set outputs the same complete list, itself included.

Example: two channels in one group

The channel shop_de (hostname shop.example.de, language de_DE) and the channel shop_en (hostname shop.example.com, language en_US) share the same group_code:

Channel Enabled hreflang group x-default language
shop_de yes main empty
shop_en yes main empty

The product is sold in both channels and has the slugs blaues-sofa (de_DE) and blue-sofa (en_US). Both product pages then carry the same block:

<link rel="alternate" hreflang="de-DE" href="https://shop.example.de/de_DE/products/blaues-sofa">
<link rel="alternate" hreflang="en-US" href="https://shop.example.com/en_US/products/blue-sofa">
<link rel="alternate" hreflang="x-default" href="https://shop.example.de/de_DE/products/blaues-sofa">

The x-default points at the default language of the first channel in the group (lowest row ID), here shop_de.

Decision flow

graph TD
    A["Page is rendered"] --> B{"Channel setting enabled?"}
    B -->|"No"| Z["No hreflang links"]
    B -->|"Yes"| C{"Page is canonical: no noindex, no facet, not page 2 or later, no differing canonical?"}
    C -->|"No"| Z
    C -->|"Yes"| D["Build the set: languages of the channel or of all active group channels"]
    D --> E["One page per hreflang value: duplicates are dropped"]
    E --> F{"Is the page itself in the set?"}
    F -->|"No"| Z
    F -->|"Yes"| G{"Enough alternates and enough distinct URLs?"}
    G -->|"No"| Z
    G -->|"Yes"| H["Determine x-default and append it"]
    H --> I["Output alternates in the head and in the Shop API"]

Rules: when a language is listed

A page has to be part of its own set (self-reference). If its own entry is missing, for example because it is noindex or its value was taken by another channel, no alternates are output at all, not even for the other languages.

x-default

There is exactly one x-default per set. It is determined like this:

  1. Each channel of the group is asked in turn, starting with the lowest row ID.
  2. If the channel has an x-default language set, the page in that language counts. It may also belong to another channel of the group (the form accepts any of them), the channel's own page is preferred.
  3. If no language is set, the channel's default language counts.
  4. If that page does not exist, the next channel is asked.
  5. If no channel has a matching page, the first alternate is used as x-default.

Handling duplicate values

Situation Format Result
Two channels of a group have de_DE any The channel with the lower row ID keeps de-DE, the other one drops out. Saving in the admin is refused.
One channel has de_DE, another de_AT language-region Two values de-DE and de-AT, all fine
One channel has de_DE, another de_AT language Both become de: a conflict as above

Settings

Two keys under mmd_sylius_seo_suite_pro.hreflang:

Setting Default Description
hreflang.format language-region How the value is written: language-region turns de_DE into de-DE, language keeps only the language (de)
hreflang.min_alternates 2 A page only gets hreflang links when its set has at least this many alternates (x-default not counted). At least 1.
# config/packages/mmd_sylius_seo_suite_pro.yaml
mmd_sylius_seo_suite_pro:
    hreflang:
        format: language-region
        min_alternates: 2

More on this on the Configuration page.

Settings in the admin

Open SEO Suite Pro > hreflang button. There is one optional row per channel, a channel without a row uses the defaults (hreflang on, no group, the channel's default language as x-default). There is no sidebar entry of its own, use Back to SEO Suite Pro to return.

Field Default Description
Channel One row per channel, each channel only once
Output hreflang links on Off: the channel outputs no links and is not linked by the other channels of its group either
hreflang group empty Optional code of lower-case letters, digits, - and _, starting with a letter or digit, at most 64 characters. The same code means the channels are alternates of each other.
x-default language empty Language for visitors without a matching language. Must belong to the channel (or, in a group, to a channel of the group). Empty means the channel's default language. In a group, the setting of the first channel (lowest row) counts.

On save, ValidHreflangSettings checks:

The priority in a group is the row ID. The lowest ID wins on duplicates and decides the x-default. A newly created row always has the highest ID and therefore comes last.

Own page types

If you want to add hreflang to your own page types (landing pages, for example), implement Mmd\SyliusSeoSuiteProPlugin\Hreflang\HreflangAlternatesProviderInterface. The provider only says in which languages of a channel a page exists. Grouping, duplicates, x-default and host are handled by the plugin. The details are on the Extending page. Product, category and home page are built in.

Storefront and Shop API

In the storefront, the template renders a link for every entry of seo.alternates. This covers product pages, category listings and the home page.

The Shop API returns the same list in the alternates field of the Seo resource, built by the same service:

Both paths sit under your shop's API prefix (Sylius uses /api/v2 by default). The field is a list of {hreflang, href}, empty when the page gets no alternates:

{
  "alternates": [
    { "hreflang": "de-DE", "href": "https://shop.example.de/de_DE/products/blaues-sofa" },
    { "hreflang": "en-US", "href": "https://shop.example.com/en_US/products/blue-sofa" },
    { "hreflang": "x-default", "href": "https://shop.example.de/de_DE/products/blaues-sofa" }
  ]
}

The URLs use the channels' hostnames and the scheme of the API request. A headless frontend on another host maps them itself. There is no API endpoint for the home page, its alternates are rendered by Twig only. More about the API is on the Shop API page.

Cost

The settings of all channels are read with one query per request, one more query fetches the page's translations, and all channels with locales and menu category come in a single query. Neither the number of languages nor the number of channels in a group adds queries. If the table is missing (migration not run yet), the defaults apply and a warning is logged once. If building the alternates fails, the page is rendered without them, never with an error.

More help: Troubleshooting.