Adding a Custom MODX Manager Menu for Recent Resources
MODX Revolution’s default menu can bury frequently used tools. Learn how to register a custom <code>modAction</code> and <code>modManagerMenu</code> to surface a “Recent Resources” page directly under Components.
08 May 2026, 20:52 UTC

The problem: custom tools stranded outside the manager
When a MODX Revolution site grows past a handful of pages, the utilities your team actually reaches for—a link checker, a staging sync button, a custom report—end up buried in the file tree, the Extras grid, or a browser bookmark. Each extra click adds friction to the daily content workflow.
The useful takeaway: you can surface those tools inside the manager by registering a custom modAction and attaching it to a modManagerMenu node. The page then runs inside MODX’s authenticated session, so it inherits the lexicon, layout, and permission system instead of living as an isolated script.
How MODX assembles the manager menu
MODX Revolution stores the manager navigation in the database as modManagerMenu records. Each row carries a lexicon key for the label, a parent key (for example, components), an action reference, and a comma‑separated list of permissions required to see the item.
The action reference usually points to a modAction record. That record tells the manager which controller class to load when the item is clicked. A controller is a PHP class that extends MODX’s manager controller base class; it queries data, checks permissions, and renders output through the manager’s template system. In short: the menu row is the button, the action is the wiring, and the controller is the page.
Worked example: a “Recent Resources” page under Components
Suppose editors need to see the last ten edited resources without opening the resource tree and re‑sorting columns. This walkthrough assumes MODX Revolution 2.8.x or later; object schemas and controller base classes can shift between major versions.
1. The controller
Create a PHP file in your component namespace directory (for example, core/components/recentresources/). The class below extends modManagerController, checks the view_document permission before querying, and passes a row array to a Smarty template.
<?php
class RecentResourcesIndexManagerController extends modManagerController
{
public $pageTitle = 'Recent Resources';
public function process(array $scriptProperties = [])
{
if (!$this->modx->hasPermission('view_document')) {
$this->failure($this->modx->lexicon('access_denied'));
return;
}
$query = $this->modx->newQuery('modResource');
$query->sortby('editedon', 'DESC');
$query->limit(10);
$resources = $this->modx->getCollection('modResource', $query);
$rows = [];
foreach ($resources as $resource) {
$rows[] = [
'id' => $resource->get('id'),
'pagetitle' => $resource->get('pagetitle'),
'editedon' => $resource->get('editedon')
? date('Y-m-d H:i', $resource->get('editedon'))
: '—',
];
}
$this->setPlaceholder('rows', $rows);
}
public function getPageTitle()
{
return $this->pageTitle;
}
public function getTemplateFile()
{
return 'recentResourcesTemplate.tpl';
}
public function getLanguageTopics()
{
return ['recentresources:default'];
}
}
Place a matching Smarty template in the same component directory. A minimal version might look like this:
<h2>Recent Resources</h2>
<table>
<thead>
<tr><th>ID</th><th>Title</th><th>Last Edited</th></tr>
</thead>
<tbody>
{foreach from=$rows item=row}
<tr>
<td>{$row.id}</td>
<td>{$row.pagetitle}</td>
<td>{$row.editedon}</td>
</tr>
{/foreach}
</tbody>
</table>
2. Register the namespace, action, and menu
Run the following script once from the manager as a temporary snippet (Elements > Snippets > New Snippet), or through a command‑line bootstrap if you have one. You need an authenticated administrator session. The script checks for existing records before creating them, but always verify in System > Menus before re‑running.
<?php
$corePath = $modx->getOption('core_path') . 'components/recentresources/';
$namespace = $modx->getObject('modNamespace', ['name' => 'recentresources']);
if (!$namespace) {
$namespace = $modx->newObject('modNamespace');
$namespace->fromArray([
'name' => 'recentresources',
'path' => $corePath,
], '', true);
$namespace->save();
}
$action = $modx->getObject('modAction', [
'namespace' => 'recentresources',
'controller' => 'index',
]);
if (!$action) {
$action = $modx->newObject('modAction');
$action->fromArray([
'namespace' => 'recentresources',
'parent' => 0,
'controller' => 'index',
'haslayout' => 1,
'lang_topics' => 'recentresources:default',
], '', true);
$action->save();
}
$menu = $modx->getObject('modManagerMenu', [
'text' => 'recentresources',
'parent' => 'components',
]);
if (!$menu) {
$menu = $modx->newObject('modManagerMenu');
$menu->fromArray([
'text' => 'recentresources',
'parent' => 'components',
'action' => $action->get('id'),
'description' => 'recentresources.desc',
'permissions' => 'view_document',
], '', true);
$menu->save();
}
$modx->cacheManager->refresh();
return 'Registration complete. Reload the manager.';
Expected check: after saving the snippet and running it, reload the manager. A “Recent Resources” item should appear under the Components menu. Clicking it should load the table of resources.
Risk: if the snippet is interrupted mid‑run, you may end up with a namespace but no action, or an action but no menu. Inspect the three tables in System > Namespaces, Actions, and Menus, and delete orphaned records before re‑running.
Rollback: to remove the feature, delete the modManagerMenu row first, then the modAction, then the modNamespace, and refresh the cache. Deleting the namespace first can orphan the action and menu.
Trade‑offs and limitations
- Permission checks are not optional. The
permissionsfield on the menu row only controls visibility. A user who knows the action ID can still load the controller directly unless you call$modx->hasPermission()insideprocess(). Always gate data access in the controller. - Upgrades can break the wiring. Major MODX releases occasionally refactor the manager controller base class or the menu tree build process. After an upgrade, confirm the item still renders; if it disappears, re‑run the registration script or reinstall your transport package.
- Menu overhead is small but real. Each custom row adds a database read when the manager builds the navigation tree. A handful of items is negligible; hundreds can measurably slow manager load times.
- Deployment hygiene. Ad‑hoc snippets are fine for a single site, but for multiple environments wrap the logic in a transport package so the action, menu, and files are versioned and uninstallable.
A practical checklist
- Create the controller class and template in
core/components/yourcomponent/. - Register a
modNamespaceso MODX can resolve the component path. - Create a
modActionthat points to the controller. - Insert a
modManagerMenuwith the correct parent key and permission list. - Clear the manager cache via Manage > Clear Cache or
$modx->cacheManager->refresh(). - Test with an administrator account and a limited user account to confirm both visibility and data access are enforced.
For deeper customization—such as adding custom top‑level menus or integrating with the ExtJS grid—consult the official MODX Revolution manager customization documentation for your installed version.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.