Sylius SEO Suite Pro: Redirects
Redirects in Sylius SEO Suite Pro: 301/302, chains and loops, automatic redirects on slug change, 404 monitoring, CSV import and export, console commands and settings.
Sylius SEO Suite Pro: Redirects
Redirects keep rankings and backlinks intact when a URL changes. A redirect answers a request for an old path with a 301 or 302 to the new one. You maintain them by hand, they are created automatically when a slug changes, and you can import and export them as CSV. On top of that, 404 monitoring counts which paths lead nowhere.
Overview
- A redirect is only served when the shop has no page of its own for the path. Existing pages cost nothing.
- There is at most one hop per request, so chains are flattened when they are saved.
- Paths are compared without regard to upper or lower case.
- Redirects apply to the storefront. There is no API resource for them (see the Shop API / Headless section below).
A redirect in detail
| Field | Description |
|---|---|
Old URL path (source) |
The requested path including the shop's locale prefix, for example /en_US/products/old-name. Normalized on save: leading slash, no host, no query string, no fragment, no trailing slash (except for the root /). A pasted full URL is cut down to its path. Up to 255 characters. |
Target (target) |
A path on the shop (starts with exactly one /) or an absolute http(s):// URL. Nothing else. Up to 2048 characters. |
Redirect type (status) |
301 (default) or 302. Other values are rejected. |
| Channel | A single channel, or empty for all channels. |
| Active | A disabled redirect is kept but not served, and it takes no part in loop checks. |
There is exactly one redirect per path and channel. A redirect for the visitor's channel wins over one for all channels.
Paths are not case-sensitive:
/Sofaand/sofaare the same path. Nothing else is folded together,/überand/uberor/straßeand/strassestay different paths.
The list also shows the hit counter, the time of the last hit and the origin (Manual, Automatic on slug change, Import).
When a redirect runs
graph TD
A["Storefront request"] --> B{"Does the shop have a page?"}
B -->|"Yes"| C["Page is served normally"]
B -->|"No, 404"| D{"GET or HEAD, no admin, no API, no path starting with underscore?"}
D -->|"No"| E["Plain 404 page, nothing is counted"]
D -->|"Yes"| F{"Active redirect found for path and channel?"}
F -->|"Yes"| G["Increase hit counter"]
G --> H["301 or 302 to the target, query string is appended"]
F -->|"No"| I{"404 log enabled?"}
I -->|"Yes"| J["Count the path in the 404 log, then plain 404 page"]
I -->|"No"| K["Plain 404 page"]
Only storefront GET and HEAD requests are handled. These are left alone:
- the admin area (path from
sylius_admin.path_name,/adminby default) - the API (
/api) - Symfony's internal routes, meaning everything that starts with
/_(profiler, web debug toolbar, fragments)
Cost per request:
| Case | Database access |
|---|---|
| A normal, existing page | none |
| A 404 with a redirect | one lookup and one UPDATE (hit counter, last hit) |
| A 404 without a redirect | one lookup, plus counting in the 404 log |
If the redirect table is missing, the lookup fails or a stored target is invalid, the visitor gets the normal 404 page. The error is logged, there is never a 500.
Query string
The request's query string is appended exactly as the visitor sent it when the target is a path without a query string of its own. Campaign parameters survive the redirect:
| Request | Target | Redirects to |
|---|---|---|
/en_US/old?utm_source=mail |
/en_US/new |
/en_US/new?utm_source=mail |
/en_US/old?utm_source=mail |
/en_US/new?ref=1 |
/en_US/new?ref=1 (own query is kept) |
/en_US/old?utm_source=mail |
https://example.com/new |
https://example.com/new (an absolute target receives nothing) |
Non-ASCII characters in the target (umlauts, a stray space) are percent-encoded in the Location header.
Browsers cache a 301 and search engines pass it on. Use 302 for anything that is only temporary.
Chains and loops
On save in the admin and during import, the plugin checks and flattens:
- The target must not be the source. If following the existing active redirects from the target leads back to the source (A to B and B to A, or a longer cycle), you get a form error and nothing is saved. A chain longer than ten hops also counts as a cycle.
- New redirect X to Y while Y to Z exists: X points straight at Z.
- If W to X exists and you create X to Y, W is bent to Y.
- If any redirect on the way is a 302, the resulting redirect is a 302 as well.
- Only active redirects in the same channel count. A redirect for all channels covers every channel, one for a single channel only that one.
graph LR
A["Before: /a"] -->|"301"| B["/b"]
C["After: /a"] -->|"301"| E["/c"]
D["After: /b"] -->|"301"| E
The example shows a new redirect from /b to /c while /a already points at /b: /b now points at /c, and the existing redirect from /a is bent to /c too. The visitor only ever needs one hop.
Flattening is a snapshot taken at save time. If you change a redirect's target later, redirects that point at it keep the old target. A redirect for all channels is only flattened through other redirects for all channels. If it points at a path that a single channel redirects elsewhere, visitors in that channel make two hops.
A redirect for all channels is checked for loops against the redirects of every channel. If one channel has
/bto/a, then/ato/bfor all channels is a form error.
Automatic redirects on a slug change
When the slug of a product or category translation changes, the plugin automatically creates a 301 from the old to the new URL (origin Automatic). This applies to every changed locale and every channel in which the page is shown and that offers the locale. A product counts in its own channels, a category in all channels. The shop router builds the paths from the routes sylius_shop_product_show and sylius_shop_product_index, so the locale prefix of your routes is respected.
Example: you change a product's slug from old to new. It is sold in the channels web and outlet, both offer en_US. Two automatic redirects now exist:
| Source | Target | Status | Channel | Origin |
|---|---|---|---|---|
/en_US/products/old |
/en_US/products/new |
301 | web |
Automatic |
/en_US/products/old |
/en_US/products/new |
301 | outlet |
Automatic |
The exact spelling of the paths depends on your shop's routes. If a redirect from /en_US/products/older to /en_US/products/old already existed, it is bent to /en_US/products/new so the visitor stays at one hop.
To switch it off:
# config/packages/mmd_sylius_seo_suite_pro.yaml
mmd_sylius_seo_suite_pro:
redirects:
auto_on_slug_change: false
The details:
- If you rename back (
atoband laterbtoa), the redirect that has the new URL as its source is removed instead of building a loop. A redirect that already has the old URL as its source is overwritten. Redirects that pointed at the old URL follow the new one. Repeated renames therefore stay one hop deep. - A change that only differs in letter case creates nothing.
- Disabled products and categories get the redirect too. It takes effect once they are enabled again.
- The listener collects the change while Doctrine flushes and writes the redirect afterwards. Any error is logged and swallowed, saving the product or category is never blocked. If the flush is rolled back, no redirect is created.
- Children of a category need nothing: Sylius stores the full permalink in every taxon translation and does not rewrite the children when a parent's slug changes. If your shop rewrites them through the ORM, every changed translation is a slug change of its own and gets its redirect.
Slugs changed by a DQL or SQL bulk update or by direct SQL do not trigger Doctrine listeners and create no redirect. Renaming thousands of products in one flush writes that many rows. That is a one-off cost of the change itself, not a cost of page views.
404 monitoring
A 404 without a matching redirect is counted per path and channel: path, number of requests, first and last time and the last referrer (shortened to 255 characters). The 404 log screen lists the paths, most requested first.
Growth is bounded:
- Asset-like paths are not counted, namely file extensions such as
css,js,json,xml,txt, images (png,jpg,webp,svg,icoand more), fonts, video and audio,pdf,zipand probing endingsphp,asp,aspx,jspandcgi. - Paths longer than 255 characters, invalid UTF-8 and paths with control characters are not counted.
- The log holds about
redirects.not_found_log_limitrows (default 5000). When it is full, a new path replaces the row that was seen longest ago, so the log always shows what is requested right now. Counting and inserting are not atomic, so parallel requests can overshoot the limit by a few rows.
Remove old rows with a cron job:
bin/console mmd:seo-suite-pro:redirects:prune-log # not seen for 90 days
bin/console mmd:seo-suite-pro:redirects:prune-log --days=30
# Crontab: daily at 03:30
30 3 * * * cd /var/www/shop && bin/console mmd:seo-suite-pro:redirects:prune-log --days=60
--days must be a whole number, zero or more. Switch counting off with redirects.log_not_found: false. If the table has not been migrated yet, counting is skipped silently.
From the 404 log, the row action Create redirect opens the redirect form with path and channel prefilled. As soon as you save a redirect for the path, the row disappears from the log. The audit dashboard additionally shows the ten most requested 404 paths without a redirect, see SEO audit.
CSV import and export
The format has four columns, the first row names them:
source,target,status,channel
/en_US/products/old-name,/en_US/products/new-name,301,
/en_US/old-page,https://example.com/new-page,302,shop
/en_US/summer-sale,/en_US/sale,,outlet
| Column | Required | Description |
|---|---|---|
source |
yes | Old path, normalized as in the form |
target |
yes | Target path or absolute http(s) URL |
status |
no | 301 or 302. Empty means 301. Anything else is a row error. |
channel |
no | Channel code. Empty means all channels. An unknown code is a row error. |
The import is tolerant about what spreadsheets produce:
- UTF-8 with or without a byte order mark. If the file is not valid UTF-8, it is read as Windows-1252.
- Comma, semicolon or tab as delimiter, detected from the header line.
- Any column order, upper or lower case column names, empty lines are skipped.
Every row goes through the same validation as the admin form, including the chain and loop rules, and is saved on its own. A later row therefore sees the earlier ones, and one bad row never costs the others. Bad rows are reported with their line number and skipped. Imported redirects have the origin Import.
Existing redirects are not overwritten. A row for a path and channel that already exists is an error. Delete or edit the existing redirect first.
Limits: 1 MB file size and 2000 data rows. A larger file is refused as a whole. From row 2001 the import stops with a message, the rows read up to then stay saved. If the header lacks source or target, nothing is imported.
Using import and export
Admin
- Open SEO Suite Pro > Redirects.
- Click Import and choose the CSV file.
- After submitting, the screen shows the number of imported redirects and a list of rejected rows with line numbers.
- The Export button downloads the file
redirects.csv.
Console
bin/console mmd:seo-suite-pro:redirects:import redirects.csv
bin/console mmd:seo-suite-pro:redirects:export redirects.csv
bin/console mmd:seo-suite-pro:redirects:export > redirects.csv
The import prints every rejected row as line N: ... and reports at the end how many redirects were imported and how many lines were refused. If at least one line failed, the command exits with an error code, the valid lines are saved anyway. Without a file argument, the export writes to standard output.
The export contains only
source,target,statusandchannel, and it contains all redirects, disabled ones included. The active switch, the origin and the hit counter are not exported. A re-import makes every redirect active.
Admin screens
The plugin has a single sidebar entry (group MMKreativ Plugins > SEO Suite Pro). You reach all other screens through buttons:
| Screen | Reached via | Actions |
|---|---|---|
| Redirects | SEO Suite Pro > Redirects button | Create, edit, delete, bulk delete |
| 404 log | Redirects > 404 log button | Create redirect (prefilled), delete, bulk delete. No create or edit. |
| Import | Redirects > Import button | Upload CSV |
| Export | Redirects > Export button | Download CSV |
The redirect list can be searched by source and target and filtered by channel (rows for all channels included), origin and active. You can sort by hits and last hit, among others. The 404 log is searched by path and referrer and filtered by channel. By default it is sorted by the number of requests, descending.
The lists are Sylius grids (mmd_sylius_seo_suite_pro_plugin_admin_seo_redirect and mmd_sylius_seo_suite_pro_plugin_admin_seo_not_found_log).
Console commands
| Command | Purpose | Options |
|---|---|---|
mmd:seo-suite-pro:redirects:export [file] |
All redirects as CSV | file optional, otherwise standard output |
mmd:seo-suite-pro:redirects:import file |
Import CSV, same validation as in the admin | file required |
mmd:seo-suite-pro:redirects:prune-log |
Delete old rows from the 404 log | --days / -d, default 90 |
Put prune-log into a cron job, export and import are manual tools.
Settings
All keys live under mmd_sylius_seo_suite_pro.redirects. More on configuration is on the Configuration page.
| Setting | Default | Description |
|---|---|---|
redirects.auto_on_slug_change |
true |
Automatically creates a 301 when a product or category slug changes |
redirects.log_not_found |
true |
Counts paths that end in 404 and have no redirect |
redirects.not_found_log_limit |
5000 |
Upper limit of rows in the 404 log (at least 1) |
# config/packages/mmd_sylius_seo_suite_pro.yaml
mmd_sylius_seo_suite_pro:
redirects:
auto_on_slug_change: true
log_not_found: true
not_found_log_limit: 5000
Security
- Targets and sources are validated: the target is a path with exactly one leading
/or anhttp(s)://URL with a host and without credentials. No other scheme (javascript:,data:,ftp:), no//host, no whitespace, no control character, no backslash. A line break can therefore never split a header. - The screens sit behind the admin login. Create, edit, delete and bulk delete carry Sylius' CSRF token, the import form carries its own. The export is a read-only
GET. - The uploaded file is only read from PHP's temporary upload and is never moved, stored or executed.
- On export, cells that a spreadsheet would read as a formula are protected with a leading quote.
Shop API / Headless
Redirects are an HTTP-level concern, like robots.txt: they answer the request before any page is rendered. The listener handles storefront requests only, the API (/api) is excluded, and there is no API resource for redirects. A headless frontend on another host has to handle its own 404 pages, because redirects are only looked up on the path of the shop's own host. You can read them out through the CSV export and carry them into the frontend.
Troubleshooting
| Symptom | Cause and solution |
|---|---|
| The redirect does not trigger | The path exists in the shop as a real page, redirects only apply to a 404. Also check Active, channel and the spelling including the locale prefix. |
| Import row rejected, already exists | Path and channel already exist, existing redirects are not overwritten. |
| Form error about a loop | The chain leads back to the source, see Chains and loops. |
| No redirect after a slug change | Automatic redirects are switched off, the channel does not offer the locale, or the slug was changed by an SQL bulk update. |
| The log stays empty | redirects.log_not_found is off, the path is asset-like, or the migration is missing (mmd:seo-suite-pro:setup). |
More help: Troubleshooting and Technical details.