@koehler8/cms 1.0.0-beta.30 → 1.0.0-beta.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { onServerPrefetch, ref, watch } from 'vue';
|
|
2
|
-
import { loadConfigData, mergeConfigTrees } from '../utils/loadConfig.js';
|
|
2
|
+
import { loadConfigData, mergeConfigTrees, peekConfigSync } from '../utils/loadConfig.js';
|
|
3
3
|
|
|
4
4
|
function normalizePath(value) {
|
|
5
5
|
if (!value || typeof value !== 'string') return '/';
|
|
@@ -122,7 +122,39 @@ export function usePageConfig({ pageId, pagePath, locale, onPageLoaded } = {}) {
|
|
|
122
122
|
return null;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
// Synchronously apply a resolved config to the reactive refs. Kept pure and
|
|
126
|
+
// synchronous so it can run during the first client render (matching the
|
|
127
|
+
// prerendered DOM) as well as after an async load. Throws if the config has
|
|
128
|
+
// no selectable page, so callers can fall back.
|
|
129
|
+
function applyConfig(config) {
|
|
130
|
+
siteData.value = config;
|
|
131
|
+
const selectedPage = selectPage(config);
|
|
132
|
+
|
|
133
|
+
if (!selectedPage) {
|
|
134
|
+
throw new Error('No pages defined in configuration.');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const sharedContent = config?.shared?.content;
|
|
138
|
+
const mergedContent = mergeWithSharedContent(sharedContent, selectedPage.content);
|
|
139
|
+
currentPage.value = {
|
|
140
|
+
...selectedPage,
|
|
141
|
+
content: mergedContent,
|
|
142
|
+
};
|
|
143
|
+
pageContent.value = mergedContent;
|
|
144
|
+
|
|
145
|
+
if (typeof onPageLoaded === 'function') {
|
|
146
|
+
onPageLoaded({ config, page: selectedPage, content: mergedContent });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
componentKeys.value = Array.isArray(selectedPage.components)
|
|
150
|
+
? selectedPage.components.map((entry) => (entry && typeof entry === 'object' ? { ...entry } : entry))
|
|
151
|
+
: [];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// soft: when true, keep the currently-rendered components visible while the
|
|
155
|
+
// new config loads (no blank-out). Used by the post-hydration locale
|
|
156
|
+
// reconcile so a saved-locale switch doesn't reintroduce a flash.
|
|
157
|
+
async function syncPage({ soft = false } = {}) {
|
|
126
158
|
const requestId = ++activeRequest;
|
|
127
159
|
const currentLocale = typeof locale === 'function' ? locale() : locale;
|
|
128
160
|
const localeKey = (currentLocale || '').toString().toLowerCase() || 'default';
|
|
@@ -131,7 +163,7 @@ export function usePageConfig({ pageId, pagePath, locale, onPageLoaded } = {}) {
|
|
|
131
163
|
|
|
132
164
|
if (requestId === activeRequest) {
|
|
133
165
|
isLoading.value = true;
|
|
134
|
-
if (shouldReload) {
|
|
166
|
+
if (shouldReload && !soft) {
|
|
135
167
|
componentKeys.value = [];
|
|
136
168
|
}
|
|
137
169
|
loadError.value = null;
|
|
@@ -145,28 +177,7 @@ export function usePageConfig({ pageId, pagePath, locale, onPageLoaded } = {}) {
|
|
|
145
177
|
|
|
146
178
|
if (requestId !== activeRequest) return;
|
|
147
179
|
|
|
148
|
-
|
|
149
|
-
const selectedPage = selectPage(cachedConfig);
|
|
150
|
-
|
|
151
|
-
if (!selectedPage) {
|
|
152
|
-
throw new Error('No pages defined in configuration.');
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const sharedContent = cachedConfig?.shared?.content;
|
|
156
|
-
const mergedContent = mergeWithSharedContent(sharedContent, selectedPage.content);
|
|
157
|
-
currentPage.value = {
|
|
158
|
-
...selectedPage,
|
|
159
|
-
content: mergedContent,
|
|
160
|
-
};
|
|
161
|
-
pageContent.value = mergedContent;
|
|
162
|
-
|
|
163
|
-
if (typeof onPageLoaded === 'function') {
|
|
164
|
-
onPageLoaded({ config: cachedConfig, page: selectedPage, content: mergedContent });
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
componentKeys.value = Array.isArray(selectedPage.components)
|
|
168
|
-
? selectedPage.components.map((entry) => (entry && typeof entry === 'object' ? { ...entry } : entry))
|
|
169
|
-
: [];
|
|
180
|
+
applyConfig(cachedConfig);
|
|
170
181
|
} catch (error) {
|
|
171
182
|
if (requestId !== activeRequest) return;
|
|
172
183
|
if (import.meta.env.DEV) {
|
|
@@ -186,11 +197,53 @@ export function usePageConfig({ pageId, pagePath, locale, onPageLoaded } = {}) {
|
|
|
186
197
|
}
|
|
187
198
|
}
|
|
188
199
|
|
|
200
|
+
// Client-only: populate refs synchronously from the config the server pass
|
|
201
|
+
// already resolved + serialized (primed into a sync cache by main.js before
|
|
202
|
+
// mount). This makes the first client render reproduce the prerendered DOM
|
|
203
|
+
// instead of an empty <main> while the async loader runs — the fix for the
|
|
204
|
+
// paint -> blank -> paint white-flash. Returns false on a cache miss, so the
|
|
205
|
+
// caller falls back to the async path (previous behavior).
|
|
206
|
+
function hydrateFromSyncCache() {
|
|
207
|
+
const currentLocale = typeof locale === 'function' ? locale() : locale;
|
|
208
|
+
const localeKey = (currentLocale || '').toString().toLowerCase() || 'default';
|
|
209
|
+
const localeForConfig = localeKey === 'default' ? undefined : localeKey;
|
|
210
|
+
|
|
211
|
+
const primed = peekConfigSync(localeForConfig);
|
|
212
|
+
if (!primed) return false;
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
applyConfig(primed);
|
|
216
|
+
} catch {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
cachedConfig = primed;
|
|
220
|
+
lastLocaleKey = localeKey;
|
|
221
|
+
|
|
222
|
+
// Preserve the saved-locale auto-switch on base-locale routes: the server
|
|
223
|
+
// has no localStorage, so it rendered the base locale. If a returning
|
|
224
|
+
// visitor stored a different locale, reconcile AFTER the first paint
|
|
225
|
+
// (soft = no blank) so that paint still matches the server but the saved
|
|
226
|
+
// choice is honored.
|
|
227
|
+
if (localeForConfig === undefined && typeof localStorage !== 'undefined') {
|
|
228
|
+
let stored = '';
|
|
229
|
+
try {
|
|
230
|
+
stored = (localStorage.getItem('locale') || '').trim().toLowerCase();
|
|
231
|
+
} catch {
|
|
232
|
+
stored = '';
|
|
233
|
+
}
|
|
234
|
+
if (stored) {
|
|
235
|
+
lastLocaleKey = null; // force the reconcile to reload with localStorage resolution
|
|
236
|
+
syncPage({ soft: true });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
|
|
189
242
|
if (import.meta.env.SSR) {
|
|
190
243
|
onServerPrefetch(async () => {
|
|
191
244
|
await syncPage();
|
|
192
245
|
});
|
|
193
|
-
} else {
|
|
246
|
+
} else if (!hydrateFromSyncCache()) {
|
|
194
247
|
syncPage();
|
|
195
248
|
}
|
|
196
249
|
|
package/src/main.js
CHANGED
|
@@ -6,7 +6,7 @@ import App from './App.vue';
|
|
|
6
6
|
import { routes, resolveHistory, applyRouterGuards } from './router/index.js';
|
|
7
7
|
|
|
8
8
|
import { shouldEnableAnalytics, scheduleAnalyticsLoad } from './utils/cookieConsent.js';
|
|
9
|
-
import { loadConfigData } from './utils/loadConfig.js';
|
|
9
|
+
import { loadConfigData, primeConfigSync } from './utils/loadConfig.js';
|
|
10
10
|
import { persistAttributionFromLocation } from './utils/trackingContext.js';
|
|
11
11
|
import { applyThemeVariables } from './themes/themeManager.js';
|
|
12
12
|
import { setActiveThemeKey } from './utils/themeColors.js';
|
|
@@ -158,6 +158,10 @@ export function createCmsApp() {
|
|
|
158
158
|
initialState.siteTheme = existingTheme;
|
|
159
159
|
setActiveThemeKey(existingTheme);
|
|
160
160
|
applySiteTheme(existingTheme, ctx.head);
|
|
161
|
+
// Expose the server-resolved config for the first synchronous client
|
|
162
|
+
// render (see usePageConfig.hydrateFromSyncCache) so the prerendered
|
|
163
|
+
// DOM isn't replaced by an empty render while the async loader runs.
|
|
164
|
+
primeConfigSync(localeKey, initialState.siteConfig);
|
|
161
165
|
return initialState.siteConfig;
|
|
162
166
|
}
|
|
163
167
|
|
package/src/utils/loadConfig.js
CHANGED
|
@@ -309,11 +309,43 @@ let _loadConfigData = async () => {
|
|
|
309
309
|
let _availableLocales = [];
|
|
310
310
|
let _baseLocale = '';
|
|
311
311
|
|
|
312
|
+
// Synchronous, client-side cache of the config the SSR/SSG pass already
|
|
313
|
+
// resolved. Warmed once before mount (see main.js, from the vite-ssg-
|
|
314
|
+
// serialized `initialState.siteConfig`) so the first client render can
|
|
315
|
+
// populate synchronously and match the prerendered DOM — eliminating the
|
|
316
|
+
// paint -> blank -> paint flash caused by config arriving asynchronously via
|
|
317
|
+
// lazy dynamic-import chunks. Keyed by normalized locale; SSR never reads it.
|
|
318
|
+
const _syncConfigCache = new Map();
|
|
319
|
+
|
|
320
|
+
function normalizeConfigCacheKey(locale) {
|
|
321
|
+
if (typeof locale === 'string') {
|
|
322
|
+
const trimmed = locale.trim().toLowerCase();
|
|
323
|
+
if (trimmed) return trimmed;
|
|
324
|
+
}
|
|
325
|
+
return 'default';
|
|
326
|
+
}
|
|
327
|
+
|
|
312
328
|
export function setConfigLoader(instance) {
|
|
313
329
|
if (!instance) return;
|
|
314
330
|
_loadConfigData = instance.loadConfigData;
|
|
315
331
|
_availableLocales = instance.availableLocales || [];
|
|
316
332
|
_baseLocale = typeof instance.baseLocale === 'string' ? instance.baseLocale : '';
|
|
333
|
+
_syncConfigCache.clear();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Store a resolved config for synchronous retrieval during the first client
|
|
337
|
+
// render. Pass the config exactly as the server resolved it (e.g. the
|
|
338
|
+
// serialized `initialState.siteConfig`) so `peekConfigSync` returns a value
|
|
339
|
+
// that matches the prerendered DOM.
|
|
340
|
+
export function primeConfigSync(locale, config) {
|
|
341
|
+
if (!config || typeof config !== 'object') return;
|
|
342
|
+
_syncConfigCache.set(normalizeConfigCacheKey(locale), config);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Retrieve a primed config synchronously, or null on a miss. Client-only fast
|
|
346
|
+
// path used by usePageConfig; a miss falls back to the async loader.
|
|
347
|
+
export function peekConfigSync(locale) {
|
|
348
|
+
return _syncConfigCache.get(normalizeConfigCacheKey(locale)) || null;
|
|
317
349
|
}
|
|
318
350
|
|
|
319
351
|
export { _availableLocales as availableLocales };
|