"Headless Drupal" sounds straightforward — Drupal manages content, Next.js renders it. In practice there are a handful of sharp edges that nobody writes about until they've already cut themselves. Here are the ones that caught me.
CORS on the Drupal side
By default Drupal's JSON:API will block cross-origin requests. You need to configure the cors.config in services.yml explicitly. Don't just set allowedOrigins: ['*'] on production — scope it to your frontend domain.
# web/sites/default/services.yml
cors.config:
enabled: true
allowedHeaders: ['Content-Type', 'Authorization']
allowedMethods: ['GET', 'POST', 'PATCH', 'DELETE']
allowedOrigins: ['https://your-frontend.vercel.app']
exposedHeaders: false
maxAge: false
supportsCredentials: falseLocal development: Add http://localhost:3000 to allowedOrigins in your dev services.local.yml. Never commit it to your main services.yml.
Preview mode
Drupal's JSON:API doesn't expose unpublished content to anonymous requests. For editorial preview you need the jsonapi_extras module plus a custom Next.js preview route that authenticates against Drupal's OAuth token endpoint before fetching draft content.
Caching gotchas
Next.js ISR and Drupal's internal page cache can fight each other. The symptom is stale content on the frontend even after a Drupal save. The fix is a cache tag purge webhook — Drupal sends a purge request to your Next.js revalidation endpoint on node save. Simple, reliable, worth the 30 minutes to set up.
// Next.js API route: /api/revalidate
export async function POST(request: Request) {
const { tag, secret } = await request.json();
if (secret !== process.env.REVALIDATE_SECRET) {
return Response.json({ error: 'Invalid secret' }, { status: 401 });
}
revalidateTag(tag);
return Response.json({ revalidated: true });
}Image handling
Drupal serves images at their original URL. Next.js Image component needs the domain explicitly whitelisted in next.config.ts. Also set up an image style in Drupal at the exact dimensions your frontend needs — fetching full-resolution originals and resizing client-side is wasteful.