Plugins
YiiPress extension points are configured through Yii3 DI. Content processors transform Markdown and rendered HTML during builds; related-content and table-of-contents processors add shared page behavior.
Lifecycle hooks cover build-wide and render-wide plugin behavior that is outside a single content processor.
Content processors
Content processors transform entry content through a pipeline. Each processor receives the output of the previous one.
Processor interface
A processor implements YiiPress\Processor\ContentProcessorInterface:
interface ContentProcessorInterface { public function process(string $content, Entry $entry): string; }
Content processor pipeline
ContentProcessorPipeline chains processors in order:
Two separate pipelines are configured via the Yii3 DI container in config/common/di/content-pipeline.php:
- contentPipeline — used by
EntryRenderer:MarkdownProcessor→SyntaxHighlightProcessor - feedPipeline — used by
FeedGenerator:MarkdownProcessoronly (no syntax highlighting in feeds)
Built-in processors
MarkdownProcessor
Converts Markdown to HTML using the reusable native PHP extension package, iliaal/mdparser. Accepts MarkdownConfig via constructor for feature toggles
(tables, strikethrough, tasklists, GitHub-style alerts, etc.).
The extension exposes the PHP API as MdParser\Parser and
MdParser\Options, backed by bundled MD4C sources. YiiPress
downloads the package from Packagist during image and binary builds, compiles it,
and enables it as ext-mdparser.
SyntaxHighlightProcessor
Highlights code blocks server-side during build. No client-side JavaScript is needed.
Use standard fenced code blocks with a language identifier:
```php echo "Hello, world!"; ```
The highlighter is a reusable native PHP extension package, yiipress/highlighter. The extension
defines the PHP API as YiiPress\Highlighter, builds a Rust
library with syntect and rayon,
then statically links that library into ext-highlighter. It processes all
<pre><code class="language-xxx"> blocks in the rendered HTML, replacing them with
inline-styled highlighted output. SyntaxHighlightProcessor preserves the normalized language
identifier in a surrounding <div class="code-block" data-language="php"> and emits a visible
.code-language-label, so custom themes can style or reposition the label without inspecting code.
The bundled minimal theme displays the language label and adds a client-side Copy button to
rendered code blocks. Unlabeled blocks remain unchanged and still receive a copy button.
LaTeX math
When markdown.latex_math is enabled, mdparser emits math spans as <span class="math"> elements. Display math adds the display class. YiiPress detects those elements and injects KaTeX rendering assets only on pages that contain math.
Inline math uses $...$, and display math uses $$...$$:
Euler: $e^{i\pi} + 1 = 0$ $$ \int_0^1 x^2\,dx $$
YiiPress ships a small browser enhancer at assets/plugins/latex-math.js and loads fixed-version KaTeX CSS/JS from jsDelivr for the renderer itself.
The native extension passes explicit input and output lengths so repeated highlighting calls avoid
extra C-string scans at the PHP/Rust boundary.
Rayon parallelizes highlighting across code blocks within a single page, which helps
when a page contains many code blocks (e.g., documentation pages).
The extension is downloaded from Packagist during YiiPress image and binary builds, compiled, and enabled as ext-highlighter.
No additional setup is needed when using the YiiPress static binary.
Use it directly from PHP:
use YiiPress\Highlighter; $html = (new Highlighter())->highlightHtml($html, 'Solarized (dark)');
Highlight raw code without wrapping it in <pre><code> first:
$html = (new Highlighter())->highlight('echo "Hello";', 'php');
Use class_exists(Highlighter::class) to detect whether the extension is loaded.
Supported languages include all syntect defaults (PHP, JavaScript, Python, Rust, YAML,
Bash, SQL, HTML, CSS, and many more). Code blocks with an unrecognized language are
highlighted as plain text.
The highlighting color scheme is configured site-wide in content/config.yaml via
highlight_theme. If omitted, YiiPress uses syntect's InspiredGitHub theme.
MermaidProcessor
Renders Mermaid diagrams on the client side.
Use fenced code blocks with mermaid language identifier:
Flowchart:
```mermaid flowchart LR A[Start] --> B{Condition} B -->|Yes| C[Action 1] B -->|No| D[Action 2] ```
Sequence diagram:
```mermaid sequenceDiagram Alice->>John: Hello John, how are you? John-->>Alice: Great! Alice-)John: See you later! ```
Gantt chart:
```mermaid gantt title Project Timeline dateFormat YYYY-MM-DD section Phase 1 Task 1 :a1, 2024-01-01, 30d Task 2 :after a1, 20d ```
The processor converts the code block to a <div class="mermaid"> element.
Mermaid.js (loaded via CDN in the template) renders the diagram as SVG in the browser.
HTML minification preserves the diagram source whitespace because Mermaid uses line breaks to separate statements when semicolons are omitted.
Supported diagram types: flowcharts, sequence diagrams, Gantt charts, pie charts, class diagrams, state diagrams,
user journey maps, and more.
Note: Mermaid.js is only loaded on pages that contain diagrams to reduce bandwidth.
For full syntax reference, see Mermaid documentation.
YouTubeProcessor
Expands YouTube shortcodes into responsive embed HTML before markdown processing.
<div class="shortcode shortcode-youtube"><div class="video-container"><iframe src="https://www.youtube.com/embed/dQw4w9WgXcQ" width="560" height="315" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen loading="lazy" title="YouTube video player"></iframe></div></div>
Optional start time (in seconds):
<div class="shortcode shortcode-youtube"><div class="video-container"><iframe src="https://www.youtube.com/embed/dQw4w9WgXcQ?start=30" width="560" height="315" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen loading="lazy" title="YouTube video player"></iframe></div></div>
With custom dimensions:
<div class="shortcode shortcode-youtube"><div class="video-container"><iframe src="https://www.youtube.com/embed/dQw4w9WgXcQ" width="640" height="360" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen loading="lazy" title="YouTube video player"></iframe></div></div>
Generated HTML includes:
- Responsive iframe container with
.video-containerwrapper - Lazy loading (
loading="lazy") - Fullscreen support (
allowfullscreen) - Accessible title attribute
- CSS classes:
.shortcode,.shortcode-youtube
VimeoProcessor
Expands Vimeo shortcodes into responsive embed HTML before markdown processing.
<div class="shortcode shortcode-vimeo"><div class="video-container"><iframe src="https://player.vimeo.com/video/123456789?dnt=1" width="560" height="315" frameborder="0" allow="autoplay; fullscreen; picture-in-picture; clipboard-write; encrypted-media" allowfullscreen loading="lazy" title="Vimeo video player"></iframe></div></div>
With custom dimensions:
<div class="shortcode shortcode-vimeo"><div class="video-container"><iframe src="https://player.vimeo.com/video/123456789?dnt=1" width="640" height="360" frameborder="0" allow="autoplay; fullscreen; picture-in-picture; clipboard-write; encrypted-media" allowfullscreen loading="lazy" title="Vimeo video player"></iframe></div></div>
Generated HTML includes:
- Responsive iframe container with
.video-containerwrapper - Privacy-friendly embed (
dnt=1- do not track) - Lazy loading (
loading="lazy") - Fullscreen support (
allowfullscreen) - Accessible title attribute
- CSS classes:
.shortcode,.shortcode-vimeo
Both shortcode processors support:
- Self-closing (
/]) and regular syntax - Double quotes, single quotes, or no quotes for attribute values (no spaces)
- Case-insensitive shortcode names
Tabbed code groups
Use [code-group] to present two or more alternative code examples as tabs. Wrap each
fenced code block in [code-tab] and set its visible tab label:
<!-- yiipress-code-group:start --> <!-- yiipress-code-tab:bnBt --> ```sh npm install ``` <!-- /yiipress-code-tab --> <!-- yiipress-code-tab:cG5wbQ== --> ```sh pnpm install ``` <!-- /yiipress-code-tab --> <!-- yiipress-code-group:end -->
Every group must contain at least two [code-tab] shortcodes, and every tab must have a
non-empty label. Content outside the tab shortcodes is not allowed within a group.
Visitors can select tabs with a pointer or use Left/Right arrow, Home, and End keys.
When JavaScript is unavailable, all labeled examples remain visible.
The built-in processor registers its tab stylesheet and script only on pages containing a
code group. Plugin assets are copied and linked automatically, including on nested pages
and when asset fingerprinting is disabled.
FAQ question blocks
Use a [question] shortcode with a title for an answer that should be expandable
without JavaScript. The answer is normal Markdown:
[question title="How do I install YiiPress?"] Download the binary for your platform and run **`yiipress init:content`**. [/question]
YiiPress escapes the title and renders semantic <details> and <summary> elements.
Question shortcodes cannot be nested. Malformed, nested, and unclosed shortcodes are left
as literal content instead of consuming the rest of the page.
By default, questions remain where they are written. Set faq_level in entry front matter
to change grouping:
--- title: Troubleshooting faq_level: 2 ---
falseor omitted — render every question inline.0— collect every question into one FAQ section at the end of the page.1through6— collect questions at the end of each matching heading section.
Grouping preserves question order. Questions before the first matching heading form a
section immediately before that heading; questions after the last matching heading form a
section at the page end. YiiPress does not emit FAQPage JSON-LD automatically because
search engines restrict FAQ rich results to specific authoritative sites and duplicated
answer data can become stale. Custom layouts may derive structured data from their own
content policy.
Project Processors
Static binary users can add site-level content processors without editing Yii3 DI configuration. Put PHP processor files in content/processors/; files matching *.php are discovered automatically, sorted by filename, and inserted before Markdown rendering in both the page and feed pipelines.
Create a no-op processor scaffold, then edit its returned callable:
yiipress init:plugin "Badge Labels"
This creates content/processors/badge-labels.php. Pass --content-dir when the project uses a different content directory.
Each file must return either a YiiPress\Processor\ContentProcessorInterface instance or a callable with the same shape:
<?php use YiiPress\Content\Model\Entry; return static function (string $content, Entry $entry): string { return str_replace('[badge:stable]', '**Stable**', $content); };
For more control, configure processors explicitly in content/config.yaml:
processors: content: before_markdown: - processors/badge.php after_markdown: - processors/html-postprocess.php feed: before_markdown: - processors/badge.php
Use before_markdown for processors that transform Markdown syntax. Use after_markdown for processors that transform rendered HTML. Processor paths are resolved from the content directory and must stay inside it. If the processors config key is omitted, YiiPress discovers content/processors/*.php; if processors contains a list or structured config, YiiPress uses only those files. Set processors: false to disable project processors.
TweetProcessor
Expands tweet shortcodes into Twitter embed HTML before markdown processing.
The Twitter widget JS is injected into <head> only on pages that contain tweet embeds.
<div class="shortcode shortcode-tweet"><blockquote class="twitter-tweet" data-dnt="true"><a href="https://twitter.com/i/web/status/1234567890"></a></blockquote></div>
Generated HTML includes:
- A
<blockquote class="twitter-tweet">element with a link to the tweet - Privacy-friendly embed (
data-dnt="true"— do not track) - CSS classes:
.shortcode,.shortcode-tweet - The Twitter widget script (
platform.twitter.com/widgets.js) injected once per page
OEmbedProcessor
Expands standalone provider URLs into embed HTML before markdown processing.
Providers are pluggable. Each provider implements YiiPress\Processor\OEmbed\OEmbedInterface and owns both:
- URL matching logic
- the generated embed HTML
interface OEmbedInterface { public function supportsOEmbed(string $url): bool; public function replaceOEmbed(string $url): ?string; }
The built-in shortcode processors implement OEmbedInterface directly, so each provider owns its shortcode parsing,
standalone URL matching, and generated HTML in one class.
Register providers through OEmbedProcessor in config/common/di/content-pipeline.php:
OEmbedProcessor::class => [ 'class' => OEmbedProcessor::class, '__construct()' => [ Reference::to(YouTubeProcessor::class), Reference::to(VimeoProcessor::class), Reference::to(TweetProcessor::class), ], ],
Supported providers:
- YouTube watch URLs (
https://www.youtube.com/watch?v=...) - YouTube short URLs (
https://youtu.be/...) - Vimeo video URLs (
https://vimeo.com/123456789) - Twitter/X status URLs (
https://twitter.com/.../status/...,https://x.com/.../status/...)
Example:
<div class="shortcode shortcode-youtube"><div class="video-container"><iframe src="https://www.youtube.com/embed/dQw4w9WgXcQ" width="560" height="315" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen loading="lazy" title="YouTube video player"></iframe></div></div> <div class="shortcode shortcode-vimeo"><div class="video-container"><iframe src="https://player.vimeo.com/video/123456789?dnt=1" width="560" height="315" frameborder="0" allow="autoplay; fullscreen; picture-in-picture; clipboard-write; encrypted-media" allowfullscreen loading="lazy" title="Vimeo video player"></iframe></div></div> <div class="shortcode shortcode-tweet"><blockquote class="twitter-tweet" data-dnt="true"><a href="https://twitter.com/i/web/status/1234567890"></a></blockquote></div>
Each URL must appear on its own line. Inline links remain unchanged.
Generated HTML uses the same wrappers and classes as the built-in shortcode processors:
.shortcode-youtube.shortcode-vimeo.shortcode-tweet
For tweet/status embeds, the existing Twitter widget script is injected automatically because the generated
HTML matches the same marker format as TweetProcessor.
Related content
Suggests other entries that share tags and categories with the current one. When enabled,
an in-memory RelatedIndex is built once per build from all indexed entries and injects a
$related variable into entry templates — a list of YiiPress\Content\Model\RelatedEntry
objects ordered by relevance.
Each related entry exposes:
title— source entry titlepermalink— resolved URLdate—DateTimeImmutable|nullsummary— entry summary (front matter or auto-generated)score— relevance score (shared tags ×tag_weight+ shared categories ×category_weight)
Enable in content/config.yaml (disabled by default):
related: true
Or configure:
related: limit: 5 # maximum number of related entries per page (default: 5) tag_weight: 2 # score per shared tag (default: 2) category_weight: 3 # score per shared category (default: 3) same_collection_only: true # only suggest entries from the same collection (default: true)
Scoring uses an inverted term → entry index, so building the full related graph runs in
time proportional to the number of term postings rather than O(N²).
Templates can render it:
<?php if (!empty($related)): ?> <section class="related"> <h2><?= htmlspecialchars($t('related_posts')) ?></h2> <ul> <?php foreach ($related as $item): ?> <li><a href="<?= htmlspecialchars($item->permalink) ?>"><?= htmlspecialchars($item->title) ?></a></li> <?php endforeach; ?> </ul> </section> <?php endif; ?>
The bundled minimal theme renders a localized related-posts section automatically when the
feature is enabled.
TocProcessor
Generates a table of contents from headings in the rendered HTML.
Enabled by default. Disable globally in content/config.yaml:
toc: false
When enabled, the processor:
- Injects
idattributes into all heading tags (<h1>–<h6>), slugified from the heading text - Renders a hover permalink anchor inside each heading
- Deduplicates IDs by appending a numeric suffix (
intro,intro-2,intro-3) - Keeps existing heading
idattributes unchanged - Passes a
$tocvariable to entry templates — a list of{id, text, level}entries
Templates can render the TOC as a navigation list:
<?php if ($toc !== []): ?> <nav class="toc"> <ol> <?php foreach ($toc as $item): ?> <li class="toc-level-<?= $item['level'] ?>"> <a href="#<?= htmlspecialchars($item['id']) ?>"><?= htmlspecialchars($item['text']) ?></a> </li> <?php endforeach; ?> </ol> </nav> <?php endif; ?>
Writing a custom processor
Create a class implementing ContentProcessorInterface. For example, a shortcode processor:
final class ShortcodeProcessor implements ContentProcessorInterface { public function process(string $content, Entry $entry): string { return preg_replace( '/\<div class="shortcode shortcode-youtube"><div class="video-container"><iframe src="https://www.youtube.com/embed/([^" width="560" height="315" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen loading="lazy" title="YouTube video player"></iframe></div></div>+)"\/\]/', '<div class="video"><iframe src="https://www.youtube.com/embed/$1"></iframe></div>', $content, ); } }
To register it, add it to config/common/di/content-pipeline.php. Place it before MarkdownProcessor since it operates on markdown:
return [ ContentProcessorPipeline::class => [ '__construct()' => [ new ShortcodeProcessor(), new MarkdownProcessor(), new SyntaxHighlightProcessor(), ], ], ];
Processor order matters — each processor receives the output of the previous one.
Lifecycle hooks
Lifecycle hooks use PSR-14 events through yiisoft/yii-event and yiisoft/event-dispatcher. They are intended for plugin behavior that needs build lifecycle context, final page HTML, or build-wide side effects.
Available events:
BuildStartedEvent— fired after site config, navigation, collections, and authors are parsed, before entries and support files are writtenBuildFinishedEvent— fired after a successful build, once entries, support files, assets, sitemap, search index, taxonomies, and author pages are generatedRenderStartedEvent— fired before an entry or standalone page render startsRenderFinishedEvent— fired after final page HTML is rendered; listeners may replace the HTML viasetHtml()
Register listeners in the Yii event configuration group:
// config/common/events.php use YiiPress\Hook\BuildFinishedEvent; use YiiPress\Hook\RenderFinishedEvent; return [ RenderFinishedEvent::class => [ static function (RenderFinishedEvent $event): void { $event->setHtml(str_replace('</body>', '<!-- generated by plugin --></body>', $event->html())); }, ], BuildFinishedEvent::class => [ static function (BuildFinishedEvent $event): void { file_put_contents($event->context->outputDir . '/plugin.txt', 'done'); }, ], ];
Then include that file in config/configuration.php:
'events' => 'common/events.php',
BuildContext exposes rootPath, contentDir, outputDir, worker count, draft/future flags, and build mode flags. Render events expose the current SiteConfig, Entry, and permalink.
BuildFinishedEvent is a successful-build event. If the build throws before completion, YiiPress does not dispatch it.
Render events are dispatched in the process that renders the entry. With multiple workers, this is a forked worker process. RenderFinishedEvent::setHtml() works in parallel builds because the worker writes the returned HTML, but listener-owned in-memory aggregation such as counters, collected permalinks, or object mutations is not visible in the parent process. Use build-level events, external storage, or a single worker for aggregation that must survive the whole build.
When no event dispatcher is injected, hooks use a fast null path and do not allocate per-render event objects. In the default YiiPress app configuration, yiisoft/yii-event provides the dispatcher and empty listener collection.