Exploring Kirby CMS: A Frontend Developer's Perspective

What I learned using Kirby for everything from marketing sites to flexible, content-driven platforms.

My first experience with Kirby CMS came around two years ago, when we were tasked with overhauling the frontend of a base Kirby project—something like a white-label product. These projects can be daunting on their own, let alone when they are built on a system you know little to nothing about.

That feeling changed almost as soon as I opened the Panel. I was greeted by a minimalist, intuitive control panel, and I immediately knew where to find the things I wanted to change. From an admin or editor's perspective, it feels familiar: a place where you can navigate around and take care of business without fighting the system.

Coding in Kirby gave me much the same impression. The folder structure is clear, and there is very little noise when you are editing a particular part of a site. Blueprints use YAML to describe the content structure, while the rest is PHP backed by a well-documented API. You don't feel overwhelmed by a completely new stack before you can get anything done.

Since then, I have worked on several kinds of Kirby projects, ranging from straightforward marketing websites to more complex sites with generic layouts and reusable block structures driving full-blown blogs. After two years of real project work, here is what has stood out to me.

Getting started

One quick thing before diving in: Kirby isn't fully open source. You can run it locally for as long as you like, but using it on a public server requires a license. The pricing is reasonable, and discounts are available for non-profits and education, so it isn't a one-size-fits-all cost. I don't mind that trade-off. It is a direct way to support the team building the software you rely on.

On the backend side, blueprints are YAML files that describe your site's content model and editing interface. If you are coming from the TYPO3 world and enjoy working with Content Blocks, the idea will feel familiar—except that blueprints are used throughout Kirby.

You have a consistent set of field types that can be used wherever you need them. Whether you are defining a complex data structure or a one-off section, you don't have to learn two completely different approaches.

DDEV also has a Kirby CMS quickstart based on the official Starterkit, so trying Kirby and making your first changes takes very little effort.

Templating and frontend

Being a frontend developer, I have to get into templating early. At first, it freaked me out: there is no separate templating language. Instead, you write plain HTML with PHP around it for conditions, loops and output.

Then you quickly realise the upside: yet again, there is very little new to learn.

Kirby exposes content through different kinds of PHP objects, so PHP is also the most direct way to work with it. You get useful HTML, URL, translation and string helpers that keep templates cleaner.

For example, the Html::attr() helper turns an array of attributes into a ready-to-print string and automatically skips anything that resolves to false or null:

php
<a <?= Html::attr([
    'href' => $url,
    'class' => 'button',
    'target' => $external ? '_blank' : null,
    'rel' => $external ? 'noopener noreferrer' : null,
]) ?>>
  <?= htmlspecialchars($label, ENT_QUOTES, 'UTF-8') ?>
</a>

You define the attributes dynamically and let the helper decide what to render. There is no manual string concatenation and no leftover target="" when the condition doesn't apply.

Complex logic should live in a dedicated controller anyway, and setting one up is straightforward. The controller shares its filename with the template it belongs to:

plaintext
/site/templates/article.php
/site/controllers/article.php

The array returned by the controller becomes available to the corresponding template. You can work with the same $page, $site and $kirby objects on both sides, so nothing needs to be reshaped just to cross that boundary. Not having a template engine really isn't the end of the world.

Reusable template fragments are called snippets. They are plain PHP files in the snippets folder, pulled into a template with snippet(). You pass their data explicitly instead of relying on whatever happens to be in scope:

php
<?php snippet('teaser', ['item' => $item]) ?>

Inside snippets/teaser.php, you work with $item directly—no lookup and no magic. If you are coming from TYPO3 and Fluid, it is much like rendering a partial:

html
<f:render partial="Teaser" arguments="{item: item}" />

The result is a self-contained fragment that only knows about the data you hand it, so it can be reused without silently depending on the page around it.

Third-party template engines

There are community integrations for PHP-based template languages too. If you are coming from Laravel, you can use the Blade integration created by Lukas Leitsch. For those with a Symfony background, there is also a Twig integration by the JUST team.

If those don't satisfy your needs, Kirby's plugin interface lets you bring your own template engine. We have something fun cooking on that front ourselves, so stay tuned.

Bring your own frontend

Kirby is unopinionated about the frontend framework or build tooling you bring. If you want to use Vite, the community-maintained kirby-vite plugin by Arnoson is a good place to start.

In my case, I already had a battle-tested Vite integration for TYPO3 and DDEV environments. It serves everything we need day to day and gives me full control over what that part of the stack does. Porting it to Kirby's plugin system took surprisingly little effort.

Knowing Vue helps when you want to extend the Panel, since Kirby's own Panel is built with Vue and exposes a component system for plugins. It is worth checking which Vue and Panel APIs your specific Kirby version supports before starting a plugin, because that compatibility matters more here than it does on the public-facing frontend.

Going fully headless is an option too. Kirby doesn't force you to render HTML at all; the headless setup cookbook is a useful starting point if that is the route you are after.

What is more interesting to me, though, is that you don't have to commit fully to one approach. Kirby can represent the same content in different formats depending on the request. A page can render as HTML for a browser and as JSON for an API consumer without duplicating the content model.

You can place a page.json.php representation alongside the regular page.php template, and Kirby selects the right one from the requested extension. Same blueprint, same fields, different representation on the way out. It is a nice middle ground when one part of a project needs a normal website and another needs to feed a mobile app or a separate frontend.

Panel, blueprints and the backend

Working with the Panel day to day confirms what that first impression promised. It remains intuitive once you are building a real project instead of just poking around.

Blueprint consistency is a big part of that. Once you have written one, you have basically seen the shape of all of them—whether you are describing a whole page, a single tab or one block inside a layout builder.

Take a tab for site-wide company settings in site.yml:

site.yml
company:
  label: Company Info
  fields:
    companyname:
      label: Company Name
      type: text
    logo:
      label: Logo
      type: files
      max: 1

Now compare that with a quote block in blocks/quote.yml:

blocks/quote.yml
name: Quote
icon: quote-right
fields:
  text:
    label: Quote
    type: textarea
  author:
    label: Author
    type: text

The same fields key, the same labels and types, and the same field definitions appear throughout the system. A text field stays a text field whether it is in a global tab, a regular page or nested inside a block.

Permissions follow the same readable approach. Locking down what an editor role can and cannot do is another YAML file:

yaml
title: Editor
permissions:
  files:
    changeName: false
  pages:
    delete: false

Once a field exists in a blueprint, pulling it into a template means calling it as a method on the page object. It returns a Field object, giving you helpers such as kirbytext() for KirbyText and html() for escaped plain text.

Content is optional by nature—someone will always leave a field empty—so you can check it with isNotEmpty() before printing anything:

php
<?php if ($page->text()->isNotEmpty()): ?>
  <?= $page->text()->kirbytext() ?>
<?php endif; ?>

There is no need to sprinkle !empty() throughout a template or guess whether a missing field returns null or an empty string. The Field object already knows and tells you.

Flexibility—and knowing when to limit it

Kirby lets you go as generic as you want, and that is exactly where I have learned to be careful.

It is tempting to create one mega-flexible block or field structure that could theoretically handle any layout a client might dream up. In practice, that flexibility can work against you. Editors end up staring at a wall of optional fields without knowing which ones matter on the page they are editing. Developers end up writing defensive template code for combinations that will never actually happen.

Some constraints serve both sides better. Decide upfront what a section is for instead of leaving every door open.

Where Kirby's flexibility genuinely pays off is in the Panel layout. Blueprints let you control section widths, group fields into tabs and display content as a grid, list, table or cards. None of that requires a new approach; it is just more configuration around the fields you already use.

Putting two related fields next to each other is a simple width option. That is useful for an event's start and end dates, where seeing both at once actually matters:

yaml
fields:
  startdate:
    label: Start Date
    type: date
    width: 1/2
  enddate:
    label: End Date
    type: date
    width: 1/2

Splitting a blueprint into tabs works in much the same way:

yaml
tabs:
  content:
    label: Content
    fields:
      text:
        type: textarea
  seo:
    label: SEO
    fields:
      metatitle:
        type: text

You can also configure useful previews so editors don't have to open every item just to find the right image or heading. A structure field, for example, can use cards with a preview image and caption:

yaml
fields:
  team:
    type: structure
    layout: cards
    image:
      query: item.photo.toFile
    info: "{{ item.role }}"
    fields:
      name:
        type: text
      role:
        type: text
      photo:
        type: files
        max: 1

None of this layout work happens in a vacuum, though. Sitting down with editors and walking them through a couple of options before committing to one is worth the time. What looks efficient to a developer isn't always what feels natural to the person filling it in every day. A five-minute conversation early on can save a lot of rework later.

Extending Kirby

Kirby's plugin API is straightforward once you get into it. There are clear extension points across the system instead of one catch-all hook. You can register blueprints, add your own methods, react to events, or provide templates and snippets—all bundled as part of a plugin, so the result doesn't feel bolted on afterwards.

Extending the Panel is a different story at first. Figuring out exactly what it exposes takes some digging before it clicks. Once it does, building a Panel component feels much like writing regular Vue with Kirby's components and helpers layered on top.

Kirby's Panel Lab and UI documentation are particularly useful here. Panel extensions are closely tied to the Kirby version they target, so I would always check the matching component documentation before starting serious plugin work.

Documentation, ecosystem and community

Kirby's documentation deserves praise. It was a huge help when I got started, with property lists and working examples for rendering or otherwise using almost every part of the system. I spent a good chunk of my early days in the reference docs, making sure I wasn't reinventing something that already existed.

Of course, having every option documented doesn't stop you from occasionally reaching for the wrong tool. That is where the cookbook earns its keep. It is full of real-world examples covering common scenarios and a fair share of uncommon ones too, so figuring out the direction Kirby prefers rarely takes long.

If the documentation and cookbook don't cover something, the community often does. The Kirby forum is active enough that many problems I have encountered already had an answer—sometimes directly from the core team.

The plugin ecosystem holds up well too. There is a healthy mix of free and paid options, including robust SEO and form plugins for more demanding projects. The plugin directory is well organised, so browsing it doesn't become a chore.

This still only scratches the surface. There is plenty more worth exploring on the documentation and plugin side alone, and we will come back to some of it in future articles.

Final thoughts

After a couple of years working with Kirby, it has become one of the CMSs I reach for naturally. It stays out of the way when a project is simple, but gives you enough flexibility when the content model and frontend become more ambitious.

What I appreciate most is the consistency. Blueprints, fields, templates and plugins feel like parts of the same system rather than features collected over time. That makes Kirby pleasant to learn—and, more importantly, pleasant to keep working with after the first project is finished.

If you are considering Kirby for your next website, we can help shape not only the frontend, but also the content architecture and editorial experience behind it. Get in touch with us at Digital Zombies, and let's see whether Kirby is the right fit for what you are building.