@koehler8/cms 1.0.0-beta.23 β†’ 1.0.0-beta.25

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.23",
3
+ "version": "1.0.0-beta.25",
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",
@@ -97,7 +97,7 @@
97
97
  <li v-for="locale in availableLocales" :key="locale">
98
98
  <a
99
99
  class="locale-option"
100
- :href="`/${locale}`"
100
+ :href="localeHref(locale)"
101
101
  @click="handleLocaleLinkClick(locale)"
102
102
  >
103
103
  <span class="locale-flag" aria-hidden="true">{{ localeEmojis[locale] || '🌐' }}</span>
@@ -120,7 +120,7 @@ import { computed, inject, nextTick, onBeforeUnmount, onMounted, ref, useSlots,
120
120
  import { resolveAsset } from '../utils/assetResolver.js';
121
121
  import { trackEvent } from '../utils/analytics.js';
122
122
  import { getExtensionComponent } from '../extensions/extensionLoader.js';
123
- import { availableLocales as siteLocales } from '../utils/loadConfig.js';
123
+ import { availableLocales as siteLocales, baseLocale as siteBaseLocale } from '../utils/loadConfig.js';
124
124
 
125
125
  const slots = useSlots();
126
126
  const hasActionsSlot = computed(() => Boolean(slots.actions));
@@ -129,6 +129,14 @@ const isLangOpen = ref(false);
129
129
  const isHeaderCompact = ref(false);
130
130
  const currentLocale = ref('');
131
131
  const availableLocales = computed(() => siteLocales.map((code) => code.trim()).filter(Boolean));
132
+
133
+ // The base locale is served at the unprefixed root path (`/`) β€” clicking
134
+ // the dropdown item for it should NOT navigate to `/{baseLocale}` because
135
+ // that URL doesn't pre-render and would 404. Build the link path
136
+ // accordingly: base β†’ `/`, others β†’ `/{locale}`.
137
+ const localeHref = (locale) => {
138
+ return locale === siteBaseLocale ? '/' : `/${locale}`;
139
+ };
132
140
  const localeLabels = {
133
141
  en: 'English',
134
142
  fr: 'FranΓ§ais',
@@ -406,6 +414,17 @@ const updateLocaleMenuPosition = () => {
406
414
  });
407
415
  }
408
416
  isLangOpen.value = false;
417
+ // Persist the user's explicit choice so it survives navigation to the
418
+ // unprefixed base-locale URL (`/`). The router guard only writes
419
+ // localStorage when a locale param is present in the URL β€” clicking
420
+ // the base locale navigates to `/` (no param), so the write must
421
+ // happen here instead. Also keeps loadConfigData's localStorage
422
+ // fallback in sync with the active selection.
423
+ if (!import.meta.env.SSR && typeof localStorage !== 'undefined') {
424
+ try {
425
+ localStorage.setItem('locale', locale);
426
+ } catch {}
427
+ }
409
428
  };
410
429
 
411
430
  onMounted(() => {
@@ -5,6 +5,19 @@ import Home from '../components/Home.vue';
5
5
 
6
6
  import { availableLocales, baseLocale } from '../utils/loadConfig.js';
7
7
 
8
+ // Test for `/{baseLocale}` or `/{baseLocale}/...` paths β€” these no longer
9
+ // pre-render or match the locale layout (the canonical URL for the base
10
+ // locale is unprefixed). Used by the router guard to redirect in-SPA
11
+ // navigation so external clicks land on the canonical URL instead of the
12
+ // SPA catch-all β†’ 404 path.
13
+ function stripBaseLocalePrefix(targetPath, base) {
14
+ if (!base || !targetPath) return null;
15
+ const root = `/${base}`;
16
+ if (targetPath === root) return '/';
17
+ if (targetPath.startsWith(`${root}/`)) return targetPath.slice(root.length) || '/';
18
+ return null;
19
+ }
20
+
8
21
  const LocaleLayout = {
9
22
  name: 'LocaleLayout',
10
23
  render() {
@@ -85,6 +98,17 @@ function normalizeLocale(value = '') {
85
98
 
86
99
  export function applyRouterGuards(router, localePrefixes = computeLocalePrefixes()) {
87
100
  router.beforeEach((to) => {
101
+ // SPA-side redirect for stale base-locale-prefixed URLs. When a user
102
+ // navigates inside the app to `/en/about` (and `en` is base), the URL
103
+ // doesn't pre-render and the catch-all renders the 404 page. Rewrite
104
+ // to the canonical unprefixed equivalent. Direct external hits to
105
+ // these URLs still need an Amplify customRule on the host side β€” see
106
+ // CLAUDE.md "URL hygiene".
107
+ const stripped = stripBaseLocalePrefix(to.path, baseLocale);
108
+ if (stripped !== null && stripped !== to.path) {
109
+ return { path: stripped, replace: true };
110
+ }
111
+
88
112
  const rawLocale = (to.params.locale || '').toString().trim();
89
113
 
90
114
  if (rawLocale) {
@@ -112,6 +136,19 @@ export function applyRouterGuards(router, localePrefixes = computeLocalePrefixes
112
136
  }
113
137
 
114
138
  if (!import.meta.env.SSR) {
139
+ // Browser-language auto-redirect on first visit only. If the user
140
+ // already has a stored locale preference (set by either an explicit
141
+ // dropdown click or a previous visit to a locale-prefixed URL),
142
+ // respect that β€” don't second-guess them with browser-language
143
+ // detection. This prevents the bug where clicking "EN" on /de
144
+ // navigates to / and the auto-detect bounces back to /de because
145
+ // the browser is set to German.
146
+ let storedPreference = '';
147
+ try {
148
+ storedPreference = (localStorage.getItem('locale') || '').toString().toLowerCase();
149
+ } catch {}
150
+ if (storedPreference) return;
151
+
115
152
  const navLang = (navigator.language || navigator.userLanguage || '').toString();
116
153
  const shortLang = normalizeLocale(navLang || '');
117
154