🎉 Features
Navigation for the preview view
We've added a handy page tree navigation dropdown to the preview view. Jump effortlessly to the preview of other pages #7335

Query language
Kirby 5.1 adds a number of improvements to the Kirby Query language. Since the query language plays such an essential part for our blueprints, the new query improvements will push the possibilities for blueprints even further.
The improvements are based on a new AST-based query parser and runner that has been added in addition to the previous (legacy) parser. The legacy parser which will remain the default parser for now but we aim to replace it with the new runner in Kirby 6. Consider the new runner in beta state during Kirby 5.x releases. #6788
The new AST-based runner (thx to @rasteiner for starting off the work on this) runs not only more reliably and performant, but also allow us to add more functionalities. Those are not supported by the legacy runner.
- Support for more operators:
- Logic (
AND
/&&
/OR
/||
) - Compare (
==
,!=
,>
,<
,<=
,>=
) - Math (
+
,-
,*
,/
,%
)
- Logic (
- Closures in queries can now accept arguments
- Queries support subscript access notation, e.g.
page[site.pageMethodName]
query.runner
option to switch toKirby\Query\Runners\DefaultRunner::class
to activate the new AST-based runner (See docs below for more information how to create a custom runner)
The new query runner is stricter in parsing queries. In some edge cases, this will require you to access an array/object via subscript notation - in rare cases on the top level via the new this
keyword, e.g. this["indentifier.with dots and spaces"]
.
Batch delete for structures
New batch mode for structure fields to delete multiple rows at once. You can activate it by using the new batch
option in your blueprint (#7400):
structure:
batch: true
fields:
text:
type: text
structures.mov
Searchable info in multiselect fields
You can now search through the info in a multiselect field by enabling the search
> info
option. (Thanks to @aofn) #7089
fields:
multiselect:
type: multiselect
options:
...
search:
info: true

Stats field
We’ve turned the stats section into an additional new field. The stats section keeps existing, but with the new stats field there are more layout possibilities and options for simpler blueprint setups. The field has the exact same options as the stats section (https://getkirby.com/docs/reference/panel/sections/stats). #7392
title: Shop
fields:
stats:
type: stats
size: huge
reports:
- label: Revenue
value: €29,682
info: +112.5%
link: https://getkirby.com/shop
theme: positive
- label: Orders
value: 265
info: +82.8%
theme: positive
- label: Avg. Transaction
value: €112.01
info: +16.3%
theme: positive
- label: Refunds
value: €15.20
info: +10.5%
theme: positive
- label: Discount sales
value: €1,422
info: n/a
Imagick thumb driver
New imagick
thumbs driver, using the Imagick
class instead of the convert
command line command #6754
// /site/config/config.php
return [
'thumbs' => [
'driver' => 'imagick'
]
];
Set the default panel.theme
You can now set the default Panel theme that is used when the user has not set any custom setting in their account view. Supported values: "light"
, "dark"
or "system"
(default, matches the user's operating system) #7341
// site/config/config.php
return [
'panel' => [
'theme' => 'light'
]
];
New Collection::join()
method
This new method is available in all collections #7340 (thx to @adamkiss)
$venues = $page->children()->join(' | ', fn ($child) => $child->venue());
$venues = $page->children()->join(as: fn ($child) => snippet('venue', ['page' => $child], return: true));
New session.cookieDomain
option
The option allows to share the session cookie across subdomains with a shared sessions
folder feedback.getkirby.com/628
Note
You can read more about the behavior of the domain
attribute for cookies in the MDN docs.
Note that setting the cookieDomain
option disables Kirby setting the path
attribute to the cookie (relevant in subfolder setups).
return [
'session' => [
'durationNormal' => 7200, // default: 2 hours
'durationLong' => 1209600, // default: 2 weeks
'timeout' => 1800, // default: half an hour
'cookieDomain' => 'getkirby.com', // default: automatic browser behavior
'cookieName' => 'kirby_session',
'gcInterval' => 100 // default: cleanup every ~100 requests
]
];
Dialog search input improvements
The dialog search input has a new button to clear the input #7513
search.mov
Option to delete a license
The license dialog now includes a button to remove the license file #6962 [#6960](https://

✨ Enhancements
Support for nested dialogs
We've added history support for panel.dialog
for nested dialogs #7519 Dialogs can now correctly open more dialogs which will be stacked on top of each other and close correctly.
Improved UI for the structure field
We've improved the UI when the structure field is disabled that allows to open drawer to see all fields as well as navigate through all entries when paginated https://feedback.getkirby.com/687
Improved language detector
Our language detector will now correctly detect languages based on locales. You can now easily set up two English locales (en_GB and en_US) for example and the detector will pick the right one and redirect correctly. (thanks to @tobiasfabian) #7178
Change the panel translation via URL
You can now set the Panel interface translation on demand with the new translation
query parameter. E.g. https://example.com/panel/login?translation=it
#6616
Better fatal view
We've added a link to the PHP version support table from the fatal view that is displayed on unsupported PHP versions
New skeleton
theme for items
We've added a new sekeleton
theme for the k-item
component to represent a loading state #7521

Snippet controllers
With Kirby 5.1 you can create a small plugin that enables support for snippets and block controllers: one can add a controller for e.g. the snippet site/snippets/header.php
to site/controllers/snippets/header.php
. And since blocks are also just snippets, e.g. site/controllers/snippets/blocks/video.php
.
As there can be many snippet calls within a single request, checking for the existence of a controller for each of these can impact your site’s performance.
<?php
use Kirby\Cms\App;
use Kirby\Template\Snippet;
use Kirby\Toolkit\Str;
class SnippetWithController extends Snippet
{
protected static $controllers = [];
public static function controller(
string|null $file,
array $data = []
): array {
if (
$file === null ||
str_starts_with($file, static::root()) === false
) {
return $data;
}
if (isset(static::$controllers[$file])) {
return array_replace_recursive(
$data,
static::$controllers[$file]
);
}
$name = ltrim(Str::before(Str::after($file, static::root()), '.php'), '/');
// if the snippet has a name, we can load the controller
// and merge the data with the controller's data
if ($name !== null) {
$data = array_replace_recursive(
$data,
static::$controllers[$file] = App::instance()->controller('snippets/' . $name, $data)
);
}
return $data;
}
public static function factory(
string|array|null $name,
array $data = [],
bool $slots = false
): static|string {
$file = $name !== null ? static::file($name) : null;
$data = static::controller($file, $data);
return parent::factory($name, $data, $slots);
}
public function render(array $data = [], array $slots = []): string
{
$data = array_replace_recursive(
static::controller($this->file, $data),
$data
);
return parent::render($data, $slots);
}
}
App::plugin('getkirby/snippet-controllers', [
'components' => [
'snippet' => function (
App $kirby,
string|array|null $name,
array $data = [],
bool $slots = false
): Snippet|string {
return SnippetWithController::factory($name, $data, $slots);
},
]
]);
And more …
- Stats (
stats
field and section as well as thek-stats
/k-stat
components) now support passing adrawer
instead of a link or dialog #7472 - New
<k-stats-field>
component #7389 - New methods
Kirby\Cms\License::delete()
andKirby\Cms\License::root()
#7211
github.com//issues/6960) - New
LanguageVariable::language()
method #7379 - New
Kirby\Panel\Ui\Stat
andKirby\Panel\Ui\Stats
backend components, which are used to create the correct information for the<k-stat>
/<k-stats>
Vue component #7373 #7374 - Dialog and drawer routes: support passing a
controller
class name or closure hat returns an object with::load()
and::submit()
methods that will be used then asload
/submit
event actions. Ideally used with returning aKirby\Panel\Controller\Dialog
/Kirby\Panel\Controller\Drawer
object. If the named class also defines a static::for()
method, it will be used to create an instance of the class. - The
<k-label>
component now also acceptsfalse
for the input prop. This will switch the label element to an h2 and also remove the<abbr>
for the invalid state. All fields that don't store any values will need this. We can also use it for the info field, which did still create its own headline. - New Collector classes #7433
- New
Kirby\Panel\Collector\PagesCollector
class - New
Kirby\Panel\Collector\FilesCollector
class - New
Kirby\Panel\Collector\UsersCollector
class
- New
- New
Kirby\Panel\Ui\Upload
class to handle upload settings in sections and potentially also in fields #7436 - New Model Item Classes to create all the necessary props for a
<k-item>
component for a model, such as text, info, image, link, etc. #7456Kirby\Panel\Ui\Item\FileItem
Kirby\Panel\Ui\Item\ModelItem
Kirby\Panel\Ui\Item\PageItem
Kirby\Panel\Ui\Item\UserItem
- New
Uuid::toUrl()
method that returns the model's absolute URL and includes query and/or fragment that was included in the UUID uri #7484 - Better IDE support for the return type of
Page::uuid()
,Site::uuid()
,File::uuid()
andUser::uuid()
#7484 - New
closed
listener for Panel dialogs and drawers that gets called after the dialog has been closed #7519 Kirby\Uuid\Uuid::is()
now accepts an array of multiple types to test against #7543- New
Kirby\Http\Query::merge()
andKirby\Http\Params::merge()
methods #7546
🐛 Bug fixes
- Toggles field:
- Highlight selected value even when disabled. #7357
- Stronger focus color
- Layout field: fixed disabled styling #7358
- Radio and checkboxes field: fixed styling when disabled #7359
currentColor
isn't enforced as fill for all SVGs anymore (only for.k-icon
) #7297- Inputs: Hide placeholder text when disabled #7360
Kirby\Panel\Ui\Component::render()
now only filters out props that arenull
, not empty arrays #7384- Fixed destructive CSS rules in the Lab for
h2
that easily bled into component rules. - Add a space before or after HTML tags before applying strip_tags, but only if they are adjacent to word characters. #7306
- UUID for new page is the same as the latest page UUID #7405
- Fixed some Panel route regular expressions that sometimes would match more than wanted #7467
- Fixed a case where a subclass of
Kirby\Http\Response
would falsely converted to aKirby\Http\Response
object #7478 - Fixed UUID permalinks for languages with custom URL/without a URL prefix #7450
- Range input: Tooltip keeps a stable width based on the
max
value #7417 - Support query and fragments in
link
KirbyTag when using a UUID #7477 - No more orphaned media files when changing a page's status to draft #6573
- Closing all drawers now doesn't leave the darkened overlay visible #6030
- Subsequent Panel view request cancel out ongoing (slow) previous view request to ensure navigation always lands at the latest requested view #6536
- Show loading spinner during dialog and drawer refreshes #7514
- Fixed dialog button layout when there's only one button #7519
- Support more MIME type fors .wav (thx @pReya) #7531
- Writer field: Hide nodes dropdown when only one node available #7305
☠️ Deprecated
- The
::url()
method ofPageUuid
andFileUUid
has been renamed to::toPermalink()
and will be removed in a future release. #7484 - The
im
thumbs driver option has been deprecated. Useimagick
instead. #6754 Kirby\Http\Router::find()
: Passing$ignore = null
has been deprecated (pass an empty array or omit the argument instead) #7547
♻️ Refactored
- Move since tag to own section in Lab docs #7296
- Support
App::controller()
to return controllers nested in subfolders insidesite/controllers
#7368 - Move empty reports handling from
<k-stats-section>
to<k-stats>
#7376 - Export props from
<k-stats>
- Simplified config for area dialogs, drawers and drop-down #7377
- The
stats
field uses the newKirby\Panel\Ui\Stat
andKirby\Panel\Ui\Stats
classes to clean up the props generation for the frontend. - New
Kirby\Form\Mixin
Traits forafter
,autofocus
,before
,help
,icon
,label
,placeholder
andwidth
props #7396 - Improve the inline docs for all field mixins. #7396
- Use the new Collector classes in the files and pages section, as well as the users view. #7435
- Always use the
list
settings for table layouts inKirby\Panel\Model::image()
#7449 - Use new Model Item classes in the files and pages sections and the users view to clean up the code. #7456
- Use new Model Item classes in
::pickerData()
methods and in theKirby\Panel\Controller\Search
controller to clean up all places where such item props are created. #7457 - Support passing a
Kirby\Http\Response
object toKirby\Cms\Responder::send()
. #7478 - Refactored users view as controller #7476
🧹 Housekeeping
- Switch to PHP attributes for PHPUnit annotations
- Remove Vite's deprecated
splitVendorChunkPlugin
. #7354 - Replace deprecated
$mock->addMethods()
in unit tests #7356 - Using PHP attributes for PHPUnit annotations in
Kirby\Database
package #7318 - Remove unused methods from
<k-stats>
- Using nullish coalescing assignment in JavaScript #7515
- Clean up code style of the
Kirby\Http
classes #7547
📚 Additional Docs
Custom Query Runners
query.runner
config option
Kirby offers a new runner for its query syntax based on parsing the query as abstract syntax tree. This runner is more reliable and efficient than the rudimentary legacy runner (which in Kirby 5 remains the default). The new runner is currently in its beta phase. You can activate it by setting the query.runner
config option:
<?php
use Kirby\Query\Runners\DefaultRunner;
return [
'query.runner' => DefaultRunner::class
];
Query syntax
When using the new query runner, you can also use logical, comparison and math operators in your queries as well as receive arguments in closures:
// new operators
$query = new Query('age > 25 && name != "John"');
$query = new Query('price + vat');
// arguments in query closures
$query = new Query('(foo) => foo.homer');
$data = [];
$bar = $query->resolve($data);
$bar = $bar(['homer' => 'simpson']);
$this->assertSame('simpson', $bar);
Queries will still be tried to resolve first directly from the data context, e.g. when your query is null
and your data ['null' => 'foo']
the result will directly be 'foo'
. Same for user.username
and ['user.username' => 'foo']
.