@koehler8/cms 1.0.0-beta.32 → 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@koehler8/cms",
3
- "version": "1.0.0-beta.32",
3
+ "version": "1.0.0-beta.33",
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",
@@ -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
- return locale === siteBaseLocale ? '/' : `/${locale}`;
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({
@@ -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) {