# Kirby CMS

Een flat-file CMS in PHP: content, templates, snippets, blueprints, fields, blocks en layouts.

Multimedia & Creatieve Technologie · 3.1 The Agency · Zelfstudie · versie 1.0.0 · 2026-10-01

Tags: kirby, php, cms

Slides: <https://slides.mctantwerp.be/nl/the-agency/20-kirby>

<!-- slide 2 -->

## Agenda

1. Wat is Kirby
2. Installeren
3. Content
4. Templates
5. Snippets
6. Blueprints
7. Fields
8. Blocks en layouts

> **Toelichting:** De eerste drie hoofdstukken gaan over hoe Kirby denkt: mappen en tekstbestanden in plaats van een database. Templates en snippets zijn de frontend, blueprints en fields zijn het Panel voor je klant. Blocks en layouts zijn het laatste stuk, en het stuk dat het meest indruk maakt op een klant.

<!-- slide 3 -->

## Na deze les kan je

- Uitleggen hoe Kirby content bewaart, en wanneer je beter een ander CMS of Laravel kiest
- Een Kirby-site installeren en een eerste account in het Panel aanmaken
- Templates en snippets schrijven die velden, pagina's en afbeeldingen tonen
- Met blueprints bepalen welke velden je klant in het Panel ziet
- Een pagina laten opbouwen met blocks en layouts, met een eigen block erbij

> **Toelichting:** Dit is ook de checklist voor de opdracht op het einde. Controllers, page models, plugins, meertalige sites en deployen komen in de volgende les, Kirby CMS: verder.

<!-- slide 4 -->

## 1. Wat is Kirby

> **Toelichting:** In The Agency bouw je sites voor echte klanten, en die willen hun teksten zelf aanpassen zonder jou te bellen. Daar heb je een CMS voor. Kirby is het CMS dat we daarvoor gebruiken: klein genoeg om in een paar dagen te leren, en flexibel genoeg om elk design te bouwen.

<!-- slide 5 -->

**Een CMS zonder database.**

Elke pagina is een map, elke tekst staat in een .txt-bestand. Het Panel is een editor bovenop die bestanden.

> **Toelichting:** Flat-file is het woord dat je daarvoor tegenkomt. Het voordeel: je hele site, content inbegrepen, is een map die je kan kopiëren, in git zetten of in een zip naar een collega sturen. Geen database dump, geen migraties. Het nadeel zie je een paar slides verder.

<!-- slide 6 -->

### Kirby in het kort

- Kirby is geschreven in PHP, door Bastian Allgeier en een klein team in Duitsland. Het bestaat sinds 2012.
- Er zijn geen themes. Je schrijft je eigen HTML en CSS, Kirby levert de content.
- Het **Panel** is de admin voor je klant. Welke velden erin staan, bepaal je zelf met YAML-bestanden.
- Kirby 5 is de huidige versie. Het Panel kan wijzigingen tonen voor ze online gaan, en heeft een dark mode.

> **Toelichting:** Kirby 5 kwam uit in 2025, de huidige release is 5.6. Tutorials voor Kirby 3 en 4 zijn meestal nog bruikbaar: templates, snippets en blueprints werken hetzelfde. Wat veranderde, zit vooral in het Panel. Twijfel je, kijk dan in de docs naar het versienummer bij een feature.

<!-- slide 7 -->

### Kirby, WordPress of Laravel

|   | Kirby | WordPress | Laravel |
| --- | --- | --- | --- |
| Content | Mappen en .txt | MySQL | Je eigen database |
| Frontend | Je eigen templates | Een theme en plugins | Je eigen views |
| Admin | Panel, uit blueprints | wp-admin | Zelf bouwen, of Filament |
| Sterk in | Sites met een redactie | Wat de klant al kent | Applicaties met logica |

Kirby kies je voor een site waar een redactie teksten en beelden beheert: een portfolio, een campagnesite, een site voor een organisatie. Zodra users zelf data maken of er logica achter zit, ben je beter af met Laravel.

> **Toelichting:** WordPress draait op een database en een theme, en de meeste functies komen uit plugins. Dat is snel opgezet, maar je vecht vaak tegen een theme dat iemand anders bouwde. Laravel is een framework: daar bouw je alles zelf, ook de admin. Filament is een package dat je een admin geeft in Laravel. Kirby zit ertussen: een kant-en-klaar Panel, en een frontend die volledig van jou is.

<!-- slide 8 -->

### Wanneer niet

- Duizenden records met relaties en zoekfilters, zoals een webshop met voorraad. Daar wil je een database met indexen.
- Users die zelf content maken: een forum, reviews, een community. Kirby is gemaakt voor een redactie, niet voor duizend schrijvers.
- Data die elke seconde verandert, zoals bestellingen of reservaties. Tekstbestanden schrijven gaat niet goed met veel requests tegelijk.
- Een klant met een eigen WordPress-team dat de site daarna overneemt. Dat is geen technische reden, maar wel een goede.

> **Toelichting:** Kirby kan wel met een database praten, en sommige sites halen hun producten uit een externe API. Maar als het grootste deel van je project data in een database is, bouw je een applicatie, en dan is Laravel het betere gereedschap. Twijfel je bij een klantproject, bespreek het dan in de stand-up voor je begint.

<!-- slide 9 -->

### Licentie

- **€99 Basic**: Per site, voor wie minder dan een miljoen euro omzet heeft. De meeste kleine klanten.
- **€349 Enterprise**: Per site, zonder grens op omzet. Voor grotere bedrijven en organisaties.
- **€0 Student**: Gratis voor je eigen projecten, na een controle dat je student bent.

Eenmalig, geen abonnement, met drie jaar updates. Lokaal en op een testserver werk je zonder licentie. Pas bij de livegang koop je er één.

Meer in de docs: [getkirby.com/buy](https://getkirby.com/buy)

> **Toelichting:** Bij een klantproject koopt de klant de licentie, op zijn naam, voor zijn domein. Reken ze mee in je offerte. Vraag voor je eigen portfolio een gratis studentenlicentie aan op de site van Kirby: je bewijst dat je student bent, en je krijgt een licentie voor je eigen projecten.

<!-- slide 10 -->

### Waar staat de tekst van een pagina in een Kirby-site?

- A. In een MySQL-database
- B. In een .txt-bestand in de map content
- C. In de cloud van Kirby
- D. In een SQLite-bestand in site

**Antwoord:** B. In een .txt-bestand in de map content

Elke pagina is een map in content, met een tekstbestand en de afbeeldingen ernaast. Een database is er niet, ook geen SQLite. En Kirby zelf ziet je content nooit: alles draait op je eigen server.

> **Toelichting:** Wie WordPress gewend is, zoekt vaak eerst naar een database-instelling. In Kirby bestaat die niet. Daarom zet je een Kirby-site met content en al in git, en is een backup een kopie van een map.

<!-- slide 11 -->

## 2. Installeren

> **Toelichting:** Een Kirby-site staat in een paar minuten. Zet ze nu op, zodat je elk voorbeeld in de rest van de les zelf kan uitproberen. Wie Herd gebruikt, zet de map in de map van Herd en surft naar de .test-url.

<!-- slide 12 -->

### Starterkit of Plainkit

- **Starterkit**: Een volledige voorbeeldsite met templates, blueprints en content. Om in rond te neuzen.
- **Plainkit**: Leeg, op één template en één blueprint na. Het vertrekpunt voor een echt project.

```console
$ composer create-project getkirby/plainkit portfolio
$ cd portfolio && composer start
PHP Development Server (http://localhost:8000) started
```

Meer in de docs: [Quickstart](https://getkirby.com/docs/guide/quickstart)

> **Toelichting:** composer start is een script uit de composer.json van de Plainkit. Het start de ingebouwde server van PHP met de router van Kirby, op poort 8000. Stop hem met Ctrl+C. Krijg je een fout over een PHP-versie, kijk dan welke versie php -v toont: Kirby 5 vraagt 8.2 tot 8.5.

<!-- slide 13 -->

### De mappen

```bash
portfolio/
├── assets/
├── content/
├── kirby/
├── media/
├── site/
│   ├── blueprints/
│   ├── config/
│   ├── snippets/
│   └── templates/
└── index.php
```

- `assets` bevat je CSS, JavaScript, fonts en de beelden van het design.
- `content` bevat alle pagina's, als mappen met een .txt erin.
- `kirby` is de core, een gewone Composer-dependency. Daar kom je niet aan.
- `media` maakt Kirby zelf: thumbs en kopieën van je bestanden. Niet in git.
- `site` is jouw code: templates, snippets, blueprints en config.

> **Toelichting:** Pas nooit iets aan in de map kirby. Een composer update zet ze terug, en dan ben je je aanpassing kwijt. Alles wat je wil veranderen, kan vanuit site: een snippet, een blueprint of een plugin met dezelfde naam wint van wat in kirby staat.

<!-- slide 14 -->

### Het Panel

1. **/panel**: Surf naar localhost:8000/panel.
2. **Eerste account**: Kirby vraagt een e-mail en een wachtwoord, en maakt je admin.
3. **site/accounts**: Daar komt je user terecht, als map met een paar bestanden.

> [!WARNING]
> Op een publieke server weigert Kirby dat eerste account, anders kan de eerste bezoeker zich admin maken. Maak het lokaal aan en zet het mee online, of zet tijdelijk `'panel' => ['install' => true]` in je config.

Meer in de docs: [The Panel](https://getkirby.com/docs/guide/quickstart)

> **Toelichting:** Na het aanmaken staat er een map in site/accounts met je user in. Die map staat in de .gitignore van de Plainkit, en dat is terecht: daar zit de hash van je wachtwoord. Wachtwoord vergeten op je lokale site? Verwijder de map in site/accounts en maak opnieuw een account aan.

<!-- slide 15 -->

### config.php

*site/config/config.php*

```php
<?php

return [
    'debug' => true,
];
```

De config is één PHP-bestand dat een array teruggeeft. Alles wat je in Kirby instelt, komt hier: debug, de cache, routes, hooks, de talen. Zet `debug` meteen aan, dan zie je bij een fout de melding met de regel waar het misloopt.

Meer in de docs: [Options](https://getkirby.com/docs/reference/system/options)

> **Toelichting:** Zonder debug toont Kirby bij een fout een kale foutpagina zonder uitleg, en zoek je je een ongeluk. Op productie zet je het uit, want de foutpagina toont je paden en stukken code. In de volgende les maak je een config per omgeving, zodat debug lokaal aan staat en online vanzelf uit.

<!-- slide 16 -->

## 3. Content

> **Toelichting:** Dit is het hoofdstuk waar Kirby het meest verschilt van wat je kent. Snap je hoe mappen en tekstbestanden pagina's worden, dan valt de rest op zijn plaats: templates lezen die bestanden, het Panel schrijft ze.

<!-- slide 17 -->

### Een pagina is een map

De map content van een kleine Kirby-site, met per pagina de url, de template en de status: home/ wordt / met home.php (unlisted); 1_projects/ wordt /projects met projects.php (listed); 1_herfstcampagne/ wordt /projects/herfstcampagne met project.php (listed); unity-showcase/ wordt /projects/unity-showcase met project.php (draft); contact/ wordt /contact met contact.php (unlisted).

> **Toelichting:** Kirby leest de map content bij elk request. Een nieuwe map is meteen een nieuwe pagina, zonder registratie. Hernoem je een map, dan verandert de url. Daarom werkt een redacteur liever in het Panel: dat past de map voor je aan, en zet links naar de pagina met de uuid om.

<!-- slide 18 -->

### Het tekstbestand

*content/1_projects/1_herfstcampagne/project.txt*

```yaml
Title: Herfstcampagne

----

Client: Stad Antwerpen

----

Date: 2026-09-15

----

Text: Een campagnesite met een **quiz** per district.
```

- Elk veld begint met zijn naam en een dubbelpunt. Velden zijn gescheiden door `----`.
- Een waarde mag over meerdere regels lopen, tot de volgende vier streepjes.
- Tekst in een textarea is markdown, met wat extra van Kirby erbij.
- Kirby voegt zelf een `Uuid` toe: een id die blijft, ook als de map een andere naam krijgt.

> **Toelichting:** Hoofdletters in de veldnaam maken niets uit: Title en title zijn hetzelfde veld, en in je template schrijf je ze klein. Moet er in je tekst zelf een regel met vier streepjes staan, zet er dan een spatie voor. Een tekstbestand met de hand aanpassen mag gerust, het Panel leest het gewoon opnieuw.

<!-- slide 19 -->

### Listed, unlisted, draft

- `1_projects` is listed. Het nummer zet de pagina in je menu en bepaalt de volgorde.
- `contact` is unlisted. De url werkt, maar `listed()` slaat de pagina over.
- `_drafts/unity-showcase` is een draft. Alleen wie ingelogd is, ziet de pagina.
- `20260915_herfstcampagne` heeft een datum als nummer, voor een blog die op datum sorteert.

> **Toelichting:** Die laatste stel je in de blueprint in met num: date. Het Panel zet dan de datum uit het veld date als prefix, in plaats van een volgnummer. Sleep je in het Panel een listed pagina naar een andere plaats, dan hernoemt Kirby de mappen met nieuwe nummers.

<!-- slide 20 -->

### Panel en bestand

Drie kolommen naast elkaar: de blueprint "Project" met de velden client (text), featured (toggle), text (textarea), het formulier dat het Panel daarvan maakt, en project.txt dat meeschrijft terwijl je typt.

Typ in het Panel in het midden, en kijk hoe de blueprint links bepaalt welk veld waar in het bestand rechts komt.

> **Toelichting:** Het Panel bewaart je wijzigingen eerst apart. Pas als je op Save klikt, schrijft het ze in project.txt. Tot dan zie je ze in de preview, en kan je ze weggooien. Dat is nieuw in Kirby 5. In Kirby 4 en ouder kwam alles meteen in het bestand.

<!-- slide 21 -->

### Je maakt een map content/about zonder nummer. Wat gebeurt er?

- A. De pagina geeft een 404
- B. Ze staat in het menu, achteraan
- C. Ze is bereikbaar op /about, maar niet listed
- D. Ze is een draft

**Antwoord:** C. Ze is bereikbaar op /about, maar niet listed

Zonder nummer is een pagina unlisted. De url werkt, maar $site->children()->listed() slaat ze over, dus een menu dat listed() gebruikt, toont ze niet. Een draft is ze niet: een draft staat in een map \_drafts en is enkel zichtbaar als je ingelogd bent.

> **Toelichting:** Wil je ze toch in het menu? Zet er een nummer voor, of sleep ze in het Panel in de lijst met gepubliceerde pagina's. Kirby hernoemt de map dan zelf.

<!-- slide 22 -->

## 4. Templates

> **Toelichting:** Templates zijn waar de content HTML wordt. Er zit geen template engine zoals Blade of Twig tussen: het is gewoon PHP met HTML errond. Dat voelt ouderwets, maar je leert er niets nieuws voor, en je ziet altijd wat er gebeurt.

<!-- slide 23 -->

### Een template

*site/templates/project.php*

```php
<?php snippet('header') ?>

<article>
  <h1><?= $page->title()->esc() ?></h1>
  <p><?= $page->client()->esc() ?></p>
  <time><?= $page->date()->toDate('d/m/Y') ?></time>

  <?= $page->text()->kt() ?>
</article>

<?php snippet('footer') ?>
```

1. Een snippet met de kop van je pagina. Daarover gaat hoofdstuk 5.
2. `$page` is de pagina die nu getoond wordt. Elk veld is een methode: `title()`, `client()`.
3. `toDate()` zet een datum om naar het formaat dat je kiest.
4. `kt()` zet de markdown uit het tekstvak om naar HTML.

> **Toelichting:** Bestaat project.php niet, dan valt Kirby terug op default.php. Die zit in de Plainkit en toont enkel de titel. Krijg je een pagina die er te kaal uitziet, controleer dan eerst of de naam van het tekstbestand en die van de template overeenkomen.

<!-- slide 24 -->

### Vier variabelen

- **`$page`**: De pagina die nu gerenderd wordt, met al haar velden, bestanden en kinderen.
- **`$site`**: De hele site. Haar eigen velden komen uit content/site.txt.
- **`$pages`**: De pagina's op het hoogste niveau, hetzelfde als $site->children().
- **`$kirby`**: De app zelf: config, de ingelogde user, de request, de taal.

Ze staan klaar in elke template en elke snippet, zonder import of use.

> **Toelichting:** $kirby gebruik je minder dan je denkt. Je komt het tegen voor de ingelogde user ($kirby->user()), de huidige taal ($kirby->language()) of een config-optie ($kirby->option('debug')). Buiten een template, zoals in een plugin, krijg je hetzelfde object met de functie kirby().

<!-- slide 25 -->

### Een veld is een object

```php
$page->title()                    // een Field, geen string
$page->title()->esc()             // veilig in je HTML
$page->text()->kt()               // Kirbytext naar HTML
$page->date()->toDate('d/m/Y')    // 15/09/2026
$page->featured()->toBool()       // true of false
$page->client()->isNotEmpty()     // staat er iets in?
$page->client()->or('Eigen werk') // standaardwaarde
```

> [!WARNING]
> `<?= $page->title() ?>` zet de waarde zonder escaping in je HTML. Typt een redacteur een `<script>`, dan draait die bij elke bezoeker. Gebruik `esc()` voor alles wat geen `kt()` krijgt.

Meer in de docs: [Field methods](https://getkirby.com/docs/reference/templates/field-methods)

> **Toelichting:** Alle field methods staan in de reference, gegroepeerd per soort: tekst, datum, collecties. or() is handig voor een standaardwaarde, isNotEmpty() en isEmpty() voor een if rond een stuk HTML dat er alleen moet staan als het veld ingevuld is.

<!-- slide 26 -->

### Kirbytext

*In het tekstbestand*

```md
Een campagnesite met een **quiz**
per district.

(image: cover.jpg caption: De homepage)
```

*Na kt()*

```html
<p>Een campagnesite met een
<strong>quiz</strong> per district.</p>
<figure>
  <img src=".../cover.jpg" alt="">
  <figcaption>De homepage</figcaption>
</figure>
```

Kirbytext is markdown met kirbytags tussen haakjes. `(image: cover.jpg)` zoekt het bestand in de map van de pagina en maakt er een figure van. Ook links, video's en e-mailadressen hebben een kirbytag.

Meer in de docs: [KirbyTags](https://getkirby.com/docs/reference/plugins/extensions/kirbytags)

> **Toelichting:** Waarom kt() en niet markdown()? markdown() kent de kirbytags niet, dus (image: cover.jpg) blijft dan letterlijk staan. Sleept een redacteur een afbeelding in het tekstvak, dan zet het Panel er zelf een kirbytag voor in. Eigen kirbytags maken kan ook, met een plugin.

<!-- slide 27 -->

### Pagina's ophalen

```php
$projects = page('projects')->children()->listed();

$latest = $projects
    ->sortBy('date', 'desc')
    ->limit(3);

$featured = $projects->filterBy('featured', true);
$antwerpen = $projects->filterBy('client', 'Stad Antwerpen');
```

1. `page()` zoekt een pagina op, `children()` geeft haar kinderen, `listed()` houdt de gepubliceerde over.
2. Sorteren en beperken, zoals in een query: de drie nieuwste projecten.
3. `filterBy()` vergelijkt een veld met een waarde.

Meer in de docs: [$pages](https://getkirby.com/docs/reference/objects/cms/pages)

> **Toelichting:** Elke methode geeft een nieuwe collectie terug, dus de originele $projects blijft ongemoeid. Dat lijkt op de collections van Laravel. page('projects') zoekt op id, het pad zonder nummers. page('projects/herfstcampagne') werkt dus ook.

<!-- slide 28 -->

### Een lijst en een menu

*site/templates/projects.php*

```php
<?php $projects = $page->children()->listed() ?>

<?php foreach ($projects as $project): ?>
  <a href="<?= $project->url() ?>">
    <?= $project->title()->esc() ?>
  </a>
<?php endforeach ?>
```

*site/snippets/nav.php*

```php
<nav>
  <?php foreach ($pages->listed() as $item): ?>
    <a href="<?= $item->url() ?>"
      <?= e($item->isOpen(), 'aria-current="page"') ?>>
      <?= $item->title()->esc() ?>
    </a>
  <?php endforeach ?>
</nav>
```

Een foreach over een collectie, en `url()` voor de link. `e()` is een helper van Kirby: het tweede argument komt er enkel als het eerste true is.

> **Toelichting:** isOpen() is true voor de huidige pagina en voor al haar parents. Zo blijft Projecten actief in je menu terwijl je op een projectpagina zit. isActive() is enkel true voor de pagina zelf. aria-current vertelt een schermlezer welke link de huidige pagina is, en je kan er in CSS op stijlen.

<!-- slide 29 -->

### Afbeeldingen

```php
<?php if ($cover = $page->image('cover.jpg')): ?>
  <img
    src="<?= $cover->resize(1200)->url() ?>"
    srcset="<?= $cover->srcset([400, 800, 1200]) ?>"
    alt="<?= $cover->alt()->esc() ?>"
  >
<?php endif ?>
```

1. `image()` geeft `null` als het bestand er niet is. De `if` voorkomt een fout.
2. `resize()` maakt een kleinere kopie in `media`.
3. `srcset()` doet dat in drie breedtes, de browser kiest.
4. De alt staat in `cover.jpg.txt`, naast het beeld.

Meer in de docs: [Resize images on the fly](https://getkirby.com/docs/guide/files/resize-images-on-the-fly)

> **Toelichting:** Een thumb wordt gemaakt de eerste keer dat iemand de url opvraagt, daarna komt hij uit media. Daarom is de eerste keer laden trager. Wil je weten welke formaten je nodig hebt, kijk dan hoe breed de afbeelding in je design maximaal wordt, en neem dat maal twee voor retina.

<!-- slide 30 -->

## 5. Snippets

> **Toelichting:** Een site heeft op elke pagina dezelfde head, navigatie en footer, en vaak dezelfde kaartjes op verschillende overzichten. Snippets zijn de manier om die HTML één keer te schrijven.

<!-- slide 31 -->

### Snippets

*site/templates/projects.php*

```php
<?php foreach ($projects as $project): ?>
  <?php snippet('project-card', [
    'project' => $project,
  ]) ?>
<?php endforeach ?>
```

*site/snippets/project-card.php*

```php
<article class="card">
  <h2><?= $project->title()->esc() ?></h2>
  <p><?= $project->client()->esc() ?></p>
  <a href="<?= $project->url() ?>">Bekijk</a>
</article>
```

Een snippet krijgt `$page`, `$site` en `$kirby` vanzelf. Al de rest geef je mee als array, en elke sleutel wordt een variabele in de snippet.

Meer in de docs: [Snippets](https://getkirby.com/docs/guide/templates/snippets)

> **Toelichting:** Hou snippets dom: geen queries erin, enkel tonen wat je meekrijgt. Haal je in een snippet zelf pagina's op, dan weet je in de template niet meer welke data er gebruikt wordt. Snippets in submappen roep je op met hun pad: snippet('cards/project').

<!-- slide 32 -->

### Een layout-snippet

*site/snippets/layout.php*

```php
<!doctype html>
<html lang="nl">
<head>
  <title><?= $page->title()->esc() ?></title>
  <?= css('assets/css/main.css') ?>
</head>
<body>
  <?php snippet('nav') ?>
  <main><?= $slot ?></main>
</body>
</html>
```

Een snippet met de hele pagina rond je inhoud: de head, de navigatie, de footer.

`$slot` is de plaats waar de template haar eigen HTML invult. `css()` maakt de link-tag naar je stylesheet.

> **Toelichting:** De oudere manier, die je in veel tutorials ziet, is een snippet header en een snippet footer. Dan open je de html-tag in het ene bestand en sluit je hem in het andere, en dat gaat mis zodra iemand een div toevoegt. Met een slot staat de hele pagina in één bestand.

<!-- slide 33 -->

### Slots

*site/templates/project.php*

```php
<?php snippet('layout', slots: true) ?>

  <h1><?= $page->title()->esc() ?></h1>
  <?= $page->text()->kt() ?>

<?php endsnippet() ?>
```

Met `slots: true` en `endsnippet()` komt alles ertussen in `$slot` terecht.

Zo bevat elke template enkel wat eigen is aan die pagina. Kies hiervoor in een nieuw project.

Meer in de docs: [Slots](https://getkirby.com/docs/guide/templates/snippets)

> **Toelichting:** Benoemde slots bestaan ook. In de template schrijf je slot('aside') en endslot(), in de snippet lees je $slots->aside(). Handig voor een sidebar die per pagina verschilt. In de meeste sites heb je genoeg aan het ene standaardslot.

<!-- slide 34 -->

## 6. Blueprints

> **Toelichting:** Tot hier was alles frontend. Nu het deel dat je klant ziet: het Panel. Een goed Panel is het verschil tussen een klant die zijn site zelf bijhoudt, en een klant die je elke week mailt voor een tekstje.

<!-- slide 35 -->

**Jij beslist wat je klant mag aanpassen.**

Zonder blueprint toont het Panel enkel een titel. Met een blueprint krijgt je klant precies de velden die hij nodig heeft, en niets meer.

> **Toelichting:** Een blueprint is YAML: inspringen met spaties, geen tabs. Krijg je in het Panel een foutmelding over je blueprint, kijk dan eerst naar de inspringing. VS Code met de YAML-extensie van Red Hat toont die fouten meteen.

<!-- slide 36 -->

### Een eerste blueprint

*site/blueprints/pages/project.yml*

```yaml
title: Project

fields:
  client:
    label: Klant
    type: text
  date:
    label: Datum
    type: date
  text:
    label: Tekst
    type: textarea
```

- Overal dezelfde naam: `project.yml`, `project.txt` en `project.php` horen bij elkaar.
- De sleutel, zoals `client`, is de naam van het veld in het tekstbestand en in je template.
- `label` is wat je klant ziet, in de taal van de klant.

Meer in de docs: [Blueprints](https://getkirby.com/docs/guide/blueprints/introduction)

> **Toelichting:** Het titelveld zit er altijd bij, ook als je het niet vermeldt. Hou de sleutels kort en in het Engels, want die typ je in elke template. Het label mag in de taal van de klant, en kan zelfs per taal: label: { nl: Klant, en: Client }.

<!-- slide 37 -->

### Vier niveaus

Een schets van een pagina in het Panel met de vier niveaus van een blueprint in elkaar: tabs (Bovenaan, voor een lang formulier), columns (Naast elkaar, met een breedte als 2/3), sections (Een formulier, een lijst pagina's of bestanden), fields (Wat de klant invult).

> **Toelichting:** Je hebt ze niet allemaal nodig. Een blueprint met enkel fields, zoals op de vorige slide, is perfect voor een eenvoudige pagina. Columns en sections komen erbij als je naast het formulier bestanden of subpagina's wil tonen, tabs pas als het formulier te lang wordt.

<!-- slide 38 -->

### Kolommen en secties

*site/blueprints/pages/project.yml*

```yaml
columns:
  main:
    width: 2/3
    sections:
      content:
        type: fields
        fields:
          client: { label: Klant, type: text }
          text: { label: Tekst, type: textarea }
  sidebar:
    width: 1/3
    sections:
      gallery: { type: files }
```

- Een `fields` section is het formulier.
- Een `files` section toont de bestanden in de map van de pagina, en laat je klant er uploaden.
- Een `pages` section is een lijst subpagina's, met een knop om er een toe te voegen.

Meer in de docs: [Layout](https://getkirby.com/docs/guide/blueprints/layout)

> **Toelichting:** De korte schrijfwijze met accolades is gewone YAML: client: { label: Klant, type: text } is hetzelfde als drie regels. Handig voor een veld met weinig opties. De sleutels van columns en sections, zoals main en gallery, kies je zelf. Ze moeten enkel uniek zijn in de blueprint.

<!-- slide 39 -->

### Een overzichtspagina

*site/blueprints/pages/projects.yml*

```yaml
title: Projecten

sections:
  drafts:
    label: In de maak
    type: pages
    status: draft
    template: project
  published:
    label: Online
    type: pages
    status: listed
    layout: cards
```

- `template` geeft elk nieuw project de blueprint project, zonder dat de klant moet kiezen.
- `status` toont enkel drafts, listed of unlisted pagina's.
- `layout` kiest tussen `list`, `cards` en `table`.

Meer in de docs: [Pages section](https://getkirby.com/docs/reference/panel/sections/pages)

> **Toelichting:** Zonder template mag de klant zelf een blueprint kiezen voor een nieuwe pagina, uit alle blueprints die er zijn. Dat wil je bijna nooit. Met templates (meervoud) en een lijst geef je de keuze uit een paar, zoals project en case.

<!-- slide 40 -->

### site.yml

*site/blueprints/site.yml*

```yaml
title: Portfolio

sections:
  pages:
    type: pages
    label: Pagina's

  settings:
    type: fields
    fields:
      footer:
        label: Tekst in de footer
        type: writer
```

- site.yml is het dashboard: het eerste wat je klant ziet na het inloggen.
- Goed voor wat over de hele site gaat, zoals de footer of social links.
- De velden komen in `content/site.txt`, en je leest ze met `$site->footer()`.

> **Toelichting:** writer is een veld waar je klant tekst opmaakt zonder markdown te kennen: vet, cursief, links. In de template echo je het zonder kt(), want het is al HTML. Kirby filtert die HTML wel, zodat er geen script in kan.

<!-- slide 41 -->

### Je maakt site/blueprints/pages/event.yml. Welke template rendert een pagina met die blueprint?

- A. site/templates/event.php, of default.php als die er niet is
- B. Altijd default.php
- C. De blueprint zelf
- D. site/snippets/event.php

**Antwoord:** A. site/templates/event.php, of default.php als die er niet is

Een nieuwe pagina met de blueprint event krijgt event.txt, en event.txt zoekt event.php. De blueprint bepaalt enkel het Panel, nooit de HTML. Snippets roep je zelf op vanuit een template, Kirby kiest ze nooit op naam van de pagina.

> **Toelichting:** Omgekeerd werkt het ook: heb je een template event.php maar geen blueprint, dan toont het Panel voor die pagina's enkel de titel. Maak ze dus altijd per twee.

<!-- slide 42 -->

## 7. Fields

> **Toelichting:** Kirby heeft meer dan dertig soorten velden. Je hoeft ze niet allemaal te kennen. Weet welke groepen er zijn, en zoek de opties op in de reference wanneer je er een nodig hebt.

<!-- slide 43 -->

### Veldtypes

- **Tekst**: `text` `textarea` `writer` `email`
- **Keuzes**: `select` `radio` `toggle` `tags` `date`
- **Verwijzingen**: `files` `pages` `users` `link`
- **Samengesteld**: `structure` `object` `entries` `blocks` `layout`

Tekst en keuzes bewaren een waarde. Verwijzingen bewaren een id naar een bestand, een pagina of een user. Samengestelde velden bewaren een lijst of een structuur, als YAML of JSON in het tekstbestand.

Meer in de docs: [Fields](https://getkirby.com/docs/reference/panel/fields)

> **Toelichting:** writer is een WYSIWYG met een beperkte set opmaak, textarea is markdown met een toolbar. Kies writer voor klanten die nog nooit markdown zagen, textarea als er kirbytags in moeten. entries is nieuw in Kirby 5: een lijst van één soort waarde, zoals een rij tags met een vaste keuze.

<!-- slide 44 -->

### Van veld naar template

| Veld | In de template |
| --- | --- |
| `text` | `$page->client()->esc()` |
| `textarea` | `$page->text()->kt()` |
| `toggle` | `$page->featured()->toBool()` |
| `tags` | `$page->tags()->split()` |
| `files` | `$page->cover()->toFile()` |
| `pages` | `$page->related()->toPages()` |

In het tekstbestand staat altijd tekst. Een `to`-methode maakt er weer iets bruikbaars van: een boolean, een array, een bestand of een collectie pagina's.

> **Toelichting:** split() splitst op een komma, en kan ook op een ander teken: split(';'). toFile() geeft één bestand, toFiles() een collectie. Bij pages hetzelfde met toPage() en toPages(). Elke field method staat in de reference, met het veldtype waar ze bij hoort.

<!-- slide 45 -->

### Een coverbeeld

*project.yml*

```yaml
cover:
  label: Coverbeeld
  type: files
  multiple: false
```

*project.php*

```php
<?php if ($cover = $page->cover()->toFile()): ?>
  <img
    src="<?= $cover->resize(1200)->url() ?>"
    alt="<?= $cover->alt()->esc() ?>"
  >
<?php endif ?>
```

In het tekstbestand staat enkel een verwijzing naar het bestand. `toFile()` zoekt het op, of geeft `null` als het veld leeg is. Een project zonder cover mag je pagina niet breken, vandaar de `if`.

Meer in de docs: [Files field](https://getkirby.com/docs/reference/panel/fields/files)

> **Toelichting:** Waarom een files field en niet gewoon cover.jpg? Met een files field kiest je klant zelf welk beeld de cover is, en mag het bestand eender welke naam hebben. image('cover.jpg') werkt enkel zolang niemand het bestand hernoemt.

<!-- slide 46 -->

### Structure

*about.yml*

```yaml
team:
  label: Team
  type: structure
  fields:
    name:
      label: Naam
      type: text
    role:
      label: Rol
      type: text
```

*about.php*

```php
<?php foreach ($page->team()->toStructure() as $member): ?>
  <li>
    <strong><?= $member->name()->esc() ?></strong>
    <?= $member->role()->esc() ?>
  </li>
<?php endforeach ?>
```

Een tabel in het Panel, met een rij per teamlid. `toStructure()` geeft de rijen, en elke rij heeft dezelfde field methods als een pagina.

> **Toelichting:** Een structure wordt als YAML in het tekstbestand bewaard, alle rijen in één veld. Dat is prima voor een team van tien, maar niet voor een lijst van tweehonderd, en een rij kan geen eigen url krijgen. Heeft elk item een eigen pagina nodig, maak er dan subpagina's van.

<!-- slide 47 -->

### Velden die je klant helpen

```yaml
client:
  label: Klant
  type: text
  required: true
  width: 1/2
  help: Zoals op de factuur
type:
  label: Soort project
  type: select
  options: { intern: Eigen project, extern: Voor een klant }
url:
  label: Website
  type: url
  when:
    type: extern
```

- `required` laat niet opslaan zolang het veld leeg is.
- `width` zet twee velden naast elkaar.
- `help` zet een zin uitleg onder het veld.
- `when` toont een veld enkel als een ander veld een bepaalde waarde heeft. Hier verschijnt Website pas als het een project voor een klant is.

> **Toelichting:** Hier maak je als bureau het verschil. Een klant die het Panel snapt, belt je niet voor elke tekstwijziging. Test je Panel eens met iemand die het nog nooit zag: waar die twijfelt, hoort een help-tekst of een beter label.

<!-- slide 48 -->

## 8. Blocks en layouts

> **Toelichting:** Dit is het deel waar klanten het meest naar vragen: zelf een pagina opbouwen uit stukken. Het is ook het deel waar een site het snelst lelijk wordt als je te veel vrijheid geeft. De kunst is kiezen welke blokken er mogen.

<!-- slide 49 -->

### Waarom blocks

- Een textarea is één lange tekst. Afbeeldingen en citaten zitten ertussen als kirbytags.
- Met blocks stapelt je klant een titel, een tekst, een beeld of een video, in eender welke volgorde.
- Jij kiest welke types er bestaan, en elk type heeft zijn eigen HTML en CSS.
- Het werkt zoals de editor van Notion, of de blokken van WordPress.

> **Toelichting:** Vraag je klant: is deze pagina vooral tekst, of een opeenvolging van verschillende stukken? Voor een blogpost of een nieuwsbericht is een writer of textarea vaak genoeg, en sneller om in te typen. Blocks lonen voor een projectpagina met beelden, citaten en video's door elkaar.

<!-- slide 50 -->

### Het blocks field

*project.yml*

```yaml
text:
  label: Inhoud
  type: blocks
  fieldsets:
    - heading
    - text
    - image
    - quote
    - video
```

*project.php*

```php
<?= $page->text()->toBlocks() ?>
```

Elk type block heet een fieldset. Zonder `fieldsets` krijgt je klant alle ingebouwde blocks. Beperk ze: wie vijftien soorten blokken ziet, gebruikt ze ook alle vijftien. `toBlocks()` zet alles om naar HTML.

Meer in de docs: [Blocks field](https://getkirby.com/docs/reference/panel/fields/blocks)

> **Toelichting:** Noem het veld niet content of blocks. $page->content() bestaat al als methode van Page, en dan krijg je iets anders terug dan je veld. text of body zijn veilige namen.

<!-- slide 51 -->

### Eigen HTML per block

*site/snippets/blocks/quote.php*

```php
<blockquote class="quote">
  <?= $block->text() ?>

  <?php if ($block->citation()->isNotEmpty()): ?>
    <cite><?= $block->citation()->esc() ?></cite>
  <?php endif ?>
</blockquote>
```

De HTML van de ingebouwde blocks staat in `kirby/config/blocks`. Een snippet met dezelfde naam in `site/snippets/blocks` wint, zo krijgt elk block de classes van je eigen CSS. De velden van het block zitten in `$block`.

> **Toelichting:** Elk ingebouwd block heeft zijn velden. Een quote heeft text en citation, een image heeft image, alt, caption en link. Welke velden er zijn, zie je in de blueprints in kirby/config/blocks, of in de reference bij elk block.

<!-- slide 52 -->

### Een eigen block

*site/blueprints/blocks/cta.yml*

```yaml
name: Call to action
icon: star
fields:
  text:
    type: text
  label:
    type: text
  link:
    type: link
```

*site/snippets/blocks/cta.php*

```php
<section class="cta">
  <p><?= $block->text()->esc() ?></p>
  <a href="<?= $block->link()->toUrl() ?>">
    <?= $block->label()->esc() ?>
  </a>
</section>
```

Een blueprint voor de velden in het Panel, een snippet voor de HTML. Allebei met dezelfde naam.

> **Toelichting:** Na deze twee bestanden zet je cta in de fieldsets van je blocks field, en het block staat in het Panel. Het link-veld kan een url, een pagina, een bestand of een e-mailadres zijn. toUrl() maakt er altijd een url van, ook als de klant een pagina koos.

<!-- slide 53 -->

### Het layout field

De layouts "1/1", "1/2, 1/2", "1/3, 2/3", "1/3, 1/3, 1/3" als knoppen in het Panel, met de gekozen rij op een grid van twaalf kolommen en wat span() per kolom teruggeeft.

Kies een layout bovenaan, en kijk wat span() per kolom teruggeeft op een grid van twaalf.

> **Toelichting:** Een layout field bestaat uit rijen. De redacteur voegt een rij toe, kiest een van de layouts uit je blueprint, en vult elke kolom met blocks. Geef een handvol layouts, niet alles wat kan: een pagina met tien soorten kolommen ziet er zelden goed uit.

<!-- slide 54 -->

### Layouts in de blueprint

*site/blueprints/pages/home.yml*

```yaml
layout:
  label: Pagina
  type: layout
  layouts:
    - "1/1"
    - "1/2, 1/2"
    - "1/3, 2/3"
  fieldsets:
    - heading
    - text
    - image
    - cta
```

- `layouts` zijn de rijen waaruit je klant kiest.
- `fieldsets` zegt welke blocks in een kolom mogen, je eigen `cta` inbegrepen.
- `settings` geeft elke rij extra velden, zoals een achtergrond.

Meer in de docs: [Layout field](https://getkirby.com/docs/reference/panel/fields/layout)

> **Toelichting:** settings zijn extra velden per rij, zoals een achtergrondkleur of een id om naar te linken. In de template lees je ze met $layout->background(). De breuken schrijf je zoals width in een blueprint, met aanhalingstekens rond de hele rij.

<!-- slide 55 -->

### Layouts renderen

*home.php*

```php
<?php foreach ($page->layout()->toLayouts() as $layout): ?>
  <section class="grid">
    <?php foreach ($layout->columns() as $column): ?>
      <div style="--span: <?= $column->span() ?>">
        <?= $column->blocks() ?>
      </div>
    <?php endforeach ?>
  </section>
<?php endforeach ?>
```

*main.css*

```css
.grid {
  display: grid;
  grid-template-columns:
    repeat(12, 1fr);
}
.grid > div {
  grid-column: span var(--span);
}
```

Twee loops: de rijen, en per rij de kolommen. `span()` rekent de breuk om naar kolommen op twaalf, 1/3 wordt 4. Die waarde gaat in een CSS-variabele, en het grid doet de rest.

> **Toelichting:** Onder een bepaalde breedte zet je alle kolommen onder elkaar: in een media query wordt grid-column dan span 12. span() rekent standaard op twaalf kolommen. Werk je met een ander grid, geef dan het aantal mee: span(6).

<!-- slide 56 -->

### Blocks of layout

- **Blocks**: Eén kolom die je zelf in je template plaatst. Voor een projectpagina, een blogpost, een over-ons.
- **Layout**: Rijen en kolommen die de klant zelf kiest. Voor een homepage of een landingspagina.

> [!TIP]
> Begin met blocks. Een layout field geef je pas als de klant echt zelf pagina's opbouwt.

> **Toelichting:** Een klant die het layout field krijgt, gaat er ook mee spelen. Dat is prima op een homepage of een landingspagina voor een campagne. Op een projectpagina wil je dat elk project er hetzelfde uitziet, en dan geef je blocks in een kolom die jij in de template plaatst.

<!-- slide 57 -->

### Waar pas je de HTML van het ingebouwde image block aan?

- A. kirby/config/blocks/image/image.php
- B. site/snippets/blocks/image.php
- C. site/templates/image.php
- D. site/blueprints/blocks/image.yml

**Antwoord:** B. site/snippets/blocks/image.php

Een snippet met dezelfde naam in site/snippets/blocks wint van de ingebouwde. Het bestand in kirby aanpassen lijkt te werken, tot een composer update het terugzet. De blueprint in site/blueprints/blocks verandert de velden in het Panel, niet de HTML. Een template rendert een pagina, nooit een block.

> **Toelichting:** Kopieer het originele bestand uit kirby/config/blocks als vertrekpunt. Dan weet je zeker dat je geen veld vergeet, zoals de caption of de link van een image block.

<!-- slide 58 -->

### Opdracht: Een portfolio in Kirby

Je eigen portfolio, of een site voor een project van The Agency. Start van de Plainkit.

1. Een blueprint en template voor projects en project, met klant, datum, cover en een blocks field
2. Een layout-snippet met een slot, en een menu met listed()
3. Op de homepage de drie laatste projecten, als kaartjes uit een snippet
4. Eén eigen block, met een blueprint en een snippet
5. Klaar als je in het Panel een project aanmaakt, publiceert en het op de homepage ziet verschijnen

> **Toelichting:** Begin bij de blueprint van project: maak er twee projecten mee in het Panel, en schrijf pas daarna de template. Dan heb je meteen content om te tonen. Vast bij het menu? Kijk terug naar Een lijst en een menu. Vast bij je eigen block? Een eigen block, en controleer of de naam in fieldsets dezelfde is als die van je bestanden.

## Begrippen

- **Kirby**: Een CMS in PHP zonder database. De content staat in mappen en tekstbestanden, het Panel is een editor daarbovenop, en de HTML schrijf je zelf. (slide 6)
- **Flat-file CMS**: Een CMS dat zijn content in bestanden bewaart in plaats van in een database. Makkelijk te backuppen en in git te zetten, minder geschikt voor duizenden records met relaties. (slide 6)
- **Panel**: De admin van Kirby op /panel, waar een redacteur pagina's en bestanden beheert. Wat er in het Panel staat, bepaal jij met blueprints. (slide 14)
- **Plainkit**: Een lege Kirby-installatie met één template en één blueprint. Het vertrekpunt voor een echt project, de Starterkit is om te leren. (slide 12)
- **Listed en unlisted**: Een map met een nummer ervoor (1_projects) is listed en komt in je menu, het nummer bepaalt de volgorde. Zonder nummer is een pagina unlisted: bereikbaar via de url, maar niet in listed(). (slide 19)
- **Draft**: Een pagina in de map \_drafts van zijn parent. Alleen zichtbaar voor wie in het Panel ingelogd is. (slide 19)
- **Template**: Een PHP-bestand in site/templates dat een pagina rendert. De naam van het tekstbestand kiest de template: project.txt gebruikt project.php, en zonder match default.php. (slide 23)
- **Field method**: Een methode op een veld die de waarde omzet, zoals esc(), kt(), toDate() of toFile(). $page->title() geeft een veld terug, geen string. (slide 25)
- **Kirbytext**: Markdown met extra tags tussen haakjes, zoals (image: cover.jpg). kt() zet het om naar HTML. (slide 26)
- **Snippet**: Een stuk template dat je hergebruikt, in site/snippets. Krijgt $page, $site en $kirby vanzelf, en extra data via een array. (slide 30)
- **Slot**: Een plaats in een snippet die de template invult, tussen snippet(..., slots: true) en endsnippet(). Zo wordt een snippet een layout rond je pagina. (slide 33)
- **Blueprint**: Een YAML-bestand in site/blueprints dat zegt welke velden en secties het Panel toont voor een pagina, een bestand, de site of een user. (slide 36)
- **Section**: Een blok in een blueprint: fields voor een formulier, pages voor een lijst subpagina's, files voor bestanden, info voor uitleg. (slide 38)
- **Structure field**: Een veld met een tabel van rijen, elk met dezelfde velden. Goed voor een team of een FAQ, niet voor honderden items: daar maak je pagina's van. (slide 46)
- **Blocks field**: Een veld waarin de redacteur blokken stapelt: heading, text, image, quote, video of je eigen types. Elk blok heeft een snippet in site/snippets/blocks. (slide 50)
- **Fieldset**: Een type block dat een blocks of layout field toelaat, zoals heading of image. Een eigen fieldset is een blueprint in site/blueprints/blocks. (slide 50)
- **Layout field**: Rijen met kolommen, en blocks in elke kolom. De redacteur kiest per rij een layout uit de lijst in de blueprint. (slide 53)

## Links en bronnen

### In de presentatie

- [Kirby licenses](https://getkirby.com/buy) (slide 9): Basic en Enterprise, gratis voor studenten
- [Kirby Quickstart](https://getkirby.com/docs/guide/quickstart) (slide 12, slide 14): Plainkit, Starterkit en de PHP-versies van Kirby 5
- [Options](https://getkirby.com/docs/reference/system/options) (slide 15)
- [Field methods](https://getkirby.com/docs/reference/templates/field-methods) (slide 25)
- [KirbyTags](https://getkirby.com/docs/reference/plugins/extensions/kirbytags) (slide 26)
- [$pages](https://getkirby.com/docs/reference/objects/cms/pages) (slide 27)
- [Resize images on the fly](https://getkirby.com/docs/guide/files/resize-images-on-the-fly) (slide 29)
- [Snippets](https://getkirby.com/docs/guide/templates/snippets) (slide 31, slide 33): Slots en endsnippet()
- [Blueprints](https://getkirby.com/docs/guide/blueprints/introduction) (slide 36)
- [Blueprint layout](https://getkirby.com/docs/guide/blueprints/layout) (slide 38): Tabs, columns, sections en fields
- [Pages section](https://getkirby.com/docs/reference/panel/sections/pages) (slide 39)
- [Fields](https://getkirby.com/docs/reference/panel/fields) (slide 43)
- [Files field](https://getkirby.com/docs/reference/panel/fields/files) (slide 45)
- [Blocks field](https://getkirby.com/docs/reference/panel/fields/blocks) (slide 50): Fieldsets, groepen, eigen blocks en snippets per block
- [Layout field](https://getkirby.com/docs/reference/panel/fields/layout) (slide 54): layouts, settings en span()

### Bronnen

- [Kirby Guide](https://getkirby.com/docs/guide)
- [Kirby 5 release notes](https://getkirby.com/releases/5): Changes en preview, dark mode, entries field, site controller
- [Creating pages](https://getkirby.com/docs/guide/content/creating-pages): Mapnamen, nummers, \_drafts en de naam van het tekstbestand
- [Plainkit .gitignore](https://github.com/getkirby/plainkit/blob/main/.gitignore)
