feat: add /api/revalidate webhook for on-demand ISR
POST with x-revalidate-secret header and { tag } body calls
revalidateTag to purge a collection from the Next.js data cache.
Guarded by REVALIDATE_SECRET env var.
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
import { revalidateTag } from 'next/cache';
|
||||
import { type NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
/**
|
||||
* POST /api/revalidate
|
||||
*
|
||||
* Webhook endpoint for on-demand ISR. PocketBase (or any external
|
||||
* caller) sends this request after mutating CMS content so the
|
||||
* relevant tag is purged from the Next.js data cache.
|
||||
*
|
||||
* Expected body: `{ "tag": "<collection-name>" }`
|
||||
* Required header: `x-revalidate-secret: <REVALIDATE_SECRET>`
|
||||
*/
|
||||
export async function POST(request: NextRequest): Promise<NextResponse> {
|
||||
const secret = request.headers.get('x-revalidate-secret');
|
||||
|
||||
if (secret !== process.env.REVALIDATE_SECRET) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
let body: unknown;
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (
|
||||
typeof body !== 'object' ||
|
||||
body === null ||
|
||||
!('tag' in body) ||
|
||||
typeof (body as Record<string, unknown>).tag !== 'string'
|
||||
) {
|
||||
return NextResponse.json({ error: 'Missing or invalid "tag" field' }, { status: 400 });
|
||||
}
|
||||
|
||||
const tag = (body as { tag: string }).tag;
|
||||
|
||||
/* Second arg is required by the Next.js 15 type signature;
|
||||
* "max" means the purge propagates indefinitely — correct for
|
||||
* an on-demand webhook that has no TTL of its own. */
|
||||
revalidateTag(tag, 'max');
|
||||
|
||||
return NextResponse.json({ revalidated: true, tag }, { status: 200 });
|
||||
}
|
||||
Reference in New Issue
Block a user