SproetS website van Drupal naar Directus
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.servicenuxt-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:
- Pages
- Stoney Cars
- Projects
- Fonts
- Blog Posts
- Bordjesman (singleton)
- Bordjesman Locations
- Trefwoorden
- Galleries
- 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.