@koehler8/cms 1.4.0 → 1.4.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.4.1
4
+
5
+ ### Fixed
6
+
7
+ - **The embedded config payload is now actually used on locale-prefixed routes.**
8
+ vite-ssg runs the setup fn *before* `app.use(router)`, so `router.currentRoute`
9
+ is still `START_LOCATION` (`'/'`, `params: {}`) there — identically on every
10
+ page. During SSG that was already handled, because vite-ssg hands the route
11
+ over on `ctx.routePath`. On the **client** it calls the setup fn with no
12
+ argument at all, so there was nothing to resolve and the code fell through to
13
+ `START_LOCATION`.
14
+
15
+ `loadSiteConfig` derives its cache key from `params.locale`, so the key came
16
+ out `'default'` on every client page while the SSG had stamped the real locale
17
+ on `siteConfigLocale`. On a locale-prefixed route the two never matched, so
18
+ `primeConfigSync` was skipped, `hydrateFromSyncCache` missed, and the client
19
+ discarded the payload it had just been served and awaited a full config load
20
+ **inside the setup fn, on the critical path, before mount** — the exact
21
+ regression the priming machinery exists to prevent.
22
+
23
+ Measured on a multi-locale site, same machine and build, only this patch
24
+ differing. First config chunk on a `/th/` piece page: **44 ms before**
25
+ (DOMContentLoaded 34 ms — on the critical path), **1364 ms after** (deferred
26
+ to `requestIdleCallback`, as designed). The load is *deferred, not removed*:
27
+ `trimConfigToPage` stamps `pagesPartial` on every page, so the idle reload
28
+ still runs.
29
+
30
+ ⚠️ A cross-environment comparison hides this completely. A production bundle
31
+ that needs ~1.4 s to download and execute shows its config chunks at ~1607 ms,
32
+ which *looks* deferred when it is in fact immediate. Only a same-environment
33
+ A/B separates the two.
34
+
35
+ **No action needed in a site.** Single-locale sites already stamped `default`
36
+ and primed correctly, so their behaviour is unchanged; multi-locale sites gain
37
+ the deferral.
38
+
3
39
  ## 1.4.0
4
40
 
5
41
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@koehler8/cms",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
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",
package/src/main.js CHANGED
@@ -8,6 +8,7 @@ import { routes, resolveHistory, applyRouterGuards } from './router/index.js';
8
8
  import { shouldEnableAnalytics, scheduleAnalyticsLoad, configureAnalyticsConsentMode } from './utils/cookieConsent.js';
9
9
  import { loadConfigData, primeConfigSync, trimConfigToPage, resolvePageIdForRoute } from './utils/loadConfig.js';
10
10
  import { applyInitialStateTrims } from './utils/initialStateTrim.js';
11
+ import { currentRouteHref } from './utils/currentRoute.js';
11
12
  import { persistAttributionFromLocation } from './utils/trackingContext.js';
12
13
  import { applyThemeVariables } from './themes/themeManager.js';
13
14
  import { setActiveThemeKey } from './utils/themeColors.js';
@@ -136,19 +137,22 @@ export function createCmsApp() {
136
137
  };
137
138
 
138
139
  const extractCurrentRoute = () => {
139
- // During SSG, vite-ssg passes the route being pre-rendered as a string
140
- // on `ctx.routePath` and only pushes the router AFTER this setup fn runs
141
- // — so `router.currentRoute` is still vue-router's START_LOCATION ('/')
142
- // here, identically on every page. Resolve `routePath` through the router
143
- // (a pure match, no navigation) to recover the real path + `params.locale`.
144
- // Without this, `resolveCurrentPageId` sees '/' for every route, so the
145
- // payload trim targets the wrong page — or, when '/' is ambiguous, none.
146
- const routePath = ctx?.routePath;
147
- if (typeof routePath === 'string' && routePath) {
140
+ // vite-ssg runs this setup fn BEFORE `app.use(router)`, so
141
+ // `router.currentRoute` is still vue-router's START_LOCATION ('/') here,
142
+ // identically on every page — during SSG *and* on the client. The href
143
+ // that IS authoritative differs per half (ctx.routePath vs the window's
144
+ // own location); currentRouteHref picks it, with the full account of why
145
+ // the client half is not optional. Resolve it through the router — a pure
146
+ // match, no navigation — to recover the real path + `params.locale`.
147
+ const href = currentRouteHref(
148
+ ctx,
149
+ typeof window !== 'undefined' ? window.location : null,
150
+ );
151
+ if (href) {
148
152
  try {
149
- return router.resolve(routePath);
153
+ return router.resolve(href);
150
154
  } catch {
151
- return { path: routePath, params: {} };
155
+ return { path: href, params: {} };
152
156
  }
153
157
  }
154
158
  if (ctx?.route) {
@@ -0,0 +1,49 @@
1
+ // Which href the app should resolve through the router to learn the route it is
2
+ // actually on.
3
+ //
4
+ // Both halves of the lifecycle are blind to this in the same way, for the same
5
+ // reason: vite-ssg runs the setup fn BEFORE `app.use(router)`, so
6
+ // `router.currentRoute` is still vue-router's START_LOCATION ('/', `params: {}`)
7
+ // — identically on every page.
8
+ //
9
+ // - During SSG, vite-ssg hands the route being pre-rendered over as a string on
10
+ // `ctx.routePath`, so that is authoritative.
11
+ // - On the CLIENT it calls the setup fn with no argument at all: no
12
+ // `ctx.routePath`, no `ctx.route`. The only authoritative source left is the
13
+ // window's own location.
14
+ //
15
+ // Falling through to START_LOCATION on the client is not a cosmetic miss. The
16
+ // config cache key is derived from `params.locale`, so it came out 'default' on
17
+ // every page while the SSG had stamped the real locale on `siteConfigLocale`.
18
+ // The two never matched on a locale-prefixed route, `primeConfigSync` was
19
+ // skipped, and the client discarded the embedded payload and re-fetched the
20
+ // whole config tree on the critical path — the exact regression the priming
21
+ // machinery exists to prevent. Measured on sparkbang.com/th/<piece>/ before the
22
+ // fix: 151 JS requests / 405 KB, 112 of them per-piece config chunks, while the
23
+ // 17 KB embedded payload was parsed and thrown away.
24
+ //
25
+ // Pure and dependency-free so the fall-through can be pinned by a unit test;
26
+ // `main.js` owns the router.resolve() call itself (a pure match, no navigation).
27
+ //
28
+ // Returns null when the caller should use `ctx.route` as-is, or when there is
29
+ // nothing authoritative to resolve and START_LOCATION is genuinely all there is.
30
+
31
+ /**
32
+ * @param {{routePath?: unknown, route?: unknown}|null|undefined} ctx
33
+ * The vite-ssg setup context. `routePath` is set during SSG only.
34
+ * @param {{pathname?: unknown, search?: unknown}|null|undefined} location
35
+ * `window.location` on the client; omit/null on the server.
36
+ * @returns {string|null} the href to resolve, or null to fall back.
37
+ */
38
+ export function currentRouteHref(ctx, location) {
39
+ const routePath = ctx?.routePath;
40
+ if (typeof routePath === 'string' && routePath) return routePath;
41
+ // A context that already carries a resolved route needs no href at all.
42
+ if (ctx?.route) return null;
43
+ const pathname = location?.pathname;
44
+ if (typeof pathname === 'string' && pathname) {
45
+ const search = typeof location?.search === 'string' ? location.search : '';
46
+ return `${pathname}${search}`;
47
+ }
48
+ return null;
49
+ }