@koehler8/cms 1.0.0-beta.22 β 1.0.0-beta.24
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 +10 -2
- package/src/router/index.js +24 -0
- package/vite-plugin.js +12 -5
package/package.json
CHANGED
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
<li v-for="locale in availableLocales" :key="locale">
|
|
98
98
|
<a
|
|
99
99
|
class="locale-option"
|
|
100
|
-
:href="
|
|
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',
|
package/src/router/index.js
CHANGED
|
@@ -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) {
|
package/vite-plugin.js
CHANGED
|
@@ -466,16 +466,23 @@ export default function cmsPlugin(options = {}) {
|
|
|
466
466
|
if (!siteConfig) {
|
|
467
467
|
throw new Error(`[@koehler8/cms] site config not found at ${configDir}`);
|
|
468
468
|
}
|
|
469
|
-
// Discover locales actually present on disk:
|
|
470
|
-
// content/ that
|
|
471
|
-
//
|
|
472
|
-
//
|
|
469
|
+
// Discover locales actually present on disk: any directory under
|
|
470
|
+
// content/ that has at least one of `site.json`, `shared.json`, or
|
|
471
|
+
// a `pages/` subdir is treated as a real locale. (Earlier the check
|
|
472
|
+
// required `site.json` specifically β too strict for sites that
|
|
473
|
+
// author per-locale page overrides without overriding the site
|
|
474
|
+
// metadata, which is the common pattern for translation-only locale
|
|
475
|
+
// dirs.)
|
|
476
|
+
const localeDirHasContent = (dir) =>
|
|
477
|
+
fs.existsSync(path.join(dir, 'site.json'))
|
|
478
|
+
|| fs.existsSync(path.join(dir, 'shared.json'))
|
|
479
|
+
|| fs.existsSync(path.join(dir, 'pages'));
|
|
473
480
|
availableLocales = fs.existsSync(contentDir)
|
|
474
481
|
? fs
|
|
475
482
|
.readdirSync(contentDir, { withFileTypes: true })
|
|
476
483
|
.filter((d) => d.isDirectory())
|
|
477
484
|
.map((d) => d.name)
|
|
478
|
-
.filter((name) =>
|
|
485
|
+
.filter((name) => localeDirHasContent(path.join(contentDir, name)))
|
|
479
486
|
.sort()
|
|
480
487
|
: [];
|
|
481
488
|
pagePaths = collectPagePaths(siteConfig);
|