Files

178 lines
5.9 KiB
TypeScript

import { safeTry } from '@session/util-js/try';
import { parseBody } from 'next-sanity/webhook';
import { revalidatePath, revalidateTag } from 'next/cache';
import { type NextRequest, NextResponse } from 'next/server';
import logger from '../lib/logger';
import type { PageSchemaType } from '../schemas/page';
import type { PostSchemaType } from '../schemas/post';
type RssGeneratorConfig = {
/** The CMS content type that the generator should be run for.*/
_type: string;
/**
* The function that generates the RSS feed. This function should return a
* Promise that resolves when the feed has been generated.
*/
generateRssFeed: () => Promise<void>;
};
type CreateRevalidateHandlerOptions = {
/** The secret used to verify the webhook request. */
revalidateSecret: string;
schemaUrls: Record<string, string>;
/** An array of RSS generator configurations. {@link RssGeneratorConfig} */
rssGenerators?: Array<RssGeneratorConfig>;
};
/**
* Creates a revalidate handler for Sanity CMS content.
*
* @param revalidateSecret - The secret used to verify the webhook request.
* @param client - The Sanity client.
* @param rssGenerators - An array of RSS generator configurations. {@link RssGeneratorConfig}
* @param schemaUrls - The schema URLs for the CMS content types.
*
* @throws {TypeError} If the `revalidateSecret` is not provided.
* @throws {TypeError} If the `rssGenerators` is not an array.
* @throws {TypeError} If any of the `rssGenerators` have a `_type` that is not a non-empty string.
* @throws {TypeError} If any of the `rssGenerators` have a `generateRssFeed` that is not a function.
* @returns The revalidate handler.
*
* @example
* export const { POST } = createRevalidateHandler({
* revalidateSecret: REVALIDATE_SECRET,
* rssGenerators: [
* {
* _type: 'post',
* generateRssFeed: async () => {
* // Generate the RSS feed
* },
* },
* ],
* });
*/
export const createRevalidateHandler = ({
revalidateSecret,
schemaUrls,
rssGenerators,
}: CreateRevalidateHandlerOptions) => {
if (!revalidateSecret) {
throw new TypeError('Revalidate secret is required to create a revalidate handler');
}
const rssGeneratorsMap: Map<string, () => Promise<void>> = new Map();
if (rssGenerators) {
if (!Array.isArray(rssGenerators)) {
throw new TypeError('rssGenerators must be an array');
}
if (rssGenerators.length > 0) {
for (const { _type, generateRssFeed } of rssGenerators) {
if (typeof _type !== 'string' || _type.length === 0) {
throw new TypeError('_type must be a non-empty string');
}
if (typeof generateRssFeed !== 'function') {
throw new TypeError('generateRssFeed must be a function');
}
rssGeneratorsMap.set(_type, generateRssFeed);
}
logger.info(`Creating revalidate handler with rss generators: ${rssGeneratorsMap.keys()}`);
}
}
/**
* Revalidate the cache for a specific CMS resource
*
* This endpoint is used to revalidate the cache for a specific CMS resource.
* It is called by the CMS when a resource is published or updated. The CMS
* sends a POST request to this endpoint with a JSON body containing the type
* of the resource and the ID of the resource. The endpoint then revalidates
* the cache for the resource and returns a JSON response with the status of the revalidation.
*
* @param req - The incoming request with a JSON body containing the type and ID of the resource
* and a signature to verify the request
* @returns The result of the revalidation as a JSON response
*/
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Complexity out of necessity
const revalidateHandler = async (req: NextRequest) => {
try {
if (!revalidateSecret) {
return new NextResponse('Missing revalidate secret', {
status: 500,
});
}
const { isValidSignature, body } = await parseBody<PageSchemaType | PostSchemaType>(
req,
revalidateSecret
);
if (!isValidSignature) {
return new NextResponse(
JSON.stringify({ message: 'Invalid signature', isValidSignature, body }),
{ status: 401 }
);
}
const type = body?._type;
if (!type) {
return new NextResponse(JSON.stringify({ message: 'Bad Request', body }), { status: 400 });
}
// If the `_type` is `post`, then all `client.fetch` calls with
// `{next: {tags: ['post']}}` will be revalidated
revalidateTag(type);
if (type === 'post' || type === 'page') {
const slug = body.slug.current;
if (slug.length > 0) {
revalidateTag(slug);
let schemaUrl = schemaUrls[type];
if (schemaUrl) {
if (!schemaUrl.startsWith('/')) {
schemaUrl = `/${schemaUrl}`;
}
if (!schemaUrl.endsWith('/')) {
schemaUrl = `${schemaUrl}/`;
}
revalidatePath(`${schemaUrl}`);
revalidatePath(`${schemaUrl}${slug}`);
}
}
}
/**
* If there are any RSS generators configured, then we revalidate them
* here. This is useful for when you want to generate RSS feeds for
* specific content types.
*/
if (rssGeneratorsMap.size > 0 && rssGeneratorsMap.has(body._type)) {
const generator = rssGeneratorsMap.get(body._type);
if (generator) {
const [err] = await safeTry(generator());
if (err) console.error(err);
} else {
console.error(`No generator found for type ${body._type}`);
}
}
return NextResponse.json({
status: 200,
revalidated: true,
now: Date.now(),
body,
});
} catch (err) {
logger.error(err);
return new NextResponse(err instanceof Error ? err.message : 'Internal Server Error', {
status: 500,
});
}
};
return { POST: revalidateHandler };
};