SproetS website van Drupal naar Directus

SproetS website van Drupal naar Directus

Sjoerd 6 februari 2026

Vandaag ben ik begonnen aan een nieuw project: de complete rebuild van `sjoerdkaandorp.nl`. De oude Drupal site mocht vervangen worden door een moderne headless setup met Directus en Nuxt. Dit is het verhaal van dag 1.

De huidige sjoerdkaandorp.nl draait op Drupal, maar ik wilde naar dezelfde stack als mijn blog: Directus als headless CMS en Nuxt als frontend. Het verschil: deze site moet meerdere entiteiten bedienen onder één dak.

Wat moet de site kunnen?

De nieuwe site moet vijf verschillende onderdelen bedienen:

  • Sjoerd Kaandorp - Persoonlijke pagina met CV
  • Bordjesman - Archief van een kunstproject met locaties en gallery
  • Stoney Cars - Collectie van gevonden speelgoedauto's met details
  • SproetS - Portfolio met projecten
  • SproetS Fonts - Lettertype collectie met downloads
  • Blog - Artikelen over al het bovenstaande

Tijdens ontwikkeling draait alles via sjoerdkaandorp.nl met subpaths. Later kunnen subdomeinen of aparte domains toegevoegd worden.

Data model ontwerp

Voor ik begon met bouwen wilde ik eerst het schema uitdenken. In Drupal zou je dit met Content Types en Views doen, maar in Directus werk je met Collections en Relations.

Ik heb het model eerst in Mermaid uitgewerkt:

erDiagram
    PAGES {
        uuid id PK
        string title
        string slug UK
        text content
        uuid featured_image FK
        string page_type
        string status
    }
    
    STONEY_CARS {
        uuid id PK
        string title
        string slug UK
        text content
        uuid featured_image FK
        int bouwjaar
        string vindplaats
        string status
    }
    
    FONTS {
        uuid id PK
        string title
        string slug UK
        text content
        uuid featured_image FK
        int release_year
        string designer
        string license
        string status
    }
    
    PROJECTS {
        uuid id PK
        string title
        string slug UK
        text content
        uuid featured_image FK
        string client
        int year
        string status
    }
    
    BLOG_POSTS {
        uuid id PK
        string title
        string slug UK
        text excerpt
        text content
        uuid featured_image FK
        string status
        uuid author FK
    }
    
    BORDJESMAN {
        uuid id PK
        string title
        text content
        uuid featured_image FK
        geometry geo_location
    }
    
    BORDJESMAN_LOCATIONS {
        uuid id PK
        uuid bordjesman_id FK
        string location_name
        date visit_date
        geometry coordinates
    }
    
    TREFWOORDEN {
        uuid id PK
        string naam UK
        string slug UK
        string type
        string kleur
    }
    
    GALLERIES {
        uuid id PK
        string title
        string entity_type
        uuid entity_id
    }
    
    GALLERY_ITEMS {
        uuid id PK
        uuid gallery_id FK
        string item_type
        uuid file_id FK
        string video_url
        int sort_order
    }
    
    DIRECTUS_FILES {
        uuid id PK
        string filename
        string type
    }
    
    STONEY_CARS ||--o| GALLERIES : "has"
    FONTS ||--o| GALLERIES : "has"
    PROJECTS ||--o| GALLERIES : "has"
    PROJECTS ||--o{ TREFWOORDEN : "has"
    BLOG_POSTS ||--o{ TREFWOORDEN : "has"
    BORDJESMAN ||--o| GALLERIES : "has"
    BORDJESMAN ||--o{ BORDJESMAN_LOCATIONS : "has"
    GALLERIES ||--o{ GALLERY_ITEMS : "contains"

Belangrijke ontwerpkeuzes

Unified Pages collectie In plaats van voor elke statische pagina een aparte collectie, heb ik één Pages collectie met een page_type veld. Dit maakt het onderhoud eenvoudiger.

Herbruikbare Galleries Galleries zijn een aparte collectie die gebruikt kan worden door Stoney Cars, Projects, Fonts en Bordjesman. Elk Gallery Item kan een afbeelding, een self-hosted video of een YouTube/Vimeo embed zijn.

Trefwoorden in plaats van Categories Ik gebruik het Nederlandse "Trefwoorden" en filter op type (project/blog/algemeen) om ze per context te kunnen gebruiken.

Bordjesman als Singleton Bordjesman is geen collectie maar een singleton - er is maar één instantie. De locaties waar Bordjesman is geweest zijn een aparte collectie met geo-coördinaten voor een toekomstige kaartweergave.

Image ratio 16:9 Alle images gebruiken 16:9 aspect ratio met vier presets:

  • Thumbnail: 480x270 (crop)
  • Medium: 960x540 (contain)
  • Large: 1920x1080 (contain)
  • OG Image: 1280x720 (crop voor social media)

Server setup

De infrastructuur staat op mijn VPS, waar ook desktoptraveller.nl draait. Ik heb dezelfde aanpak gebruikt:

Directory structuur

/var/www/sites/
├── sjoerdkaandorp.nl-admin/    # Directus backend
└── sjoerdkaandorp.nl-web/      # Nuxt frontend

Stack keuzes

Database: SQLite In eerste instantie wilde ik PostgreSQL gebruiken, maar die bleek niet geïnstalleerd. Omdat desktoptraveller.nl prima draait op SQLite heb ik daar ook voor gekozen. Voor dit project is dat meer dan voldoende.

Directus: Port 8056 Directus draait op poort 8056 (desktoptraveller gebruikt 8055). Toegankelijk via https://admin.sjoerdkaandorp.nl.

Nuxt: Port 3002 Nuxt frontend draait op poort 3002 (desktoptraveller gebruikt 3000). Toegankelijk via https://sjoerdkaandorp.nl.

Apache reverse proxy Apache proxyt beide applicaties met SSL certificaten van Let's Encrypt. Geen fancy Nginx setup nodig - Apache doet het prima en was al geconfigureerd.

Systemd services Beide apps draaien als systemd services:

  • directus-sjoerdkaandorp.service
  • nuxt-sjoerdkaandorp.service

Dit zorgt voor automatische restart bij crashes en startup bij server reboot.

Installatie proces

Directus installatie

cd /var/www/sites/sjoerdkaandorp.nl-admin
npm init -y
npm install directus

Het configureren ging via een .env bestand:

PORT=8056
PUBLIC_URL=https://admin.sjoerdkaandorp.nl

DB_CLIENT=sqlite3
DB_FILENAME=./data.db

KEY=<generated>
SECRET=<generated>

ADMIN_EMAIL=sjoerd@sproets.nl
ADMIN_PASSWORD=TempPassword123!

STORAGE_LOCATIONS=local
STORAGE_LOCAL_ROOT=./uploads

CORS_ENABLED=true
CORS_ORIGIN=https://sjoerdkaandorp.nl

De database initialiseren:

npx directus bootstrap

Dit creëert alle system tables en de admin user.

Nuxt installatie

cd /var/www/sites/sjoerdkaandorp.nl-web
npx nuxi@latest init . --force
npm install
npm install @directus/sdk marked

De Directus composable (zoals ik ook op desktoptraveller.nl gebruik):

// composables/useDirectusItems.ts
import { createDirectus, rest, readItems } from '@directus/sdk'

export const useDirectusItems = () => {
  const config = useRuntimeConfig()
  const directus = createDirectus(config.public.directusUrl).with(rest())

  const getItems = async ({ collection, params }: { collection: string; params?: any }) => {
    try {
      return await directus.request(readItems(collection, params))
    } catch (error) {
      console.error(`Error fetching ${collection}:`, error)
      return []
    }
  }

  return { getItems }
}

En de Nuxt configuratie:

// nuxt.config.ts
export default defineNuxtConfig({
  compatibilityDate: '2024-11-01',
  devtools: { enabled: true },
  
  runtimeConfig: {
    public: {
      directusUrl: 'https://admin.sjoerdkaandorp.nl'
    }
  },
  
  app: {
    head: {
      title: 'SproetS - Sjoerd Kaandorp',
      meta: [
        { charset: 'utf-8' },
        { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      ]
    }
  }
})

Build voor productie:

npm run build

SSL certificaten

Voor admin.sjoerdkaandorp.nl was een nieuw certificaat nodig:

sudo certbot --apache -d admin.sjoerdkaandorp.nl

Let's Encrypt deed de rest automatisch, inclusief auto-renewal setup.

Apache configuratie

De virtual host voor de Nuxt frontend:

<VirtualHost *:443>
    ServerName sjoerdkaandorp.nl
    ServerAlias www.sjoerdkaandorp.nl
    
    ProxyPreserveHost On
    ProxyPass / http://localhost:3002/
    ProxyPassReverse / http://localhost:3002/
    
    # WebSocket support
    RewriteEngine on
    RewriteCond %{HTTP:Upgrade} websocket [NC]
    RewriteCond %{HTTP:Connection} upgrade [NC]
    RewriteRule ^/?(.*) "ws://localhost:3002/$1" [P,L]
    
    SSLEngine on
    SSLCertificateFile /etc/letsencrypt/live/sjoerdkaandorp.nl/cert.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/sjoerdkaandorp.nl/privkey.pem
    SSLCertificateChainFile /etc/letsencrypt/live/sjoerdkaandorp.nl/chain.pem
</VirtualHost>

Vergelijkbare setup voor admin.sjoerdkaandorp.nl maar dan naar poort 8056.

Wat ik heb geleerd

Live deployment is OK

Normaal zou je eerst een staging environment opzetten, maar voor dit project was de oude Drupal site toch al aan vervanging toe. Direct live gaan met de nieuwe stack was een bewuste keuze. Bezoekers krijgen even een lege Nuxt welkomstpagina te zien - niet ideaal maar acceptabel voor een persoonlijke site.

SQLite is prima

Ik had PostgreSQL gepland maar SQLite blijkt perfect voor dit soort projecten. Desktoptraveller.nl draait er ook op zonder problemen. De database is één bestand, backups zijn simpel, en er is geen aparte database daemon nodig.

pkill kan te gretig zijn

Tijdens het testen wilde ik een achtergrond Directus proces stoppen met pkill -f "directus start". Dat stopte helaas BEIDE Directus instanties (desktoptraveller en sjoerdkaandorp). Lesson learned: gebruik specifiekere proces-identificatie of gewoon systemctl.

Systemd services zijn essentieel

Services met auto-restart op crashes en bij server reboots zijn onmisbaar. Het maakt het verschil tussen "de site is af" en "de site blijft draaien".

Collections guide

Het aanmaken van alle collections in Directus doe ik morgen via de UI. Ik heb een complete stap-voor-stap guide geschreven met:

  • Alle 10 collections met fields
  • Field types en constraints
  • Relations tussen collections (M2M, O2O, M2O)
  • Image transformation presets
  • Checklist om voortgang bij te houden

De guide bevat precies welke knoppen je moet klikken in de Directus interface. Dat is overzichtelijker dan het via JSON of API imports te doen, en je leert de interface beter kennen.

Wat nog moet gebeuren

Collections aanmaken

Alle collections in Directus UI bouwen volgens de guide. Dit omvat:

  1. Pages
  2. Stoney Cars
  3. Projects
  4. Fonts
  5. Blog Posts
  6. Bordjesman (singleton)
  7. Bordjesman Locations
  8. Trefwoorden
  9. Galleries
  10. Gallery Items

Plus alle relations en image transformations.

Content toevoegen

Test content in Directus voor elke collectie. Minimaal één item per type om de Nuxt frontend mee te kunnen testen.

Nuxt pages bouwen

De frontend bestaat nu alleen uit de default welkomstpagina. Nog te bouwen:

  • Homepage met navigatie naar alle secties
  • Dynamic routes voor Pages, Projects, Fonts, Cars, Blog
  • Gallery component voor slideshow functionaliteit
  • Bordjesman pagina met locations
  • Blog overview en detail pagina's
  • Menu navigatie (header: Logo, Bordjesman, Stoney Cars, Fonts, Projecten / footer: CV, Contact)

Server API voor complexe queries

Voor dingen zoals vorige/volgende navigatie in blog posts (zie mijn eerdere blogpost) moet ik waarschijnlijk server API routes maken in Nuxt, zoals bij desktoptraveller.nl.

Styling

Tailwind CSS toevoegen en een design maken dat alle vijf entiteiten goed tot hun recht laat komen. 16:9 images maken de layout gelukkig al consistent.

SEO

Alle meta tags, Open Graph images, sitemap generatie. De basis staat al in het data model maar moet nog geïmplementeerd in Nuxt.

Reflectie

Het opzetten van de infrastructuur kostte ongeveer anderhalf uur. Het schema bedenken en documenteren nog eens een uur. Drupal zou sneller zijn geweest voor het admin gedeelte (Content Types zijn snel gemaakt), maar de headless aanpak geeft veel meer flexibiliteit voor de frontend.

De scheiding tussen backend en frontend betekent wel dat je beide kanten moet bouwen en onderhouden. Bij Drupal kreeg je een admin interface en basic theming "gratis". Maar de controle die je nu hebt over de frontend - elke pixel, elke API call, elke performance optimalisatie - maakt het de moeite waard.

Morgen verder met het bouwen van de collections. Dan wordt het pas echt interessant om te zien hoe de verschillende entiteiten met elkaar verhouden in de praktijk.


Update: De infrastructuur draait stabiel. Beide sites zijn bereikbaar via HTTPS. Directus admin werkt. Nuxt toont de default pagina. Nu verder met content modeling.