
Redirects in Joomla
Every website that lives long enough collects broken links. Articles get renamed, sections get restructured, whole sites get migrated to a new platform. Each change can break URLs that other websites, bookmarks, and search engines still point at.
Without a plan, every old link ends up on a generic 404 page, and the link equity that took years to build slowly evaporates.
Joomla solves this problem with a small but powerful core component called Redirects (com_redirect). It logs every missed URL, lets you point it at a working page, and sends the correct HTTP status code to browsers and search engines. No .htaccess edits and no server access needed.
How the Joomla Redirects component keeps broken links and old URLs under control
This article explains how the Redirects component works, how it captures 404 errors, how to use it during migrations, and how to avoid the most common mistakes. The goal is to help administrators, editors, and developers keep their Joomla sites clean, fast, and SEO-friendly.
1. The Basics
1.1 What is the Redirects Component?
The Redirects component (com_redirect) is Joomla's built-in tool for managing HTTP redirects and capturing 404 (Page Not Found) errors. It has shipped with Joomla core since version 1.6.
In short, it does four things:
- It watches for 404 errors through the System - Redirect plugin.
- It logs every missed URL in the database, together with the referrer and a hit counter.
- It lets you map old URLs to new ones with the correct HTTP status code.
- It supports bulk import for migrations, so you can prepare hundreds of redirects at once.
The flow is simple: an old URL returns a 404, Joomla logs it, an administrator fills in a new URL, and the next visitor gets redirected automatically.
1.2 Where Do I Find the Redirects?
In the Joomla 6 backend, three places are relevant:
Components → Redirects (manage redirects and 404 log)
Components → Redirects → Options (component-wide settings, ACL)
System → Manage → Plugins → System - Redirect (collector and handler)
The component lives at administrator/components/com_redirect/. The plugin that hooks into the error system lives at plugins/system/redirect/.
1.3 The Two Pieces
The Redirects feature is built from two parts that work together:
| Part | Role |
|---|---|
com_redirect |
Backend admin UI, bulk import, status codes. |
plg_system_redirect |
Error-event listener, fires on every 404. |
Both must be enabled. The component without the plugin is just a passive list. The plugin without the component has nothing to read from.
1.4 Enable the Plugin
A fresh Joomla install ships the System - Redirect plugin disabled. Until you switch it on, none of the redirects in the component fire and no 404s are logged.
Go to System → Manage → Plugins, search for redirect, and enable the plugin.
1.5 Your First Redirect
To create a redirect manually, open Components → Redirects → New and fill in:
| Field | Example |
|---|---|
| Old URL | /old-article |
| New URL | /new-shiny-article |
| Comment | "Renamed during 2026 content refresh" |
| Status | Enabled |
| HTTP Status Code | 301 Moved Permanently |
Save the form, then visit /old-article. The browser is forwarded to /new-shiny-article with a 301 status code.
2. The 404 Workflow
2.1 How Collection Works
With the plugin enabled and Collect URLs = Yes, the flow looks like this:
Visitor requests /missing-page
└── Joomla raises 404
└── plg_system_redirect.onError fires
├── Checks #__redirect_links for a published match
├── If found → redirect with the stored status code
└── If not → log the URL (unpublished) for later triage
When no match exists, Joomla still shows the 404 page to the visitor. The URL is just recorded silently for the administrator to handle later.
2.2 Collection Modes
Two plugin options control what gets stored:
| Option | What it does |
|---|---|
| Collect URLs | On = log every new 404. Off = only redirect existing entries, never grow the list. |
| Include URL with Domain Name | Store the full https://site.tld/path (Yes) or just the path /path (No). |
Full URLs are useful when one Joomla install serves multiple domains. Path-only is cleaner for a single domain.
2.3 Triaging the 404 List
Components → Redirects shows every captured URL. A typical workflow is:
- Sort by Hits descending - fix the most-visited 404s first.
- Open an entry, fill in New URL, choose the status code, set it to Enabled.
- Save. The redirect is live and the row stops being a 404.
- Bulk-trash the noise (spam scans, hacker probes, ancient junk).
The Referrer column tells you where the broken link comes from. An external referrer means an inbound link you should redirect. An internal referrer means a broken link inside your own site that you should fix at the source.
2.4 Excluding URLs from Collection
A busy site collects a lot of junk - scanner probes for wp-admin, .env, phpmyadmin, and so on. You can ignore them at the source with System → Plugins → System - Redirect → Exclude URLs.
Each row in the subform has:
| Field | Meaning |
|---|---|
| Term | A path fragment or regular expression. |
| Regular Expression | Treat Term as a regex (Yes/No). |
Two patterns are hardcoded to skip: any URL that contains mosConfig_ or =http. These are classic injection patterns, and Joomla never logs them.
3. HTTP Status Codes
3.1 Why the Status Code Matters
A redirect is not just "go here instead". The HTTP status code tells browsers, crawlers, and link-equity algorithms why the URL has moved.
| Code | Name | When to use |
|---|---|---|
| 301 | Moved Permanently | Article renamed for good - passes SEO equity. |
| 302 | Found (Temporary) | Short-term redirect, A/B test, seasonal page. |
| 303 | See Other | Redirect after a POST (forms). |
| 307 | Temporary Redirect | Like 302, but preserves the HTTP method. |
| 308 | Permanent Redirect | Like 301, preserves the HTTP method. |
In daily practice you only pick two: 301 for "the page moved forever" and 302 for "just for now".
3.2 Unlocking Custom Status Codes
By default, the component forces every redirect to 301, no matter what you selected on the row. To honour the per-row code you must enable one switch:
Components → Redirects → Options → Use Custom Redirect Status Codes = Yes
Without this switch the HTTP Status Code dropdown on each row is decorative. Turn it on before you import a CSV that mixes 301 and 302 codes, otherwise every imported row is silently flattened to 301.
3.3 What the Redirect Looks Like
When the plugin sends a redirect, the response looks like this:
HTTP/1.1 301 Moved Permanently
Location: https://site.tld/new-shiny-article
The original query string is preserved if the old URL in your row has no query string of its own. If you stored /search?q=foo, the query is part of the match, not appended to the destination.
4. Bulk Operations and Migrations
4.1 Bulk Import
For migrations you almost never want to type redirects one by one. The component supports a plain text import format, one redirect per line:
/old-article-1|/new-article-1
/old-article-2|/new-article-2
/old-blog/2024/01/foo|/blog/foo
The default separator is the pipe character (|). You can change it under Options → Bulk Separator if your URLs contain pipes.
4.2 Default State for Imported Rows
Options → Default Imported State controls whether imported rows are immediately active:
| Choice | What happens |
|---|---|
| Enabled | Imported rows go live the moment the import finishes. |
| Disabled | Imported rows land disabled - you can review before publishing. |
Pick Disabled for big migration drops you want to test first. Pick Enabled for trusted CSV exports from a known source.
4.3 The Migration Playbook
When you replatform, restructure, or rename URLs in bulk, the following nine steps keep the work under control:
- Export old URLs before the cutover. Crawl the live site or pull from
#__contentthrough the router. Store the list safely. - Map old to new. In a spreadsheet, pair every old URL with the best replacement. Be explicit about URLs that have no direct match.
- Decide the default for unmapped URLs. Usually the category page, not the homepage.
- Build the import file in the format
old|newper line. - Stage first. Import on a staging environment with Default Imported State = Disabled, then spot-check.
- Cutover. Switch to production, enable the rows, verify a handful with
curl -I(see section 9). - Monitor the 404 log daily for the first two weeks. Sort by hits and fix the top of the list.
- Cross-reference Google Search Console. Its Coverage / Not Found report shows which broken URLs are still indexed.
- Audit chains and loops after one month. See section 8 for the SQL.
A migration is the only time you actually want a large redirect table. After three months, prune anything with zero hits.
5. The Anatomy of a Redirect
5.1 The Single Table
The whole component lives in one database table:
#__redirect_links one row per redirect or 404 capture
No taxonomy, no metadata sidecar, no per-language variants. The main columns are:
id auto-increment primary key
old_url the matched URL (indexed)
new_url the destination (NULL = unresolved 404)
referer where the visitor came from
comment free-text admin note
hits incremented on each match
published 1=Enabled, 0=Disabled, 2=Archived, -2=Trashed
created_date first time this URL was seen
modified_date last admin edit
header HTTP status code, default 301
The old_url field is the search key. The index on it keeps lookups fast even with thousands of rows.
5.2 URL Normalisation - The Matching Trick
The plugin does not look up the raw URL the visitor requested. Instead, it builds twelve variants and runs a single WHERE old_url IN (...) query:
1. Full URL (scheme + host + path + query + fragment)
2. Relative URL (path + query + fragment)
3. Root-relative (Uri::root() stripped)
4. Root-relative with leading "/"
5. Without query (scheme + host + path + fragment)
6. Relative without query (path + fragment)
7-12. The same six, but lowercased
What this means in practice:
- You can store
/old-pageorhttps://site.tld/old-page. Both match. - Case matters at storage, but the lowercased variants let
/Old-Pagestill find/old-page. - A redirect for
/foomatches both/fooand/foo?utm=x. - A redirect for
/foo?bar=1matches only/foo?bar=1. Query strings inold_urlrequire exact match.
5.3 The onError Hook
The plugin subscribes to one event:
public static function getSubscribedEvents(): array
{
return ['onError' => 'handleError'];
}
The handler stops early unless the error code is exactly 404 and the request is not in the administrator. Backend 404s are intentionally ignored - admins never get redirected by their own rules.
5.4 Why Duplicates Are Dangerous
When the IN (...) query returns multiple rows (because the same URL is stored as both a full URL and a relative URL), the plugin picks the first published match in the order of the variant list. The full URL comes first, so it wins over the relative URL.
The practical rule: keep your redirects in one consistent shape, usually root-relative paths. Do not store the same redirect three different ways.
6. Permissions and Workflow
6.1 ACL Actions
The component supports the standard Joomla ACL actions. You set them at Components → Redirects → Options → Permissions:
| Action | What it controls |
|---|---|
| Configure | Edit the component options. |
| Access Administration Interface | Open the Redirects list at all. |
| Create | Add new redirect rows. |
| Delete | Trash and empty trash. |
| Edit | Modify any row. |
| Edit State | Publish, unpublish, or archive. |
6.2 Suggested Roles
A common pattern is to give a content editor group Create, Edit, and Edit State, but withhold Configure:
| Role | Permission | Daily task |
|---|---|---|
| Super User | All | One-time setup and migration imports. |
| SEO Editor | Create / Edit / Edit State | Triage the 404 log weekly, fix the top hits. |
| Author | None | Broken-link cleanup is editorial work, not authoring. |
7. Multilingual Redirects
7.1 Per-language URLs
On a multilingual Joomla site, every translation has its own URL, usually prefixed by the language code:
/en/old-article → /en/new-article
/nl/oude-pagina → /nl/nieuwe-pagina
/de/altes-zeug → /de/neuer-artikel
The com_redirect component is language-agnostic. There is no language column in #__redirect_links. The match is purely on the URL string, so you must store the language prefix yourself.
7.2 The Cross-language Trap
Two tempting but wrong patterns are common:
/en/old-article → / (homepage redirect)
/nl/oude-pagina → /en/new-article (cross-language jump)
Both confuse visitors and Google. A Dutch visitor suddenly lands on English content, or an English visitor loses context entirely.
The rule is simple: redirect within the same language. If a translation does not exist yet, point the URL at the closest in-language equivalent (a category page or the in-language homepage), not the site root.
7.3 Menu Associations vs Redirects
Joomla's multilingual menu associations already handle "give me this page in language X" through the language switcher. That is not a redirect, it is a route.
Use com_redirect only when:
- An article truly moved in one language.
- An entire URL pattern changed after a SEF tweak in one language.
- A translation was removed and needs to point somewhere sensible.
7.4 The Language Filter Trap
The System - Language Filter plugin can itself redirect requests to add or remove a language code. If both plugins fight, you get a redirect loop:
/article → /en/article (Language Filter)
/en/article → /article (your redirect rule)
/article → /en/article (loop again)
The fix is to store the URL after Language Filter has processed it, which means always include the language prefix in both old_url and new_url.
8. Performance and Maintenance
8.1 How Fast is It?
Every 404 triggers a single indexed IN (...) query against #__redirect_links. With an index on old_url, this is microseconds even at 100,000 rows.
The slow part is the 404 itself. Joomla still bootstraps the full application before it knows the route does not resolve. The redirect lookup on top is essentially free.
8.2 Pruning the Log
A noisy site, or one under hacker scanning, builds up thousands of junk URLs. A quarterly cleanup is enough:
- Filter: State = Disabled (captured but never resolved).
- Sort by Hits = 0.
- Bulk-trash everything.
- Empty Trash.
The resolved redirects stay intact and you lose only the noise.
8.3 Three SQL Queries You Will Use Weekly
For more than a few hundred rows, the backend list becomes slow to scan. Run SQL directly instead.
Top broken URLs to fix first, sorted by hits, only the unresolved ones:
SELECT id, old_url, hits, referer
FROM jos_redirect_links
WHERE published = 0
AND new_url IS NULL
ORDER BY hits DESC
LIMIT 50;
Find redirect loops where /a points to /b and /b points back to /a:
SELECT a.old_url, a.new_url, b.new_url AS loops_to
FROM jos_redirect_links a
JOIN jos_redirect_links b
ON b.old_url = a.new_url
WHERE a.published = 1 AND b.published = 1
AND b.new_url = a.old_url;
Flatten redirect chains (/a → /b → /c becomes /a → /c):
UPDATE jos_redirect_links a
JOIN jos_redirect_links b
ON a.new_url = b.old_url
SET a.new_url = b.new_url
WHERE b.published = 1;
Run this repeatedly until no more rows change. Replace jos_ with your actual database prefix.
8.4 When the Table Outgrows Usefulness
If #__redirect_links contains millions of rows, you are storing scanner traffic, not real 404s. Options:
- Disable Collect URLs until you have cleaned the table.
- Add broad Exclude URLs patterns (
wp-,.env,.git). - Move common scanner blocks up to your web server, so the request never reaches PHP at all.
9. Common Pitfalls
9.1 My Redirect Does Not Fire
Run through this checklist, in order:
- Is
plg_system_redirectenabled? - Is the redirect row Enabled?
- Is the New URL filled in (not NULL)?
- Does the old URL really return a 404? If the page still exists, no redirect fires -
com_redirectonly acts on 404 errors. - Is the old URL stored in a shape the matcher recognises? Root-relative is the safest choice.
9.2 My Status Code is Always 301
The component option Use Custom Redirect Status Codes is No by default. Until you flip it to Yes, every redirect is sent as a 301, regardless of the row setting.
9.3 Redirects Do Not Run in the Backend
This is by design. The plugin returns early when $app->isClient('administrator') is true. The component is a frontend tool only.
9.4 The Database Keeps Growing
That is collection, not redirection. Set Collect URLs = No to stop logging new entries. Existing published redirects keep working. This is useful on sites under constant scanner traffic.
9.5 Redirect Loops
The component does no loop detection. If you map /a → /b and /b → /a, the browser cycles until it gives up. Run the loop-audit SQL from section 8.3 after every bulk import.
9.6 Chained Redirects
A chain like /a → /b → /c → /d works because each hop is a separate request, but every hop costs a round trip and erodes SEO weight. Flatten chains with the SQL in section 8.3.
9.7 Query Strings and Fragments
- Query string in
old_url: exact match required. - No query string in
old_url: matches with or without query. - Fragments (
#section): the browser strips them before sending the request, so you cannot redirect on a fragment. Do not store them.
9.8 SEF vs Raw URLs
With SEF enabled, the user-facing URL is /old-article. With SEF disabled, the same content lives at /index.php?option=com_content&view=article&id=42. These are two different keys in the redirect table. Audit your redirects after you turn SEF on or off.
9.9 Open-redirect Security
The component lets you point an old URL at any destination, including external domains. That is sometimes useful, but it is also a phishing vector:
- A compromised admin account can create a redirect to a fake-brand phishing site.
- A 404 captured from a search-engine-indexed URL gets pointed at a malicious destination.
- A stale redirect points to a domain that was sold and now hosts malware.
The defence is simple: restrict Create and Edit on com_redirect to a small, trusted group, and audit external destinations quarterly:
SELECT id, old_url, new_url
FROM jos_redirect_links
WHERE published = 1
AND new_url LIKE 'http%'
AND new_url NOT LIKE 'https://site.tld/%';
9.10 Verifying a Redirect with curl
Browsers cache aggressively and follow redirects silently. For debugging, use curl instead:
curl -I https://site.tld/old-article
You want to see exactly:
HTTP/2 301
location: https://site.tld/new-article
content-type: text/html; charset=utf-8
A 200 response means the page did not 404, so no redirect ran. A 301 when you expected a 302 means the custom-codes switch is off (see section 3.2).
To trace a full chain, add -L and -v:
curl -I -L https://site.tld/old-article
Each hop appears in order - perfect for confirming you flattened a chain.
9.11 Redirect vs Canonical
A redirect and a canonical look similar but solve different problems:
| Use a redirect when | Use a canonical when |
|---|---|
| The page truly moved. | Duplicates exist on purpose (print view, filters). |
| You want to retire the old URL. | Both URLs must stay accessible. |
| You want users sent away. | You want users to stay where they are. |
Setting both - a redirect plus a canonical on the destination - is fine. Setting a canonical that points at a redirected-away URL confuses crawlers.
10. Under the Hood (Developer View)
10.1 The Match Flow
For a developer who wants to know exactly what happens on every 404:
ErrorEvent (code 404)
└── handleError($event)
├── Bail if backend or non-404
├── Build 12 URL variants (with/without scheme, query, lowercased)
├── Skip if exclude_urls matches OR contains mosConfig_ / =http
├── SELECT * FROM #__redirect_links WHERE old_url IN (...)
├── Walk variants in order, pick first published match
├── Force status code to 301 unless "mode" option is Yes
├── Increment hits, set modified_date
└── Send Location header + status code
10.2 Programmatic API
You rarely need an API for redirects, because the table is simple enough to write to directly:
use Joomla\CMS\Factory;
$db = Factory::getContainer()->get('DatabaseDriver');
$db->setQuery(
"INSERT INTO #__redirect_links
(old_url, new_url, published, header, created_date, modified_date, hits, referer, comment)
VALUES
('/legacy', '/modern', 1, 301, NOW(), NOW(), 0, '', 'API insert')"
);
$db->execute();
10.3 Auto-redirect When an Article Alias Changes
A small custom plugin can save editors a lot of work. Hook into onContentAfterSave, detect an alias change, and insert a redirect row automatically:
public function onContentAfterSave($context, $article, $isNew): void
{
if ($context !== 'com_content.article' || $isNew) {
return;
}
if ($article->alias === $article->getOldAlias()) {
return;
}
$db = $this->getDatabase();
$db->setQuery(
"INSERT IGNORE INTO #__redirect_links
(old_url, new_url, published, header, created_date, modified_date)
VALUES (:old, :new, 1, 301, NOW(), NOW())"
)->bind(':old', $oldRoute)->bind(':new', $newRoute);
$db->execute();
}
10.4 Web Services API
Redirects are not exposed in Joomla's Web Services API in core. If you need remote management, either build a small custom component that wraps the table, or drop in a tiny REST plugin that calls LinkModel::save().
10.5 Better 404 Pages
The redirect plugin only acts when a 404 happens and no match is found. The visitor still sees the 404 page. You can improve that experience by overriding:
templates/your_template/error.php
A search box (Smart Search), a "popular pages" module, a sitemap link - anything that helps the visitor find what they wanted. A clean 404 page plus a healthy redirect table is the right combination. Redirect the high-traffic broken links and soft-land the rest.
11. Component vs Web Server Redirects
You can also configure redirects in Apache's .htaccess or in an NGINX config. Each layer has its place:
| Aspect | com_redirect | .htaccess / NGINX |
|---|---|---|
| Editor-friendly | Yes, backend UI. | No, file edits. |
| Status codes | Yes, after the switch. | Yes. |
| Regex patterns | Limited (exclude only). | Full regex. |
| Performance | After Joomla bootstrap. | Before PHP runs, much faster. |
| 404 logging | Built in. | Manual log parsing. |
| Hits per redirect | Yes. | No. |
| Bulk import | Yes (UI). | Yes (file). |
| Loop or chain detection | No. | No, but easy to spot in a file. |
| Survives a Joomla move | Yes (in DB). | No, file-path bound. |
Use com_redirect for editorial redirects (renamed articles, migrations). Use the web server for infrastructure rules (force HTTPS, www to bare domain, scanner blocks). Mixing layers is fine; mixing layers at random is what makes both fragile.
12. Best Practices and Cheat Sheet
If you only remember a few things from this article, remember these:
- Enable the System - Redirect plugin. Nothing works without it.
- Switch on Use Custom Redirect Status Codes before you import anything.
- Store URLs in one consistent shape, usually root-relative paths.
- Redirect within the same language on multilingual sites.
- Flatten chains and audit for loops after every bulk import.
- Restrict ACL on
com_redirectto a small, trusted group. - Use
curl -Ito verify redirects - browsers lie. - Prune the log quarterly to keep the table fast and useful.
Cheat Sheet
COMPONENT Components → Redirects
PLUGIN System → Plugins → System - Redirect
OPTIONS Components → Redirects → Options
CUSTOM CODES Options → Use Custom Redirect Status Codes = Yes
COLLECT 404s Plugin → Collect URLs = Yes
EXCLUDE URLs Plugin → Exclude URLs subform (term + regex flag)
BULK IMPORT Toolbar → Import (separator = "|" by default)
TABLE #__redirect_links (one row per redirect)
KEY FIELDS old_url, new_url, published, header, hits
STATUS CODES 301 (default), 302, 303, 307, 308
HARDCODED SKIP URLs containing mosConfig_ or =http
TRIGGER plg_system_redirect.onError, code 404, frontend only
MULTILINGUAL No language column - store the prefix in old_url
VERIFY curl -I https://site.tld/old (then -L for chains)
NOT CANONICAL Redirect retires the URL; canonical keeps it live
13. Summary
The Redirects component is the smallest part of Joomla that pays for itself the most often. One plugin, one component, one switch - and every renamed article keeps its inbound links and search-engine ranking.
In return for very little setup, you get:
- 404 capture: every broken link logged with hits and referrer.
- Editor-friendly management: no
.htaccessedits, no devops tickets. - SEO-correct status codes: 301s preserve the link equity you have built.
- Bulk-ready imports: pipe-separated lists for migrations.
- Single-table simplicity: easy to back up, easy to audit, easy to extend.
A clean redirect table is a sign of a well-cared-for Joomla site. If you are planning a migration, restructuring URLs, or you suspect that broken links are costing you search traffic, it pays to look at com_redirect first. It is already in your Joomla install, waiting to be switched on.


Joomla en Linux specialist voor snelle, veilige en schaalbare websites.


