As there are a number of issues arising like this, this answer explains how to override the com_content router to select a better menuitem to base an SEF URL on.
WARNING
This is a system plugin, and if you have a PHP syntax error in it you will get locked out of the Joomla admin back-end, and will have to go into phpmyadmin (or equivalent) to set to 0 the enabled
field within the plugin record in #__extensions. If you're not comfortable doing this then I would recommend not using this plugin.
Into a folder plg_custom_menurule create the following 5 files.
plg_custom_menurule/custom_menurule.xml - this is the manifest file for the plugin.
<?xml version="1.0" encoding="utf-8"?>
<extension type="plugin" group="system" method="upgrade">
<name>Custom Menurule</name>
<version>1.0.0</version>
<creationDate>today</creationDate>
<author>me</author>
<description>This plugin overrides the com_content site router where it selects the menuitem for the SEF URL</description>
<namespace path="src">My\Plugin\System\CustomMenurule</namespace>
<files>
<folder plugin="custom_menurule">services</folder>
<folder>src</folder>
</files>
</extension>
plg_custom_menurule/services/provider.php - this is just standard boilerplate code for instantiating the plugin via the dependency injection container.
<?php
defined('_JEXEC') or die;
use Joomla\CMS\Extension\PluginInterface;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Event\DispatcherInterface;
use My\Plugin\System\CustomMenurule\Extension\CustomMenurulePlugin;
return new class implements ServiceProviderInterface {
public function register(Container $container) {
$container->set(
PluginInterface::class,
function (Container $container) {
$dispatcher = $container->get(DispatcherInterface::class);
$plugin = new CustomMenurulePlugin(
$dispatcher,
(array) PluginHelper::getPlugin('system', 'custom_menurule')
);
return $plugin;
}
);
}
};
plg_custom_menurule/src/Extension/CustomMenurulePlugin.php. This is the entry point for the plugin. It registers to listen for the 'onAfterExtensionBoot' event, which is raised within loadExtension
in libraries/src/Extension/ExtensionManagerTrait.php.
When the event is raised, if it's for the site com_content then it registers in the component's child Dependency Injection Container a RouterFactory with the plugin's namespace, which will overwrite the usual com_content entry (set in administrator/components/com_content/services/provider.php).
<?php
namespace My\Plugin\System\CustomMenurule\Extension;
defined('_JEXEC') or die;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Event\Event;
use Joomla\Event\SubscriberInterface;
use Joomla\CMS\Extension\ComponentInterface;
use Joomla\CMS\Extension\Service\Provider\RouterFactory;
use Joomla\CMS\Factory;
class CustomMenurulePlugin extends CMSPlugin implements SubscriberInterface {
public static function getSubscribedEvents(): array {
return [
'onAfterExtensionBoot' => 'replaceRouterFactory',
];
}
public function replaceRouterFactory(Event $event): void {
if (!Factory::getApplication()->isClient("site")) {
return;
}
if (($event['type'] === ComponentInterface::class) && ($event['extensionName'] === "content")) {
$container = $event["container"];
$container->registerServiceProvider(new RouterFactory('\\My\\Plugin\\System\\CustomMenurule'));
}
}
}
When the RouterFactory is called up to create a Router instance, it tries to instantiate a class <namespace>\Site\Service\Router
so this is what we want to define.
plg_custom_menurule/src/Site/Service/Router.php. Our class extends the com_content router class, and we use its configuration of the Router views, and the routing rules (set up in its constructor in components/com_content/src/Service/Router.php). Then we detach the menu rule that had been set and in its place attach our own. We need also to override the getName
method.
<?php
namespace My\Plugin\System\CustomMenurule\Site\Service;
use Joomla\CMS\Application\SiteApplication;
use Joomla\CMS\Categories\CategoryFactoryInterface;
use Joomla\CMS\Component\Router\Rules\MenuRules;
use Joomla\CMS\Menu\AbstractMenu;
use Joomla\Database\DatabaseInterface;
\defined('_JEXEC') or die;
class Router extends \Joomla\Component\Content\Site\Service\Router
{
public function __construct(SiteApplication $app, AbstractMenu $menu, CategoryFactoryInterface $categoryFactory, DatabaseInterface $db)
{
parent::__construct($app, $menu, $categoryFactory, $db);
$rules = $this->getRules();
foreach ($rules as $rule) {
if ($rule instanceof \Joomla\CMS\Component\Router\Rules\MenuRules) {
$this->detachRule($rule);
break;
}
}
$this->attachRule(new \My\Plugin\System\CustomMenurule\Site\Service\MenuRules($this));
}
public function getName()
{
return "content";
}
}
plg_custom_menurule/src/Site/Service/MenuRules.php - This is our version of the preprocess
function which is used to set the Itemid of the menuitem to build an SEF URL upon. The code extends the equivalent Joomla class in libraries/src/Component/Router/Rules/MenuRules.php, so that we reuse its buildLookup
function, but override the preprocess
function.
Note that this code is based on the Joomla 4.3.4 version. Other versions may have different Joomla code in this file.
This function differs from the standard Joomla router in a number of areas:
If the Itemid has been set in the Route::_()
call then we use it, provided that the menuitem is associated with com_content.
If it's a multilingual site then we remove from the lookup
array the entry associated with the home page of the "*" language. This is for the case where you assign language-specific home pages as described in https://docs.joomla.org/J3.x:Setup_a_Multilingual_Site/Creating_menus, and unpublish the main menu module. If we don't remove this entry then it's possible to get incorrect routing, eg if the "*" home page points to one article and a language-specific home page points to a different article.
It looks through the lookup table to try and find an exact match between the parameters specified in the Route::_()
call and a menuitem on the site. If it finds one then it uses that menuitem's Itemid.
If the current page (ie the active menuitem) belongs to com_content then it uses that menuitem's Itemid.
If the above fails to find a suitable menuitem then it drops back to the standard Joomla code.
You can obviously tailor these rules to suit your own needs.
Once you have created the files above, then zip up the folder and install the extension. Then go into System / Plugins or System / Extensions and Enable the plugin.
Your feedback is particularly welcome, as it will probably benefit others as well, and I'll try and maintain this code bug-free.