Guides
Set Up Revalidation Webhook

How to Set Up the Revalidation Webhook

Configure automatic on-demand revalidation when content is published in Strapi.

Overview

Instead of waiting for cache expiration, revalidate content immediately when it's published in Strapi. This provides:

  • ✅ Instant content updates
  • ✅ No manual cache clearing
  • ✅ Efficient ISR workflow
  • ✅ Type-safe webhook handling

Step 1: Create Revalidation Route

Create app/api/revalidate/route.ts:

app/api/revalidate/route.ts
import { createStrapiRevalidator } from 'strapi-nextgen-framework';
 
// Create revalidation handler
const handler = createStrapiRevalidator({
  secret: process.env.REVALIDATION_SECRET!,
  tagMap: {
    // Map Strapi content types to cache tags
    'api::article.article': 'articles',
    'api::page.page': 'pages',
    'api::global.global': 'global',
  },
});
 
export const POST = handler;

Step 2: Add Environment Variable

Add to .env.local:

.env.local
REVALIDATION_SECRET=your-random-secret-minimum-32-characters

Generate a secure secret:

openssl rand -base64 32

Step 3: Configure Strapi Webhook

In your Strapi admin panel:

  1. Go to SettingsWebhooks
  2. Click Create new webhook
  3. Configure:
{
  "name": "Next.js Revalidation",
  "url": "https://your-site.com/api/revalidate",
  "headers": {
    "Authorization": "Bearer your-revalidation-secret"
  },
  "events": [
    "entry.create",
    "entry.update",
    "entry.delete",
    "entry.publish",
    "entry.unpublish"
  ]
}

For local development:

URL: http://localhost:3000/api/revalidate

Use ngrok (opens in a new tab) to expose your local server for testing:

ngrok http 3000
# Use the https URL in Strapi webhook

Step 4: Tag Your Data Fetching

Add cache tags to your data fetching:

lib/get-articles.ts
import { unstable_cache } from 'next/cache';
import { strapi } from '@/lib/strapi';
import { GetArticlesDocument } from '@/graphql/generated';
 
export const getArticles = unstable_cache(
  async () => {
    const data = await strapi.getCollection('articles', GetArticlesDocument);
    return data.articles?.data;
  },
  ['articles-list'],
  {
    tags: ['articles'], // 👈 This tag matches tagMap
    revalidate: 3600,   // Fallback: revalidate hourly
  }
);

How It Works

1. Content Published in Strapi

Editor clicks "Publish" on an article.

2. Webhook Triggered

Strapi sends POST to /api/revalidate:

{
  "event": "entry.publish",
  "model": "api::article.article",
  "entry": {
    "id": 1,
    "slug": "my-article",
    // ... other fields
  }
}

3. Revalidation Handler

The handler:

  1. ✅ Validates the secret from Authorization header
  2. ✅ Extracts the model from webhook payload
  3. ✅ Looks up cache tag from tagMap
  4. ✅ Calls revalidateTag('articles')
  5. ✅ Returns 200 OK

4. Next.js Revalidation

Next.js:

  1. ✅ Invalidates all cache entries with tag articles
  2. ✅ Re-fetches data on next request
  3. ✅ Updates static pages in background

Tag Map Configuration

Basic Mapping

Map Strapi model to cache tag:

tagMap: {
  'api::article.article': 'articles',
  'api::page.page': 'pages',
}

Multiple Tags

Revalidate multiple tags:

tagMap: {
  'api::article.article': ['articles', 'blog', 'feed'],
  'api::global.global': ['global', 'header', 'footer'],
}

Auto-Generation

Omit tagMap for automatic tag generation:

const handler = createStrapiRevalidator({
  secret: process.env.REVALIDATION_SECRET!,
  // No tagMap - automatically generates 'articles', 'pages', etc.
});

This generates tags like:

  • api::article.articlearticle
  • api::page.pagepage
  • api::global.globalglobal

Advanced Patterns

Conditional Revalidation

Revalidate based on entry data:

app/api/revalidate/route.ts
import { NextRequest } from 'next/server';
import { revalidateTag } from 'next/cache';
 
export async function POST(request: NextRequest) {
  // Validate secret
  const authHeader = request.headers.get('authorization');
  const secret = authHeader?.replace('Bearer ', '');
  
  if (secret !== process.env.REVALIDATION_SECRET) {
    return new Response('Unauthorized', { status: 401 });
  }
 
  // Parse webhook payload
  const payload = await request.json();
  const { model, entry, event } = payload;
 
  // Only revalidate on publish
  if (event !== 'entry.publish') {
    return new Response('Skipped', { status: 200 });
  }
 
  // Revalidate specific article page
  if (model === 'api::article.article') {
    revalidateTag('articles');
    revalidateTag(`article-${entry.slug}`);
  }
 
  return new Response('Revalidated', { status: 200 });
}

Path Revalidation

Revalidate specific paths instead of tags:

import { revalidatePath } from 'next/cache';
 
// Revalidate homepage
revalidatePath('/');
 
// Revalidate specific article
revalidatePath(`/blog/${entry.slug}`);
 
// Revalidate all blog pages
revalidatePath('/blog', 'layout');

Logging & Monitoring

Add logging for debugging:

const handler = createStrapiRevalidator({
  secret: process.env.REVALIDATION_SECRET!,
  tagMap: {
    'api::article.article': 'articles',
  },
  onRevalidate: (tag) => {
    console.log(`✅ Revalidated cache tag: ${tag}`);
  },
  onError: (error) => {
    console.error('❌ Revalidation error:', error);
  },
});

Webhook Verification

Add additional security:

import crypto from 'crypto';
 
export async function POST(request: NextRequest) {
  // Verify webhook signature (if Strapi supports it)
  const signature = request.headers.get('x-strapi-signature');
  const rawBody = await request.text();
  
  const expectedSignature = crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET!)
    .update(rawBody)
    .digest('hex');
  
  if (signature !== expectedSignature) {
    return new Response('Invalid signature', { status: 401 });
  }
 
  // Process webhook...
}

Testing Revalidation

Manual Test

curl -X POST http://localhost:3000/api/revalidate \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer your-secret" \
  -d '{
    "event": "entry.publish",
    "model": "api::article.article",
    "entry": {
      "id": 1,
      "slug": "test-article"
    }
  }'

Expected response:

{
  "revalidated": true,
  "tag": "articles"
}

Test in Strapi

  1. Go to SettingsWebhooks
  2. Find your webhook
  3. Click Trigger
  4. Check Next.js logs for revalidation

Automated Test

e2e/revalidation.spec.ts
import { test, expect } from '@playwright/test';
 
test('revalidation updates content', async ({ page, request }) => {
  // Visit page and get initial content
  await page.goto('/blog/test-article');
  const initialTitle = await page.locator('h1').textContent();
 
  // Update content in Strapi (via API)
  await request.put('http://localhost:1337/api/articles/1', {
    data: {
      title: 'Updated Title',
    },
  });
 
  // Trigger revalidation
  await request.post('http://localhost:3000/api/revalidate', {
    headers: {
      'Authorization': `Bearer ${process.env.REVALIDATION_SECRET}`,
      'Content-Type': 'application/json',
    },
    data: {
      event: 'entry.update',
      model: 'api::article.article',
      entry: { slug: 'test-article' },
    },
  });
 
  // Reload page and verify update
  await page.reload();
  const updatedTitle = await page.locator('h1').textContent();
  
  expect(updatedTitle).not.toBe(initialTitle);
  expect(updatedTitle).toBe('Updated Title');
});

Troubleshooting

Webhook Not Triggering

Problem: Content published but cache not revalidated

Solutions:

  1. Check webhook is enabled in Strapi
  2. Verify URL is correct (use ngrok for local)
  3. Check Strapi webhook logs for errors
  4. Ensure events are selected in webhook config

401 Unauthorized

Problem: Webhook returns 401

Solutions:

  1. Check Authorization header format: Bearer YOUR_SECRET
  2. Verify secret matches in .env.local
  3. Ensure no trailing spaces in secret
  4. Check request headers in Strapi webhook config

Cache Not Invalidating

Problem: Webhook succeeds but content doesn't update

Solutions:

  1. Verify cache tag matches in tagMap and unstable_cache
  2. Check you're using tags, not just time-based revalidation
  3. Clear .next folder: rm -rf .next
  4. Ensure you're in production mode: npm run build && npm start

Too Many Revalidations

Problem: Webhook floods with requests

Solutions:

  1. Filter events - only use entry.publish
  2. Add rate limiting to webhook route
  3. Use conditional logic to skip unnecessary revalidations
  4. Consider debouncing multiple updates

Best Practices

1. Secure Your Webhook

Always use strong secrets:

# Generate 256-bit secret
openssl rand -base64 32

2. Use Specific Tags

Tag granularly for targeted revalidation:

// ❌ Too broad
revalidateTag('content');
 
// ✅ Specific
revalidateTag('articles');
revalidateTag(`article-${slug}`);

3. Handle Errors Gracefully

Don't let revalidation errors crash your site:

try {
  revalidateTag('articles');
} catch (error) {
  console.error('Revalidation failed:', error);
  // Site continues working with cached data
}

4. Monitor Webhook Activity

Log all revalidations:

console.log({
  timestamp: new Date().toISOString(),
  event: payload.event,
  model: payload.model,
  tag: resolvedTag,
});

5. Use HTTPS in Production

Never use HTTP webhooks in production:

❌ http://your-site.com/api/revalidate
✅ https://your-site.com/api/revalidate

Next Steps

See Also


GPL-3.0 2025 © fuqom.