Skip to main content
Redirects in Joomla

Redirects in Joomla

27 May 2026

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:

PartRole
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:

FieldExample
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:

OptionWhat 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:

  1. Sort by Hits descending - fix the most-visited 404s first.
  2. Open an entry, fill in New URL, choose the status code, set it to Enabled.
  3. Save. The redirect is live and the row stops being a 404.
  4. 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:

FieldMeaning
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.

CodeNameWhen 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:

ChoiceWhat 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:

  1. Export old URLs before the cutover. Crawl the live site or pull from #__content through the router. Store the list safely.
  2. Map old to new. In a spreadsheet, pair every old URL with the best replacement. Be explicit about URLs that have no direct match.
  3. Decide the default for unmapped URLs. Usually the category page, not the homepage.
  4. Build the import file in the format old|new per line.
  5. Stage first. Import on a staging environment with Default Imported State = Disabled, then spot-check.
  6. Cutover. Switch to production, enable the rows, verify a handful with curl -I (see section 9).
  7. Monitor the 404 log daily for the first two weeks. Sort by hits and fix the top of the list.
  8. Cross-reference Google Search Console. Its Coverage / Not Found report shows which broken URLs are still indexed.
  9. 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-page or https://site.tld/old-page. Both match.
  • Case matters at storage, but the lowercased variants let /Old-Page still find /old-page.
  • A redirect for /foo matches both /foo and /foo?utm=x.
  • A redirect for /foo?bar=1 matches only /foo?bar=1. Query strings in old_url require 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:

ActionWhat 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:

RolePermissionDaily 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:

  1. Filter: State = Disabled (captured but never resolved).
  2. Sort by Hits = 0.
  3. Bulk-trash everything.
  4. 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:

  1. Is plg_system_redirect enabled?
  2. Is the redirect row Enabled?
  3. Is the New URL filled in (not NULL)?
  4. Does the old URL really return a 404? If the page still exists, no redirect fires - com_redirect only acts on 404 errors.
  5. 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 whenUse 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:

Aspectcom_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_redirect to a small, trusted group.
  • Use curl -I to 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 .htaccess edits, 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.

Redirects in Joomla
Peter Martin

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

© Peter Martin / db8 Website Support. All rights reserved.