Notes on Kirby CMS
Some initial notes, as I learn my way around Kirby CMS.
Development flow
See: Development flow.
Security
A secure Kirby deploy must prevent HTTP access to some of its files and folders, as outlined in the Security guide.
Both distributions (Plainkit and Starterkit) come wih an .htaccess file which enforces these rules for Apache and compatible servers. There is also a recipe for running Running Kirby on a Nginx web server.
Content modeling
See: Content modeling in Kirby.
Templating
Kirby uses plain PHP as the templating language but provides a neat, object-oriented API that makes templates easy enough to read.
Templating is pretty lean: a content file named project.txt will use the /site/templates/project.php template file, if it exists. Other than defaulting to /site/templates/default.php, there’s no other prescribed template hierarchy as with WordPress.
The mechanism for template reuse is snippets with slots, which is enough to stand in for Twig’s extend and include tags. Speaking of which: Twig is available as a separate plugin, along with other templating languages, should you find plain PHP unpalatable.
To prevent XSS vulnerabilities, values in templates can be escaped with:
- the
esc()global helper in templates $field->esc()for fields specificallyStr::esc()in other PHP code
These methods accept a $context which lets you target different escaping contexts.
Developing custom interfaces
Custom interfaces for the Panel are defined in Plugins, which tend to have two parts:
- the plugin’s
index.phpfile for back-end stuff - the plugin’s
index.jsfile for front-end stuff, preferably written as Vue components like the rest of the Panel
The front-end generally interacts with the back-end via the Kirby (REST) API.
A custom Panel section
In addition to working with the REST API, you can fetch data for a custom Panel section in the plugin’s index.php file with computed properties.
This allows the user to configure the panel section when using it in a blueprint.
For example, you could have a query property in the section that gets interpreted as a query string, just like some built-in fields work, which you can use to drill down into arrays/objects with Kirby\Toolkit\Str’s query method:
use Throwable;
try {
return Str::query($query, [
// data
]);
} catch(Throwable) {
return null;
}
Note: the
try/catchblock is necessary because anynullwithin the chain will cause the query to throw. [TODO: is that really the case?]
In a custom panel section, $this refers to the section and $this->model() refers to the object associated with the page, eg. on a user page it returns the associated user.
Dates, times, and timezones
For a quick overview of the various date & time formats, see A Venn diagram of date and time formats, according to RFC 3339 vs. ISO 8601 vs. HTML.
Kirby’s Date and Time fields don’t store the timezone information in content files. A typical date-time string looks like this, which corresponds to the "Y-m-d H:i:s" PHP format string.
Start-date: 2023-08-28 09:00:00
When used in PHP date objects, the server timezone is assumed by default. We can declare an explicit timezone as the first thing in Kirby’s index.php file:
date_default_timezone_set('Europe/Bucharest');
To send dates to the client, including the timezone we’ve set up, to be parsed as JavaScript date objects we use the DateTimeInterface::W3C format, an alias for "Y-m-d\\TH:i:sP".
new DateTimeImmutable($date_string)->format(DateTimeInterface::W3C);
In the opposite direction, we want to accept dates from the client in the format produced by date.toISOString(), which is always in explicit UTC, denoted by the suffix Z. A typical date-time string looks like this:
{
"start_date": "2023-09-16T16:26:38.428Z"
}
When Kirby prepares a date-time string for storage, the timezone seems to be ignored, resulting in the incorrect date to be stored. To work around this, we need to convert from UTC to the server timezone, then format it as Kirby would when it saves the date to the content file.
function toKirbyDate($date_string) {
return (new DateTimeImmutable($date_string))
->setTimezone(new DateTimeZone(date_default_timezone_get()))
->format("Y-m-d H:i:s");
}
Images
Images can be resized and cropped with the thumb() method, and the appropriate value for the <img srcset> attribute can be produced with the srcset() method to enable HTML responsive images. Various patterns are explored in Cookbook: responsive images.
Kirby lets users set the focus point of an image from the Panel.
You probably want locale-aware sorting
By default Kirby ignores locale rules when sorting things. In Romanian, this manifests in letters with diacritical marks (Ă, Î, etc.) being shifted to the end of the alphabet.
A Pages section’s sortBy property accepts the SORT_LOCALE_STRING keyword to enable locale-aware sorting, and so does the $pages->sortBy() method.
API route permissions for users without Panel access
Users with Panel access disabled (via permissions.access.panel: false in the user blueprint) generally don’t have access to routes under /api/, except for a few endpoints related to /api/auth to allow them to log in and out.
Kirby doesn’t seem to support per-route permissions (this suggestion may be related). To allow specific API endpoints for non-Panel users, we need to roll our own check:
'api' => [
'routes' => [
'pattern' => 'my-route',
'method' => 'GET',
'auth' => false,
'action' => function() {
// Note: You can be fancy and return HTTP 401 / 403 instead.
if (!kirby()->user() || !csrf(kirby()->request()->csrf())) {
return null;
}
return $your_thing;
}
]
]
To add a log out link to the front-end:
<a href='/panel/logout'>Log out</a>
Easy Panel shortcuts for the front-end
<footer>
<?php if($user->role()->permissions()->for('access', 'panel')): ?>
Admin:
<a href='<?= $page->panel()->url() ?>?language=<?= $kirby->language() ?>'>
<?= t('Edit page') ?>
</a>
<a href='<?= $site->panel()->url() ?>?language=<?= $kirby->language() ?>'>
<?= t('Admin panel') ?>
</a>
<?php endif; ?>
</footer>
There’s also a plugin with more functionality: pechente/kirby-admin-bar. Interesting point:
Please note that this plugin might disable Kirby staticache since it renders different content for logged-in users and guests.
Quirks and workarounds
Filling in Panel fields with JavaScript
In a page edit screen, I needed to fill in a whole set of fields with JavaScript. Kirby uses Vue 2.7 for the Panel front-end, and it would not pick up correctly on the external DOM updates.
Without looking too much into it, a workaround is to emit an input event and pause for 100ms before moving to the next field.
Instead of:
function populateFields() {
const FIELD_NAMES = ['headline', 'excerpt', 'description'];
for (const name of FIELD_NAMES) {
const input = document.querySelector(`[name=${name}]`);
inputEl.value = 'some-value';
};
}
You can do this:
async function populateFields() {
const FIELD_NAMES = ['headline', 'excerpt', 'description'];
for await (const name of FIELD_NAMES) {
const input = document.querySelector(`[name=${name}]`);
inputEl.value = 'some-value';
inputEl.dispatchEvent(new Event('input'));
await new Promise(r => setTimeout(r, 100));
};
}
Working with translations
The Multi-language documentation is a good entry point.
There’s also separate information on translating strings in blueprints. What’s not explicitly captured there is that many blueprint properties (title, label, etc.) accept the translation key directly, without using the wildcard * pattern:
# site/blueprints/site.yml
title: blueprints.site.title
sections: …
To match translation keys in YAML I use the
\w+(\.\w+)+regex in Sublime Text.
Strings inside the language definition files
A gotcha. The Languages section of a multi-language Kirby website has an interface for editing strings. Whenever the Panel user changes the value of one of the strings, the languages/<lang>.php file gets overwritten to include the updated string. This places using the visual editor at odds with syncing the site/languages folder during deployment.
The translations editor can be disabled with permissions attached to custom user roles. These don’t apply to the default Administrator role, so you’ll need to create a custom role for the other users.
Alternatively, you can move the translations to a place where they don’t interfere with the translations interface: a plugin that defines the translations key. You could move Panel translations into a plugin, as you might want to support languages for the Panel that you don’t necessarily want in the front-end.
Further reading
- Kirby’s documentation is quite good!
- Kirby support forum may have solutions for questions not answered in the official docs.
- Kirby School is a video course by Kristian Maňas.
- Moinframe by Justus Kraft.
- Kirbysites showcases how the Kirby Panel is organized in various websites (see everything at once with the power user mode).
Starter kits
tobimori/kirby-baukasten, abatteries-included Kirby 5 Boilerplate with Tailwind CSS, Alpine.js, TypeScript, Vite & other best practices
;femundfilou/kirby-blaupauseby Justus Kraft,a starter for new projects, mainly developed out of personal needs. It’s based on the tools and technologies we work with and might serve as an inspiration to others
.1612elphi/kirby-grimoire,a web publishing tool / theme for Kirby, batteries included
.