Markus Michalski FAQ

Sylius SEO Suite Pro: SEO Templates

Generate meta titles and descriptions from templates: priority chain, placeholders, empty placeholders, word-boundary shortening, language and channel, home page, Shop API and custom placeholders.

Deutsche Version

Back to overview

Sylius SEO Suite Pro: SEO Templates

An SEO template generates the meta title and/or meta description of every product or category of a channel, so you do not have to type anything per page. One row of the SEO templates screen holds a title template and a description template for one channel, one type (product, category or other) and optionally one language.

You reach the screen through MMKreativ Plugins > SEO Suite Pro (the plugin's only menu entry) and the SEO templates button on that page. The Back to SEO Suite Pro button leads back.

The type Other stands for every page that is neither a product nor a category. Today that is the home page, see Home page description.

Priority chain

For every field, the first stage that has a usable value wins: first the hand-written value, then the template, then the fallback.

graph TD
    A["Page is built"] --> B{"Hand-written value present?"}
    B -->|yes| C["Use the hand-written value, never shortened"]
    B -->|no| D{"Template present and fully resolvable?"}
    D -->|yes| E["Use the generated value, cut at a word boundary"]
    D -->|no| F["Fallback - name for the title, no tag for the description"]
Field 1. Hand-written 2. Template 3. Fallback
Title (product, category) the translation's Meta title title template the name
Description, product Sylius Core's Meta description description template none (no tag)
Description, category the plugin's Meta description field description template none (no tag)
Description, home page the channel's Description description template of type Other none (no tag)

An empty or whitespace-only value counts as not set at every stage. A hand-written value is never shortened. Without any template row everything behaves exactly as it does without this feature.

The hand-written title (meta title) and the category description exist only if you opted in to the per-entity fields in your translation classes, see Installation. Without that opt-in there is no hand-written title and the title template applies directly. Without the category field the description template applies directly.

Home page description

The home page has no entry of its own in the Sylius admin, so its description comes from two places, in this order:

  1. The channel's Description (Configuration > Channels > channel > General), trimmed. Blank counts as not set.
  2. The description template of type Other for the channel (and the language of the page, else the all-languages row). Only {channel.name} and {locale} are available, for example {channel.name}: sofas, chairs and tables with free delivery.

If both are empty, the home page has no description tags at all. Whatever wins ends up in the description meta tag, og:description and twitter:description. The title of the home page is not generated from a template: Sylius' own <title> stays, and a title template of type Other is not used.

Placeholders

Plain {token} replacement from a fixed whitelist. Twig and expressions are not evaluated, so a template cannot execute anything.

Placeholder For Description
{product.name} products name in the page's language
{product.code} products product code
{product.short_description} products short description without HTML, shortened to 150 characters
{attribute.<code>} products value of the product attribute with that code in the page's language (a language-independent value is the fallback). A select shows the labels of the chosen options, several joined by ", "; a checkbox shows Yes/No (localized), a date its ISO form
{taxon.main} products name of the product's main category (in the page's language, else in the channel's default language)
{taxon.name} categories name
{taxon.parent} categories name of the parent category (page's language, else the channel's default language); empty for the root. The first-level categories have the shop's root category as parent
{taxon.description} categories description without HTML, shortened to 150 characters
{channel.name} all types channel name
{locale} all types locale code of the page, for example en_US

Markup you type into a template is removed. Text a placeholder delivers is taken as it is: an escaped &lt;b&gt; in a description stays the text <b>.

Unknown tokens, tokens of another type ({product.name} in a category or Other template) and unbalanced braces are refused with a form error when the template is saved, so a typo cannot silently go live.

Message Trigger
Enter a title template, a description template or both. Both fields empty
The curly braces are not balanced. A placeholder is written as {product.name}, with exactly one opening and one closing brace. Open, stray or nested braces
The placeholder {{ token }} is unknown for this type. Typo or a token of another type
The channel does not offer the language {{ locale }}. Language does not belong to the chosen channel
A template for this channel, language and type already exists. Duplicate combination of channel, type and language

Empty placeholders

If any placeholder of a template has no value for a page (a product without the brand attribute, the {taxon.parent} of a root category), the whole template is skipped for that page and the next stage of the priority chain applies. Nothing half-filled such as – | Shop is ever output. The rule is the same for every page: a template is either fully resolvable or not used.

A template that contains no letter or digit after replacement (only separators) is skipped as well.

If a template should apply to every product, use only placeholders that every product has. For the others the fallback (the name) is the safe result.

Example: title template with a brand

Template (type Product, all languages) in the channel Wohnwelt:

{product.name} – {attribute.brand} | {channel.name}
Product Attribute brand Result
Ledersofa Milano Casa Nova Ledersofa Milano – Casa Nova \| Wohnwelt
Ecksofa Bergamo mit Schlaffunktion und Bettkasten Casa Nova Ecksofa Bergamo mit Schlaffunktion und Bettkasten – Casa (cut to 60 characters at a word boundary, \| Wohnwelt is dropped)
Beistelltisch Lino (not maintained) Beistelltisch Lino (template skipped, fallback to the name)

The third product has no value for {attribute.brand}. Instead of a half-empty title the fallback applies, which is the product name.

Output and shortening

The output is plain text: HTML tags are removed, entities of rich-text sources are decoded, and runs of whitespace become one space. It is not escaped: Twig's autoescape (tags), the <title> replacement and the JSON encoder (API) each escape exactly once, so nothing is escaped twice.

Generated values are cut at a word boundary, never in the middle of a word, and separators left dangling at the cut (Name – Brand |) are removed.

Limit Ellipsis
Title 60 characters no
Description 155 characters (the ellipsis counts) yes, …

If the very first word is longer than the limit, the template counts as not resolvable instead of being cut. Both limits are configurable (mmd_sylius_seo_suite_pro.seo_templates.title_max_length, default 60, minimum 10, and description_max_length, default 155, minimum 20), see Configuration.

Only generated values are shortened. Hand-written meta titles and meta descriptions always stay unchanged. The placeholders {product.short_description} and {taxon.description} also shorten their own text to 150 characters (with …) before the template applies the overall limit.

Language and channel

The language list of the form offers the languages of all channels. On save, a language the chosen channel does not offer is refused. A row for a language (de_DE) applies to exactly that locale. A row without a language applies to every language of the channel. The exact language wins.

Title and description are chosen independently: a de_DE row that only defines a title still lets the description come from the all-languages row, and a blank template never shadows the other row. There is no fallback from de_DE to de. Per channel, type and language there is at most one row (checked when saving).

Example: rows for one channel

All-languages row

Field Value
Channel Wohnwelt (WOHNWELT)
Applies to Product
Language All languages
Meta title template {product.name} \| {channel.name}
Meta description template {product.short_description} Jetzt bei {channel.name} bestellen.

de_DE row

Field Value
Channel Wohnwelt (WOHNWELT)
Applies to Product
Language German (Germany)
Meta title template {product.name} – {attribute.brand} \| {channel.name}
Meta description template (empty)

Result on a de_DE page

The title comes from the de_DE row, the description from the all-languages row, because the de_DE row's description template is empty. On an en_US page both values come from the all-languages row.

Storefront and API

The generated values travel through the same MetaData as the hand-written ones. They end up in the <title>, the description meta tag, og:title/og:description and twitter:title/twitter:description of the Storefront. In the Shop API they appear in title, description, openGraph and twitterCard of GET /api/v2/shop/seo/products/{code} and GET /api/v2/shop/seo/taxons/{code}. There is no logic in Twig or in a controller. The home page takes only its description from a template (type Other).

Shop API / Headless: The endpoint fields and the response format are described on the Shop API page. A headless frontend receives the fully resolved title and description and does not have to evaluate templates or priorities itself.

Own placeholders

Implement Mmd\SyliusSeoSuiteProPlugin\Seo\Template\PlaceholderProviderInterface. Every service implementing it is tagged mmd_sylius_seo_suite_pro.placeholder_provider automatically. The first provider that supports a token wins; use the tag's priority to run before a built-in one. A token is accepted in the admin if and only if a provider supports it, so the validation always matches what renders.

final class WarrantyPlaceholderProvider implements PlaceholderProviderInterface
{
    public function supports(string $token, string $subjectType): bool
    {
        return 'warranty' === $token && SeoTemplateInterface::SUBJECT_PRODUCT === $subjectType;
    }

    public function resolve(string $token, PlaceholderContext $context): ?string
    {
        // $context->subject, ->channel, ->locale; remember() memoises something expensive per render.
        // Return plain text. Null or '' means "no value": the whole template is skipped.
        return $context->subject instanceof ProductInterface ? '2 years warranty' : null;
    }
}

Providers must return plain text (use PlainText::fromHtml() for rich text) and must not escape. The help text of the form lists the built-in placeholders only. The renderer itself is SeoTemplateRendererInterface (service mmd_sylius_seo_suite_pro.seo_template_renderer) and can be decorated or replaced. Details on all extension points are on the Extending page.

Cost per request

The template lookup reads all rows of the current channel with one query per request and keeps them in memory (reset between requests). Title and description of any number of asks are served from that. A template with attribute placeholders fetches all attribute values of the product, joined with their definitions, in one more query, and only when an {attribute.*} token is actually used.

Title and description of a page share that lookup (the placeholder context is kept per subject, channel and locale), so it runs once per page however many attribute placeholders the templates use. The Product node of the Structured Data uses the same query.

If the template table is missing (plugin active, migrations not run yet), that counts as "no templates": the shop keeps its previous behaviour instead of answering with a 500 error. A warning is written to the log. Run the migrations with mmd:seo-suite-pro:setup, see Installation.

See also