Development of Custom Kirby Template (PHP)
Template in Kirby is regular PHP file. No template engine on top, no compilation, no abstractions. File templates/article.php renders when page has article type (content file name article.txt). Variables $page, $site, $kirby available automatically, others passed via controller.
Snippets — Building Blocks
Snippets are include files with data passing ability:
<?php snippet('header', ['title' => $page->title()]) ?>
<main>
<?php snippet('hero', ['page' => $page]) ?>
<?php snippet('content-blocks', compact('page')) ?>
</main>
<?php snippet('footer') ?>
<?php
// site/snippets/hero.php
/** @var \Kirby\Cms\Page $page */
?>
<section class="hero" style="--bg: url('<?= $page->cover()->toFile()?->url() ?>')">
<div class="hero__inner">
<h1 class="hero__title"><?= $page->title()->html() ?></h1>
<?php if ($page->subtitle()->isNotEmpty()): ?>
<p class="hero__subtitle"><?= $page->subtitle()->html() ?></p>
<?php endif ?>
</div>
</section>
Working with Fields
Kirby returns field object, not string. Methods can be chained:
// text
$page->title()->html()
$page->title()->upper()
$page->title()->slug()
$page->title()->or('Untitled')->html()
// date
$page->date()->toDate('d.m.Y')
$page->date()->toDate('U') // Unix timestamp
$page->date()->isNotEmpty()
// Markdown → HTML
$page->text()->kirbytext()
// text excerpt
$page->text()->excerpt(150, true, '...')
// numbers
$page->price()->toFloat()
$page->count()->toInt()
// boolean fields
$page->featured()->isTrue()
$page->active()->toBool()
Complex Template: List with Filtering
<?php
// site/templates/products.php
/** @var \Kirby\Cms\Page $page */
/** @var \Kirby\Cms\Pages $products */
/** @var \Kirby\Cms\Pagination $pagination */
/** @var string $activeCategory */
snippet('header');
?>
<div class="products-page">
<aside class="filters">
<nav>
<a href="<?= $page->url() ?>"
class="<?= $activeCategory ? '' : 'active' ?>">
All
</a>
<?php foreach ($categories as $cat): ?>
<a href="<?= $page->url(['params' => ['category' => $cat]]) ?>"
class="<?= $activeCategory === $cat ? 'active' : '' ?>">
<?= html($cat) ?>
</a>
<?php endforeach ?>
</nav>
</aside>
<section class="products-grid">
<?php foreach ($products as $product): ?>
<?php snippet('product-card', compact('product')) ?>
<?php endforeach ?>
<?php if ($products->isEmpty()): ?>
<p class="empty-state">Nothing found</p>
<?php endif ?>
</section>
<?php if ($pagination->hasPages()): ?>
<nav class="pagination">
<?php if ($pagination->hasPrevPage()): ?>
<a href="<?= $pagination->prevPageURL() ?>">← Back</a>
<?php endif ?>
<span><?= $pagination->page() ?> / <?= $pagination->pages() ?></span>
<?php if ($pagination->hasNextPage()): ?>
<a href="<?= $pagination->nextPageURL() ?>">Next →</a>
<?php endif ?>
</nav>
<?php endif ?>
</div>
<?php snippet('footer') ?>
Nested Structures (structure field)
Structure field in Blueprint:
gallery:
type: structure
label: Gallery
fields:
image:
type: files
max: 1
caption:
type: text
alt:
type: text
Output in template:
<?php if ($page->gallery()->isNotEmpty()): ?>
<div class="gallery">
<?php foreach ($page->gallery()->toStructure() as $item): ?>
<?php $img = $item->image()->toFile() ?>
<?php if ($img): ?>
<figure>
<img
src="<?= $img->crop(600, 400)->url() ?>"
alt="<?= $item->alt()->or($img->alt())->html() ?>"
>
<?php if ($item->caption()->isNotEmpty()): ?>
<figcaption><?= $item->caption()->html() ?></figcaption>
<?php endif ?>
</figure>
<?php endif ?>
<?php endforeach ?>
</div>
<?php endif ?>
Block Editor (blocks field)
Kirby Blocks — Gutenberg analogue. Each block renders separate snippet:
// templates/article.php
<?= $page->text()->toBlocks() ?>
Custom block callout:
# site/blueprints/blocks/callout.yml
name: Callout
icon: alert
fields:
type:
type: select
options:
info: Information
warning: Warning
danger: Danger
text:
type: writer
<?php
// site/snippets/blocks/callout.php
/** @var \Kirby\CMS\Block $block */
?>
<div class="callout callout--<?= $block->type()->html() ?>">
<?= $block->text()->kirbytext() ?>
</div>
Helpers and Global Functions
// output with escaping
html($string)
esc($string, 'attr')
esc($string, 'url')
// URL
url('blog/first-post')
url('/', ['params' => ['page' => 2]])
// image
thumb($image, ['width' => 800, 'quality' => 85])
// translation
t('read.more') // from site/languages/*.php
tt('items.count', $count) // with number for declension
Conditional Templates
Kirby allows variant templates by suffix:
-
templates/article.php— main -
templates/article.preview.php— preview mode -
templates/article.rss.php— RSS version
Switching: $page->render(['template' => 'article.rss']).
Development Timelines
Template for one page type with block editor and gallery: 2–3 days. Full template set for corporate site (homepage, services, blog, contacts, 404): 1–2 weeks including markup.







