Development flow
Installing and updating Kirby
Nowadays I install and update Kirby using Composer.
Before that, I would delete the media and kirby folders and re-fetch Kirby from GitHub using degit:
rm -rf media && rm -rf kirby && npx degit 'getkirby/kirby#5.1.3' kirby
Local environment
For local development on macOS, Kirby works well with PHP’s built-in server. The docs warn of potential issues, but I haven’t noticed any so far.
brew install php php-intl
php -S localhost:8000 kirby/router.php
(Okay, maybe having to install php-intl separately to use the intl extension in the Homebrew version of PHP is one of those snags.)
Folder structure and development workflows
The default Kirby folder structure may not be ideal for some workflows, but can be reorganized to make it easier to:
- write
.gitignorerules to keep things out of source control; - devise syncing rules for deploying local changes to the server using rsync, rclone or otherwise, without accidentally overwriting any data that may have been edited in situ by Panel users;
- protect sensitive files and folders from public access using server rules.
It seems common to move everything web-accessible to the public/ folder, and the Kirby-generated folders site/accounts/, site/sessions/, and site/cache/ into a separate storage/ folder. Since it’s also generated by Kirby, I’m also moving the license file from its default location site/config/.licence to the storage/ folder:
<?php
// public/index.php
$base = dirname(__DIR__);
require $base . '/kirby/bootstrap.php';
$kirby = new Kirby([
'roots' => [
'index' => $base . '/public',
'content' => $base . '/content',
'site' => $base . '/site',
'accounts' => $base . '/storage/accounts',
'cache' => $base . '/storage/cache',
'sessions' => $base . '/storage/sessions',
'logs' => $base . '/storage/logs',
'license' => $base . '/storage/.license'
]
]);
echo $kirby->render();
To start the local PHP server with public/ as the root folder, use:
php -S localhost:8000 -t public/ kirby/router.php
Careful: When changing the folder structure, don’t forget to update
.htaccess(or equivalent server configuration file) to keep your Kirby installation secure. The.gitignorefile also needs to be adjusted to not commit sensitive info, such as the license file, to the Git repository.
Loading JavaScript, CSS, and other types of files
Loading assets on the front-end
Sitewide assets on the front-end
In your PHP template files, you can reference JS and CSS from the site’s public assets/ folder. The js() and css() helpers will produce the <script> and <link> markup for you.
Plugin assets on the front-end
Additionally, anything placed in a plugin’s own assets/ folder will be symlinked to the public folder the first time it’s requested via the asset() interface (added in Kirby 4):
$kirby->plugin('dan/lightbox')->asset('<file>')->url()
For the physical path site/plugins/<plugin-folder>/assets/<file>, its media URL points to media/plugins/dan/lightbox/<hash>-<modifieddate>/<file>.
The js() and css() helpers readily accept plugins, or their assets, as arguments, so all these work:
<?= js($kirby->plugin('dan/lightbox')->asset('<file>')) ?>
<?= js($kirby->plugin('dan/lightbox')->assets()) ?>
<?= js($kirby->plugin('dan/lightbox')) ?>
To opt out of content hashing, use the plugin’s mediaUrl() to prefix the asset filename:
<?= js($kirby->plugin('dan/lightbox')->mediaUrl() . '/<file>') ?>
Loading assets in the Panel
Global Panel assets
Kirby can load one custom JavaScript file and one custom CSS file with the panel.js and panel.css settings, respectively.
The custom JS file is loaded as an ES module, so you can import further ES modules with it. To include traditional scripts in the page, you must use DOM methods. To use anything not natively available you’ll have to use a bundler.
Plugin Panel front-end assets
Each plugin has the following files interpreted:
index.phpfor the plugin’s back-end;index.jsfor the plugin’s Panel front-end;index.cssfor the plugin’s Panel styles.
Kirby concatenates all index.js files from all plugins into a single file, loaded as a non-module <script>. All the code in these files is therefore in the global scope, and name collisions can happen.
A bundler configured to produce bundles in the IIFE format will solve the scoping problem, as well as allowing easy imports of further JS modules and support for non-native features.
Building Panel plugins with esbuild and npm workspaces
Due to the various constraints for including JavaScript and CSS for plugins on the Panel side of things, it’s often easier to just bundle plugin code in a single JS file.
There’s PluginKit (which uses kirbyup) for the purpose, but I’ve sought a lightweight solution with as few dependencies as possible.
Initially I wanted to use a top-level esbuild command to build all plugins at once, but there’s a missing piece in the API that makes it hard to output files from site/plugins/my-plugin/src one level up to the plugin’s root folder.
Instead, I moved the esbuild commands to each plugin’s package.json.
I learned about npm workspaces. Usually meant to manage many packages from a single root folder, it has a bunch of extraneous features (symlinking packages in node_modules) but it allows us to run a command across all packages at once.
Start by declaring the plugin folders as workspaces in the top-level package.json, and forwarding any scripts you want by using npm run --workspaces, which runs the corresponding script in all workspaces:
{
"workspaces": [
"site/plugins/*"
],
"scripts": {
"build": "npm run build --workspaces --if-present",
"watch": "npm run watch --workspaces --if-present"
}
}
Adding the --if-present flag prevents npm run from throwing an error if one of the workspaces is missing a script.
Inside each plugin’s folder, we declare the scripts in package.json:
{
"scripts": {
"build": "npx esbuild --format=iife --bundle _src/index.js _src/assets/index.js --outdir=.",
"watch": "npm run build -- --watch"
}
}
I’m bundling as IIFE (immediately invoked function expression), because Kirby concatenates the output of all plugin index.js files in a single JS file.
In the top level, npm run build runs all build scripts for plugins.
However, npm run watch only watches the files for the first plugin. That’s because npm run --workspaces runs workspace scripts in sequence, and esbuild --watch prevents subsequent commands from running.
npm doesn’t support running things in parallel (npm/rfcs#190), and the packages that provide that — the popular npm-run-all and its successor npm-run-all2 — don’t have a --workspaces option.
In the interim, I do this:
{
"scripts": {
"watch": "run-p watch:*",
"watch:plugin1": "npm run watch --workspace=site/plugins/plugin1",
"watch:plugin2": "npm run watch --workspace=site/plugins/plugin2"
}
}
# Content hashing for public assets
Cache-busting techniques for public assets is trickier. There’s one approach in the docs which uses a plugin to modify the behavior of the js() helper using the components.js hook:
Kirby::plugin('some/plugin', [
'components' => [
'js' => function($kirby, $url) {
return $url . '?' . md5_file($url);
}
]
]);
It uses MD5 to append the hash as a query parameter to the asset’s URL, but it seems excessive to do this work on every request.
Instead, a common approach is to have the bundler produce as part of the build process a manifest.json file that maps asset URLs to their hashed output path. Then, a helper class implements js(), css(), and file() methods that take this manifest into account.
The approach works well when the bundler processes public sitewide assets and produces a single authoritative manifest file. Hashed filenames for plugin assets, on the other hand, are a bit of an unsolved puzzle. For now, the built-in approach that Kirby uses for hashes (hash of the file name combined with the file’s last modified date) will have to do.