After changing to modern routing (and removing ids from urls), and/or updating a Joomla 3 site to Joomla 4, old article category links with the id at the start will 404. People seem to suggest fixing this manually in the redirect component or setting a custom htaccess rule to detect numbers and redirect like this:

RewriteCond %{REQUEST_URI} [0-9]+-
RewriteRule ^(.*)/[0-9]+-(.*)$ /$1/$2 [R=301,L]

Which does cause problems if any path on the site legitimately starts with a number.

Off the top of my head, it seems like the correct solution is for Joomla to automatically create an active redirect entry (ie. there should be a setting to do it when switching on modern routing in j3 or in j4 if removing ids from articles.).

I see some old posts about catching 404s like this: https://stackoverflow.com/questions/14197831/how-to-detect-wrong-url-using-joomla-system-plugin

But I suspect I must be missing something.

Hence - before I write a system plugin to do this... is there a way for links with articles ids to automatically still "work".

  • All routing in J4 core components is "modern", legacy routing has been removed. Removing IDs from URL is totally optional.
    – Sharky
    Mar 31, 2023 at 8:19
  • You're right. I've updated my question. Apr 1, 2023 at 1:25

1 Answer 1


I wrote a system plugin based on the redirect plugin to deal with this. The xml manifest has no config and just the 1 file.

Note that after installing, go to manage plugins, search for "redirect", set to order by order and drag the automodern plugin to be after the system redirect. The redirect plugin must be also enabled and set to catch urls.

In short it:

  • Catches a 404 just like the redirect plugin.
  • Returns if the last segment isn't right pattern eg. 123-etc-etc
  • Finds a matching redirect that has no destination and is unpublished. (The redirect finding code is borrowed straight from the redirect plugin.)
  • Checks the content table to see if the id and alias exactly match an article.
  • If there's a matching article, it updates the redirect with the same url without the 123- bit and publishes it and redirects.

It's a bit rough looking but it works. The worst case scenario (i think) is that it will generate another 404. It doesn't deal with ids linked to sub-categories which would be nice to have.


use Joomla\CMS\Event\ErrorEvent;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Uri\Uri;
use Joomla\Database\ParameterType;
use Joomla\Event\SubscriberInterface;
use Joomla\String\StringHelper;

defined('_JEXEC') or die;

class PlgSystemAutomodern extends CMSPlugin implements SubscriberInterface
    protected $autoloadLanguage = false;
    protected $db;

    public static function getSubscribedEvents(): array
        return [
            'onError' => 'handleError',

    public function handleError(ErrorEvent $event)
        $app = $event->getApplication();

        if ($app->isClient('administrator') || ((int) $event->getError()->getCode() !== 404)) {

        $uri = Uri::getInstance();

        // Check for the right structure "123-xyz-etc"
        $parts = explode('/',rtrim(rawurldecode($uri->toString(['path', 'fragment'])),'/'));
        $candidate = end($parts);
        $id_etc = explode('-',$candidate);
        if(count($id_etc) < 2 || !ctype_digit($id_etc[0])){return;}

        $orgurl                = rawurldecode($uri->toString(['scheme', 'host', 'port', 'path', 'query', 'fragment']));
        $orgurlRel             = rawurldecode($uri->toString(['path', 'query', 'fragment']));
        $orgurlRootRel         = str_replace(Uri::root(), '', $orgurl);
        $orgurlRootRelSlash    = str_replace(Uri::root(), '/', $orgurl);
        $orgurlWithoutQuery    = rawurldecode($uri->toString(['scheme', 'host', 'port', 'path', 'fragment']));
        $orgurlRelWithoutQuery = rawurldecode($uri->toString(['path', 'fragment']));
        $url                   = StringHelper::strtolower(rawurldecode($uri->toString(['scheme', 'host', 'port', 'path', 'query', 'fragment'])));
        $urlRel                = StringHelper::strtolower(rawurldecode($uri->toString(['path', 'query', 'fragment'])));
        $urlRootRel            = str_replace(Uri::root(), '', $url);
        $urlRootRelSlash       = str_replace(Uri::root(), '/', $url);
        $urlWithoutQuery       = StringHelper::strtolower(rawurldecode($uri->toString(['scheme', 'host', 'port', 'path', 'fragment'])));
        $urlRelWithoutQuery    = StringHelper::strtolower(rawurldecode($uri->toString(['path', 'fragment'])));

        $query = $this->db->getQuery(true);
        $redirect = null;

        try {
            $redirects = $this->db->loadAssocList();
        } catch (Exception $e) {
            $event->setError(new Exception(Text::_('PLG_SYSTEM_REDIRECT_ERROR_UPDATING_DATABASE'), 500, $e));

        $possibleMatches = array_unique([$url,$urlRel,$urlRootRel,$urlRootRelSlash,$urlWithoutQuery,$urlRelWithoutQuery,$orgurl,$orgurlRel,$orgurlRootRel,$orgurlRootRelSlash,$orgurlWithoutQuery,$orgurlRelWithoutQuery,]);

        foreach ($possibleMatches as $match) {
            if (($index = array_search($match, array_column($redirects, 'old_url'))) !== false) {
                $redirect = (object) $redirects[$index];

                if ((int) $redirect->published === 1) {

        // If we have a redirect url but it hasn't been set up yet then check if its an exact match to an article and if so then set destination and publish and redirect
        if ($redirect !== null && ((int) $redirect->published === 0) && empty($redirect->new_url)) {

            $alias = substr($candidate,strpos($candidate,'-') + 1);

            $query = $this->db->getQuery(true);
                ->where('id = ' . $this->db->quote($id_etc[0]))
                ->where('alias = ' . $this->db->quote($alias));

            $orgurlRelWithoutQuery = rtrim($orgurlRelWithoutQuery,'/');
            $redirect->new_url = substr($orgurlRelWithoutQuery, 0, strrpos( $orgurlRelWithoutQuery, '/') + 1) . $alias;
            $redirect->published = 1;

            try {
                $this->db->updateObject('#__redirect_links', $redirect, ['id']);
            } catch (Exception $e) {
                $event->setError(new Exception(Text::_('PLG_SYSTEM_REDIRECT_ERROR_UPDATING_DATABASE'), 500, $e));

            $app->redirect($redirect->new_url, (int) 301);

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.