Templates are plain PHP files. Variables are passed via require inside an ob_start()/ob_get_clean() block, so each template has direct access to its variables as local PHP variables.
A theme is a named set of templates. Themes are registered in the Yii3 DI configuration (config/common/di/theme.php). The built-in minimal theme ships in the themes/minimal/ directory at the project root.
When resolving a template, the build process checks:
- Entry-level theme — set via
themein front matter. - Site-level default theme — set via
themeinconfig.yaml. - Built-in
minimaltheme — always used as a fallback.
Within a theme, if the requested template file is not found, other registered themes are checked as a fallback.
If a templates/ directory exists inside the content directory, it is automatically registered as a theme named local. To use it as the site default:
theme: localAn entry can override the site default theme via front matter:
---
title: My Post
theme: custom
---Themes are registered in config/common/di/theme.php:
use App\Build\Theme;
use App\Build\ThemeRegistry;
use Yiisoft\Definitions\DynamicReference;
return [
ThemeRegistry::class => DynamicReference::to(static function (): ThemeRegistry {
$registry = new ThemeRegistry();
$registry->register(new Theme('minimal', dirname(__DIR__, 3) . '/themes/minimal'));
$registry->register(new Theme('fancy', '/path/to/fancy-theme'));
return $registry;
}),
];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
| 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 |
$collection |
string |
Collection name the entry belongs to |
$nav |
?Navigation |
Navigation object or null |
Example:
<article>
<h1><?= htmlspecialchars($entryTitle) ?></h1>
<?php if ($date !== ''): ?>
<time datetime="<?= htmlspecialchars($dateISO) ?>"><?= htmlspecialchars($date) ?></time>
<?php endif; ?>
<?php if ($author !== ''): ?>
<span class="author"><?= htmlspecialchars($author) ?></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).
| 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><?= htmlspecialchars($collectionTitle) ?></h1>
<ul>
<?php foreach ($entries as $entry): ?>
<li>
<a href="<?= htmlspecialchars($entry['url']) ?>"><?= htmlspecialchars($entry['title']) ?></a>
<?php if ($entry['date'] !== ''): ?>
<time><?= htmlspecialchars($entry['date']) ?></time>
<?php endif; ?>
<?php if ($entry['summary'] !== ''): ?>
<p><?= htmlspecialchars($entry['summary']) ?></p>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
<?php if ($pagination['totalPages'] > 1): ?>
<nav class="pagination">
<?php if ($pagination['previousUrl'] !== ''): ?>
<a href="<?= htmlspecialchars($pagination['previousUrl']) ?>" rel="prev">← Previous</a>
<?php endif; ?>
<span>Page <?= $pagination['currentPage'] ?> of <?= $pagination['totalPages'] ?></span>
<?php if ($pagination['nextUrl'] !== ''): ?>
<a href="<?= htmlspecialchars($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><?= htmlspecialchars(ucfirst($taxonomyName)) ?></h1>
<ul>
<?php foreach ($terms as $term): ?>
<li><a href="/<?= htmlspecialchars($taxonomyName) ?>/<?= htmlspecialchars($term) ?>/"><?= htmlspecialchars($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 with this term |
$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') !== []): ?>
<?= \App\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.
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]) ?>Templates and partials should use Asset::url() to resolve the final public URL of a copied asset:
<?php
use App\Build\Asset;
?>
<link rel="stylesheet" href="<?= htmlspecialchars(Asset::url('assets/theme/style.css', $rootPath, $assetManifest)) ?>">
<script src="<?= htmlspecialchars(Asset::url('assets/theme/search.js', $rootPath, $assetManifest)) ?>" defer></script>This is especially useful when assets.fingerprint: true is enabled in content/config.yaml.
In that mode, Asset::url('assets/theme/style.css', $rootPath, $assetManifest) returns the hashed output path rather than the logical one.
The helper accepts logical build-relative paths such as:
assets/theme/style.cssassets/plugins/mermaid.css
This resolves partials/head.php from the active theme (with fallback to other registered themes), renders it with the given variables, and returns the HTML string.
Create a PHP file in themes/<name>/partials/:
<?php
/** @var string $title */
?>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= htmlspecialchars($title) ?></title>
<link rel="stylesheet" href="/assets/theme/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 |
navigation |
$navigation |
Main navigation <nav> list |
author-card |
$author |
Author name and bio card |
entry-card |
$entry |
Entry summary card with date |
pagination |
$collection |
Previous/next pagination links |
sidebar |
$entry |
Sidebar with author cards |
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 |
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 |
htmlspecialchars() |
htmlspecialchars($text) |
PHP built-in for escaping HTML output |
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, $collection, $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 ?\App\Content\Model\Navigation $nav */
?>
<!DOCTYPE html>
<html>
<head><title><?= htmlspecialchars($entryTitle) ?> — <?= htmlspecialchars($siteTitle) ?></title></head>
<body>
<div class="wide-container">
<h1><?= htmlspecialchars($entryTitle) ?></h1>
<div class="content"><?= $content ?></div>
</div>
</body>
</html>Then reference it in any entry's front matter with layout: wide.