Hey, thanks for the suggestions! Just to clarify, the data isn't fetched on the client at all. Here's how it works:
I have async Server Components that call data functions with "use cache" + cacheTag("events") + cacheLife("hours"). These run entirely on the server. The GraphQL call to WordPress happens server-side, and the result is cached in the server's function cache. The client component (event list) just receives the data as a prop, it never fetches anything itself.
For revalidation I have two steps:
- WordPress webhook hits a Route Handler that calls revalidateTag("events", { expire: 0 }) This force-expires the server function cache
- An SSE broker broadcasts to connected browsers, and the client calls router.refresh()
This clears the Router Cache so the browser requests fresh server-rendered content.
The stale data issue I had was because my homepage had no dynamic APIs, so Next.js treated it as a fully static route. Static routes use the Full Route Cache which has SWR semantics. It serves the stale pre-built HTML and revalidates in the background. So even though my function cache was correctly expired by the webhook, the Full Route Cache sat in front of it and kept serving the old page until a second request came in.
await connection() inside a Suspense boundary fixed it because it opts the route into PPR. Now the static shell is served instantly, but the dynamic hole (where the data lives) is reexecuted on every request. That request hits the function cache. If it's been expired by the webhook, it fetches fresh data from WordPress. If not, it serves the cached result. So I get instant page loads + fresh data within seconds of a CMS change, without any polling or client-side fetching.
It's the first time I'm using this paradigm, but I think it works. If my logic has gaps and you'd like to add anything or correct me, I'd be happy to learn