Skip to content

Commit ef31b98

Browse files
committed
fix: extend stale ttl + refresh data if there's empty payload on hydration
1 parent 847ac00 commit ef31b98

File tree

2 files changed

+26
-4
lines changed

2 files changed

+26
-4
lines changed

app/plugins/fix.client.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,19 @@ export default defineNuxtPlugin({
33
setup(nuxtApp) {
44
// TODO: investigate why this is needed
55
nuxtApp.payload.data ||= {}
6+
7+
// When a _payload.json returns an ISR fallback (empty payload), data fetching composables
8+
// with non-undefined defaults skip refetching during hydration. After hydration completes,
9+
// refresh all async data so these composables (e.g. README, skills, package analysis) fetch
10+
// fresh data.
11+
if (
12+
nuxtApp.isHydrating &&
13+
nuxtApp.payload.serverRendered &&
14+
!Object.keys(nuxtApp.payload.data).length
15+
) {
16+
nuxtApp.hooks.hookOnce('app:suspense:resolve', () => {
17+
refreshNuxtData()
18+
})
19+
}
620
},
721
})

server/plugins/payload-cache.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ const PAYLOAD_CACHE_STORAGE_KEY = 'payload-cache'
2020
/** Default TTL for cached payloads (seconds). Matches ISR expiration for package routes. */
2121
const PAYLOAD_CACHE_TTL = 60
2222

23+
/**
24+
* Grace period beyond TTL where stale payloads are still served (seconds).
25+
* Prevents a race where the HTML is served from Vercel's ISR cache right before
26+
* expiry, but the payload request arrives a moment later after our cache expires.
27+
*/
28+
const PAYLOAD_CACHE_STALE_TTL = PAYLOAD_CACHE_TTL * 2
29+
2330
interface CachedPayload {
2431
body: string
2532
statusCode: number
@@ -74,9 +81,10 @@ export default defineNitroPlugin(nitroApp => {
7481
// Verify build ID matches (extra safety beyond cache key)
7582
if (cached.buildId !== buildId) return
7683

77-
// Check TTL
84+
// Check TTL — serve stale payloads within the grace period to avoid
85+
// a race where HTML is cached by Vercel but our payload has expired
7886
const age = (Date.now() - cached.cachedAt) / 1000
79-
if (age > PAYLOAD_CACHE_TTL) return
87+
if (age > PAYLOAD_CACHE_STALE_TTL) return
8088

8189
if (import.meta.dev) {
8290
// eslint-disable-next-line no-console
@@ -103,8 +111,8 @@ export default defineNitroPlugin(nitroApp => {
103111
// render:response — Cache payloads after rendering
104112
// -------------------------------------------------------------------------
105113
nitroApp.hooks.hook('render:response', (response, ctx) => {
106-
// Don't cache error responses
107-
if (response.statusCode && response.statusCode >= 400) return
114+
// Don't cache error or unknown responses
115+
if (!response.statusCode || response.statusCode >= 400) return
108116

109117
const isPayloadRequest = PAYLOAD_URL_RE.test(ctx.event.path)
110118
const isHtmlResponse = response.headers?.['content-type']?.includes('text/html')

0 commit comments

Comments
 (0)