diff --git a/index.d.ts b/index.d.ts index cbba205..cb98160 100644 --- a/index.d.ts +++ b/index.d.ts @@ -8,6 +8,8 @@ declare global { NEXT_PUBLIC_GITHUB_REPO_LINK: string; NEXT_PUBLIC_POST_LIST_PICTURE_FALLBACK: string; NEXT_PUBLIC_HEADER_PICTURE_FALLBACK: string; + + REVALIDATE_TOKEN: string; } } } diff --git a/lib/dtos/index.ts b/lib/dtos/index.ts index 86dcf26..30b17fc 100644 --- a/lib/dtos/index.ts +++ b/lib/dtos/index.ts @@ -1,3 +1,4 @@ import 'reflect-metadata'; export * from './getPostLists'; +export * from './revalidate'; diff --git a/lib/dtos/revalidate.ts b/lib/dtos/revalidate.ts new file mode 100644 index 0000000..6ca084b --- /dev/null +++ b/lib/dtos/revalidate.ts @@ -0,0 +1,14 @@ +import { Type } from 'class-transformer'; +import { ArrayNotEmpty, IsNotEmpty } from 'class-validator'; + +export class RevalidateHeaderDto { + @IsNotEmpty() + authorization!: string; +} + +export class RevalidateBodyDto { + @IsNotEmpty() + @ArrayNotEmpty() + @Type(() => String) + paths!: string[]; +} diff --git a/pages/api/revalidate.ts b/pages/api/revalidate.ts new file mode 100644 index 0000000..8d439bf --- /dev/null +++ b/pages/api/revalidate.ts @@ -0,0 +1,88 @@ +import type { NextApiHandler } from 'next'; +import { plainToInstance } from 'class-transformer'; +import { validateOrReject, ValidationError } from 'class-validator'; +import * as fs from 'fs'; +import * as path from 'path'; + +import { RevalidateHeaderDto, RevalidateBodyDto } from 'lib/dtos'; +import type { BaseResponse } from 'lib/interfaces'; +import { BlogPostPath, getPostContent } from 'lib'; + +// TODO: now using unstable api +const handler: NextApiHandler = async (req, res) => { + if (req.method === 'POST') { + const headers = plainToInstance(RevalidateHeaderDto, req.headers); + const body = plainToInstance(RevalidateBodyDto, req.body); + + try { + await validateOrReject(headers); + await validateOrReject(body); + + if (headers.authorization !== process.env.REVALIDATE_TOKEN) { + res.status(401).json({ + statusCode: 401, + message: 'Unauthorized', + }); + } else { + let postRevalidate = false; + + const revalidatePaths = body.paths.filter((item) => { + if (item === '/friend') return true; + if (item === '/') return true; + if (item === '/archive') return true; + if (item.endsWith('.md')) { + if (fs.existsSync(path.join(BlogPostPath, path.basename(item)))) { + postRevalidate = true; + return true; + } + } + return false; + }); + console.log(`Starting revalidate paths ${revalidatePaths.join(', ')}`); + await Promise.all( + revalidatePaths.map(async (item) => { + if (item.endsWith('.md')) { + await res.unstable_revalidate( + `/post/${path.basename(item.slice(0, -3))}` + ); + const post = getPostContent(item); + if (post?.categories) { + await Promise.all( + post.categories.map(async (category) => { + await res.unstable_revalidate(`/tag/${category}`); + }) + ); + } + } else { + await res.unstable_revalidate(`${item}`); + } + }) + ); + if (postRevalidate) { + await res.unstable_revalidate('/'); + await res.unstable_revalidate('/archive'); + } + console.log('Revalidate success'); + res.status(204).send(undefined); + } + } catch (err) { + console.error('Revalidate failed'); + console.error(err); + if (err instanceof ValidationError) { + res.status(400).json({ + statusCode: 400, + message: 'Bad Request', + }); + } else { + res.status(500).json({ + statusCode: 500, + message: 'Revalidate Failed', + }); + } + } + } else { + res.status(405).setHeader('Allow', 'POST').send(undefined); + } +}; + +export default handler;