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.
- 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.
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.
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.
Install reusable themes into the project root:
themes/
└── brand/
├── entry.php
├── partials/
├── assets/
└── translation/
Use the directory name as the theme name:
theme: brandTo start editing the bundled theme in a PHAR or static binary build, initialize it into themes/custom/:
./yiipress theme:initThe 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.
If a templates/ directory exists inside the content directory, it is automatically registered as local. To use it as the site default:
theme: localAn entry can override the site default theme:
---
title: My Post
theme: custom
---Engine-level theme registration is covered in Engine.
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.
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
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.
| 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} |
Final previous/next links after per-entry overrides are applied to sidebar-derived navigation |
$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 and the entry does not set editLink: false, 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.
| 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; ?>| 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>| 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 |
| 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 |
| Variable | Type | Description |
|---|---|---|
$siteTitle |
string |
Site title |
$authorList |
list<array{title: string, url: string, avatar: string}> |
All authors |
$nav |
?Navigation |
Navigation object or null |
| 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 |
| 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 |
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 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.
<?= $partial('head', ['title' => $entryTitle . ' — ' . $siteTitle, 'rootPath' => $rootPath]) ?>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.
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')) ?>">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.
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>| 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 |
Partials follow the same theme resolution as templates: the active theme is checked first, then other registered themes as fallback.
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` |
$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() |
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.
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).
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.