Sylius SEO Suite Pro: Technical Details
Technical details of Sylius SEO Suite Pro: SeoData pipeline diagram, eight tables with ER diagram, migrations, plugin structure, request lifecycle, naming conventions and testing.
Sylius SEO Suite Pro: Technical Details
This page describes the plugin's structure for integrators: the data flow of a request, the database tables, the directory layout and the naming conventions. Extension points are covered on the extending page.
Architecture: the SeoData pipeline
Storefront and Shop API read from the same source, the SeoDataProviderInterface. This keeps the two frontends from drifting apart. The result is an immutable SeoData object holding meta data, Open Graph, Twitter Card, structured data and hreflang alternates.
graph TD
REQ["Storefront request"] --> HOOK["Twig hook sylius_shop.base#metatags"]
HOOK --> FN["Twig function mmd_seo_suite_pro_seo"]
FN --> CTX["RequestSubjectResolver: product, category or home page"]
CTX --> PROV["SeoDataProviderInterface"]
API["API Platform: GET /shop/seo/products/code and /shop/seo/taxons/code"] --> APIP["SeoResourceProvider"]
APIP --> PROV
PROV --> META["SubjectMetaDataResolver: meta title, description, canonical, robots"]
META --> TPL["SEO template renderer and placeholders"]
META --> FACET["FacetRobotsPolicy"]
PROV --> SCHEMA["StructuredDataBuilder: schema providers and enrichers"]
PROV --> SOCIAL["SocialTagsFactory and OgImageCardUrlResolver"]
PROV --> HREF["HreflangAlternatesBuilder"]
META --> DATA["SeoData"]
SCHEMA --> DATA
SOCIAL --> DATA
HREF --> DATA
DATA --> TWIG["Template shop/metatags.html.twig"]
DATA --> RES["API resource Seo"]
TWIG --> TITLE["TitleListener replaces title on kernel.response"]
The steps in detail:
- The hook
sylius_shop.base#metatagscalls the Twig functionmmd_seo_suite_pro_seo(). TheRequestSubjectResolverfinds the page's subject again from route and slug (sylius_shop_product_show,sylius_shop_product_index), because the hook runs with almost no Twig context. On all other pages (cart, checkout, account) the function returnsnull, and the home page is recognised throughsylius_shop_homepage. - The provider assembles the parts: meta data including templates, facet robots for filtered listings, JSON-LD nodes, Open Graph and Twitter Card (with the URL of a generated card if there is one) and the hreflang alternates. For hreflang the rule is: not on
noindexpages, not on filtered listings and not beyond page 1. - The template
shop/metatags.html.twigrenders meta tags, Open Graph, Twitter, canonical, hreflang and one JSON-LD block per node (throughmmd_seo_suite_pro_json_ld(), which escapes against breaking out of the script tag). - The
<title>is replaced in the finished HTML, because the Sylius layout renders it through a Twig block instead of a hook. - The Shop API (
/shop/seo/products/{code},/shop/seo/taxons/{code}) uses the same provider. See Shop API.
Tables and data model
The plugin creates nine tables. All carry the prefix mmd_sylius_seo_suite_pro_, and none has a foreign key to a Sylius core table. The link to a channel is purely logical through the channel_code column.
erDiagram
SYLIUS_CHANNEL ||--o| CHANNEL_SETTINGS : channel_code
SYLIUS_CHANNEL ||--o{ SEO_TEMPLATE : channel_code
SYLIUS_CHANNEL ||--o| HREFLANG_SETTINGS : channel_code
SYLIUS_CHANNEL ||--o| SCHEMA_SETTINGS : channel_code
SYLIUS_CHANNEL ||--o| OG_IMAGE_SETTINGS : channel_code
SYLIUS_CHANNEL ||--o{ REDIRECT : channel_code
SYLIUS_CHANNEL ||--o{ NOT_FOUND_LOG : channel_code
SYLIUS_CHANNEL ||--o{ AUDIT_RUN : channel_code
AUDIT_RUN ||--o{ AUDIT_ISSUE : run_id
The diagram shows logical relationships only. There are no database foreign keys, neither between the plugin and Sylius nor between an audit run and its findings. The link runs through
channel_codeandrun_id. For a redirect,channel_codeis optional (empty means all channels).
| Entity | Table | Purpose |
|---|---|---|
ChannelSettings |
mmd_sylius_seo_suite_pro_channel_settings |
Per-channel settings: robots.txt content, facet robots, social tags, Open Graph and Twitter details |
SeoTemplate |
mmd_sylius_seo_suite_pro_template |
Title and description templates per channel, language (empty = all) and type |
HreflangSettings |
mmd_sylius_seo_suite_pro_hreflang_settings |
hreflang per channel: activation, group, x-default language |
SchemaSettings |
mmd_sylius_seo_suite_pro_schema_settings |
Attribute mapping for GTIN, MPN, brand and additional properties in the Product node |
OgImageSettings |
mmd_sylius_seo_suite_pro_og_image_settings |
Design and switches of the generated social cards per channel |
Redirect |
mmd_sylius_seo_suite_pro_redirect |
Redirects (manual, automatic, import) with a hit counter |
NotFoundLog |
mmd_sylius_seo_suite_pro_not_found_log |
Log of 404 paths without a redirect |
AuditRun |
mmd_sylius_seo_suite_pro_audit_run |
One audit run per channel with status, heartbeat and lock (internal, plain SQL) |
AuditIssue |
mmd_sylius_seo_suite_pro_audit_issue |
A single finding of an audit run |
The entities (and their interfaces) live in src/Entity. The resources are registered with Sylius under the alias prefix mmd_sylius_seo_suite_pro_plugin.
Migrations
There are eight migrations in src/Migrations, one per feature block (channel settings, social columns of the channel settings, templates, redirects with the 404 log, hreflang, schema, audit, OG images). They are written for MySQL 8 and MariaDB only: on any other platform every migration skips itself (skipIf) and is recorded as executed, so the tables are not created.
The path columns of Redirect (source, source_key, target) and NotFoundLog (path, path_key) use the collation utf8mb4_bin. Redirect sources are compared through the lower-cased source_key, so matching is case-insensitive while the original path is preserved exactly. The audit lock relies on the unique lock_key, which is set to NULL after a run.
The migrations are executed with bin/console mmd:seo-suite-pro:setup. The command runs only this plugin's open migrations, never the host's, and requires the Sylius core migrations to be done first. Doctrine's table doctrine_migration_versions is the single source of truth. See installation.
Plugin structure
sylius-seo-suite-pro/
βββ src/
β βββ Api/ API Platform resource Seo and state provider
β βββ Audit/ audit run, store, dashboard and the checks (Check/)
β βββ Command/ console commands (setup, audit, og-image, redirects)
β βββ Controller/ /robots.txt, card route /seo/og/... and admin controllers (Admin/)
β βββ DependencyInjection/ configuration tree and bundle extension
β βββ Doctrine/ listener for automatic redirects on slug change
β βββ Entity/ Doctrine entities and their interfaces
β βββ EventListener/ 404 redirect, title replacement, resource listener for redirects
β βββ Factory/ redirect prefilled from the 404 log
β βββ Form/ admin forms (Type/) and SEO fields on the translation forms (Extension/)
β βββ Grid/ custom grid filters
β βββ Hreflang/ hreflang providers, builder, URL building and settings lookup
β βββ Menu/ admin sidebar entry
β βββ Migrations/ the eight migrations
β βββ Model/ interfaces and traits for the shop's translation classes
β βββ OgImage/ planner, source loading, renderer (GD), file cache, prune
β βββ Redirect/ path rules, store, chains, 404 recorder, CSV import and export
β βββ Schema/ schema providers (JSON-LD), enrichers, GTIN check, attribute loader
β βββ Seo/ meta data, SeoData, provider, template renderer, facets, sitemap exclusion
β βββ Settings/ repositories for the settings entities
β βββ Twig/ Twig functions and filters
β βββ Validator/ constraints for templates, redirects, hreflang and schema
β βββ MmdSyliusSeoSuiteProPlugin.php bundle class
βββ config/
β βββ config.yaml base bundle configuration
β βββ services.php service definitions and tags
β βββ grids/ admin grids
β βββ routes/ admin.yaml and shop.yaml (to be imported by the host)
β βββ twig_hooks/ hook registrations for admin and shop
βββ Resources/fonts/ shipped fonts DejaVu Sans and Sans Bold for the OG cards
βββ templates/
β βββ admin/ admin templates (audit, redirects, forms, grid actions)
β βββ shop/metatags.html.twig head template for the storefront
βββ translations/ messages and validators in German and English
βββ tests/ unit, integration, functional, Behat and TestApplication
Lifecycle of a storefront request
On every product, category and home page the SeoData pipeline runs exactly once. The result is stored as an attribute on the main request (_mmd_seo_suite_pro_data), so further calls of the Twig function within the same request cost nothing. The title is inserted into the finished HTML at kernel.response.
Cost and memoisation per request:
- Templates: the template rows of the current channel are loaded with a single query and kept in memory. Any number of requests for title and description are served from it. Only when a template uses an
{attribute.*}placeholder does one more query for the product's attribute values follow, and that also runs just once per page. - Settings: the lookups for hreflang, schema and OG images likewise read their rows once and keep them in memory. The channel list for hreflang is loaded once per request. The memory is cleared between requests through
kernel.reset. - OG images: while the page is built, only the card URL including its hash is computed from data in memory. No image is read, fetched or drawn. Drawing happens on the first request of the card URL, or ahead of time with
mmd:seo-suite-pro:og-image:generate. - Fault tolerance: missing tables and errors while building hreflang or single schema nodes do not take the page down. They are logged and the output is omitted.
- 404 handling: only when a 404 occurs (
kernel.exception, priority 10, GET and HEAD on the storefront only) one query for the redirect is added, on a match the hit count is incremented, otherwise the 404 log entry is written. Admin,/apiand/_*are excluded. - Slug changes: a Doctrine listener (
onFlush,postFlush) creates redirects. Errors there are logged and never block saving. - Not on shop requests: the audit, the prune commands and import and export run exclusively on the console or in the admin.
Naming conventions
| Area | Prefix or pattern | Example |
|---|---|---|
| Console commands | mmd:seo-suite-pro: |
mmd:seo-suite-pro:audit:run |
| Database tables | mmd_sylius_seo_suite_pro_ |
mmd_sylius_seo_suite_pro_redirect |
| Configuration root | mmd_sylius_seo_suite_pro |
config/packages/mmd_sylius_seo_suite_pro.yaml |
| Service IDs, parameters, translation keys | mmd_sylius_seo_suite_pro. |
mmd_sylius_seo_suite_pro.sitemap_exclusion |
| Service tags (extension points) | mmd_sylius_seo_suite_pro.<kind> |
mmd_sylius_seo_suite_pro.schema_provider |
| Manual admin routes | mmd_sylius_seo_suite_pro_admin_ |
mmd_sylius_seo_suite_pro_admin_audit |
| Generated resource routes | mmd_sylius_seo_suite_pro_plugin_admin_<resource>_<action> |
mmd_sylius_seo_suite_pro_plugin_admin_seo_redirect_index |
| Shop routes | mmd_sylius_seo_suite_pro_ |
mmd_sylius_seo_suite_pro_robots_txt, mmd_sylius_seo_suite_pro_og_image |
| Own hook roots | mmd_sylius_seo_suite_pro. |
mmd_sylius_seo_suite_pro.seo_audit.index |
| Hook keys inside core hooks, Twig functions | mmd_seo_suite_pro_ |
mmd_seo_suite_pro_seo |
| Resource alias and UI keys | mmd_sylius_seo_suite_pro_plugin |
mmd_sylius_seo_suite_pro_plugin.ui.<resource> |
Resource names carry a prefix (for example seo_suite_pro_channel_settings) because Sylius names the admin hooks after the resource part of the alias only. A generic name would show this plugin's buttons on other plugins' pages. Admin URLs and table names are not affected.
The admin routes sit under %sylius_admin.path_name%, not under a hardcoded /admin. The shop routes (/robots.txt and /seo/og/...) belong at the top level and not under the locale prefix.
Classes marked
@internal(for example the admin controllers and some listeners) are not extension API. Anything not covered by the interfaces on the extending page counts as an implementation detail. Extend through the tagged interfaces, see extending.
Development and tests
Little of this matters for integrators. The repository ships a Docker environment, no local PHP is needed:
make init # copy compose.override.yml, composer install, build assets, start containers
make static # PHPStan and ECS
make phpunit # unit, integration and functional tests
make php-shell # shell in the PHP container
The integration and functional tests build the schema themselves from the ORM mapping and need only a reachable database (APP_ENV=test). tests/TestApplication holds the product and category subclasses with their migration, as a shop writes them to opt in to the SEO fields. It is the reference for the guide in the installation page.