From 9d9fdcd9fcaaaee5e2ddac4d2b0e3435ebd03845 Mon Sep 17 00:00:00 2001 From: Charis <26616127+charislam@users.noreply.github.com> Date: Thu, 24 Apr 2025 14:59:38 -0400 Subject: [PATCH] feat (docs): on-demand revalidation for graphql and wrappers (#35177) Before: Revalidation was turned off entirely for wrappers pages, and done daily for GraphQL pages. After: Revalidation is via tag. There is an /api/revalidate endpoint that is used by GitHub Actions on the federated repos to trigger the revalidation. This decreases the number of unnecessary page generations by Vercel, and gives the maintainers of federated repos more fine-grained control over when to reploy their docs (see the docs deployment playbook for more info). --- apps/docs/app/api/revalidate/route.test.ts | 19 +++++++++++-------- apps/docs/app/api/revalidate/route.ts | 10 ++++------ .../extensions/wrappers/[[...slug]]/page.tsx | 6 ++++-- .../app/guides/graphql/[[...slug]]/page.tsx | 7 ++++--- apps/docs/features/helpers.fetch.ts | 11 +++++++++++ 5 files changed, 34 insertions(+), 19 deletions(-) diff --git a/apps/docs/app/api/revalidate/route.test.ts b/apps/docs/app/api/revalidate/route.test.ts index befeabd43e..66a30304d4 100644 --- a/apps/docs/app/api/revalidate/route.test.ts +++ b/apps/docs/app/api/revalidate/route.test.ts @@ -40,6 +40,9 @@ describe('_handleRevalidateRequest', () => { process.env.NEXT_PUBLIC_SUPABASE_URL = 'http://localhost:3000' process.env.SUPABASE_SECRET_KEY = 'secret_key' + // Silence intentional console errors for cleaner test output + vi.spyOn(console, 'error').mockImplementation(() => {}) + // Mock current date mockDate = new Date('2023-01-01T12:00:00Z') vi.setSystemTime(mockDate) @@ -109,7 +112,7 @@ describe('_handleRevalidateRequest', () => { headers: { Authorization: 'Bearer basic_key', }, - body: JSON.stringify({ tags: ['tag1', 'tag2'] }), + body: JSON.stringify({ tags: ['graphql', 'wrappers'] }), }) vi.mocked(headers).mockReturnValue(new Headers(request.headers)) @@ -119,8 +122,8 @@ describe('_handleRevalidateRequest', () => { const response = await _handleRevalidateRequest(request) expect(response.status).toBe(204) expect(revalidateTag).toHaveBeenCalledTimes(2) - expect(revalidateTag).toHaveBeenCalledWith('tag1') - expect(revalidateTag).toHaveBeenCalledWith('tag2') + expect(revalidateTag).toHaveBeenCalledWith('graphql') + expect(revalidateTag).toHaveBeenCalledWith('wrappers') }) it('should return 429 if last revalidation was less than 6 hours ago with basic permissions', async () => { @@ -129,7 +132,7 @@ describe('_handleRevalidateRequest', () => { headers: { Authorization: 'Bearer basic_key', }, - body: JSON.stringify({ tags: ['tag1'] }), + body: JSON.stringify({ tags: ['graphql'] }), }) vi.mocked(headers).mockReturnValue(new Headers(request.headers)) @@ -150,7 +153,7 @@ describe('_handleRevalidateRequest', () => { headers: { Authorization: 'Bearer basic_key', }, - body: JSON.stringify({ tags: ['tag1'] }), + body: JSON.stringify({ tags: ['graphql'] }), }) vi.mocked(headers).mockReturnValue(new Headers(request.headers)) @@ -162,7 +165,7 @@ describe('_handleRevalidateRequest', () => { const response = await _handleRevalidateRequest(request) expect(response.status).toBe(204) - expect(revalidateTag).toHaveBeenCalledWith('tag1') + expect(revalidateTag).toHaveBeenCalledWith('graphql') }) it('should revalidate regardless of last revalidation time with override permissions', async () => { @@ -171,7 +174,7 @@ describe('_handleRevalidateRequest', () => { headers: { Authorization: 'Bearer override_key', }, - body: JSON.stringify({ tags: ['tag1'] }), + body: JSON.stringify({ tags: ['graphql'] }), }) vi.mocked(headers).mockReturnValue(new Headers(request.headers)) @@ -183,6 +186,6 @@ describe('_handleRevalidateRequest', () => { const response = await _handleRevalidateRequest(request) expect(response.status).toBe(204) - expect(revalidateTag).toHaveBeenCalledWith('tag1') + expect(revalidateTag).toHaveBeenCalledWith('graphql') }) }) diff --git a/apps/docs/app/api/revalidate/route.ts b/apps/docs/app/api/revalidate/route.ts index 1c751fb651..86b033e476 100644 --- a/apps/docs/app/api/revalidate/route.ts +++ b/apps/docs/app/api/revalidate/route.ts @@ -1,10 +1,10 @@ import { createClient } from '@supabase/supabase-js' +import { type Database } from 'common' import { revalidateTag } from 'next/cache' import { headers } from 'next/headers' import { type NextRequest } from 'next/server' import { z } from 'zod' - -import { type Database } from 'common' +import { VALID_REVALIDATION_TAGS } from '~/features/helpers.fetch' enum AuthorizationLevel { Unauthorized, @@ -13,7 +13,7 @@ enum AuthorizationLevel { } const requestBodySchema = z.object({ - tags: z.array(z.string()), + tags: z.array(z.enum(VALID_REVALIDATION_TAGS)), }) export const POST = handleError(_handleRevalidateRequest) @@ -35,14 +35,12 @@ export async function _handleRevalidateRequest(request: NextRequest) { } let authorizationLevel = AuthorizationLevel.Unauthorized - - const token = authorization.replace(/^Bearer\s+/, '') + const token = authorization.replace(/^Bearer /, '') if (overrideKeys.includes(token)) { authorizationLevel = AuthorizationLevel.Override } else if (basicKeys.includes(token)) { authorizationLevel = AuthorizationLevel.Basic } - if (authorizationLevel === AuthorizationLevel.Unauthorized) { return new Response('Invalid Authorization header', { status: 401 }) } diff --git a/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx b/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx index 76c8c71bf4..0bbbee5459 100644 --- a/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx @@ -11,12 +11,12 @@ import { removeRedundantH1, } from '~/features/docs/GuidesMdx.utils' import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' -import { fetchRevalidatePerDay } from '~/features/helpers.fetch' import { GUIDES_DIRECTORY, isValidGuideFrontmatter } from '~/lib/docs' import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform' import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition' import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle' import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs' +import { REVALIDATION_TAGS } from '~/features/helpers.fetch' export const dynamicParams = false @@ -196,7 +196,9 @@ const getContent = async (params: Params) => { const repoPath = `${org}/${repo}/${branch}/${docsDir}/${remoteFile}` editLink = `${org}/${repo}/blob/${branch}/${docsDir}/${remoteFile}` - const response = await fetchRevalidatePerDay(`https://raw.githubusercontent.com/${repoPath}`) + const response = await fetch(`https://raw.githubusercontent.com/${repoPath}`, { + next: { tags: [REVALIDATION_TAGS.WRAPPERS] }, + }) const rawContent = await response.text() const { content: contentWithoutFrontmatter } = matter(rawContent) diff --git a/apps/docs/app/guides/graphql/[[...slug]]/page.tsx b/apps/docs/app/guides/graphql/[[...slug]]/page.tsx index fbf0d93073..69ba3cf793 100644 --- a/apps/docs/app/guides/graphql/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/graphql/[[...slug]]/page.tsx @@ -5,11 +5,11 @@ import rehypeSlug from 'rehype-slug' import { genGuideMeta } from '~/features/docs/GuidesMdx.utils' import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' -import { fetchRevalidatePerDay_TEMP_TESTING } from '~/features/helpers.fetch' import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform' import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition' import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle' import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs' +import { REVALIDATION_TAGS } from '~/features/helpers.fetch' export const dynamicParams = false @@ -135,8 +135,9 @@ const getContent = async ({ slug }: Params) => { const editLink = newEditLink(`${org}/${repo}/blob/${branch}/${docsDir}/${remoteFile}`) - const response = await fetchRevalidatePerDay_TEMP_TESTING( - `https://raw.githubusercontent.com/${org}/${repo}/${branch}/${docsDir}/${remoteFile}` + const response = await fetch( + `https://raw.githubusercontent.com/${org}/${repo}/${branch}/${docsDir}/${remoteFile}`, + { next: { tags: [REVALIDATION_TAGS.GRAPHQL] } } ) const content = await response.text() diff --git a/apps/docs/features/helpers.fetch.ts b/apps/docs/features/helpers.fetch.ts index 494e0444fe..1275a4817c 100644 --- a/apps/docs/features/helpers.fetch.ts +++ b/apps/docs/features/helpers.fetch.ts @@ -8,6 +8,17 @@ import { ONE_DAY_IN_SECONDS } from './helpers.time' +export const REVALIDATION_TAGS = { + GRAPHQL: 'graphql', + WRAPPERS: 'wrappers', +} as const +// Casting to avoid problems with using this as a Zod enum, TypeScript does +// not recognize the casted type as a supertype of the original type +export const VALID_REVALIDATION_TAGS = Object.values(REVALIDATION_TAGS) as unknown as readonly [ + string, + ...string[], +] + function fetchWithNextOptions({ next, cache,