# Bouw een projectensite

Een tutorial: van een pagina in gewone HTML naar bewerkbare tekst, gestructureerde data, detailpagina's en de API.

Deze tutorial bouwt top-down de projectensite van Fieldwork, een voorbeeldstudio voor ontwerp. Hij begint met een pagina in gewone HTML en voegt stap voor stap bewerkbare tekst, gestructureerde projectdata, detailpagina's en de API toe. Het resultaat is het voorbeeld op de homepage en in onze demo-applicatie. De volledige broncode staat in [projects.zip](/examples/projects.zip).

Je hebt een project nodig met een draaiende lokale site. Zie [Installatie](/nl/docs/learn/installation/).

## 1. Schrijf de pagina

Een route is een map in `ext/uri/`. De homepage is de map zelf. Vervang `ext/uri/index.tpl` van de startpagina:

```html
[#set page-title="Work"#]
[#set body-classes="bg-stone-50 text-stone-900"#]
[#html#]
```

`index.tpl` zet een paar paginavariabelen en geeft dan door aan `[#html#]`, de standaard pagina-shell. De shell maakt de head van het document, de stylesheets en scripts, en de body van de pagina uit `main.tpl` in dezelfde map. Schrijf `ext/uri/main.tpl` als gewone HTML:

```html
<section class="mx-auto max-w-7xl px-8 pb-10 pt-16">
  <h1 class="max-w-4xl text-6xl font-semibold leading-none tracking-tight">Places people use every day</h1>
  <div class="mt-8 max-w-2xl text-xl leading-relaxed text-stone-600">
    <p>Fieldwork designs wayfinding, services and digital tools for libraries, ports, schools and markets.</p>
  </div>
</section>
```

De classes zijn Tailwind CSS, dat het project standaard bouwt. Elke HTML en CSS werkt hier: de pagina is van jou. Draai `./nimbly watch` terwijl je bewerkt, zodat nieuwe classes in de stylesheet komen.

## 2. Voeg een header en footer toe

De shell maakt ook `header.tpl` en `footer.tpl` uit de map van de route. De header wordt door meerdere pagina's gedeeld, dus wordt hij een herbruikbare template. Een map in `ext/tpl/` met een `index.tpl` roep je aan bij zijn naam. Maak `ext/tpl/site-header/index.tpl`:

```html
<div class="mx-auto flex max-w-7xl flex-col gap-4 px-8 py-6 sm:flex-row sm:items-center sm:justify-between sm:py-8">
  <a href="[#base-url#]/" class="text-xl font-semibold tracking-tight">Fieldwork</a>
  <nav class="flex flex-wrap gap-x-6 gap-y-2 text-stone-600 sm:gap-8">
    <a href="[#base-url#]/#work" class="text-stone-900">Work</a>
    <a href="[#base-url#]/#contact">Contact</a>
  </nav>
</div>
```

Op telefoons staan logo en menu onder elkaar; vanaf het breakpoint `sm` delen ze één rij. Daarna `ext/tpl/site-footer/index.tpl`:

```html
<div id="contact" class="mx-auto max-w-7xl px-8 py-12 text-stone-500">Fieldwork is an example studio.</div>
```

Roep ze vervolgens aan vanuit de route. `ext/uri/header.tpl` bevat één regel, `[#site-header#]`, en `ext/uri/footer.tpl` bevat `[#site-footer#]`. `[#base-url#]` houdt links werkend als de site in een submap draait.

## 3. Maak de tekst bewerkbaar

Redacteuren moeten de kop en de introductie kunnen aanpassen zonder developer. Maak beide bewerkbaar in `main.tpl`:

```html
<section class="mx-auto max-w-7xl px-8 pb-10 pt-16">
  <h1 class="max-w-4xl text-6xl font-semibold leading-none tracking-tight"
      data-nb-edit="[#cfield title#]" data-nb-edit-options='{"plain":true}'>
    [#get-html [#cfield title#] plain#]
  </h1>
  <div class="mt-8 max-w-2xl text-xl leading-relaxed text-stone-600" data-nb-edit="[#cfield intro#]">
    [#get-html [#cfield intro#]#]
  </div>
</section>
```

`[#cfield title#]` noemt een contentveld dat bij deze pagina hoort, en `[#get-html#]` toont het. Met `data-nb-edit` kan een ingelogde redacteur het op de pagina aanpassen: open de site terwijl je bent ingelogd, kies **Edit** in de pagina-acties en typ. Nimbly bewaart de tekst per pagina. Er is geen datastructuur nodig.

## 4. Definieer de projecten

De projecten van de studio herhalen zich en hebben dezelfde velden, dus worden ze een resource. Een resource is een map in `ext/data/` met een `.meta`-bestand dat de velden beschrijft. Maak `ext/data/projects/.meta`:

```json
{
  "name": {"singular": "Project", "plural": "Projects"},
  "title_field": "title",
  "fields": {
    "title": {"name": "Title", "type": "text", "required": true},
    "title_slug": {"name": "URL slug", "type": "slug", "source": "title", "admin_col": false},
    "sector": {
      "name": "Sector", "type": "select", "admin_filter": true,
      "options": {
        "culture": "Culture", "public": "Public", "retail": "Retail",
        "education": "Education", "mobility": "Mobility"
      }
    },
    "year": {"name": "Year", "type": "text"},
    "published": {"name": "Published", "type": "boolean", "admin_filter": true},
    "cover": {"name": "Cover", "type": "image"},
    "description": {
      "name": "Description", "type": "html",
      "buttons": "bold,italic,anchor", "admin_col": false
    }
  },
  "index": ["title_slug"],
  "sort": {"field": "year", "flags": "numeric", "order": "desc"}
}
```

Dat ene bestand geeft je:

- een overzicht op `/nb-admin/projects` met kolommen, zoeken en filters op sector en publicatie,
- formulieren om projecten toe te voegen en te bewerken, met een afbeeldingskiezer en een teksteditor,
- validatie van de verplichte titel,
- de REST API op `/api/v1/projects`,
- rechten per rol en een geschiedenis van elke wijziging.

Het veld `title_slug` wordt afgeleid van de titel en geïndexeerd, zodat een project via zijn adres te vinden is. Voeg in de admin vier of vijf projecten toe, met een omslagafbeelding, en zet ze op gepubliceerd. Elk record wordt opgeslagen als JSON-bestand in `ext/data/projects/`.

## 5. Toon de projecten op de pagina

Een kaart toont één project. Die wordt alleen op deze pagina gebruikt, dus mag hij in de map van de route staan: een `.tpl`-bestand naast `index.tpl` is een template voor die route, geen nieuw adres. Maak `ext/uri/card-project.tpl`:

```html
<li id="project-[#item.uuid#]">
  <div class="overflow-hidden rounded-2xl bg-stone-200" data-nb-edit-img="projects.[#item.uuid#].cover">
    [#get-img-html [#item.cover#] sizes="sm-100,md-50,lg-33"#]
  </div>
  <p class="mt-5 text-sm uppercase tracking-widest text-stone-500">[#item.sector#] · [#item.year#]</p>
  <h3 class="mt-2 text-2xl font-semibold"><a href="[#base-url#]/projects/[#item.title_slug#]">[#item.title#]</a></h3>
  <div class="mt-3 leading-relaxed text-stone-600" data-nb-edit="projects.[#item.uuid#].description">
    [#get-html item.description#]
  </div>
</li>
```

Voeg de lijst toe aan `main.tpl`, onder de introductie:

```html
<section id="work" class="mx-auto max-w-7xl px-8 py-12">
  [#data projects filter=published:1 sort=year|numeric|desc#]
  <ul class="grid gap-x-8 gap-y-14 md:grid-cols-2 lg:grid-cols-3">
    [#repeat data.projects tpl=card-project#]
  </ul>
</section>
```

`[#data#]` selecteert de gepubliceerde projecten, nieuwste eerst, en `[#repeat#]` toont de kaart voor elk ervan, met het record in `item`. `[#get-img-html#]` schrijft een responsieve afbeelding: de server maakt de formaten die de layout vraagt, in WebP. De beschrijving en de omslag zijn zowel op de pagina als in het admin-formulier bewerkbaar, omdat ze naar het veld van het record verwijzen.

De template bevat geen logica: hij selecteert, herhaalt en toont. Heeft een pagina een beslissing of berekening nodig, dan gaat die in een [library](/nl/docs/concepts/libraries/).

## 6. Geef elk project een pagina

Het adres van een project is `/projects/` gevolgd door zijn slug. Een mapnaam tussen haakjes is een dynamisch segment. Maak `ext/uri/projects/(slug)/route.inc`, dat bepaalt of het adres bij een gepubliceerd project hoort:

```php
<?php
$parts = router_match(__FILE__);
if ($parts === false) return;

// A slug is lowercase letters, digits and dashes; anything else is not a project
$slug = $parts[0];
if (!preg_match('/^[a-z0-9-]{1,120}$/', $slug)) return;

load_library('data');
load_library('util');
$records = data_read_index('projects', 'title_slug', md5_uuid($slug));
$record = $records ? reset($records) : null;
if (!$record || empty($record['published'])) return;

set_variable_dot('project', $record);
router_accept();
```

De route controleert de invoer, zoekt de slug op in de index en accepteert het request alleen voor een gepubliceerd project. Al het andere eindigt in een 404. Voeg `index.tpl` toe:

```html
[#set page-title="[#project.title#]"#]
[#set body-classes="bg-stone-50 text-stone-900"#]
[#html#]
```

een `header.tpl` met `[#site-header#]`, een `footer.tpl` met `[#site-footer#]`, en `main.tpl`:

```html
<article class="mx-auto max-w-4xl px-8 pb-24 pt-10">
  <a href="[#base-url#]/#work" class="text-sm text-stone-500">← Work</a>
  <p class="mt-10 text-sm uppercase tracking-widest text-stone-500">[#project.sector#] · [#project.year#]</p>
  <h1 class="mt-3 text-5xl font-semibold leading-tight tracking-tight">[#project.title#]</h1>
  <div class="mt-10 overflow-hidden rounded-2xl bg-stone-200">
    [#get-img-html [#project.cover#] sizes="sm-100,md-100,lg-75"#]
  </div>
  <div class="mt-10 max-w-2xl text-xl leading-relaxed text-stone-600">
    [#get-html project.description#]
  </div>
</article>
```

Dynamische routes worden geregistreerd, zodat de router weet dat hij ze moet vragen. Draai:

```bash
./nimbly routes:sync
```

De projecttitels op de homepage leiden nu naar hun eigen pagina's.

## 7. Bekijk de pagina vanuit de admin

Redacteuren werken in de admin en willen de pagina zien die ze bewerken. Voeg een knop toe aan het actiepaneel van elk project. Maak `ext/tpl/btn-preview/index.tpl`:

```html
<a href="[#base-url#]/projects/[#record.title_slug#]" class="[#btn-class-secondary#] flex w-full items-center justify-center text-center">[#text Preview#]</a>
```

en zet hem in de `.meta` van de resource, naast `fields`:

```json
"record_actions": [
  {"template": "btn-preview", "scope": "view,edit"}
]
```

## 8. Gebruik de data overal

Dezelfde records zijn beschikbaar via de REST API, voor een app, een scherm of een ander systeem. Een aanroeper logt in als Nimbly-gebruiker, krijgt een token en vraagt records op.

Een gebruiker kan alleen een token aanvragen als API-toegang voor zijn account aan staat. Zet **API access** aan in het actiepaneel van de gebruiker in de admin, naast het bewerkformulier op `/nb-admin/users/<uuid>`; daarvoor is een rol nodig die gebruikers mag bewerken. Zet je het uit, dan werkt het token van die gebruiker meteen niet meer.

Daarna:

```bash
TOKEN=$(curl -s -X POST http://localhost/api/v1/auth/token \
  -H "Content-Type: application/json" \
  -d '{"email": "you@example.com", "password": "your password"}' | jq -r .token)

curl "http://localhost/api/v1/projects?filter=published:1&sort=year|desc" \
  -H "Authorization: Bearer $TOKEN"
```

Wat een aanroeper mag, volgt uit zijn rol, precies zoals in de admin. Geef bij een echte integratie de aanroeper een eigen gebruiker met een rol die alleen projecten mag lezen. Zie [Integraties](/nl/docs/guides/integrations/).

## 9. Verder: een kaart met een andere library

Het voorbeeld heeft ook een kaart van de projecten op `/map`, gebouwd met Leaflet, een library die het project standaard niet bevat. De `.meta` voegt een veld `location-picker` toe, zodat redacteuren elk project in het admin-formulier op een kaart plaatsen. De route laadt Leaflet en zijn eigen script alleen op die pagina, en leest de projecten met `[#data#]`, als JSON doorgegeven aan het script. Dat patroon werkt voor elke library of elk framework. Zie `uri/map/` in de download en [Frontendstack](/nl/docs/guides/frontend/).

## Wat je hebt gebouwd

| Bouwsteen | Bestanden | Rol |
|---|---|---|
| Routes | `ext/uri/`, `ext/uri/projects/(slug)/` | Adressen en paginaopbouw |
| Templates | `site-header`, `site-footer`, `card-project`, `btn-preview` | Presentatie, hergebruikt op meerdere pagina's |
| Resource | `ext/data/projects/.meta` | Data, admin, API en rechten |
| Content | paginavelden via `[#cfield#]` | Tekst die redacteuren ter plekke aanpassen |

Verder vanaf hier:

- [Atomaire compositie](/nl/docs/concepts/atomic-design/) legt uit hoe deze bouwstenen samenwerken.
- [Resources](/nl/docs/concepts/resources/) behandelt velden, indexen en geschiedenis.
- [Gebruikers en rechten](/nl/docs/guides/permissions/) richt een redacteursrol in.
