Kirby's approach to file storage is beautifully simple: every file lives inside its page folder. It's fast, it's clean, and it works great for page-specific content like article images. But when you have images that are used frequently on multiple pages, you end up uploading the same file over and over.
A global media page can fix that, but going fully global has its own downsides. So how about a combination of both? That's exactly what we're looking at today.
This concept was shared by Sebastian from JUNO during one of the first Kirby Meetups in Hamburg. Big shout out to them.
Setting Up the Global Media Page
First, create a new folder in your content directory:
content/globalmedia/globalmedia.txt
Then create a matching blueprint. The blueprint is straightforward, just one section for images:
title: Global Media
options:
extends: permissions/default
delete: false
preview: false
duplicate: false
changeStatus: false
changeSlug: false
move: false
status:
unlisted: Published
sections:
images:
type: files
layout: table
search: true
size: tiny
template: image
We lock down the options so editors can't accidentally duplicate or move the page. It's a utility page, not content.
Blocking the Frontend Route
Since this page only stores media, it shouldn't be reachable on the frontend. Add a route to your config.php:
return [
"routes" => [
[
"pattern" => "/globalmedia",
"method" => "GET",
"action" => function () {
return;
}
]
]
];
Now visiting /globalmedia returns a 404.
The Page Method: Local or Global?
We need a way to decide, e.g. per template, whether file uploads should go to the page itself or to the global media page.
Create a plugin (e.g. site/plugins/project-helper) and add a page method:
Kirby::plugin('project/helper', [
'pageMethods' => [
'mediaPage' => function () {
$mediaPage = page('globalmedia');
if (!V::in($this->intendedTemplate(), ['note'])) return $mediaPage ?? $this;
return $this;
}
]
]);
The idea: templates listed in the array, note in this case, keep their files locally. Everything else uses the global media page. So a note stores its images in its own folder, but a page like "About" pulls from the shared pool.
Wiring Up File Fields
Now update your file fields to use this method. For example, a cover field from the starterkit:
cover:
type: files
multiple: false
query: page.mediaPage.images.template('image')
uploads:
parent: page.mediaPage
template: image
The uploads.parent tells Kirby where to store new uploads, and the query tells the field where to look for existing files. Both point to page.mediaPage, which returns either the page itself or the global media page.
Do the same for image blocks or any other file fields where this applies.
Quick Access in the Panel
For convenience, add the global media page to your panel sidebar. I'm using the Kirby Panel Menu plugin for this, which generates a sidebar menu.
return [
"panel" => [
"menu" => fn($kirby) => panelMenu($kirby)
->site()
->page('Media', 'globalmedia', ['icon' => 'images'])
->area('users')
->area('system')
->toArray()
]
];
With everything in place, each template gets the storage strategy that makes sense for it.
The beauty of this approach is that you define the rules once in the plugin, and every file field across your site respects them automatically.