@koehler8/cms 1.0.0-beta.33 → 1.0.0-beta.35
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 +1 -1
- package/src/components/Header.vue +18 -6
- package/src/composables/usePageConfig.js +20 -9
- package/src/composables/usePageMeta.js +5 -0
- package/src/main.js +25 -1
- package/src/utils/loadConfig.js +43 -0
- package/templates/scaffolds/site/package.json +1 -1
- package/vite-plugin.js +8 -0
package/package.json
CHANGED
|
@@ -130,14 +130,26 @@ const isHeaderCompact = ref(false);
|
|
|
130
130
|
const currentLocale = ref('');
|
|
131
131
|
const availableLocales = computed(() => siteLocales.map((code) => code.trim()).filter(Boolean));
|
|
132
132
|
|
|
133
|
-
//
|
|
134
|
-
// the
|
|
135
|
-
//
|
|
136
|
-
//
|
|
133
|
+
// Switching language keeps the visitor on the SAME page rather than bouncing
|
|
134
|
+
// them to the locale's home. Slugs are shared across locales, so take the
|
|
135
|
+
// current path, strip any leading non-base locale segment, and re-prefix with
|
|
136
|
+
// the target locale. The base locale is served unprefixed (`/path`); others at
|
|
137
|
+
// `/{locale}/path`. (`/{baseLocale}/path` doesn't pre-render and would 404,
|
|
138
|
+
// hence the base stays unprefixed.)
|
|
137
139
|
const localeHref = (locale) => {
|
|
138
|
-
|
|
140
|
+
const segments = (route.path || '/').split('/').filter(Boolean);
|
|
141
|
+
if (segments.length) {
|
|
142
|
+
const first = segments[0].toLowerCase();
|
|
143
|
+
if (first !== siteBaseLocale && availableLocales.value.includes(first)) {
|
|
144
|
+
segments.shift();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const rest = segments.join('/');
|
|
139
148
|
const slash = injectedSiteData.value?.site?.trailingSlash === true ? '/' : '';
|
|
140
|
-
|
|
149
|
+
if (locale === siteBaseLocale) {
|
|
150
|
+
return rest ? `/${rest}${slash}` : '/';
|
|
151
|
+
}
|
|
152
|
+
return rest ? `/${locale}/${rest}${slash}` : `/${locale}${slash}`;
|
|
141
153
|
};
|
|
142
154
|
const localeLabels = {
|
|
143
155
|
en: 'English',
|
|
@@ -219,11 +219,20 @@ export function usePageConfig({ pageId, pagePath, locale, onPageLoaded } = {}) {
|
|
|
219
219
|
cachedConfig = primed;
|
|
220
220
|
lastLocaleKey = localeKey;
|
|
221
221
|
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
//
|
|
222
|
+
// Decide whether a soft (no blank-out) background reload of the FULL config
|
|
223
|
+
// is needed after this first synchronous paint. Two independent triggers,
|
|
224
|
+
// both satisfied by the same single reload:
|
|
225
|
+
//
|
|
226
|
+
// 1. Partial embed: the server trimmed the embedded config to just this
|
|
227
|
+
// page (site.trimInitialState), so the primed config can't resolve
|
|
228
|
+
// OTHER pages for in-SPA navigation. Load the full config now; the
|
|
229
|
+
// current page re-applies identically, so there's no visible change.
|
|
230
|
+
// 2. Saved-locale switch: the server has no localStorage, so a base-locale
|
|
231
|
+
// route rendered the base locale. If a returning visitor stored a
|
|
232
|
+
// different locale, reconcile to it. (loadConfigData reads localStorage
|
|
233
|
+
// when no explicit locale is passed, so the same reload resolves it.)
|
|
234
|
+
let needsFullReload = Boolean(primed && primed.pagesPartial);
|
|
235
|
+
|
|
227
236
|
if (localeForConfig === undefined && typeof localStorage !== 'undefined') {
|
|
228
237
|
let stored = '';
|
|
229
238
|
try {
|
|
@@ -231,10 +240,12 @@ export function usePageConfig({ pageId, pagePath, locale, onPageLoaded } = {}) {
|
|
|
231
240
|
} catch {
|
|
232
241
|
stored = '';
|
|
233
242
|
}
|
|
234
|
-
if (stored)
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
243
|
+
if (stored) needsFullReload = true;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (needsFullReload) {
|
|
247
|
+
lastLocaleKey = null; // force syncPage to reload (localStorage-resolved for base routes)
|
|
248
|
+
syncPage({ soft: true });
|
|
238
249
|
}
|
|
239
250
|
return true;
|
|
240
251
|
}
|
|
@@ -231,6 +231,11 @@ export function usePageMeta({ siteData, currentPage, locale }) {
|
|
|
231
231
|
}
|
|
232
232
|
|
|
233
233
|
return {
|
|
234
|
+
// Reflect the page's locale on <html lang> during SSR/prerender so the
|
|
235
|
+
// on-disk HTML for /{locale}/... ships the right language (the router
|
|
236
|
+
// guard only sets this client-side, after hydration). Base-locale pages
|
|
237
|
+
// fall back to the configured base locale.
|
|
238
|
+
htmlAttrs: { lang: localeValue.value || configBaseLocale || 'en' },
|
|
234
239
|
title: pageMetaTitle.value,
|
|
235
240
|
meta,
|
|
236
241
|
link,
|
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, primeConfigSync } from './utils/loadConfig.js';
|
|
9
|
+
import { loadConfigData, primeConfigSync, trimConfigToPage, resolvePageIdForRoute } 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';
|
|
@@ -145,6 +145,18 @@ export function createCmsApp() {
|
|
|
145
145
|
return null;
|
|
146
146
|
};
|
|
147
147
|
|
|
148
|
+
// SSG only: resolve the route being pre-rendered to its page id so the
|
|
149
|
+
// embedded config can be trimmed to that single page when the site opts
|
|
150
|
+
// into `site.trimInitialState`. The matching rules live in the pure,
|
|
151
|
+
// unit-tested resolvePageIdForRoute (returns null on ambiguity → keep the
|
|
152
|
+
// full config, a safe no-op).
|
|
153
|
+
const resolveCurrentPageId = (config) => {
|
|
154
|
+
const route = extractCurrentRoute();
|
|
155
|
+
const rawPath = typeof route?.path === 'string' ? route.path : '';
|
|
156
|
+
const localeParam = pickLocaleParam(route?.params?.locale);
|
|
157
|
+
return resolvePageIdForRoute(config, rawPath, localeParam);
|
|
158
|
+
};
|
|
159
|
+
|
|
148
160
|
const loadSiteConfig = async (options = {}) => {
|
|
149
161
|
const requestedLocale =
|
|
150
162
|
pickLocaleParam(options.locale) ??
|
|
@@ -178,6 +190,18 @@ export function createCmsApp() {
|
|
|
178
190
|
if (!isClient) {
|
|
179
191
|
try {
|
|
180
192
|
await loadSiteConfig();
|
|
193
|
+
// Opt-in payload trim: embed only the current page's config in
|
|
194
|
+
// window.__INITIAL_STATE__ rather than every page's (the full-site
|
|
195
|
+
// blob is byte-identical on every page and can dominate page weight).
|
|
196
|
+
// The rendered HTML body is unaffected — usePageConfig resolves the
|
|
197
|
+
// SSR render from its own loadConfigData() call, not from
|
|
198
|
+
// initialState — so this only shrinks the serialized hydration blob.
|
|
199
|
+
if (initialState.siteConfig?.site?.trimInitialState === true) {
|
|
200
|
+
const currentPageId = resolveCurrentPageId(initialState.siteConfig);
|
|
201
|
+
if (currentPageId) {
|
|
202
|
+
initialState.siteConfig = trimConfigToPage(initialState.siteConfig, currentPageId);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
181
205
|
} catch (error) {
|
|
182
206
|
console.error('Failed to load site configuration during SSG build', error);
|
|
183
207
|
}
|
package/src/utils/loadConfig.js
CHANGED
|
@@ -34,6 +34,49 @@ export function cloneConfig(value) {
|
|
|
34
34
|
return value;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
// Reduce a resolved config's `pages` map to a single page, preserving `site`
|
|
38
|
+
// and `shared` (which every page needs globally). Used by the SSG pass when a
|
|
39
|
+
// site opts into `site.trimInitialState` so each pre-rendered page embeds only
|
|
40
|
+
// its own page config in `window.__INITIAL_STATE__` instead of the entire
|
|
41
|
+
// site's pages (which is byte-identical on every page and can dominate the
|
|
42
|
+
// page weight). Marks the result `pagesPartial: true` so the client knows the
|
|
43
|
+
// embedded config can't resolve other pages and must lazy-load the full config
|
|
44
|
+
// before in-SPA navigation (see usePageConfig.hydrateFromSyncCache). Returns
|
|
45
|
+
// the config untouched when the page id is absent — a safe no-op fallback so a
|
|
46
|
+
// mis-resolved route never trims away the page actually being rendered.
|
|
47
|
+
export function trimConfigToPage(config, pageId) {
|
|
48
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) return config;
|
|
49
|
+
if (!config.pages || typeof config.pages !== 'object') return config;
|
|
50
|
+
if (!pageId || !Object.prototype.hasOwnProperty.call(config.pages, pageId)) return config;
|
|
51
|
+
return { ...config, pages: { [pageId]: config.pages[pageId] }, pagesPartial: true };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Map a route being pre-rendered to its page id, for the SSG trim above.
|
|
55
|
+
// Strips a leading `/{locale}` segment (non-base locales render at
|
|
56
|
+
// `/{locale}/path`), normalizes trailing/duplicate slashes, and matches against
|
|
57
|
+
// each page's `path` (defaulting a path-less page to '/'). Returns the id only
|
|
58
|
+
// on an UNAMBIGUOUS single match — home, path-less, or colliding routes return
|
|
59
|
+
// null so the caller keeps the full config (a safe no-op). Pure/exported so the
|
|
60
|
+
// locale-strip + matching rules are unit-testable without a full SSG build.
|
|
61
|
+
export function resolvePageIdForRoute(config, routePath, localeParam) {
|
|
62
|
+
if (typeof routePath !== 'string' || !routePath) return null;
|
|
63
|
+
let target = routePath;
|
|
64
|
+
if (localeParam && typeof localeParam === 'string') {
|
|
65
|
+
const prefix = `/${localeParam}`;
|
|
66
|
+
if (target === prefix) target = '/';
|
|
67
|
+
else if (target.startsWith(`${prefix}/`)) target = target.slice(prefix.length);
|
|
68
|
+
}
|
|
69
|
+
const normalize = (p) =>
|
|
70
|
+
(typeof p === 'string' ? p : '/').replace(/\/+/g, '/').replace(/\/+$/, '') || '/';
|
|
71
|
+
target = normalize(target);
|
|
72
|
+
const pages = (config && config.pages) || {};
|
|
73
|
+
const matches = Object.entries(pages).filter(([, data]) => {
|
|
74
|
+
const pagePath = data && typeof data.path === 'string' && data.path.trim() ? data.path : '/';
|
|
75
|
+
return normalize(pagePath) === target;
|
|
76
|
+
});
|
|
77
|
+
return matches.length === 1 ? matches[0][0] : null;
|
|
78
|
+
}
|
|
79
|
+
|
|
37
80
|
export function mergeConfigTrees(target, source, options = {}) {
|
|
38
81
|
const { cloneTarget = false, skipEmpty = true } = options;
|
|
39
82
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "vite",
|
|
8
8
|
"build": "vite build",
|
|
9
|
-
"build:ssg": "vite-ssg build",
|
|
9
|
+
"build:ssg": "NODE_OPTIONS=--max-old-space-size=4096 vite-ssg build",
|
|
10
10
|
"preview": "vite preview",
|
|
11
11
|
"generate:public-assets": "cms-generate-public-assets --site-dir ./site"
|
|
12
12
|
},
|
package/vite-plugin.js
CHANGED
|
@@ -576,6 +576,14 @@ export default function cmsPlugin(options = {}) {
|
|
|
576
576
|
},
|
|
577
577
|
ssgOptions: {
|
|
578
578
|
dirStyle: 'nested',
|
|
579
|
+
// Cap vite-ssg's render pool (default is 20). The SSG pre-render
|
|
580
|
+
// leaves a fixed retained cost per route (one jsdom document survives
|
|
581
|
+
// each page inside vite-ssg's loop), so peak heap is dominated by that
|
|
582
|
+
// accumulation rather than the concurrent batch — but a smaller pool
|
|
583
|
+
// still trims the transient working set layered on top and makes peak
|
|
584
|
+
// more predictable on large multi-locale sites. This is a mitigation,
|
|
585
|
+
// not the bound; see SSG-MEMORY-PLAN.md for the durable fix.
|
|
586
|
+
concurrency: 8,
|
|
579
587
|
// After vite-ssg pre-renders every route, copy the rendered
|
|
580
588
|
// dist/404/index.html to dist/404.html. AWS Amplify (and most
|
|
581
589
|
// static hosts) auto-serve a top-level 404.html for any
|