@@ -5,7 +5,7 @@ import type { Ref, ComputedRef, Component, VNode } from 'vue'
55import { getDrupalBaseUrl , getMenuBaseUrl } from './server'
66import type { UseFetchOptions , AsyncData } from '#app'
77import { 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'
99import type { DrupalCePage , DrupalCeApiResponse } from '../../types'
1010
1111export 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