Extensions #
Extensions let you attach custom PHP logic to your Glaze project. There are two independent mechanisms, both activated by the #[GlazeExtension] attribute:
-
Template helpers — invokable classes callable as
$this->extension('name')from any Sugar template. -
Build event subscribers — classes with methods decorated with
#[ListensTo]that react to specific moments in the build pipeline.
A single class can do both at the same time.
The #[GlazeExtension] attribute #
The attribute accepts two parameters:
| Parameter | Type | Purpose |
|---|---|---|
name |
string|null |
Extension identity. Used as the config key for opt-in and as the template helper name. null for anonymous auto-discovered-only classes. |
helper |
bool |
When true, registers the class as a named template helper. Requires a non-empty name and __invoke(). Defaults to false. |
Having a name alone does not register a template helper — you must opt in explicitly with helper: true. This means a named class without helper: true is addressable from glaze.neon but invisible to templates.
Auto-discovery #
Create an extensions/ directory at your project root. Any class decorated with #[GlazeExtension] placed there is discovered and registered automatically.
The directory is extensions/ by default. Override it in glaze.neon:
paths:
extensions: src/Extensions
Classes that do not carry #[GlazeExtension] are silently skipped, so helper files can live alongside extension classes without issue.
Template helpers #
A template helper is a named, invokable class. Provide a non-empty name, set helper: true, and implement __invoke().
extensions/LatestRelease.php:
<?php
use Glaze\Template\Extension\GlazeExtension;
#[GlazeExtension('version', helper: true)]
final class LatestRelease
{
public function __invoke(): string
{
return trim((string)file_get_contents(__DIR__ . '/../VERSION'));
}
}
Call it from any Sugar template:
<footer>Version <?= $this->extension('version') ?></footer>
Extension results are memoized per build – however many templates call the same extension, the underlying PHP runs exactly once.
Arguments are forwarded to __invoke() on its first invocation:
#[GlazeExtension('asset', helper: true)]
final class AssetExtension
{
public function __invoke(string $path): string
{
return '/assets/' . ltrim($path, '/');
}
}
<link rel="stylesheet" href="<?= $this->extension('asset', 'css/site.css') ?>">
Note: Because results are cached after the first call, passing different arguments on subsequent calls returns the result from the first invocation. Design extensions that take arguments as single-invocation helpers.
Calling an unregistered name throws a RuntimeException:
Glaze extension "versin" is not registered. Available: version, asset
Build event subscribers #
Extensions can also listen to events fired during the static build. Add one or more public methods decorated with #[ListensTo(BuildEvent::X)].
An anonymous subscriber needs no name — omit the argument entirely:
<?php
use Glaze\Build\Event\BuildCompletedEvent;
use Glaze\Build\Event\BuildEvent;
use Glaze\Build\Event\PageWrittenEvent;
use Glaze\Template\Extension\GlazeExtension;
use Glaze\Template\Extension\ListensTo;
#[GlazeExtension]
final class SitemapGenerator
{
private array $urls = [];
#[ListensTo(BuildEvent::PageWritten)]
public function collect(PageWrittenEvent $event): void
{
$this->urls[] = $event->page->urlPath;
}
#[ListensTo(BuildEvent::BuildCompleted)]
public function write(BuildCompletedEvent $event): void
{
$sitemap = $this->buildSitemap($this->urls, $event->config->site->baseUrl);
file_put_contents($event->config->outputPath() . '/sitemap.xml', $sitemap);
}
}
A named subscriber has a name but no helper: true. It is auto-discovered and can be opted into from glaze.neon (see Core extensions below), but it never registers as a template helper:
#[GlazeExtension('my-subscriber')]
final class MySubscriber
{
#[ListensTo(BuildEvent::BuildCompleted)]
public function run(BuildCompletedEvent $event): void { ... }
}
A class with helper: true and event listeners registers as both a template helper and a subscriber:
#[GlazeExtension('stats', helper: true)]
final class StatsExtension
{
private int $pageCount = 0;
#[ListensTo(BuildEvent::PageWritten)]
public function count(PageWrittenEvent $event): void
{
$this->pageCount++;
}
public function __invoke(): int
{
return $this->pageCount;
}
}
Build events reference #
| Event | Payload | Mutable fields | When |
|---|---|---|---|
BuildEvent::BuildStarted |
BuildStartedEvent |
— | Before content discovery |
BuildEvent::ContentDiscovered |
ContentDiscoveredEvent |
$pages |
After discovery, before rendering |
BuildEvent::DjotConverterCreated |
DjotConverterCreatedEvent |
converter object |
After Djot converter creation, before convert |
BuildEvent::SugarRendererCreated |
SugarRendererCreatedEvent |
renderer object |
When Sugar renderer instance is created |
BuildEvent::PageRendered |
PageRenderedEvent |
$html |
After each page renders, before writing |
BuildEvent::PageWritten |
PageWrittenEvent |
— | After each page is written to disk |
BuildEvent::BuildCompleted |
BuildCompletedEvent |
— | After all pages and assets are written |
Mutable fields let you modify the build in place. Set ContentDiscoveredEvent::$pages to inject or remove pages, or set PageRenderedEvent::$html to post-process rendered output.
BuildStartedEvent #
$event->config — BuildConfig
Useful for opening file handles, recording a start time, or validating external prerequisites.
ContentDiscoveredEvent #
$event->pages — ContentPage[] (mutable)
$event->config — BuildConfig
Mutate $pages to inject virtual pages, reorder, augment metadata, or filter the list.
Virtual pages #
A virtual page is a synthetic entry you inject into the page list via ContentDiscovered.
Virtual pages participate in build progress output (the counter and label) but are completely invisible to the site index — they do not appear in navigation, sections, or template collections.
The builder skips the Djot/Sugar render pipeline for them entirely; writing the actual file is the extension’s own responsibility, typically inside BuildCompleted.
Create one with the ContentPage::virtual() named constructor:
ContentPage::virtual(
urlPath: '/sitemap.xml', // canonical public URL
outputRelativePath: 'sitemap.xml', // path under the output directory
title: 'Sitemap', // label shown in build progress
meta: [], // optional arbitrary metadata
)
A complete pattern — register in ContentDiscovered, write in BuildCompleted:
#[GlazeExtension('my-sitemap')]
final class SitemapExtension
{
#[ListensTo(BuildEvent::ContentDiscovered)]
public function registerVirtualPage(ContentDiscoveredEvent $event): void
{
$event->pages[] = ContentPage::virtual('/sitemap.xml', 'sitemap.xml', 'Sitemap');
}
#[ListensTo(BuildEvent::BuildCompleted)]
public function write(BuildCompletedEvent $event): void
{
$xml = $this->buildXml($event->config->site->baseUrl);
file_put_contents($event->config->outputPath() . '/sitemap.xml', $xml);
}
}
The virtual page appears in the build progress counter alongside regular pages (3 / 18) and shows its urlPath as the label. It is never passed to PageRendered or PageWritten listeners.
DjotConverterCreatedEvent #
$event->converter — DjotConverter
$event->page — ContentPage
$event->config — BuildConfig
Useful for registering custom Djot extensions per page render:
#[ListensTo(BuildEvent::DjotConverterCreated)]
public function registerDjot(DjotConverterCreatedEvent $event): void
{
$event->converter->addExtension(new MyDjotExtension());
}
SugarRendererCreatedEvent #
$event->renderer — SugarPageRenderer
$event->template — string
$event->config — BuildConfig
Useful for registering custom Sugar extensions on newly-created page renderers:
#[ListensTo(BuildEvent::SugarRendererCreated)]
public function registerSugar(SugarRendererCreatedEvent $event): void
{
$event->renderer->addExtension(new MySugarExtension());
}
PageRenderedEvent #
$event->page — ContentPage
$event->html — string (mutable)
$event->config — BuildConfig
Mutate $html to minify output, inject analytics snippets, or extract content for a search index.
PageWrittenEvent #
$event->page — ContentPage
$event->destination — string (absolute output path)
$event->config — BuildConfig
Useful for accumulating sitemap entries, search-index records, or per-page statistics.
BuildCompletedEvent #
$event->writtenFiles — string[] (absolute paths)
$event->config — BuildConfig
$event->duration — float (seconds)
Useful for writing derived files (sitemap, search index, RSS feed) and triggering post-build hooks.
Core extensions #
Glaze ships with built-in opt-in extensions (sitemap, llms-txt, search-index) that are activated via the extensions key in glaze.neon. See Core extensions for the full reference.