Global and Local Media Storage in Kirby CMS

Video

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.