Skip to content

Back to Toolbox

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:

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 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/catch block is necessary because any null within 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 $image->thumb() method, and the appropriate value for the <img srcset> attribute can be produced with the $image->srcset() method to enable HTML responsive images. Various patterns are explored in Cookbook: responsive images.

Presets can be defined in the config under the thumbs property. They can be referenced by name in the $image->thumb() and $image->srcset() methods. The $image->resize() method is a wrapper over $image->thumb() for ad-hoc resizing, but it’s probably best to keep all size definitions in the config file.

Kirby lets users set the focus point of an image from the Panel.

To change the HTML markup for images in the image and gallery blocks, modify their block snippet.

Limitations

When requesting an image size larger than the original, the srcset() function will assume the image was upscaled, when it’s not (see [kirby#2071]).

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.

Users and permissions

See Users and permissions.

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.

Providing contextual help

See: Contextual help.

Further reading

Starter kits

Useful plugins