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
queryproperty. 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:
code— an inline code block for code snippets and examples.gallery— A list of images to create galleries of all sorts.heading— Headline from<h1>to<h6>.image— A single image.line— A separating line<hr>list— Ordered and unordered lists.markdown— A plaintext HTML/markdown/kirbytext block. This is perfect to combine WYSIWYG content with custom HTML, Kirbytext or Markdown.quote— A quote with optional citationtable— A table block based on the structure field.text— A simple text block with multiple paragraphsvideo— A Youtube or Vimeo video embed.
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:
- Draft: the page is only visible to editors in the Panel
- Unlisted: the page is published, but should only be available to people who know its URL.
- Public: the page is published, and should be enumerated in various lists, in the sitemap, etc.
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:
$page->children()returns Public and Unlisted subpages.$page->drafts()returns Draft subpages.$page->childrenAndDrafts()returns all subpages, regardless of status.
A collection of Pages can be filtered with these methods:
$pages->listed()returns Public pages;$pages->unlisted()returns Unlisted pages;$pages->published()returns Public and Unlisted pages;- note that
$pages->drafts()returns Draft subpages for each page in the collection rather than filtering the collection.
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).