Skip to content

Back to Notes on Kirby CMS

Content modeling in Kirby

Site content exists as a plain-text file system hierarchy inside the /content/ folder. Each page in the website gets a folder with a main content file, plus any images or other supporting files.

Content files are a loose collection of key-value pairs called Fields. Any field defined in a content file will be available to templates.

Content can be managed with a text editor, as you would for a static site generator such as Eleventy or Hugo, but also with Kirby’s admin interface, called the Panel.

To create and manage content visually, we need to define schemas for our content types with Blueprints. All blueprints are stored in the site/blueprints/ folder. You can have blueprints for: pages, files, users, and the site (the Panel dashboard homepage). Additionally, you can make reusable bits.

A page blueprint lists the fields, along with their type, as well as the layout for presenting them ot the user.

Content created and edited with the Panel reflects directly to files inside the /content/ folder.

A field guide to Kirby field types

Simple form inputs

Text fields accept single-line plain text, optionally validated with a regex pattern. There’s also specialized validation with Email, Tel, and Url, similar to how standard HTML <input> types behave.

The Slug field kebab-cases its input, and can be derived from another field in the blueprint.

Numeric values can use the Number or Range fields.

The Color field provides a color picker and/or predefined color swatches.

Dates can be picked with the Date field, which optionally includes a separate time input. If you need just the latter, there’s a dedicated Time field. Like the standard HTML input type=datetime-local, a Date field doesn’t store the timezone.

Finally, Hidden fields store content properties that shouldn’t be editable through the Panel, but should still be available for things like when conditions.

Rich text

Choices

Relationships

The Link field lets you mix links to pages, files, and arbitrary URLs. This makes it a great candidate for building sitewide navigation menus:

mainmenu:
	type: structure
	fields:
		link:
			type: link
		title:
			type: text

For internal links, Kirby stores the model’s permalink using the @/ prefix. These can be resolved to their corresponding URLs with the $field->toUrl() method.

The Link field is reused for the hyperlink functionality in Writer and Textarea fields, and by extension any blocks that use them. For Textearea fields, running $field->kirbytext() will resolve the permalink. Writer fields, on the other hand, store their content as plain HTML, so to replace permalinks involves DOM parsing. There’s now an experimental $field->permalinksToUrls() method to address this issue.

There are also specialized fields to attach references to internal resources (pages, files, and users).

The Files field. It’s similar to the Files section but has a slightly different configuration.

The Pages field. There’s also the Pages section, with which it shares some configuration.

The Pages field doesn’t currently allow yo to create pages on the fly (see #271). This is only possible in a Pages section.

The Users field lets you pick one or more users.

Tip: Relationships to files, pages, and users can also be expressed with other field types (such as Select, Multiselect, etc.) by using their query property. This is useful for contexts that don’t support all field types, such as the Page creation dialog or the Entries field, or if you prefer a different presentation in the user interface.

Structures

The Object field.

The Structure field.

The Entries is a simplified alternative to the Structure field when it contains a single field. Unlike the Structure field, the Entries field does not currently support all field types.

Blocks

The Blocks field lets you build a page as a sequence of blocks of various types. In addition to the built-in block types, you can make your own custom blocks. In its simplest form, you can define your custom block as a set of fields. You can also extend the built-in blocks, whose definitions are all located in kirby/config/blocks.

The Page Builder guide.

The built-in block types:

Recipes:

The Layout field arranges Blocks into various layouts. You can attach layout settings (sets of fields, optionally organized in tabs) to make the individual layouts configurable.

By defining custom blocks for dynamic content, such as a Posts block that lists the latest blog posts, you can build the entire site with one Layout field. A similar pattern can be used to build out the header and footer menus.

User interface helpers

These fields help with organizing the Panel user interface. They don’t map to properties of the underlying content.

In addition, all fields have a when property to show the field only when a condition is met.

Modeling data as pages

Should a piece of data be a page?

Kirby has three statuses for pages:

The physical order of Public pages can be changed with drag-and-drop in the Panel. Unlisted pages don’t have a user-specified order (although they can still be sorted later).

To get a collection of Pages, you can use the methods:

A collection of Pages can be filtered with these methods:

Subpages can be used for many-to-one relationships.

Dynamic title and slug

The Page creation dialog allows you to create pages on the fly. The default fields shown are the title, and the slug. You can specify additional fields for this dialog, but not all field types are currently supported.

This feature is controlled with the create option in the page blueprint:


create:
  title: "{{ page.uuid.id }}"
  slug: "{{ page.uuid.id }}"
  fields:
    - start_date
    - venue
  redirect: false
  status: unlisted

The settings above exemplify how you can derive the title and slug of the new page from other pieces of information, in this case the page’s unique ID. Setting the title and slug to a string will hide them from the page creation dialog, and only show the additional fields we’ve specified (start_date and venue). We keep the user on the page list after creating the new page with redirect: false and the status option controls the status of the new page.

You can drive the title and/or slug from any of the page fields or, for that matter, any other place in the website that can be expressed through a query string.

You should make sure the slug query does produce a value that’s unique among the page’s siblings, as duplicate slugs are not well handled in the page creation UI as of Kirby 5.1.4.

With titles and slugs derived from the page UUID, the Duplicate page action becomes cumbersome to the user, as it exposes these properties in the duplication UI, without an easy way to obtain new values for the page.

If you go with an UUID-derived title, it can be unsightly when editing the page. You could override the title in a custom page model:

use Kirby\Cms\Field;
class ScreeningPage extends Page {
	public function title(): Field {
		$start_date = $this->start_date();
		return parent::title()->value(
			$start_date ? 
				$start_date->toDate(
					new IntlDateFormatter(
						"en_EN", 
						IntlDateFormatter::FULL, 
						IntlDateFormatter::SHORT
					)
				) : 
				'Unscheduled'
		);
	}
}
title: Event
create:
  title: 'dummy'
  slug: 'dummy'
  fields:
    - start_date
options:
	changeTitle: false
	changeSlug: false
class EventPage extends Page {

    public static function create(array $props): static {
        $start_date = $props['content']['start_date'];
        $props['slug'] = $start_date;
        $props['content']['title'] = $start_date;
        return parent::create($props);
    }

    public function update(
        array|null $input = null, 
        string|null $languageCode = null, 
        bool $validate = false
    ): static {
        $start_date = $input['start_date'];
        $input['title'] = $start_date;
        $page = parent::update($input, $languageCode, $validate);
        return $page->changeSlug($start_date, $languageCode);
    }
}

The code above is based on a solution proposed by Lukas Kleinschmidt. The only problem is that upon saving the page after making changes, the Panel throws an error when it tries to reload the original slug (see kirby#2377).