Next.js met Headless Drupal: Cache invalidatie met Drupal Cache Tags
Bij het bouwen van de nieuwe website voor Wageningen Universiteit & Research stond ik voor een veelvoorkomende uitdaging: hoe combineer je de snelheid van Next.js met de flexibiliteit van een headless Drupal CMS, zonder dat editors urenlang moeten wachten op cache invalidatie?
Het antwoord ligt in een combinatie van Drupal Cache Tags en Next.js On-Demand Revalidation. In dit artikel deel ik hoe we dit hebben opgezet en welke valkuilen je moet vermijden.
Het probleem: Te veel cache, te weinig flexibiliteit
Wanneer je een Next.js site bouwt met Drupal als headless CMS, wil je eigenlijk twee dingen tegelijk:
- Snelle pagina's voor bezoekers (dankzij Next.js static site generation en caching)
- Directe updates als een editor content aanpast in Drupal
Het probleem is dat deze twee doelen haaks op elkaar staan. Next.js cached graag alles om performance te maximaliseren, terwijl editors verwachten dat hun wijzigingen direct zichtbaar zijn.
De meeste teams kiezen voor een van deze oplossingen:
- Revalidate elke X seconden → inefficiënt en nog steeds vertraging
- Alles server-side renderen → langzame pagina's
- Handmatig cache clearen → frustrerende workflow voor editors
Vaak zien we dat er wel wordt gekozen voor Incremental Static Regeneration (ISR) maar dat revalidate times dan vrij kort worden gezet om te voorkomen dat content updates laat (later) zichtbaar zijn. Geen van deze opties was acceptabel voor een platform dat tienduizenden studenten en onderzoekers moet bedienen.
De oplossing: Drupal Cache Tags + Next.js Revalidation
Drupal heeft een krachtig cache tag systeem dat perfect aansluit bij wat we nodig hebben. Elk content item in Drupal krijgt automatisch cache tags mee, zoals:
node:123taxonomy_term:45
user:1
Wanneer content wijzigt, invalideert Drupal automatisch alle cache entries met die specifieke tags. Op deze manier kunnen we de pagina's in Next.js in theorie weken cachen. Wij gebruiken zelf 1 uur. Mocht het revalideren dan toch misgaan, zitten we niet al te lang met oude data. Op zich een simpel concept toch?
Architectuur overzicht
Hier is hoe de flow werkt:
- Editor past een artikel aan in Drupal
- Drupal stuurt een webhook naar Next.js met de relevante cache tags
- Next.js revalideert alleen de pagina's die die specifieke tags gebruiken
- Binnen enkele seconden is de update live
Dit betekent dat we het beste van twee werelden krijgen: snelle, gecachte pagina's voor 99,9% van de requests, en directe updates wanneer content wijzigt.
De uitdaging: Drupal cache tags vs Next.js cache tags
Het eerste idee zou zijn om Drupal's cache tags direct te gebruiken als Next.js cache tags. Het probleem is dat één Drupal cache tag (bijvoorbeeld node:123) gebruikt kan worden door tientallen verschillende GraphQL queries op verschillende pagina's.
De uitdaging zit hem in het feit dat je als front-end nog niet weet welke Drupal entities je terug krijgt op het moment van de fetch. Hoe zorg je er dan voor dat je de fetch voorziet van de juiste tags zodat je deze later kan invalideren als de Drupal cache tags die erbij horen invalideren?
Onze oplossing: Een mapping layer tussen Drupal cache tags en Next.js cache tags.
Implementatie: Cache tag mapping
De kern van onze aanpak is dat elke GraphQL query een unieke ID krijgt, en we opslaan welke Drupal cache tags bij die query horen. In ons geval sturen we de Drupal cache tags mee in de header van de response. Dit kan mogelijk ook op andere manieren.
// Generate an unique ID based on the query + variables
const queryId = generateQueryId(query, variables);
const queryString = queryToString(query);
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: queryString,
variables,
}),
cache: 'force-cache',
next: {
// Tag this request with queryId
tags: [queryId],
},
});
const data = await response.json();
if (response.headers) {
// Parse and save tags
const cacheTagsHeader = response.headers.get('X-Cache-Tags');
const cacheTags = parseCacheTags(cacheTagsHeader);
if (cacheTags.length > 0) {
// Save associates in database (non-blocking)
// Don't await to not delay the GraphQL response
storeQueryCacheTags(queryId, cacheTags).catch((error) => {
console.error('Failed to store query cache tags:', error);
});
}
}
return data;De generateQueryId functie maakt een hash van de query en variables. Dit betekent dat dezelfde query met dezelfde parameters altijd dezelfde ID krijgt, maar verschillende queries (of dezelfde query met andere parameters) krijgen unieke IDs.
Cache tags opslaan in Redis
Voor het opslaan van de mapping tussen query IDs en Drupal cache tags gebruik ik Redis. Je zou hiervoor ook PostgreSQL, MySQL of een andere database kunnen gebruiken - het gaat om de relatie die je opslaat.
export async function storeQueryCacheTags(queryId: string, cacheTags: string[]) {
// Save mapping: queryId -> [cache tags]
await redis.set(
`query:${queryId}:tags`,
JSON.stringify(cacheTags),
'EX',
60 * 60 * 24 * 7 // 7 days TTL
);
// Save reversed mappings: cache tag -> [queryIds]
// This creates efficient lookups for webhooks
for (const tag of cacheTags) {
await redis.sadd(`tag:${tag}:queries`, queryId);
await redis.expire(`tag:${tag}:queries`, 60 * 60 * 24 * 7);
}
}We slaan twee mappings op:
query:ABC123:tags→["node:123", "taxonomy_term:45"]tag:node:123:queries→["ABC123", "DEF456", "GHI789"]
De tweede mapping maakt het mogelijk om snel op te zoeken: "Welke queries gebruiken deze Drupal cache tag?"
Webhook endpoint voor revalidation
Wanneer Drupal content wijzigt, stuurt het een webhook met de relevante cache tags. Ons Next.js endpoint gebruikt die tags om de juiste queries te revalideren.
async function handler(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const path = searchParams.get('path');
const tags = searchParams.get('tags'); // Can be comma-separated list of Drupal cache tags
const secret = searchParams.get('secret');
// Validate secret.
if (secret !== process.env.DRUPAL_REVALIDATE_SECRET) {
return new Response('Invalid secret.', { status: 401 });
}
const revalidated = {
tags: null as RevalidateResult,
path: null as RevalidateResult,
queryIds: [] as string[],
};
if (!path && !tags) {
return Response.json(
{
revalidated: false,
now: Date.now(),
message: 'Missing path or tags to revalidate',
},
{ status: 400 }
);
}
if (tags) {
// Support both single tag and comma-separated tags
const cacheTags = tags.split(',');
// Query database to find all queryIds associated with these cache tags
const queryIds = await getQueryIdsByCacheTags(cacheTags);
// Revalidate each queryId (which is used as a Next.js cache tag)
for (const queryId of queryIds) {
revalidateTag(queryId);
}
await deleteCacheTagsByQueryIds(queryIds)
revalidated.tags = { revalidated: true, value: tags };
revalidated.queryIds = queryIds;
}
if (path) {
revalidatePath(path);
revalidated.path = { revalidated: true, value: path };
}
return Response.json({
revalidated: true,
details: revalidated,
now: Date.now(),
});
}
De lookup functie haalt de queryIds op uit Redis, waarna we alle tags revalideren die daarbij horen. Na het revalideren van de tags verwijderen we de mappings weer uit Redis. Via deze route kan je ook een heel pad revalideren mocht dit nodig zijn.
Drupal configuratie: Webhooks instellen
Aan de Drupal kant moet je webhooks configureren die bij content updates getriggerd worden. Drupal heeft hiervoor modules of je kunt een custom module schrijven die bij node updates een HTTP request stuurt naar je Next.js endpoint.
De belangrijkste requirement is dat Drupal de relevante cache tags meestuurt in de webhook payload. Drupal's cache tag systeem verzamelt automatisch alle relevante tags (de node zelf, referenced taxonomy terms, media items, etc.), dus je hoeft dit niet handmatig te bepalen.
In de webhook configuratie stel je in:
- Trigger: Node update/create/delete
- Endpoint:
https://jouw-nextjs-site.nl/api/revalidate - Payload: Cache tags van de gewijzigde entity
- Authentication: Secret token voor beveiliging
Waarom deze aanpak werkt
Deze mapping layer lost een aantal belangrijke problemen op:
1. Precisie Je revalideert alleen de queries die écht geraakt worden door de content wijziging. Als een artikel wordt aangepast, revalideren alleen de queries die dat specifieke artikel ophalen - niet elke query die ooit een artikel ophaalt.
2. Schaalbaarheid Zelfs met duizenden pagina's blijft het systeem snel, omdat je precies weet welke queries opnieuw moeten draaien. Geen onnodige rebuilds.
3. Debugging Door de mapping in Redis op te slaan, kun je exact zien welke queries welke Drupal content gebruiken. Dit is ontzettend waardevol bij het debuggen van cache issues.
4. Flexibiliteit Als je later wilt optimaliseren (bijvoorbeeld: batch revalidations, of prioriteit geven aan bepaalde queries), heb je alle informatie die je nodig hebt in Redis.
Te brede cache tags
Een veelvoorkomende fout is het gebruik van te brede cache tags aan de Drupal kant. Als Drupal bijvoorbeeld node_list als tag meestuurt bij elke node update, zouden alle queries die een node list ophalen gerevalideerd worden - zelfs als die specifieke node niet in die list staat.
Betere aanpak:
- Zorg dat Drupal specifieke tags meegeeft:
node:123in plaats vannode_list - Voor overzichtspaginas: gebruik taxonomy tags of custom tags die de scope beperken
- Test je Drupal setup: welke tags worden er exact meegegeven bij verschillende content updates?
Redis cache groei
Omdat we voor elke unieke query een mapping opslaan, kan je Redis database groeien als je veel verschillende queries hebt. Een paar strategieën om dit te beheren:
1. TTL instellen Zet een time-to-live van bijvoorbeeld 7 dagen op de mappings. Als een query langer dan een week niet gebruikt wordt, is de kans groot dat de pagina niet vaak bezocht wordt en kun je de mapping laten vervallen.
2. Cleanup job Draai periodiek een job die oude, ongebruikte mappings opruimt:
3. Monitoring Houd Redis memory usage in de gaten. Bij Wageningen University met enkele duizenden pagina's en honderden unieke queries blijft de Redis database onder de 100MB - verwaarloosbaar voor een productie environment.
Horizontal scaling en cache
Bij het schalen van Next.js over meerdere servers ontstaat er een cache synchronisatie probleem. Elke server heeft zijn eigen cache, en een revalidation request raakt maar één server.
De oplossing is het gebruik van een custom cache handler die een gedeelde cache gebruikt in plaats van de lokale in-memory cache van Next.js.
Zo delen alle Next.js instances dezelfde Redis cache, en werkt revalidation consistent over alle servers.
Resultaten
Na implementatie van dit systeem:
- Editors zien wijzigingen binnen 2-5 seconden
- 99%+ van de requests worden uit cache geserveerd (sub-100ms response times)
- Geen onnodige rebuilds (alleen affected queries worden gerevalideerd)
- Schaalbaar tot duizenden pagina's dankzij de query mapping
- Minder load op Drupal
Bij Wageningen University hebben we grote aantallen pagina's en honderden unieke GraphQL queries. De Redis database blijft onder de 100MB, en revalidations gebeuren in gemiddeld 150-300ms.
Conclusie
Het combineren van Next.js met een headless Drupal CMS hoeft geen compromis te zijn tussen snelheid en gebruiksvriendelijkheid. Door een mapping layer te bouwen tussen Drupal's cache tags en Next.js query IDs, krijg je het beste van beide werelden: snelle pagina's voor gebruikers en een prettige workflow voor editors.
De implementatie vereist wat extra setup (Redis, webhook configuratie), maar de payoff is enorm. Voor enterprise projecten waar performance én content workflows kritiek zijn, is deze aanpak een no-brainer.
Het mooie is dat deze architectuur schaalbaar is: of je nu 100 pagina's hebt of 10.000, het systeem blijft efficiënt omdat je alleen revalideert wat echt nodig is.
Deze aanpak is bijzonder geschikt voor enterprise headless CMS implementaties, grote Next.js en Drupal-omgevingen, multi-language websites en platformen met complexe GraphQL queries.
Vragen of opmerkingen? Ik help graag bij het opzetten van een vergelijkbare architectuur voor jouw project. Neem contact met me op.
Tags: Next.js, Drupal, Headless CMS, Cache Optimization, On-Demand Revalidation, GraphQL, Enterprise Web Development, Redis