Website Development on Kirby CMS
Kirby is a file-based CMS on PHP without database. Content is stored in folders and text files, templates are pure PHP, admin panel is built-in and works out of box. Well suited for corporate sites, portfolios, blogs, small catalogs where simple operation without MySQL and migrations is needed.
Project Structure
site/
├── blueprints/ # field schemas panel
│ ├── pages/
│ └── files/
├── collections/ # reusable page sets
├── config/ # configuration, hooks, routes
├── models/ # extended page models
├── plugins/ # third-party and custom plugins
├── snippets/ # reusable template parts
├── templates/ # page templates
└── controllers/ # logic before rendering
content/
├── home/
│ └── home.txt
├── about/
│ └── about.txt
└── blog/
├── blog.txt
├── 2024-01-15_first-post/
│ └── article.txt
└── 2024-02-20_second-post/
└── article.txt
Template System
Kirby matches template by content filename. File article.txt → template templates/article.php:
<?php snippet('header') ?>
<article class="post">
<header>
<h1><?= $page->title()->html() ?></h1>
<time datetime="<?= $page->date()->toDate('Y-m-d') ?>">
<?= $page->date()->toDate('d.m.Y') ?>
</time>
</header>
<div class="content">
<?= $page->text()->kirbytext() ?>
</div>
<?php if ($page->tags()->isNotEmpty()): ?>
<footer class="tags">
<?php foreach ($page->tags()->split() as $tag): ?>
<a href="<?= $site->url() ?>/blog/tag:<?= $tag ?>">
<?= html($tag) ?>
</a>
<?php endforeach ?>
</footer>
<?php endif ?>
</article>
<?php snippet('footer') ?>
Blueprints — Field Schemas
Blueprint describes which fields available to editor for each page type:
# site/blueprints/pages/article.yml
title: Article
columns:
main:
width: 2/3
sections:
content:
type: fields
fields:
title:
type: text
label: Title
required: true
text:
type: writer
label: Article Text
inline: false
marks:
- bold
- italic
- link
- code
cover:
type: files
label: Cover
max: 1
query: page.images
sidebar:
width: 1/3
sections:
meta:
type: fields
fields:
date:
type: date
label: Publish Date
default: today
tags:
type: tags
label: Tags
seo_description:
type: textarea
label: SEO Description
maxlength: 160
Controllers
Controller separates logic from template:
<?php
// site/controllers/blog.php
return function ($page, $site) {
$tag = param('tag');
$articles = $page->children()
->listed()
->when($tag, fn($pages) => $pages->filterBy('tags', $tag, ','))
->sortBy('date', 'desc')
->paginate(12);
return [
'articles' => $articles,
'pagination' => $articles->pagination(),
'activeTag' => $tag,
'allTags' => $page->children()->listed()->pluck('tags', ',', true),
];
};
Template blog.php receives $articles, $pagination and $activeTag as ready variables.
Page Models
Add methods to specific page types:
<?php
// site/models/article.php
class ArticlePage extends Page
{
public function readingTime(): int
{
$words = str_word_count(strip_tags($this->text()->kirbytext()));
return (int) ceil($words / 200);
}
public function isNew(): bool
{
return $this->date()->toDate('U') > strtotime('-30 days');
}
public function relatedArticles(int $limit = 3): Pages
{
$tags = $this->tags()->split();
return $this->siblings()
->listed()
->not($this)
->filter(function ($article) use ($tags) {
return count(array_intersect(
$article->tags()->split(),
$tags
)) > 0;
})
->sortBy('date', 'desc')
->limit($limit);
}
}
In template: <?= $page->readingTime() ?> min read.
API and Headless Mode
Kirby returns JSON out of box — just add extension to URL:
GET /blog.json
GET /blog/first-post.json
Or setup custom routes:
// site/config/config.php
return [
'api' => true,
'routes' => [
[
'pattern' => 'api/articles',
'action' => function () {
$articles = page('blog')
->children()
->listed()
->sortBy('date', 'desc')
->limit(20);
return [
'data' => $articles->toArray(function ($article) {
return [
'id' => $article->id(),
'title' => $article->title()->value(),
'date' => $article->date()->toDate('Y-m-d'),
'excerpt' => $article->text()->excerpt(200),
'url' => $article->url(),
];
}),
];
},
],
],
];
Media and Images
<?php if ($cover = $page->cover()->toFile()): ?>
<img
src="<?= $cover->crop(800, 450)->url() ?>"
srcset="
<?= $cover->crop(400, 225)->url() ?> 400w,
<?= $cover->crop(800, 450)->url() ?> 800w,
<?= $cover->crop(1200, 675)->url() ?> 1200w
"
sizes="(max-width: 768px) 100vw, 800px"
alt="<?= $cover->alt()->or($page->title())->html() ?>"
loading="lazy"
>
<?php endif ?>
Kirby generates derivative images on the fly and caches them in media/.
Performance and Cache
// site/config/config.php
return [
'cache' => [
'pages' => [
'active' => true,
'type' => 'file',
'ignore' => fn($page) => $page->id() === 'search',
],
],
'content' => [
'locking' => false, // disable file locking in prod
],
];
For high-load sites, page cache is moved to Redis via third-party driver.
Development Timelines
Corporate site 5–10 pages with custom design: 2–3 weeks. Blog or catalog with filtering, search and custom fields: 3–5 weeks. If external API integration or headless mode for frontend framework needed — add 3–7 days.







