Content processors
Content processors transform entry content through a pipeline. Each processor receives the output of the previous one.
Processor interface
A processor implements App\Processor\ContentProcessorInterface:
interface ContentProcessorInterface { public function process(string $content, Entry $entry): string; }
Content processor pipeline
ContentProcessorPipeline chains processors in order:
markdown → MarkdownProcessor → SyntaxHighlightProcessor → ... → final HTML
Two separate pipelines are configured via 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 md4c. Accepts MarkdownConfig via constructor for feature toggles
(tables, strikethrough, tasklists, etc.).
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 Rust library (src/Highlighter/) built with syntect
and rayon, called from PHP via FFI. It processes all
<pre><code class="language-xxx"> blocks in the rendered HTML, replacing them with
inline-styled highlighted output.
Rayon parallelizes highlighting across code blocks within a single page, which helps
when a page contains many code blocks (e.g., documentation pages).
The library is compiled during Docker image build (multistage build) and installed as
/usr/local/lib/libyiipress_highlighter.so. No additional setup is needed.
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.
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.
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
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 - Deduplicates IDs by appending a numeric suffix (
intro,intro-2,intro-3) - Leaves headings that already have an
idattribute 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.
Content importers
Content importers convert data from external sources (Telegram, WordPress, Jekyll, REST APIs, databases, etc.) into YiiPress markdown files with front matter. They are invoked via the yii import command.
Importer interface
An importer implements App\Import\ContentImporterInterface:
interface ContentImporterInterface { public function options(): array; public function import(array $options, string $targetDirectory, string $collection): ImportResult; public function name(): string; }
options()— returns a list ofImporterOptionobjects declaring what CLI options this importer accepts. Each option becomes a--nameflag in theyii importcommand.import()— receives resolved option values as$options(keyed by option name), creates.mdfiles in$targetDirectory/$collection/, copies media to$targetDirectory/$collection/assets/, and creates_collection.yamlif missing.name()— returns the unique identifier used as thesourceargument inyii import(e.g.,telegram,wordpress).
ImporterOption
Each importer declares its options using ImporterOption:
new ImporterOption( name: 'directory', description: 'Path to the export directory', required: true, default: null, )
name— option name, used as--nameon the CLI and as key in the$optionsarray.description— help text shown inyii import --help.required— whether the option must be provided. The command validates this before callingimport().default— default value when the option is not provided (only for optional options).
ImportResult
ImportResult is a value object returned by import() containing:
totalMessages()— total number of source items found.importedCount()— number of items successfully imported.importedFiles()— list of created file paths.skippedFiles()— list of skipped items with reasons.warnings()— list of warning messages.
Built-in importers
TelegramContentImporter
Imports messages from a Telegram Desktop channel export (JSON format).
Options:
--directory— Path to the Telegram export directory containingresult.json(required)--ignore_message_ids— Comma-separated list of message IDs to skip during import (optional)
See commands.md for usage details.
Writing a custom importer
Create a class implementing ContentImporterInterface. Each importer declares its own options — a file-based importer might need a directory, while an API-based importer might need url and api-key.
For example, a REST API importer:
final class RestApiContentImporter implements ContentImporterInterface { public function options(): array { return [ new ImporterOption(name: 'url', description: 'API endpoint URL', required: true), new ImporterOption(name: 'api-key', description: 'API authentication key', required: true), new ImporterOption(name: 'limit', description: 'Max posts to import', default: '100'), ]; } public function import(array $options, string $targetDirectory, string $collection): ImportResult { $url = $options['url']; $apiKey = $options['api-key']; $limit = (int) ($options['limit'] ?? '100'); // 1. Fetch data from the API // 2. For each post, create a .md file with front matter in $targetDirectory/$collection/ // 3. Return ImportResult with stats } public function name(): string { return 'rest-api'; } }
Each generated markdown file should follow the standard YiiPress front matter format:
--- title: My Post Title date: 2024-03-15 10:30:00 tags: - php - tutorial --- Post content in markdown...
To register the importer, add it to the importers array in config/common/di/importer.php:
use App\Console\ImportCommand; use App\Import\Telegram\TelegramContentImporter; use App\Import\RestApiContentImporter; return [ ImportCommand::class => [ '__construct()' => [ 'rootPath' => dirname(__DIR__, 3), 'importers' => [ 'telegram' => new TelegramContentImporter(), 'rest-api' => new RestApiContentImporter(), ], ], ], ];
The array key must match the value returned by name() and is used as the source argument:
yii import rest-api --url=https://api.example.com/posts --api-key=secret