Developers
Custom macros
A macro is the [[Name(arguments)]] markup a page author writes to call PHP.
A super group can add macros of its own and replace any of the ones the hub
ships, by putting classes in the group's macros/ directory.
Reach for one when the group's own editors need something computed inside
ordinary page text, and need it more than once. The Coastal Resilience
Center's macro is [[GaugeStatus(pier-7)]]: it reads the latest reading for a
named gauge and prints it. A communications officer with no PHP writes that
line into any page, in the middle of a paragraph, and it stays right.
The alternatives are worth knowing before you write one:
| The group needs | Use |
|---|---|
| A snippet inside prose the editors maintain | a macro |
| A block in a template position | a group module |
| A whole page in code | a PHP page |
| Its own URLs and records | a component |
app/site/groups/<gidNumber>/macros/
The directory is created with the rest of the group's skeleton, and is filled
from the server or through the group's
repository — the group file browser
reaches only uploads.
Where group macros apply
The group's macros/ directory is added to the macro search path in two
places, both in com_groups: when a group page version is parsed
(models/page/version.php)
and when a group module is parsed
(models/module.php).
Both pass the directory as alt_macro_path to the
content/formathtml plugin, which
parses it before its own macros/ directory.
So group macros work in group pages and group modules, and only for that one
group. They do not apply to the group's wiki: the wiki runs through
wiki/parserdefault, which has a
fixed macro path and no group hook. A macro that works on a group page and
vanishes on the group's wiki page is not broken; it is out of scope.
Writing one
The file name is the macro name, lowercased, plus .php. The class is the
macro name in the Plugins\Content\Formathtml\Macros namespace, extending
Plugins\Content\Formathtml\Macro.
<?php
/**
* @package hubzero-cms
* @copyright Copyright (c) 2026 Purdue University. All rights reserved.
* @license http://opensource.org/licenses/MIT MIT
*/
namespace Plugins\Content\Formathtml\Macros;
use Plugins\Content\Formathtml\Macro;
/**
* Macro to greet the reader
*/
class HelloWorld extends Macro
{
/**
* Description of the macro, shown in the macro list
*
* @return string
*/
public function description()
{
$txt = array();
$txt['wiki'] = 'Outputs "Hello world"';
$txt['html'] = '<p>Outputs "Hello world"</p>';
return $txt['html'];
}
/**
* Generate macro output
*
* @return string
*/
public function render()
{
return 'Hello World, args = ' . $this->args;
}
}
That is macros/helloworld.php
from core, which is the shortest working example in the tree. Copy it as a
starting point: change the file name, the class name and render(), and you
have the centre's macros/gaugestatus.php.
The header above is the one this repository uses. The CMS is MIT licensed, and that is what the sample says. Older documentation showed the same header declaring LGPLv3; that is out of date. A group's own code carries whatever notice the group's owner chooses.
The rules
render()is required. It returns the markup that replaces the macro call. Return a string; returning nothing removes the macro from the page.description()is optional and is used only where the hub lists available macros.$this->argsis the raw text between the parentheses. The base class givesgetArguments(), which splits it on commas and trims, andgetArgument($index, $default).- The parser also sets
$this->option,$this->scope,$this->pagename,$this->domain,$this->pageidand$this->filepathbefore callingrender(). In a group pagepagenameanddomainare the group's alias andfilepathis the group'suploadsdirectory. public $allowPartial = truelets the macro run during a partial parse as well. The base class defaults it tofalse, and without it a partial parse renders Macro "Name" not allowed. instead of your output.- A macro name is matched case-insensitively and lowercased before the file is
looked up, so
[[GaugeStatus()]],[[gaugestatus()]]and[[GAUGESTATUS()]]all loadgaugestatus.php.
Macros in a subdirectory
A dot in the macro name is a directory separator. [[Group.Members()]] loads
group/members.php and expects
Plugins\Content\Formathtml\Macros\Group\Members. Recreate the same
directory structure under the group's macros/ folder.
Overriding a macro the hub ships
- Copy the original out of
core/plugins/content/formathtml/macros/, keeping its path and file name, into the group'smacros/directory. - Change what you need in
render().
Keep the class name and the namespace exactly as they were. The parser searches the group directory first and stops at the first file it finds, so the group's copy is the one that is included; because both files declare the same class, changing the name would break every other reference to it.
When a macro does not appear
A macro whose file cannot be found renders as nothing at all — no error, no
placeholder, the markup simply vanishes. So does one whose class name does not
match what the parser expects, because the file is included and then
class_exists() fails. If a call disappears from the page, check in this
order:
- the file name against the macro name, lowercased, with
.php; - the namespace line,
Plugins\Content\Formathtml\Macros; - the class name, and its capitalisation;
- that you are looking at a group page or module, not the group's wiki.
Rewritten and checked against 2.4-main @ 91d03d0a23 on 2026-09-10.