Skip to content

Commit 2a1d79e

Browse files
authored
fix: fix page meta key to keep including query and allow CDNs to ignore them (#411)
* fix: fix page meta key to keep including query * keep removing trailing slash * fix: fix issues with cdn query key varying * default to route fullpath key * fix imports and hydration
1 parent 5686642 commit 2a1d79e

File tree

2 files changed

+82
-20
lines changed

2 files changed

+82
-20
lines changed

playground/pages/[...slug].vue

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ const layout = getPageLayout(page)
1616
usePageHead(page)
1717
1818
definePageMeta({
19-
key: (route) => route.path,
19+
// Use fullPath minus hash for cache key to ensure:
20+
// - Different query params (e.g., ?page=2) create separate cache entries
21+
// - Hash fragments (e.g., #gallery--slide--1) don't affect caching
22+
// - Components can watch route.hash for transient UI state
23+
key: (route) => route.fullPath.split('#')[0],
2024
})
2125
</script>

src/runtime/composables/useDrupalCe/index.ts

Lines changed: 77 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type { Ref, ComputedRef, Component, VNode } from 'vue'
55
import { getDrupalBaseUrl, getMenuBaseUrl } from './server'
66
import type { UseFetchOptions, AsyncData } from '#app'
77
import { callWithNuxt } from '#app'
8-
import { useRuntimeConfig, useState, useFetch, navigateTo, createError, h, resolveComponent, setResponseStatus, useNuxtApp, useRequestHeaders, ref, unref, watch, useRequestEvent, computed, useHead, defineComponent, toRef } from '#imports'
8+
import { useRuntimeConfig, useState, useFetch, navigateTo, createError, h, resolveComponent, setResponseStatus, useNuxtApp, useRequestHeaders, ref, unref, watch, useRequestEvent, computed, useHead, defineComponent, toRef, useRoute, useRouter } from '#imports'
99
import type { DrupalCePage, DrupalCeApiResponse } from '../../types'
1010

1111
export const useDrupalCe = () => {
@@ -112,20 +112,64 @@ export const useDrupalCe = () => {
112112
}
113113

114114
/**
115-
* Helper to compute the page cache key
115+
* Helper to compute page cache key
116+
*
117+
* Uses a special '__ssr__' cache key during SSR to handle CDN query parameter filtering.
118+
* CDNs typically strip tracking parameters (utm_*, fbclid, etc.) from their cache keys:
119+
*
120+
* 1. CDN caches: /blog?page=1 (strips utm_source=newsletter)
121+
* 2. User visits: /blog?page=1&utm_source=newsletter
122+
* 3. CDN serves cached HTML from step 1
123+
* 4. Client sees full URL with utm_source in browser
124+
*
125+
* Without the __ssr__ key, this would cause:
126+
* - SSR: Cache key for /blog?page=1
127+
* - Client: Cache key for /blog?page=1&utm_source=newsletter
128+
* - Different keys → re-fetch during hydration → DOM manipulation → errors
129+
*
130+
* The __ssr__ key solves this by:
131+
* - SSR always uses '__ssr__' key (only one page rendered per request)
132+
* - Client moves __ssr__ cache to proper key on first access
133+
* - After move, all subsequent calls use normal keys
134+
*
135+
* @param skipProxy Whether proxy is being skipped
136+
* @param nuxtApp Nuxt app instance (needed to move __ssr__ cache on client)
116137
*/
117-
const computePageKey = (path: string, query: Record<string, any> = {}, skipDrupalCeApiProxy: boolean = false): string => {
118-
const sanitizedPathKey = path.endsWith('/') && path !== '/' ? path.slice(0, -1) : path
119-
const params = Object.keys(query).length > 0
120-
? `?${new URLSearchParams(unref(query) as Record<string, string>).toString()}`
121-
: ''
122-
return `page-${sanitizedPathKey}${params}${skipDrupalCeApiProxy ? '-direct' : '-proxy'}`
138+
const computePageKey = (skipProxy: boolean, nuxtApp: any): string => {
139+
// During SSR, always use the special __ssr__ key since only one page is rendered per request
140+
if (import.meta.server) {
141+
return '__ssr__'
142+
}
143+
144+
// Get path with query params from current route (without hash)
145+
// During hydration, use nuxtApp's router which is always available
146+
const route = nuxtApp.$router?.currentRoute?.value || useRoute()
147+
const pathWithQuery = route.fullPath.split('#')[0]
148+
149+
// On client-side, calculate the proper cache key with full path and query parameters
150+
// Remove trailing slash from path as it might cause issues in SSG (except for homepage)
151+
const sanitized = pathWithQuery.replace(/\/(\?|$)/, '$1')
152+
const proxyMode = skipProxy ? '-direct' : '-proxy'
153+
const properKey = `page-${sanitized}${proxyMode}`
154+
155+
// During initial hydration, if __ssr__ cache exists, move it to the proper key
156+
// This ensures the SSR data is available under the correct key for this URL
157+
if (nuxtApp.payload.data['__ssr__']) {
158+
nuxtApp.payload.data[properKey] = nuxtApp.payload.data['__ssr__']
159+
delete nuxtApp.payload.data['__ssr__']
160+
}
161+
162+
return properKey
123163
}
124164

125165
/**
126166
* Fetches page data from Drupal, handles redirects, errors and messages
167+
*
168+
* By default, the cache key is generated from the current route's fullPath (without hash).
169+
* This can be customized by providing useFetchOptions.key.
170+
*
127171
* @param path Path of the Drupal page to fetch
128-
* @param useFetchOptions Optional Nuxt useFetch options
172+
* @param useFetchOptions Optional Nuxt useFetch options. Can include custom cache key via 'key' property.
129173
* @param overrideErrorHandler Optional error handler
130174
* @param skipDrupalCeApiProxy Force skip the Drupal CE API proxy. Defaults to false.
131175
* The proxy might still be skipped if serverApiProxy is set to false globally.
@@ -134,7 +178,12 @@ export const useDrupalCe = () => {
134178
const nuxtApp = useNuxtApp()
135179
const currentPageKey = useState<string>('drupal-ce-current-page-key')
136180

137-
useFetchOptions.key = computePageKey(path, useFetchOptions.query || {}, skipDrupalCeApiProxy)
181+
// Build cache key from current route's fullPath (without hash) if not already provided
182+
// Callers can optionally provide a custom key via useFetchOptions.key
183+
const skipProxy = !(config.serverApiProxy && !skipDrupalCeApiProxy)
184+
if (!useFetchOptions.key) {
185+
useFetchOptions.key = computePageKey(skipProxy, nuxtApp)
186+
}
138187

139188
// Check if page data is provided by custom page response (e.g. form submission via POST)
140189
// This is only available during SSR
@@ -278,26 +327,35 @@ export const useDrupalCe = () => {
278327
* Get the current page data ref.
279328
* Returns the useFetch cached data for the current page.
280329
* Layout components (breadcrumbs, page title, social share, etc.) can use this to access page data from the current route.
330+
*
331+
* By default, the cache key is generated from the current route's fullPath (without hash).
332+
* This can be customized by providing a custom key.
333+
*
334+
* @param customKey Optional custom cache key. If not provided, uses current route's cache key.
281335
*/
282-
const getPage = (): Ref<DrupalCePage> => {
336+
const getPage = (customKey?: string): Ref<DrupalCePage> => {
283337
const currentPageKey = useState<string>('drupal-ce-current-page-key', () => '')
284338

285339
// Set up route watcher to keep currentPageKey in sync (for KeepAlive scenarios)
286-
if (import.meta.client) {
340+
// Only needed when using default key (not custom key)
341+
if (!customKey && import.meta.client) {
287342
const watcherInitialized = useState<boolean>('drupal-ce-watcher-init', () => false)
288343

289344
if (!watcherInitialized.value) {
290345
watcherInitialized.value = true
291346
try {
292-
const route = useRoute()
293347
const router = useRouter()
348+
const nuxtApp = useNuxtApp()
349+
350+
// Determine proxy mode based on config (same logic as fetchPage)
351+
const skipProxy = !config.serverApiProxy
294352

295353
// Update key on initial load
296-
currentPageKey.value = computePageKey(route.path, route.query as Record<string, any>, false)
354+
currentPageKey.value = computePageKey(skipProxy, nuxtApp)
297355

298356
// Use router.afterEach to ensure navigation is fully complete before updating
299-
router.afterEach((to) => {
300-
currentPageKey.value = computePageKey(to.path, to.query as Record<string, any>, false)
357+
router.afterEach(() => {
358+
currentPageKey.value = computePageKey(skipProxy, nuxtApp)
301359
})
302360
}
303361
catch (e) {
@@ -306,11 +364,11 @@ export const useDrupalCe = () => {
306364
}
307365
}
308366

309-
// Return computed ref that looks up the current page key in the reactive Nuxt payload
310-
// This properly tracks reactivity since nuxtApp.payload.data is reactive
367+
// Return computed ref that looks up the page data in the reactive Nuxt payload
368+
// Uses custom key if provided, otherwise uses current route's key
311369
return computed(() => {
312370
const nuxtApp = useNuxtApp()
313-
const key = currentPageKey.value
371+
const key = customKey || currentPageKey.value
314372
if (key && nuxtApp.payload.data[key]) {
315373
return nuxtApp.payload.data[key]
316374
}

0 commit comments

Comments
 (0)