Templates
Templates control the HTML around your Markdown content. YiiPress uses plain PHP templates, so you can write normal HTML and add small PHP expressions where dynamic values are needed.
Most sites do not need a full custom theme. Start by overriding one template in content/templates/, then add more files only when you need them.
Quick customization
- Create
content/templates/. - Set the local theme in
content/config.yaml:
theme: local
- Add a template file such as
content/templates/entry.php.
A minimal entry template:
<?php /** @var string $siteTitle */ /** @var string $entryTitle */ /** @var string $content */ /** @var Closure $h */ ?> <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title><?= $h($entryTitle) ?> - <?= $h($siteTitle) ?></title> </head> <body> <main> <h1><?= $h($entryTitle) ?></h1> <?= $content ?> </main> </body> </html>
Use $h() for text that should be escaped. Rendered Markdown content in $content is already HTML and should not be escaped again.
Themes
A theme is a named set of template files. YiiPress ships with the built-in minimal theme. Project themes under themes/<name>/ are registered automatically, and a project-local content/templates/ directory is automatically available as the local theme.
Theme resolution order
When YiiPress renders a page, it chooses templates in this order:
- Entry-level theme — set via
themein front matter. - Site-level default theme — set via
themeinconfig.yaml. - Built-in
minimaltheme — fallback when a template is missing.
Within a theme, YiiPress uses the requested file when it exists and falls back to other registered themes when it does not. That means a project theme or local theme can override only entry.php and keep every other page type from minimal.
Project themes
Install reusable themes into the project root:
themes/
└── brand/
├── entry.php
├── partials/
├── assets/
└── translation/
Use the directory name as the theme name:
theme: brand
To start editing the bundled theme in a PHAR or static binary build, initialize it into themes/custom/:
./yiipress theme:init
The command updates content/config.yaml automatically to set theme: custom.
Theme directory names may contain letters, numbers, _, and -, and must start with a letter or number. If a project theme has the same name as an already registered built-in theme, the built-in theme is kept.
Local theme
If a templates/ directory exists inside the content directory, it is automatically registered as local. To use it as the site default:
theme: local
Per-entry theme
An entry can override the site default theme:
--- title: My Post theme: custom ---
Engine-level theme registration is covered in Engine.
UI translations
Theme-localized UI labels live in translation/<language>.yaml inside the theme directory. The bundled minimal theme ships with English and Russian translations.
Use translation files for labels that are part of the theme, such as "Search", "Related posts", pagination controls, and month names. If a key is missing, YiiPress falls back to the site default UI language, then English, then the key name.
Built-in templates
The built-in theme uses these template files:
themes/minimal/
├── entry.php # Single entry page
├── collection_listing.php # Collection listing with pagination
├── taxonomy_index.php # Taxonomy index (all terms)
├── taxonomy_term.php # Single taxonomy term (entries with this term)
├── author.php # Single author page
├── author_index.php # Author listing page
├── archive_yearly.php # Yearly archive
├── archive_monthly.php # Monthly archive
Template variables
Common variables
All built-in page templates receive these additional variables:
| Variable | Type | Description |
|---|---|---|
$language | string | Effective page language code used for <html lang="…"> |
$uiLanguage | string | Server-rendered default UI language for theme chrome |
$uiLanguages | list<string> | Available UI languages exposed by the site |
$uiCatalogs | array<string, array<string, string>> | Theme UI catalogs for client-side switching |
$ui | YiiPress\I18n\UiText | Injected localized UI-text helper for bundled theme labels |
$data | array<string, mixed> | Site data loaded from content/data/*.yaml or content/data/*.yml |
$h | Closure(string, int, ?string, bool): string | Injected alias for htmlspecialchars() |
$t | Closure(string, array): string | Injected shortcut for $ui->get() in templates |
Example:
<html lang="<?= $h($language) ?>"> <button aria-label="<?= $h($t('search')) ?>"> <span><?= $h($data['company']['name'] ?? '') ?></span>
In the bundled minimal theme, $language is the content language of the current page, while the remembered UI language can differ and is applied client-side after load.
Built-in templates and partials expect $ui to be passed by the renderer; PageTemplateRenderer, TemplateContext, and EntryRenderer automatically provide $t, and all render paths inject $h.
Entry template (entry.php)
| Variable | Type | Description |
|---|---|---|
$siteTitle | string | Site title from config.yaml |
$entryTitle | string | Entry title |
$content | string | Rendered HTML content |
$date | string | Formatted date using date_format from config.yaml or empty |
$dateISO | string | ISO 8601 date (Y-m-d) for HTML5 datetime attribute or empty |
$author | string | Comma-separated author names |
$entryAuthors | list<array{slug: string, title: string, url: string}> | Entry authors; url links to the author page when author_pages is enabled and the author file exists |
$collection | string | Collection name the entry belongs to |
$extra | array<string, mixed> | Custom front matter under extra |
$showTitle | bool | Whether the bundled entry template renders the generated <h1> |
$permalink | string | Current entry permalink |
$nav | ?Navigation | Navigation object or null |
$toc | list<array> | Table of contents entries ({id, text, level}) or empty list |
$related | list<RelatedEntry> | Related entries ordered by relevance or empty list |
$language | string | Effective language code for the current entry |
$translations | list<Translation> | Alternate-language versions of the current entry |
$navigationPager | ?array{previous: ?array, next: ?array} | Previous/next links resolved from sidebar navigation when enabled |
$lastUpdated | ?array{iso: string, text: string} | Source file modification time when last_updated is enabled |
$editPageUrl | string | Resolved edit-page URL when edit_page is configured, otherwise empty |
$reportIssueUrl | string | Resolved issue-report URL when report_issue is configured, otherwise empty |
Example:
<article> <h1><?= $h($entryTitle) ?></h1> <?php if ($date !== ''): ?> <time datetime="<?= $h($dateISO) ?>"><?= $h($date) ?></time> <?php endif; ?> <?php if ($entryAuthors !== []): ?> <span class="author"> <?php foreach ($entryAuthors as $index => $entryAuthor): ?> <?= $index > 0 ? ', ' : '' ?> <?php if ($entryAuthor['url'] !== ''): ?> <a href="<?= $h($entryAuthor['url']) ?>"><?= $h($entryAuthor['title']) ?></a> <?php else: ?> <?= $h($entryAuthor['title']) ?> <?php endif; ?> <?php endforeach; ?> </span> <?php endif; ?> <div class="content"><?= $content ?></div> </article>
Note: Use $dateISO for the datetime attribute (HTML5 compliance) and $date for display text (uses configured format). In the bundled minimal theme, set top-level showTitle: false to suppress the generated entry <h1> while keeping the page title available for metadata and navigation.
The bundled minimal theme also uses $ui to localize built-in labels such as
"Related posts", "Other languages", "Search", pagination controls, and the remembered UI-language selector in the header.
Collection listing template (collection_listing.php)
| Variable | Type | Description |
|---|---|---|
$siteTitle | string | Site title |
$collectionTitle | string | Collection title |
$entries | list<array{title: string, url: string, date: string, summary: string}> | Entries for the current page |
$pagination | array{currentPage: int, totalPages: int, previousUrl: string, nextUrl: string} | Pagination data |
$nav | ?Navigation | Navigation object or null |
Example:
<h1><?= $h($collectionTitle) ?></h1> <ul> <?php foreach ($entries as $entry): ?> <li> <a href="<?= $h($entry['url']) ?>"><?= $h($entry['title']) ?></a> <?php if ($entry['date'] !== ''): ?> <time><?= $h($entry['date']) ?></time> <?php endif; ?> <?php if ($entry['summary'] !== ''): ?> <p><?= $h($entry['summary']) ?></p> <?php endif; ?> </li> <?php endforeach; ?> </ul> <?php if ($pagination['totalPages'] > 1): ?> <nav class="pagination"> <?php if ($pagination['previousUrl'] !== ''): ?> <a href="<?= $h($pagination['previousUrl']) ?>" rel="prev">← Previous</a> <?php endif; ?> <span>Page <?= $pagination['currentPage'] ?> of <?= $pagination['totalPages'] ?></span> <?php if ($pagination['nextUrl'] !== ''): ?> <a href="<?= $h($pagination['nextUrl']) ?>" rel="next">Next →</a> <?php endif; ?> </nav> <?php endif; ?>
Taxonomy index template (taxonomy_index.php)
| Variable | Type | Description |
|---|---|---|
$siteTitle | string | Site title |
$taxonomyName | string | Taxonomy name (e.g. tags, categories) |
$terms | list<string> | All terms in this taxonomy |
$nav | ?Navigation | Navigation object or null |
Example:
<h1><?= $h(ucfirst($taxonomyName)) ?></h1> <ul> <?php foreach ($terms as $term): ?> <li><a href="<?= $h($url($taxonomyName . '/' . $term . '/')) ?>"><?= $h($term) ?></a></li> <?php endforeach; ?> </ul>
Taxonomy term template (taxonomy_term.php)
| Variable | Type | Description |
|---|---|---|
$siteTitle | string | Site title |
$taxonomyName | string | Taxonomy name |
$term | string | Term value |
$entries | list<array{title: string, url: string, date: string}> | Entries on the current term page |
$pagination | array{currentPage: int, totalPages: int, previousUrl: string, nextUrl: string} | Pagination data |
$nav | ?Navigation | Navigation object or null |
Author page template (author.php)
| Variable | Type | Description |
|---|---|---|
$siteTitle | string | Site title |
$authorTitle | string | Author display name |
$authorEmail | string | Author email (may be empty) |
$authorUrl | string | Author URL (may be empty) |
$authorAvatar | string | Author avatar path (may be empty) |
$authorBio | string | Author bio rendered as HTML |
$entries | list<array{title: string, url: string, date: string}> | Author's entries |
$nav | ?Navigation | Navigation object or null |
Author index template (author_index.php)
| Variable | Type | Description |
|---|---|---|
$siteTitle | string | Site title |
$authorList | list<array{title: string, url: string, avatar: string}> | All authors |
$nav | ?Navigation | Navigation object or null |
Yearly archive template (archive_yearly.php)
| Variable | Type | Description |
|---|---|---|
$siteTitle | string | Site title |
$collectionName | string | Collection name |
$collectionTitle | string | Collection title |
$year | string | Year |
$months | list<string> | Months with entries (descending) |
$entries | list<array{title: string, url: string, date: string}> | Entries for this year |
$nav | ?Navigation | Navigation object or null |
Monthly archive template (archive_monthly.php)
| Variable | Type | Description |
|---|---|---|
$siteTitle | string | Site title |
$collectionName | string | Collection name |
$collectionTitle | string | Collection title |
$year | string | Year |
$month | string | Month number (zero-padded) |
$monthName | string | Month name (e.g. January) |
$entries | list<array{title: string, url: string, date: string}> | Entries for this month |
$nav | ?Navigation | Navigation object or null |
Navigation
All templates receive $nav — a Navigation object (or null if no navigation.yaml exists).
Use NavigationRenderer for HTML output:
<?php if ($nav !== null && $nav->menu('main') !== []): ?> <?= \YiiPress\Render\NavigationRenderer::render($nav, 'main') ?> <?php endif; ?>
This renders a <nav><ul><li> structure with nested lists for children. Menu names correspond to top-level keys in content/navigation.yaml.
NavigationRenderer escapes menu labels and generated attributes with HTML5-compatible Yii helpers, so raw menu data can contain characters such as &, <, >, and quotes.
Pass the optional class and current URL arguments when rendering sidebars that need active item styling:
<?= \YiiPress\Render\NavigationRenderer::render($nav, 'sidebar', $rootPath, $uiLanguage, $uiLanguage, 'docs-sidebar-nav', $permalink) ?>
The renderer adds aria-current="page" to the current link, is-current to the current <li>, and is-active-ancestor to parent <li> elements.
Partials
Partials are reusable template fragments stored in a partials/ subdirectory of a theme. Every template receives a $partial helper function that renders a partial with isolated variable scope.
Usage
<?= $partial('head', ['title' => $entryTitle . ' — ' . $siteTitle, 'rootPath' => $rootPath]) ?>
Asset helper
Templates and partials should use $themeAsset() for files in the active theme's assets/ directory:
<link rel="stylesheet" href="<?= $h($themeAsset('style.css')) ?>"> <script src="<?= $h($themeAsset('search.js')) ?>" defer></script>
This is especially useful when assets.fingerprint: true is enabled in content/config.yaml.
In that mode, $themeAsset('style.css') returns the hashed output path rather than the logical one.
Theme assets are copied to a theme-specific namespace:
assets/themes/<theme>/style.cssassets/themes/<theme>/search.js
For compatibility, YiiPress also writes assets/theme/... aliases for the first registered theme. New templates should use $themeAsset() so installed themes cannot overwrite each other's asset files.
For non-theme assets, use Asset::url() with a logical build-relative path:
<?php use YiiPress\Build\Asset; ?> <link rel="stylesheet" href="<?= $h(Asset::url('assets/plugins/mermaid.css', $rootPath, $assetManifest)) ?>">
That helper accepts logical build-relative paths such as assets/plugins/mermaid.css.
Creating a partial
Create a PHP file in themes/<name>/partials/:
<?php /** * @var string $title * @var string $rootPath * @var AssetFingerprintManifest|null $assetManifest * @var Closure(string, int, ?string, bool): string $h * @var Closure(string): string $themeAsset */ ?> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title><?= $h($title) ?></title> <link rel="stylesheet" href="<?= $h($themeAsset('style.css')) ?>">
Variable isolation
Partials receive only the variables passed via the second argument. Parent template variables do not leak into partials. This prevents accidental coupling between templates and partials.
Nesting partials
Partials can include other partials — the $partial function is automatically available inside every partial:
<div class="page"> <?= $partial('header', ['siteTitle' => $siteTitle, 'nav' => $nav]) ?> <main><?= $content ?></main> <?= $partial('footer', ['nav' => $nav]) ?> </div>
Built-in partials (minimal theme)
| Partial | Variables | Description |
|---|---|---|
head | $title | <meta> tags, <title>, stylesheet link |
header | $siteTitle, $nav | Site header with navigation and dark mode toggle |
footer | $nav | Footer navigation and dark mode script |
Theme resolution
Partials follow the same theme resolution as templates: the active theme is checked first, then other registered themes as fallback.
Template helper functions
All templates receive the following helper functions as local variables:
| Function | Signature | Description |
|---|---|---|
$partial | (string $name, array $variables = []): string | Render a partial template from the partials/ directory |
$h | (string $string, int $flags = ENT_QUOTES | ENT_SUBSTITUTE, ?string $encoding = 'UTF-8', bool $doubleEncode = true): string | Escape HTML output |
$t | (string $key, array $params = []): string | Translate a theme UI-text key via the injected $ui |
$url | (string $path): string | Build an internal site URL relative to the current output page root |
Use $url() for internal links generated by templates:
<a href="<?= $h($url('tags/php/')) ?>">#php</a> <a href="<?= $h($url('/')) ?>">Home</a>
It keeps links valid for subdirectory deployments such as GitHub Pages project sites.
Additional helpers available via static methods:
| Helper | Usage | Description |
|---|---|---|
NavigationRenderer::render() | NavigationRenderer::render($nav, 'main') | Render a navigation menu as nested <nav><ul><li> HTML |
NavigationRenderer::menuContainsUrl() | NavigationRenderer::menuContainsUrl($nav, 'sidebar', $permalink) | Check whether a menu contains the current page URL |
$h() | $h($text) | Template alias for htmlspecialchars() |
Customizing templates
To customize a built-in template, create a theme with a file of the same name. The active theme takes priority over other registered themes.
Custom layouts
Entries can use a custom layout by setting layout in front matter:
--- title: My Post layout: wide ---
The build process looks for wide.php in the active theme, then falls back to the built-in entry.php if not found.
Custom layout templates receive the same variables as the default entry template ($siteTitle, $entryTitle, $content, $date, $author, $entryAuthors, $collection, $extra, $showTitle, $nav).
Example
Create content/templates/wide.php (with theme: local in config):
<?php /** @var string $siteTitle */ /** @var string $entryTitle */ /** @var string $content */ /** @var string $date */ /** @var string $author */ /** @var ?\YiiPress\Content\Model\Navigation $nav */ ?> <!DOCTYPE html> <html> <head><title><?= $h($entryTitle) ?> — <?= $h($siteTitle) ?></title></head> <body> <div class="wide-container"> <h1><?= $h($entryTitle) ?></h1> <div class="content"><?= $content ?></div> </div> </body> </html>
Then reference it in any entry's front matter with layout: wide.