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

  1. Create content/templates/.
  2. Set the local theme in content/config.yaml:
theme: local
  1. 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:

  1. Entry-level theme — set via theme in front matter.
  2. Site-level default theme — set via theme in config.yaml.
  3. Built-in minimal theme — 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:

VariableTypeDescription
$languagestringEffective page language code used for <html lang="…">
$uiLanguagestringServer-rendered default UI language for theme chrome
$uiLanguageslist<string>Available UI languages exposed by the site
$uiCatalogsarray<string, array<string, string>>Theme UI catalogs for client-side switching
$uiYiiPress\I18n\UiTextInjected localized UI-text helper for bundled theme labels
$dataarray<string, mixed>Site data loaded from content/data/*.yaml or content/data/*.yml
$hClosure(string, int, ?string, bool): stringInjected alias for htmlspecialchars()
$tClosure(string, array): stringInjected 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)

VariableTypeDescription
$siteTitlestringSite title from config.yaml
$entryTitlestringEntry title
$contentstringRendered HTML content
$datestringFormatted date using date_format from config.yaml or empty
$dateISOstringISO 8601 date (Y-m-d) for HTML5 datetime attribute or empty
$authorstringComma-separated author names
$entryAuthorslist<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
$collectionstringCollection name the entry belongs to
$extraarray<string, mixed>Custom front matter under extra
$showTitleboolWhether the bundled entry template renders the generated <h1>
$permalinkstringCurrent entry permalink
$nav?NavigationNavigation object or null
$toclist<array>Table of contents entries ({id, text, level}) or empty list
$relatedlist<RelatedEntry>Related entries ordered by relevance or empty list
$languagestringEffective language code for the current entry
$translationslist<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
$editPageUrlstringResolved edit-page URL when edit_page is configured, otherwise empty
$reportIssueUrlstringResolved 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)

VariableTypeDescription
$siteTitlestringSite title
$collectionTitlestringCollection title
$entrieslist<array{title: string, url: string, date: string, summary: string}>Entries for the current page
$paginationarray{currentPage: int, totalPages: int, previousUrl: string, nextUrl: string}Pagination data
$nav?NavigationNavigation 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)

VariableTypeDescription
$siteTitlestringSite title
$taxonomyNamestringTaxonomy name (e.g. tags, categories)
$termslist<string>All terms in this taxonomy
$nav?NavigationNavigation 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)

VariableTypeDescription
$siteTitlestringSite title
$taxonomyNamestringTaxonomy name
$termstringTerm value
$entrieslist<array{title: string, url: string, date: string}>Entries on the current term page
$paginationarray{currentPage: int, totalPages: int, previousUrl: string, nextUrl: string}Pagination data
$nav?NavigationNavigation object or null

Author page template (author.php)

VariableTypeDescription
$siteTitlestringSite title
$authorTitlestringAuthor display name
$authorEmailstringAuthor email (may be empty)
$authorUrlstringAuthor URL (may be empty)
$authorAvatarstringAuthor avatar path (may be empty)
$authorBiostringAuthor bio rendered as HTML
$entrieslist<array{title: string, url: string, date: string}>Author's entries
$nav?NavigationNavigation object or null

Author index template (author_index.php)

VariableTypeDescription
$siteTitlestringSite title
$authorListlist<array{title: string, url: string, avatar: string}>All authors
$nav?NavigationNavigation object or null

Yearly archive template (archive_yearly.php)

VariableTypeDescription
$siteTitlestringSite title
$collectionNamestringCollection name
$collectionTitlestringCollection title
$yearstringYear
$monthslist<string>Months with entries (descending)
$entrieslist<array{title: string, url: string, date: string}>Entries for this year
$nav?NavigationNavigation object or null

Monthly archive template (archive_monthly.php)

VariableTypeDescription
$siteTitlestringSite title
$collectionNamestringCollection name
$collectionTitlestringCollection title
$yearstringYear
$monthstringMonth number (zero-padded)
$monthNamestringMonth name (e.g. January)
$entrieslist<array{title: string, url: string, date: string}>Entries for this month
$nav?NavigationNavigation 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

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.css
  • assets/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)

PartialVariablesDescription
head$title<meta> tags, <title>, stylesheet link
header$siteTitle, $navSite header with navigation and dark mode toggle
footer$navFooter 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:

FunctionSignatureDescription
$partial(string $name, array $variables = []): stringRender a partial template from the partials/ directory
$h(string $string, int $flags = ENT_QUOTES | ENT_SUBSTITUTE, ?string $encoding = 'UTF-8', bool $doubleEncode = true): stringEscape HTML output
$t(string $key, array $params = []): stringTranslate a theme UI-text key via the injected $ui
$url(string $path): stringBuild 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:

HelperUsageDescription
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.