@koehler8/cms 1.0.0-beta.29 → 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,6 +1,6 @@
1
1
  {
2
2
  "name": "@koehler8/cms",
3
- "version": "1.0.0-beta.29",
3
+ "version": "1.0.0-beta.31",
4
4
  "description": "Vertex CMS — lightweight, config-driven Vue 3 framework with theming, extensions, and SSG support",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -80,8 +80,8 @@
80
80
  "vue-router": "^5.0.0"
81
81
  },
82
82
  "dependencies": {
83
- "@unhead/vue": "^2.0.19",
84
- "ajv": "^8.17.1",
83
+ "@unhead/vue": "^2.1.15",
84
+ "ajv": "^8.20.0",
85
85
  "ajv-formats": "^3.0.1",
86
86
  "canvas": "^3.2.0",
87
87
  "dotenv": "^17.2.3",
@@ -90,9 +90,11 @@
90
90
  "sharp": "^0.33.5"
91
91
  },
92
92
  "devDependencies": {
93
- "@vue/test-utils": "^2.4.6",
94
- "happy-dom": "^20.8.9",
95
- "vitest": "^4.1.4"
93
+ "@vue/test-utils": "^2.4.10",
94
+ "happy-dom": "^20.9.0",
95
+ "vitest": "^4.1.6",
96
+ "vue": "^3.5.34",
97
+ "vue-router": "^5.0.7"
96
98
  },
97
99
  "engines": {
98
100
  "node": ">=20.19.0",
@@ -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
- async function syncPage() {
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
- siteData.value = cachedConfig;
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
 
@@ -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 };
@@ -0,0 +1,4 @@
1
+ VITE_TOKEN=__SLUG__
2
+ VITE_REGISTRY_ADDRESS=
3
+ VITE_RPC_READ=
4
+ VITE_REOWN_PROJECT_ID=