Sylius offre une base technique idéale pour un SEO e‑commerce solide, mais tout n’est pas “plug‑and‑play”. Si votre objectif est de capter du trafic acheteur et d’éviter la cannibalisation de l’index, vous avez besoin d’un socle: rich snippets, hreflang, sitemaps, canoniques, et une stratégie claire pour la pagination et les facettes. Voici un plan d’implémentation opérationnel, pensé pour Sylius, et concentré sur le trafic transactionnel.
Objectif: indexer ce qui vend, canaliser le reste
Avant les recettes techniques, clarifiez la stratégie:
- Indexer: pages produits actives, catégories stratégiques, quelques landing pages “marque x catégorie”.
- Découvrir sans indexer: pages profondes de pagination, facettes secondaires, tris.
- Bloquer: URLs parasites (sessions, UTM), duplications (page=1), variantes non différenciées.
Le reste de l’article détaille comment y parvenir proprement dans Sylius.
Rich snippets avec schema.org: Product, Offer, Breadcrumb
Les extraits enrichis augmentent le CTR et filtrent un trafic plus “chaud”. Utilisez JSON‑LD pour éviter les erreurs d’interprétation.
Product + Offer sur la fiche produit
Exemple Twig minimaliste (à adapter à vos entités/attributs):
{# templates/product/show.html.twig #}
{% set variant = product.variants.first() %}
{% set pricing = variant.channelPricings[sylius.channel.code] ?? null %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Product",
"name": "{{ product.name|e('html_attr') }}",
"image": [{% for image in product.images %}"{{ asset(image.path) }}"{% if not loop.last %}, {% endif %}{% endfor %}],
"description": "{{ product.shortDescription|striptags|u.truncate(160)|e('html_attr') }}",
"sku": "{{ variant.code }}",
{% if product.brand is defined and product.brand %}
"brand": {"@type":"Brand","name":"{{ product.brand.name|e('html_attr') }}"},
{% endif %}
"offers": {
"@type": "Offer",
"url": "{{ app.request.uri }}",
"priceCurrency": "{{ sylius.channel.baseCurrency.code }}",
"price": "{{ pricing ? (pricing.price/100) : '' }}",
"availability": "https://schema.org/{% if variant.onHand > 0 %}InStock{% else %}OutOfStock{% endif %}"
}
{% if product.averageRating is defined and product.averageRating %}
, "aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "{{ product.averageRating }}",
"reviewCount": "{{ product.reviewCount }}"
}
{% endif %}
}
</script>
Bonnes pratiques:
- Renseignez “brand”, “sku”, “gtin”/“mpn” si disponibles.
- Gardez prix, stock et URL parfaitement synchronisés avec l’affichage.
- Utilisez un include Twig pour mutualiser le bloc JSON‑LD.
BreadcrumbList sur catégories et produits
{# templates/_partials/breadcrumb.jsonld.html.twig #}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{% for crumb in breadcrumbs %}
{
"@type": "ListItem",
"position": {{ loop.index }},
"name": "{{ crumb.label|e('html_attr') }}",
"item": "{{ crumb.url }}"
}{% if not loop.last %},{% endif %}
{% endfor %}
]
}
</script>
Organization (homepage) et FAQ ciblées
- Ajoutez un “Organization” global (logo, contact, sameAs).
- Pour certaines catégories, une section FAQ (schema.org/FAQPage) peut capter de la longue traîne transactionnelle sans “blabla” inutile. Testez son impact sur le CTR.
Astuce: validez systématiquement avec l’outil de test des résultats enrichis (https://validator.schema.org/) et surveillez les rapports dans la Search Console (https://search.google.com).
Hreflang: multi‑pays et multi‑langues propres aux channels Sylius
Sylius structure nativement canaux et locales. Exploitez‑les pour des balises hreflang sans collisions.
Générer les alternates
{# templates/_seo/hreflang.html.twig #}
{% set route = app.request.attributes.get('_route') %}
{% set params = app.request.attributes.get('_route_params') ?? {} %}
{% for locale in sylius.channel.locales %}
{% set href = path(route, params|merge({'_locale': locale.code}))|absolute_url %}
<link rel="alternate" hreflang="{{ locale.code }}" href="{{ href }}">
{% endfor %}
{# x-default: page “générique” ou fallback #}
<link rel="alternate" hreflang="x-default" href="{{ path(route, params|merge({'_locale': sylius.channel.defaultLocale.code}))|absolute_url }}">
Rappels critiques:
- 1 URL = 1 langue + 1 devise cohérente. Ne changez pas de devise via cookie sans changer d’URL.
- Canonical et hreflang doivent pointer vers des versions 200 (pas de 3xx ni 404).
- Les alternates sont réciproques entre elles.
Sitemaps: index, images et sections par locale
Un sitemap bien segmenté accélère la découverte. Évitez d’y exposer des pages non canoniques ou faibles.
Routage
## config/routes/sitemap.yaml
sitemap_index:
path: /sitemap.xml
controller: App\Controller\SitemapController::index
sitemap_section:
path: /sitemaps/{section}-{_locale}.xml
controller: App\Controller\SitemapController::section
requirements: { _locale: "fr|en|de" }
Contrôleur simplifié
<?php
// src/Controller/SitemapController.php
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
final class SitemapController
{
public function __construct(
private UrlGeneratorInterface $urls,
private array $locales = ['fr','en'],
) {}
public function index(): Response
{
$xml = <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
XML;
foreach ($this->locales as $locale) {
foreach (['products','categories','cms'] as $section) {
$loc = $this->urls->generate('sitemap_section', ['section'=>$section, '_locale'=>$locale], UrlGeneratorInterface::ABSOLUTE_URL);
$lastmod = (new \DateTimeImmutable())->format(DATE_W3C);
$xml .= "<sitemap><loc>{$loc}</loc><lastmod>{$lastmod}</lastmod></sitemap>";
}
}
$xml .= '</sitemapindex>';
return new Response($xml, 200, ['Content-Type'=>'application/xml', 'Cache-Control'=>'public, s-maxage=3600']);
}
public function section(string $section, string $_locale): Response
{
// Récupérez vos URLs canoniques (produits publiés, catégories indexables…)
$urls = [
['loc' => 'https://exemple.com/produit-1', 'lastmod' => '2025-01-10T10:00:00+00:00', 'images' => ['https://exemple.com/media/p1.jpg']],
// ...
];
$xml = '<?xml version="1.0" encoding="UTF-8"?>';
$xml .= '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">';
foreach ($urls as $u) {
$xml .= "<url><loc>{$u['loc']}</loc><lastmod>{$u['lastmod']}</lastmod>";
foreach (($u['images'] ?? []) as $img) {
$xml .= "<image:image><image:loc>{$img}</image:loc></image:image>";
}
$xml .= '</url>';
}
$xml .= '</urlset>';
return new Response($xml, 200, ['Content-Type'=>'application/xml', 'Cache-Control'=>'public, s-maxage=3600']);
}
}
Bonnes pratiques:
- Générer des sitemaps par type et par locale; limitez à 50k URLs/50 Mo par fichier.
- Inclure “image:image” pour les produits.
- Ne listez que des URLs canoniques HTTP 200.
- Page catégorie paginée: n’incluez que la page 1 (ou la meilleure landing), pas les pages 2+.
Canoniques, tri, pagination: garder un graphe d’URLs propre
Canonical dynamique “nettoyé”
Ne conservez que les paramètres utiles (ex: brand), supprimez tri, ordre, page, utm, session…
{# templates/_seo/canonical.html.twig #}
{% set allowed = ['brand'] %}
{% set qs = [] %}
{% for k, v in app.request.query.all %}
{% if k in allowed and v is not empty %}
{% set qs = qs|merge([k ~ '=' ~ (v|url_encode)]) %}
{% endif %}
{% endfor %}
{% set path = app.request.pathInfo %}
{# Éviter ?page=1 #}
{% if app.request.query.get('page') == 1 %}
{% set qs = qs|filter(v => not v starts with('page=')) %}
{% endif %}
{% set canonical_url = app.request.schemeAndHttpHost ~ path ~ (qs ? '?' ~ qs|join('&') : '') %}
<link rel="canonical" href="{{ canonical_url }}">
Si votre Twig ne supporte pas “filter” avec lambda, retirez page=1 plus haut dans la logique.
Pagination: self‑canonical ou consolidation sur page 1 ?
- Objectif trafic long: self‑canonical sur page > 1, rel prev/next pour cohérence UX.
- Objectif consolidation: canonical de toutes les pages vers page 1 pour concentrer les signaux (mais ces pages seront moins susceptibles d’être indexées).
Rel prev/next (utile pour d’autres moteurs/UX, même si Google ne s’en sert plus):
{% if page > 1 %}
<link rel="prev" href="{{ path(route, params|merge({'page': page-1}))|absolute_url }}">
{% endif %}
{% if page < pages %}
<link rel="next" href="{{ path(route, params|merge({'page': page+1}))|absolute_url }}">
{% endif %}
Facettes: ouvrir le trafic acheteur, fermer la duplication
Le danger n°1 en e‑commerce: l’explosion combinatoire des filtres. Stratégie recommandée:
- Définissez une short‑list de filtres indexables par catégorie (ex: brand, taille adulte/enfant).
- Créez des “landing pages” stables (URL propres, contenu descriptif, maillage interne).
- Tout le reste: meta robots noindex,follow ou canonical vers la catégorie sans filtre.
Exemple: meta robots pour facettes non indexables
{# templates/_seo/robots-facets.html.twig #}
{% set indexables = ['brand'] %}
{% set nonIndexableFound = false %}
{% for k in app.request.query.all|keys %}
{% if k not in indexables and k not in ['page','sort','order'] %}
{% set nonIndexableFound = true %}
{% endif %}
{% endfor %}
{% if nonIndexableFound %}
<meta name="robots" content="noindex,follow">
{% endif %}
Complétez avec:
- Un routage dédié pour les landing “catégorie + marque” (URL sans paramètre ou paramètre unique stable).
- Un titre/meta/intro uniques par landing pour éviter le contenu mince.
Robots.txt et paramètres à neutraliser
- Bloquez le crawl des URLs techniques (ex: /admin, /cart, /checkout).
- Ne bloquez pas via robots.txt des pages que vous canonicalisez: laissez Google les voir pour comprendre les canoniques.
- Nettoyez côté serveur les paramètres parasites (utm_*, gclid, fbclid) avant de construire la canonical.
Exemple d’allowlist côté application avant génération de canonical et de sitemap:
- allow: brand
- drop: page, sort, order, min_price, max_price, color, size, utm_*, gclid, fbclid
Recettes rapides (check‑list)
- Product/Offer JSON‑LD sur toutes les fiches + BreadcrumbList sur liste et produit.
- Hreflang par channel/locale, avec x‑default.
- Canonical “propre” sans page=1 ni paramètres non indexables.
- rel prev/next sur listes; stratégie canonical claire pour pages 2+.
- Sitemaps segmentés par type et locale, uniquement URLs canoniques 200, avec images.
- Facettes: landing pages sélectionnées; le reste en noindex,follow ou canonical vers la catégorie.
- Titre/meta templates par type (produit, catégorie, landing) avec variables business.
- Redirections 301 pour éviter les doublons de trailing slash, majuscules, etc.
- Monitoring: Search Console (couverture, sitemaps, rich results), logs crawls.
- Automatisation: regénération des sitemaps via Messenger/cron lors de publications massives.
Mesure et itérations
- Analysez “Pages valides mais non indexées” vs “Exclues” dans la Search Console pour détecter les facettes qui fuient.
- Vérifiez le rapport “Améliorations” pour les rich snippets Product/Review.
- Log analysis: identifiez les crawls gaspillés (tri, facettes secondaires) et durcissez vos règles.
- Testez l’ouverture de 3‑5 nouvelles landing facettes par mois; mesurez sessions organiques, CTR, taux de conversion.
Conclusion: un socle SEO Sylius qui vend… et qui tient
Un SEO e‑commerce durable sur Sylius repose sur des fondations propres: rich snippets fiables, hreflang réciproques, sitemaps sélectifs, canoniques stricts et facettes sous contrôle. Cette combinaison capte un trafic prêt à acheter, sans diluer l’index.
Vous voulez auditer votre implémentation actuelle ou accélérer le déploiement de ce socle sur votre boutique Sylius ? Contactez‑nous pour un diagnostic et un plan d’action en 10 jours.