@koehler8/cms 1.0.0-beta.31 → 1.0.0-beta.33
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
|
@@ -135,7 +135,9 @@ const availableLocales = computed(() => siteLocales.map((code) => code.trim()).f
|
|
|
135
135
|
// that URL doesn't pre-render and would 404. Build the link path
|
|
136
136
|
// accordingly: base → `/`, others → `/{locale}`.
|
|
137
137
|
const localeHref = (locale) => {
|
|
138
|
-
|
|
138
|
+
if (locale === siteBaseLocale) return '/';
|
|
139
|
+
const slash = injectedSiteData.value?.site?.trailingSlash === true ? '/' : '';
|
|
140
|
+
return `/${locale}${slash}`;
|
|
139
141
|
};
|
|
140
142
|
const localeLabels = {
|
|
141
143
|
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({
|
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
|
}
|
|
@@ -6,8 +6,11 @@
|
|
|
6
6
|
* (site.draft, site.draftPaths, page.draft) regenerates the disallow list on
|
|
7
7
|
* every build.
|
|
8
8
|
*
|
|
9
|
-
* Always
|
|
10
|
-
*
|
|
9
|
+
* Always disallows /admin (a non-public route that is never listed in the
|
|
10
|
+
* sitemap). Compliance pages (/privacy, /terms, /cookies) are intentionally
|
|
11
|
+
* NOT disallowed: they are public, footer-linked, emit correct canonicals, and
|
|
12
|
+
* appear in sitemap.xml — disallowing them contradicts the sitemap and triggers
|
|
13
|
+
* "Blocked by robots.txt" (pages-in-a-sitemap) errors in Search Console. Adds:
|
|
11
14
|
* - "Disallow: /" when site.draft === true
|
|
12
15
|
* - "Disallow: <prefix>" for each entry in site.draftPaths
|
|
13
16
|
* - "Disallow: <path>" for each page with draft === true
|
|
@@ -17,7 +20,7 @@
|
|
|
17
20
|
|
|
18
21
|
import { normalizeDraftPath } from './draftMode.js';
|
|
19
22
|
|
|
20
|
-
const FRAMEWORK_DEFAULTS = ['/admin'
|
|
23
|
+
const FRAMEWORK_DEFAULTS = ['/admin'];
|
|
21
24
|
|
|
22
25
|
export function buildRobotsTxt(siteConfig, sitemapUrl = '') {
|
|
23
26
|
const lines = ['User-agent: *'];
|
|
@@ -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) {
|