Search by

asconsulting / contao-zyppy-redirect

asconsulting

Back end crafted URL redirects for Contao, with first-order and passive execution tiers.

Package info

github.com/asconsulting/contao-zyppy-redirect

Homepage

Type:contao-bundle

pkg:composer/asconsulting/contao-zyppy-redirect

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

5.0.1 2026-10-08 03:01 UTC

This package is auto-updated.

Last update: 2026-10-09 19:34:55 UTC


README

Back end crafted URL redirects for Contao 5.3+, with per-rule control over when in the request lifecycle a rule is allowed to fire.

Replaces asconsulting/redirect_manager, whose rules could only ever run on the 404 page. Existing rules import automatically — see Migration from legacy version below.

Requirements

  • PHP 8.1 or newer
  • Contao 5.3 or newer, with no upper bound. The Zyppy Suite supports the LTS releases and is tested against the current non-LTS release as well: the test suite passes on core-bundle 5.3.0, 5.7.13 and 6.0.2 (last run 2026-10-07).

Installation

composer require asconsulting/contao-zyppy-redirect:^5.0

Then run the database migration. Contao Manager does this on install and on update; on the command line it is vendor/bin/contao-console contao:migrate. It creates the tl_zyppy_redirect table and, if the old redirect_manager table is present, imports its rules.

Usage

Creating a rule

Open Design > Redirects and add a rule. Only back end administrators see the module — see Permissions below.

Field Meaning
Category An arbitrary label for grouping. Searchable and filterable.
Precedence Lower numbers are checked first when more than one rule matches the same URL.
Execution tier Passive or First order — see The two execution tiers.
Match mode How the incoming URL is compared with the source — see Match modes.
Source The path to match, e.g. /old-page. In domain mode, the hostname to match. Include a query string only if the rule should match that query string alone.
Restrict to host Apply the rule on this domain only. Empty applies it on every domain.
Target type URL, Contao page, File, or Another host, same path.
Target URL / page / file / host The destination, according to the target type. In regex mode, $1 … $n in a target URL insert the captured groups.
HTTP status code 301, 302, 303, 307 or 308.
Publish rule, Show from, Show until Enable the rule, optionally for a date range.

The two execution tiers

Every rule carries a tier, and this is the feature the old module did not have.

Tier Fires Use it when
Passive (default) Only after every Contao page, route and file has failed to resolve — i.e. at the 404 The URL is genuinely gone. This is the safe default and what you want almost every time
First order Before Contao routes the request You need to override a page that still exists — retiring a live section, forcing a canonical domain, moving a URL whose old page has not been deleted yet

A first-order rule wins against a real Contao page. That is the point of it, and also the reason the default is passive: a rule created without thinking cannot take a live page offline.

Out of scope: anything above Contao at the host level. A physical index.html in the document root is served by the web server before PHP runs, and nothing in this bundle can override it.

Paths no rule can redirect

Both tiers refuse to act on a short list of prefixes, matched on a path segment boundary — so /contaoshop is redirectable while /contao/… is not. A rule pointed at one of these is stored and shown in the back end but will never fire, which is the one case where a rule can look correct and do nothing.

Prefix Why
/contao, /_contao The back end, and Contao's front end controller routes. A first-order rule here would lock you out of the site that defines the rule
/_wdt, /_profiler, /_fragment Symfony's profiler, toolbar and fragment handler
/assets contao_images generates images on the fly at /assets/images/{path}
/bundles, /files, /system Asset namespaces. A redirect aimed into one is far more likely to be a mistake than an intent, so they are excluded deliberately

/share is not on this list, despite Contao's installer creating a public/share directory. The real share route is /_contao/share, already covered above, so a page at /share is ordinary site space and stays redirectable.

Match modes

Mode Matches Notes
Exact path /old-page One indexed lookup. Stays fast on a table with hundreds of thousands of rows
Path prefix /old and everything under /old/ Does not match /older. The unmatched tail is appended to a URL or host target
Regular expression Whatever the pattern matches Include delimiters: #^/news/(\d+)$#. $1 … $n insert captured groups into the target URL
Domain The whole hostname source holds the host to match. The incoming path is carried across unchanged

Query strings

A rule stored as /old-page matches whatever the query string is, and the incoming query is forwarded onto the target.

Note that Contao itself strips tracking parameters — utm_*, gclid, fbclid, msclkid and similar — at its HTTP cache layer, before any application code runs. Those never reach a redirect rule and so are not forwarded. That is core behaviour, not something this bundle does. A rule stored as /old-page?id=7 matches only that exact query, and does not forward it again. If the target already carries its own query string, the target's wins.

Performance

The bundle is built for installations with very large redirect tables.

  • Exact rules are never loaded into memory. They resolve through one bound, indexed SELECT … LIMIT 1.
  • A tier holding no rules costs no database access per request — one cache read and the listener returns. Measured on a live site with an empty table: zero queries against tl_zyppy_redirect across 152 application queries covering both a 404 and a rendered page. Rebuilding a tier's cache entry costs two queries and happens at most once per 60 seconds, not once per request. On a site with no first-order rules, which is most sites, the kernel.request listener is effectively free.
  • Pattern rules are compiled into the application cache, one entry per tier, rebuilt when you save, delete, copy or restore a rule. Only patterns live in memory, and a site typically has very few.
  • Publish state is computed, never written. The old module ran two UPDATE statements on every 404 and on every back end list load; this one evaluates start/stop in the query.
  • Cost on a non-404 response for the passive tier is a single integer comparison.

The HTTP cache caveat — read this one

Contao's HTTP cache serves a page without running PHP. Neither listener fires for a URL that is already in the cache, so a first order rule added for an existing page does not take effect until that cache entry is invalidated.

This looks fine on a development site, where the cache is usually cold, and does not work in production. After adding a first-order rule for a URL that already resolves, invalidate the cache.

Redirect responses themselves are sent with no-store, so the redirect never becomes a cached page in its own right.

Permissions

tl_zyppy_redirect is restricted to back end administrators by a data container voter.

This is deliberate rather than conservative: a user who can create a redirect can point any URL on the site — its home page included, at the first-order tier — at an arbitrary external target. That is a site-defacement capability, so it is not grantable through tl_user_group.

Migration from legacy version

This bundle replaces asconsulting/redirect_manager. Install this bundle and run contao:migrate. Rules in tl_asc_redirect are copied into tl_zyppy_redirect automatically.

  • The old table is not touched. Both bundles can sit installed side by side while you check the result; the old module keeps working until you uninstall it.
  • The import is idempotent. It is keyed on the legacy row id, so rules added to the old table later are picked up on the next contao:migrate.
  • Every imported rule lands as passive. That is exactly what the old module did — it was a front end module on the 404 page, so no rule could fire before routing. Promoting one to first order is a deliberate, per-rule decision you make afterwards.
  • The old module's implicit target URL > target page > target file precedence becomes an explicit target type you can see in the palette.
  • Empty start/stop dates become NULL, not 0. A stored 0 renders as 1969-12-31 and breaks saving on servers west of UTC.

After importing, the redirect_404 front end module can be removed from your 404 page and the old bundle uninstalled.

Development

composer install
vendor/bin/phpunit --no-coverage

TODO.md records the test matrix run and what remains open.

Licence

Licensed under the GNU Affero General Public License v3.0 only (AGPL-3.0-only). See LICENSE. Copyright Andrew Stevens.