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

Starter kits

Useful plugins