@koehler8/cms 1.0.0-beta.32 → 1.0.0-beta.34
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
|
@@ -130,12 +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('/');
|
|
148
|
+
const slash = injectedSiteData.value?.site?.trailingSlash === true ? '/' : '';
|
|
149
|
+
if (locale === siteBaseLocale) {
|
|
150
|
+
return rest ? `/${rest}${slash}` : '/';
|
|
151
|
+
}
|
|
152
|
+
return rest ? `/${locale}/${rest}${slash}` : `/${locale}${slash}`;
|
|
139
153
|
};
|
|
140
154
|
const localeLabels = {
|
|
141
155
|
en: 'English',
|
|
@@ -56,6 +56,7 @@ export function usePageMeta({ siteData, currentPage, locale }) {
|
|
|
56
56
|
const siteTitle = computed(() => siteData.value?.site?.title || '');
|
|
57
57
|
const siteDescription = computed(() => siteData.value?.site?.description || '');
|
|
58
58
|
const siteUrl = computed(() => siteData.value?.site?.url || '');
|
|
59
|
+
const trailingSlash = computed(() => siteData.value?.site?.trailingSlash === true);
|
|
59
60
|
const localeValue = computed(() => {
|
|
60
61
|
if (typeof locale === 'function') return locale() || '';
|
|
61
62
|
if (typeof locale === 'string') return locale;
|
|
@@ -116,6 +117,7 @@ export function usePageMeta({ siteData, currentPage, locale }) {
|
|
|
116
117
|
locale: localeValue.value,
|
|
117
118
|
baseLocale: configBaseLocale,
|
|
118
119
|
path: currentPage.value?.path || '/',
|
|
120
|
+
trailingSlash: trailingSlash.value,
|
|
119
121
|
});
|
|
120
122
|
});
|
|
121
123
|
|
|
@@ -135,6 +137,7 @@ export function usePageMeta({ siteData, currentPage, locale }) {
|
|
|
135
137
|
locale: code,
|
|
136
138
|
baseLocale: configBaseLocale,
|
|
137
139
|
path,
|
|
140
|
+
trailingSlash: trailingSlash.value,
|
|
138
141
|
});
|
|
139
142
|
if (href) {
|
|
140
143
|
links.push({ rel: 'alternate', hreflang: code, href, key: `hreflang-${code}` });
|
|
@@ -145,6 +148,7 @@ export function usePageMeta({ siteData, currentPage, locale }) {
|
|
|
145
148
|
locale: configBaseLocale,
|
|
146
149
|
baseLocale: configBaseLocale,
|
|
147
150
|
path,
|
|
151
|
+
trailingSlash: trailingSlash.value,
|
|
148
152
|
});
|
|
149
153
|
if (defaultHref) {
|
|
150
154
|
links.push({ rel: 'alternate', hreflang: 'x-default', href: defaultHref, key: 'hreflang-x-default' });
|
|
@@ -216,6 +220,7 @@ export function usePageMeta({ siteData, currentPage, locale }) {
|
|
|
216
220
|
pages: siteData.value?.pages,
|
|
217
221
|
baseLocale: configBaseLocale,
|
|
218
222
|
locale: localeValue.value,
|
|
223
|
+
trailingSlash: trailingSlash.value,
|
|
219
224
|
});
|
|
220
225
|
if (breadcrumb && !isDraft.value && !isNotFound.value) {
|
|
221
226
|
script.push({
|
|
@@ -226,6 +231,11 @@ export function usePageMeta({ siteData, currentPage, locale }) {
|
|
|
226
231
|
}
|
|
227
232
|
|
|
228
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' },
|
|
229
239
|
title: pageMetaTitle.value,
|
|
230
240
|
meta,
|
|
231
241
|
link,
|
package/src/utils/breadcrumbs.js
CHANGED
|
@@ -63,6 +63,7 @@ export function buildBreadcrumbList({
|
|
|
63
63
|
pages,
|
|
64
64
|
baseLocale,
|
|
65
65
|
locale,
|
|
66
|
+
trailingSlash = false,
|
|
66
67
|
} = {}) {
|
|
67
68
|
if (!currentPage || typeof currentPage !== 'object') return null;
|
|
68
69
|
if (currentPage.isNotFound) return null;
|
|
@@ -83,6 +84,7 @@ export function buildBreadcrumbList({
|
|
|
83
84
|
locale,
|
|
84
85
|
baseLocale,
|
|
85
86
|
path: '/',
|
|
87
|
+
trailingSlash,
|
|
86
88
|
});
|
|
87
89
|
|
|
88
90
|
const items = [
|
|
@@ -101,6 +103,7 @@ export function buildBreadcrumbList({
|
|
|
101
103
|
locale,
|
|
102
104
|
baseLocale,
|
|
103
105
|
path: prefixPath,
|
|
106
|
+
trailingSlash,
|
|
104
107
|
});
|
|
105
108
|
items.push({
|
|
106
109
|
'@type': 'ListItem',
|
|
@@ -9,6 +9,14 @@
|
|
|
9
9
|
* - Base locale → ${siteUrl}${path} e.g. https://example.com/about
|
|
10
10
|
* - Non-base → ${siteUrl}/${locale}${path} e.g. https://example.com/de/about
|
|
11
11
|
*
|
|
12
|
+
* When `trailingSlash` is true, every non-root URL gets a trailing slash
|
|
13
|
+
* (`/about/`, `/de/`, `/de/about/`) — the root (`/`) is unchanged. This aligns
|
|
14
|
+
* the canonical with how a static host serving nested `dirStyle` output
|
|
15
|
+
* (`/about/index.html`) actually serves the page (`/about/`, with `/about`
|
|
16
|
+
* 301-redirecting to it), so the canonical names the URL that returns 200
|
|
17
|
+
* instead of one that redirects. Opt in per-site via `site.trailingSlash`;
|
|
18
|
+
* default (false) is byte-identical to the no-slash scheme.
|
|
19
|
+
*
|
|
12
20
|
* Returns '' when siteUrl is missing — sitemaps require absolute URLs and
|
|
13
21
|
* canonical links without a host are useless. Callers treat '' as "skip".
|
|
14
22
|
*/
|
|
@@ -28,17 +36,18 @@ function normalizeLocale(value) {
|
|
|
28
36
|
return trimmed;
|
|
29
37
|
}
|
|
30
38
|
|
|
31
|
-
export function buildCanonicalUrl({ siteUrl, locale, baseLocale, path } = {}) {
|
|
39
|
+
export function buildCanonicalUrl({ siteUrl, locale, baseLocale, path, trailingSlash = false } = {}) {
|
|
32
40
|
const host = typeof siteUrl === 'string' ? siteUrl.trim().replace(/\/+$/, '') : '';
|
|
33
41
|
if (!host) return '';
|
|
34
42
|
|
|
35
43
|
const normalizedPath = normalizePath(path);
|
|
36
44
|
const loc = normalizeLocale(locale);
|
|
37
45
|
const base = normalizeLocale(baseLocale);
|
|
46
|
+
const slash = trailingSlash ? '/' : '';
|
|
38
47
|
|
|
39
48
|
if (!loc || loc === base) {
|
|
40
|
-
return normalizedPath === '/' ? `${host}/` : `${host}${normalizedPath}`;
|
|
49
|
+
return normalizedPath === '/' ? `${host}/` : `${host}${normalizedPath}${slash}`;
|
|
41
50
|
}
|
|
42
51
|
|
|
43
|
-
return normalizedPath === '/' ? `${host}/${loc}` : `${host}/${loc}${normalizedPath}`;
|
|
52
|
+
return normalizedPath === '/' ? `${host}/${loc}${slash}` : `${host}/${loc}${normalizedPath}${slash}`;
|
|
44
53
|
}
|
|
@@ -27,6 +27,7 @@ function escapeXml(value) {
|
|
|
27
27
|
export function buildSitemap(siteConfig, options = {}) {
|
|
28
28
|
const siteUrl = (siteConfig?.site?.url || '').trim().replace(/\/+$/, '');
|
|
29
29
|
if (!siteUrl) return '';
|
|
30
|
+
const trailingSlash = siteConfig?.site?.trailingSlash === true;
|
|
30
31
|
|
|
31
32
|
// Whole-site draft → no sitemap. robots.txt emits "Disallow: /" in this
|
|
32
33
|
// state, and listing URLs in the sitemap that the same robots blocks
|
|
@@ -52,13 +53,13 @@ export function buildSitemap(siteConfig, options = {}) {
|
|
|
52
53
|
if (seenPaths.has(pagePath)) continue;
|
|
53
54
|
seenPaths.add(pagePath);
|
|
54
55
|
|
|
55
|
-
const baseUrl = buildCanonicalUrl({ siteUrl, baseLocale, locale: baseLocale, path: pagePath });
|
|
56
|
+
const baseUrl = buildCanonicalUrl({ siteUrl, baseLocale, locale: baseLocale, path: pagePath, trailingSlash });
|
|
56
57
|
if (!baseUrl) continue;
|
|
57
58
|
|
|
58
59
|
const alternates = isMultiLocale
|
|
59
60
|
? availableLocales.map((loc) => ({
|
|
60
61
|
hreflang: loc,
|
|
61
|
-
href: buildCanonicalUrl({ siteUrl, baseLocale, locale: loc, path: pagePath }),
|
|
62
|
+
href: buildCanonicalUrl({ siteUrl, baseLocale, locale: loc, path: pagePath, trailingSlash }),
|
|
62
63
|
}))
|
|
63
64
|
: [];
|
|
64
65
|
if (isMultiLocale) {
|