From 113ca17e1f1cc177088d0719ed822736079408a0 Mon Sep 17 00:00:00 2001 From: Kneesal Date: Thu, 19 Feb 2026 10:29:46 +1300 Subject: [PATCH 1/3] feat: use turbo for root dev commands (dev:web, dev:backend, codegen) Co-authored-by: Cursor --- package.json | 4 +++- turbo.json | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 937ce737..9c549a21 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,8 @@ "prepare": "husky", "build": "turbo run build", "dev": "turbo run dev --parallel", + "dev:web": "turbo run dev --filter=@forge/web", + "dev:backend": "turbo run dev --parallel --filter=@forge/cms --filter=@forge/ai-orchestrator", "lint": "turbo run lint", "test": "turbo run test", "codegen": "pnpm --filter @forge/graphql run generate", @@ -29,4 +31,4 @@ "turbo": "^2.8.9", "typescript-eslint": "^8.56.0" } -} +} \ No newline at end of file diff --git a/turbo.json b/turbo.json index 3d943c0e..8c3e4c97 100644 --- a/turbo.json +++ b/turbo.json @@ -25,6 +25,9 @@ }, "validate": { "dependsOn": ["^validate"] + }, + "generate": { + "outputs": ["src/graphql-env.d.ts"] } } } From ec75e1d88461b33b84836f9b90d80f81a3aa9c70 Mon Sep 17 00:00:00 2001 From: Kneesal Date: Wed, 18 Mar 2026 08:50:50 +1300 Subject: [PATCH 2/3] feat(web): add ISR with Strapi webhook on-demand revalidation Enable the "use cache" directive on readPublishedContent with cacheTag and cacheLife("max") so Apollo query results are cached across requests. Upgrade the /api/revalidate route from revalidatePath to revalidateTag with Strapi webhook payload parsing for surgical cache invalidation. Resolves #497 Made-with: Cursor --- apps/web/next-env.d.ts | 2 +- apps/web/next.config.mjs | 3 ++- apps/web/src/app/api/revalidate/route.ts | 23 ++++++++++++++++++----- apps/web/src/app/page.tsx | 2 ++ apps/web/src/lib/content.ts | 24 +++++++++++++++++------- 5 files changed, 40 insertions(+), 14 deletions(-) diff --git a/apps/web/next-env.d.ts b/apps/web/next-env.d.ts index 830fb594..7506fe6a 100644 --- a/apps/web/next-env.d.ts +++ b/apps/web/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -/// +import "./.next/dev/types/routes.d.ts" // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index c3567c78..2f40dca5 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -1,8 +1,9 @@ /** @type {import('next').NextConfig} */ const nextConfig = { basePath: "/watch", + typedRoutes: true, experimental: { - typedRoutes: true, + useCache: true, }, } diff --git a/apps/web/src/app/api/revalidate/route.ts b/apps/web/src/app/api/revalidate/route.ts index c4844235..261bb60f 100644 --- a/apps/web/src/app/api/revalidate/route.ts +++ b/apps/web/src/app/api/revalidate/route.ts @@ -1,6 +1,16 @@ -import { revalidatePath } from "next/cache" +import { revalidateTag } from "next/cache" import { NextResponse } from "next/server" +interface StrapiWebhookPayload { + event: string + model: string + entry?: { + slug?: string + locale?: string + documentId?: string + } +} + export async function POST(request: Request) { const token = request.headers.get("x-forge-revalidate-token") if ( @@ -13,8 +23,11 @@ export async function POST(request: Request) { ) } - const body = (await request.json().catch(() => ({}))) as { path?: string } - const path = body.path ?? "/" - revalidatePath(path) - return NextResponse.json({ revalidated: true, path }) + const body = (await request.json().catch(() => ({}))) as StrapiWebhookPayload + const slug = body.entry?.slug + const tag = slug ? `experience:${slug}` : "experience" + + revalidateTag(tag, "max") + + return NextResponse.json({ revalidated: true, tag }) } diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index 1986c027..d2abd1b7 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -1,5 +1,7 @@ import { readPublishedContent } from "../lib/content" +export const dynamic = "force-dynamic" + export default async function HomePage() { const item = await readPublishedContent("home", "en") diff --git a/apps/web/src/lib/content.ts b/apps/web/src/lib/content.ts index 1f3348cc..4d1249b7 100644 --- a/apps/web/src/lib/content.ts +++ b/apps/web/src/lib/content.ts @@ -1,3 +1,4 @@ +import { cacheTag, cacheLife } from "next/cache" import { graphql } from "@forge/graphql" import client from "@/lib/client" @@ -10,12 +11,21 @@ const GET_EXPERIENCE = graphql(` `) export async function readPublishedContent(slug: string, locale: string) { + "use cache" + + cacheTag("experience", `experience:${slug}`, `experience:${slug}:${locale}`) + cacheLife("max") + if (!process.env.NEXT_PUBLIC_GRAPHQL_URL) return null - const result = await client.query({ - query: GET_EXPERIENCE, - variables: { slug, locale }, - }) - if (result.error) return null - const items = result.data?.experiences - return items?.[0] ?? null + try { + const result = await client.query({ + query: GET_EXPERIENCE, + variables: { slug, locale }, + }) + if (result.error) return null + const items = result.data?.experiences + return items?.[0] ?? null + } catch { + return null + } } From 83e6d6b35ba4148ae7715799f900cbe7d526db7a Mon Sep 17 00:00:00 2001 From: Kneesal Date: Wed, 18 Mar 2026 08:55:31 +1300 Subject: [PATCH 3/3] fix(web): remove force-dynamic to allow ISR static prerendering The try/catch in readPublishedContent handles build-time errors gracefully, so force-dynamic is unnecessary and would prevent the page from being statically cached. Made-with: Cursor --- apps/web/src/app/page.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index d2abd1b7..1986c027 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -1,7 +1,5 @@ import { readPublishedContent } from "../lib/content" -export const dynamic = "force-dynamic" - export default async function HomePage() { const item = await readPublishedContent("home", "en")