🚀 A new era: Kirby 4 Get to know
Skip to content

Kirby 3.8


New Unique ID system

For everlasting relationships

An illustration showing the new UUID system

Reliability built-in

With Kirby’s new UUID system, we are building reliable, unique IDs right into the core. Not just for pages, also for files and users. Unique IDs are stored in your content files and efficiently cached on demand for fast lookup.

<?= $page->uuid() ?>
⚓️ page://Eesj89FnbMzMMvs0

<?= $file->uuid() ?>
⚓️ file://VYnAL00UhvmOxq8J


Pages and files now have their own permalink that will never change – even if you rename the slug or a filename. The permalinks will automatically redirect to the current URL of the page or file. They are also great shortlinks.

<?= $page->permalink() ?>
🔗 https://yoursite.com/@/page/Eesj89FnbMzMMvs0

<?= $file->permalink() ?>
🔗 https://yoursite.com/@/file/VYnAL00UhvmOxq8J
A screenshot of the updated picker fields

Updated picker fields

All our picker fields now store UUIDs by default. Related pages and files can now safely be renamed without breaking relationships.

Cover: file://VYnAL00UhvmOxq8J
Author: user://e1UrRWjqFRiu7mWg

- page://hb38HvnQfm8HlQ6e
- page://jchKH3EufbjC37KR
- page://jb8i1Sl6cOQOdOE8

New update checks

Keep your installation healthy

The system view now shows available updates for Kirby and installed plugins

Always up to date

Our new update check in the enhanced system view makes it easy to stay informed about the version and security status of Kirby and your installed plugins.

While feature updates are not always needed for finished sites, keeping an eye on security issues and important security messages is really important to keep your sites secure and healthy.

The new update check brings this information and more right into the Panel so you can get a quick overview of the status of your site.

Fine-tuning the behavior is really simple:


// global configuration
return [
  'updates' => true | 'security' | false

// separate configuration for Kirby and plugins
return [
  'updates.kirby'   => true | 'security' | false,
  'updates.plugins' => true | false,

// or even configuration by plugin
return [
  'updates.plugins' => [
    '*'                      => true | false,
    'superwoman/*'           => true | false,
    'superwoman/superplugin' => true | false

PHP 7.4+  PHP 8+

Use the potential of a modern PHP environment

There’s no time to dwell in the past

Kirby has already supported PHP 8.0 and 8.1 right after their release. PHP 8 not only brings an incredible performance boost but also language features that really move Kirby forward.

Kirby 3.8 builds exclusively on PHP 8+. With the end-of-life of PHP 7.4 in November and wide-spread hosting support for PHP 8, we don't see any reason to stay with an outdated version if the future is so bright.

New object field

More power for your data

The new object field opens in a new drawer to edit fields comfortably

Objectively awesome

The new object field allows to create data objects. This is super handy for more complex settings, isolated entities or nested data.

The new object field shows data in a very compact way

In your blueprints

The object field definition is very similar to a structure field. You can define any set of fields for the object with the fields option.

  type: object
      type: files
      type: text
      type: email
      type: tel

In your templates

The result is stored as YAML in the content file and can be used in your templates with the new $field->toObject() method.

<?php if ($contact = $page->contact()->toObject()): ?>
  <dd><?= $contact->photo()?->toFile()->crop(200) ?></dd>
  <dd><?= $contact->name() ?></dd>
  <dd><?= $contact->email() ?></dd>
  <dd><?= $contact->phone() ?></dd>
<?php endif ?>

Kirby CLI

Supercharge your workflow

A screenshot of Kirby’s new command line interface installing the starterkit

Coming to a terminal near you

The revival of our command line interface is here and it’s better than ever before. Install Kirby and its kits in seconds, make blueprints, templates, controllers and more or extend the CLI with your own commands. Managing your Kirby installations has never been easier.

Give it a try →




  • Update check for Kirby and plugins #4676
  • New object field #4640


  • Unique IDs for pages, files, users and site #4612
  • Permanent URLs for pages and files with UUIDs #4612
  • New $users->files() method (returning a collection of all files of the users) #4499
  • Support for a new site/config/env.php file for options that are specific to a deployment: It can override all options from config.php and from the config files specific to hostname and server IP address. It can also override the url option and control which hostname-specific files will be loaded. #4580
  • New $cache->getOrSet($key, $callback, $minutes) method: Retrieves the value from cache if possible, if not via callback function and adds it to the cache #4627
  • New $cache->enabled() method to check if the cache is ready to store values #4661
  • New option to disable the output from $kirby->render() via $_ENV['KIRBY_RENDER'] = false
  • New commands root which is set to site/commands by default
  • New ability to define CLI commands in Kirby plugins:

Kirby::plugin('getkirby/commander', [
    'commands' => [
        'commander' => [
            'command' => function ($cli) {
                $cli->success('Nice command!');



  • Better gallery block features new ratio, crop and caption fields and displays images according to selected ratio #4652.
  • Better error messages for blocks and layout fields #Nolt313
  • The error dialog now displays multiple error lines per field #4629
  • System view: Empty plugin table cells display "–" #4444
  • When a login challenge has expired, the user is redirected to the login page. #4087
  • Added new onFormInput handler of the structure field on form submit event #4616
  • Each report in a k-stats widget can now define a dialog prop with the name of a dialog to be opened when the report is clicked. #4658
  • Fiber dialogs: the submit button is now disabled while processing a submit request to prevent sending multiple requests at once accidentally #4413
  • Table rows use the same outline styling when dragged as cards and lists #4682
  • Easily translate options via an i18n key #3955:
# via `*` pseudo locale
    *: my.i18n.key.a
    *: my.i18n.key.b


  • The $field->replace(), $modelWithContent->toString(), $modelWithContent->toSafeString() and Str::safeTemplate() methods now support the fallback value null (which leaves invalid tokens in the output string). #4671

Bug fixes


  • Tags field: Separates tags on blur correctly #4638
  • Tags field: Doesn't lose current input when hitting Cmd+S #4590
  • Various escaping issues for fields with options #4043 #4229
  • Picker fields: Fixed styling for disabled/non-translatable state #4306
  • Structure field: columns options now rightly overrule implied options from the field #4514
  • URL field preview is properly truncated with an ellipsis when it is too long #4677
  • Users view: The role filter is not shown anymore if there is only one role available #4673
  • The template of the home page can be changed now #4571
  • Items are properly highlighted while being dragged #4648
  • The table layout heading alignment is now working properly #4630
  • Uploaded XML files are no longer blocked because they contain links to external domains #4553
  • The Panel now suppresses error dialog when redirecting to logout #4614
  • We brought back the default slot for k-items #4635
  • When the structure field is used with limit, the index number now shows correctly #4695
  • The Panel now uses the correct favicon in dark mode #4691
  • Reduced impact of frequent lock requests on the server load #4741


  • Calling a $file method inside a custom file::url component no longer causes an infinite loop #4274
  • Str::excerpt() does not add superfluous space after stripped tag and before interpunctuation #4645
  • Fixed calling A::average() with empty array #4269
  • Site search with parameters now returns correct results #4641
  • Caching is no longer disabled when the request passes an empty Authorization header. #4634
  • Calling $app->user() no longer sets the response flag for "uses auth header" unless the api.basicAuth option is enabled. #4646
  • A::append() has been fixed for non-associative arrays. It is now an alias for A::merge() with the A::MERGE_APPEND flag. #4345
  • Language routes that return a falsy value are only called once now #4305
  • $translation->exists() now works for virtual pages #4674
  • Database queries can now filter by 'AND' and 'OR' as actual values (via ->where(), ->andWhere() and ->orWhere()) #4668
  • Prevent race condition errors for Dir::make() #4745



  • Panel drag texts now use UUIDs (KirbyText) or permalinks (Markdown) for absolute references #4612
  • Custom login views no longer need to display errors themselves with k-login-alert, instead they can this.$emit("error", error) #4577
  • The properties for the system view are now combined in the backend for easier extension and testing #4658
  • Updated npm dependencies #4639
  • New blueprint syntax for options (with polyfills for the old syntax): #4624
# manual options
# shorthand
  - a
  - b
  - c

# long form
  type: array
    - a
    - b
    - c

# api
  type: api
  url: https://api.getkirby.com
  query: Companies

# query
  type: query
  query: site.contactoptions.toStructure
  text: "{< item.nameWithHtml >}"
  value: "{{ item.twitter }}"


  • Optimized code base to take advantage of PHP 8
    • Using PHP match statements instead of switch in various places #4452
    • Using |null for nullable type hints #4453
    • Using PHP's new null safe operator #4455
    • Kirby\Toolkit\A: Improve type hinting #4495
    • Removed unused variables from PHP catch statements #4457
    • Use PHP's instanceof instead of is_a() #4609
  • Support for default caches (via Core::caches()) #4535
  • Kirby\Database\Query: New third parameter $mode for ->filterQuery()  #4564
  • Added a new Option namespace with refactored options classes #4624
  • New syntax for Helpers::handleErrors():
      fn () => // error to suppress,
      fn (int $errno, string $errstr): bool => // condition when to suppress error
      $value // return value for when the error is suppressed


  • $kirby->impersonate($user, $callback): The $callback will not be bound anymore to the $kirby instance in Kirby 3.9.0, $this inside the callback will refer to the current context and not $kirby instead. Using $this as $kirby inside the callback will throw a deprecation warning in debug mode. #4498
  • Blueprints: The headline option for sections will be removed in a future Kirby version. Use label instead. #4515
  • $kirby->server() has been deprecated and will be removed in Kirby 3.9.0. Use $kirby->environment() instead. #4515
  • Kirby\Form\Options, Kirby\Form\OptionsApi, Kirby\Form\OptionsQuery have been deprecated and will be removed in Kirby 3.9. Use their Kirby\Option equivalents instead.

Breaking changes

We try to avoid breaking changes as much as we can. But we also put a lot of effort in keeping our technical debt in Kirby as low as possible. Sometimes such breaking changes are necessary to move forward with a clean code base.

You might wonder why there are breaking changes in a minor release according to Semantic Versioning.

We stick to the following versioning scheme:


This release is Kirby (major release 8 of Kirby 3)

Traditionally, we combine patch and minor releases though and only need the fourth versioning level for regression fixes.


  • The multiselect field no longer displays the option value as info #4624


  • Kirby 3.8.0 requires a minimum of PHP 8.0.
  • uuid and permalink cannot be used as field name or custom method for any page, file, user or site anymore. Content fields can only be accessed via e.g. $model->content()->get('permalink').
  • The first-level URL path @ is now blocked.
  • Kirby's upload sanitizer no longer checks XML files for external links because they can be pretty common in many XML-based formats; if you want to keep the strict behavior, set Kirby\Sane\Xml::$allowedDomains = [], set this property to a custom allowlist or write a custom Sane handler for your XML-based format #4553
  • Kirby now automatically loads the site/config/env.php file; if you already use this file path for a different purpose, please rename the file to a different name #4580
  • A::average() returns null when passed an empty array #4649
  • The default token fallback in Str::safeTemplate() changed from an empty string to null (which leaves invalid tokens in the output string). This behavior is consistent with Str::template().
  • $translation->exists() now returns true for translations without an actual content file but where an content array has been provided
  • The deprecated master branch of the Kirby repository was deleted. If you install Kirby via Git, e.g. as a submodule, please use the main branch instead.
  • Database queries: subsequent ->where() clauses don't support passing the mode (AND|OR) as last parameter anymore, but will interpret these as actual values to filter against. Use ->andWhere() or ->orWhere() instead. #4668
  • Helpers::handleErrors()'s 2nd parameter now only determines whether the error is suppressed, its 3rd parameter defines what is returned when an error is suppressed

Removed deprecated code #4478 #4515

Removed dump component. Disable dump() via KIRBY_HELPER_DUMP instead and create your own function.

PHP methods

Removed Use instead
$file->dragText() $file->panel()->dragText()
$file->panelIcon() $file->panel()->image()
$file->panelImage() $file->panel()->image()
$file->panelOptions() $file->panel()->options()
$file->panelPath() $file->panel()->path()
$file->panelUrl() $file->panel()->url()
$file->pickerData() $file->panel()->pickerData()
$files->findById() $files->find()
$page->dragText() $page->panel()->dragText()
$page->panelIcon() $page->panel()->image()
$page->panelId() $page->panel()->id()
$page->panelImage() $page->panel()->image()
$page->panelOptions() $page->panel()->options()
$page->panelPath() $page->panel()->path()
$page->panelUrl() $page->panel()->url()
$page->pickerData() $page->panel()->pickerData()
$pages->findById() $pages->find()
$pages->findByIdRecursive() $pages->find()
$pages->findByUri() $pages->find()
$site->panelIcon() $site->panel()->image()
$site->panelImage() $site->panel()->image()
$site->panelOptions() $site->panel()->options()
$site->panelPath() $site->panel()->path()
$site->panelUrl() $site->panel()->url()
$site->pickerData() $site->panel()->pickerData()
$user->panelIcon() $user->panel()->image()
$user->panelImage() $user->panel()->image()
$user->panelOptions() $user->panel()->options()
$user->panelPath() $user->panel()->path()
$user->panelUrl() $user->panel()->url()
$user->pickerData() $user->panel()->pickerData()
Kirby\Panel\Document::customCss() Kirby\Panel\Document::customAsset('panel.css')
Kirby\Panel\Document::customJs() Kirby\Panel\Document::customAsset('panel.js')
Kirby\Toolkit\Str::isUrl() Kirby\Toolkit\V::url()
Method Change Use instead
markdown component Removed $inline parameter $options['inline']
$kirby->kirbytext() Removed $inline parameter $options['markdown']['inline']
$kirby->markdown() Passing a boolean as second parameter isn't supported anymore. $options['inline']


Removed Use instead
Kirby\Http\Server Kirby\Http\Environment

Class Aliases




Migration from Kirby 3.7.x

To ease the transition, we have compiled everything you need to know in detailed migration guides:

Migration guide for site developers →
Migration guide for plugin developers →

If you are updating from Kirby 3.6 or older, please first perform each major update step by step. Please refer to the changelogs of Kirby 3.5, Kirby 3.6 and Kirby 3.7 for details.


Get started