umberto 10.4.0 → 10.4.1

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/CHANGELOG.md CHANGED
@@ -1,6 +1,13 @@
1
1
  Changelog
2
2
  =========
3
3
 
4
+ ## [10.4.1](https://github.com/cksource/umberto/compare/v10.4.0...v10.4.1) (April 13, 2026)
5
+
6
+ ### Bug fixes
7
+
8
+ * Fixed URL in the dropdown for LTS item to have absolute URL to pass documentation validation.
9
+
10
+
4
11
  ## [10.4.0](https://github.com/cksource/umberto/compare/v10.3.0...v10.4.0) (April 9, 2026)
5
12
 
6
13
  ### Features
@@ -40,13 +47,6 @@ Changelog
40
47
 
41
48
  * Filter search results from Algolia to match the current docs version: `latest` or `lts-v47`. Filtering remains backward-compatible: hits without the docs version are still allowed.
42
49
 
43
-
44
- ## [10.1.3](https://github.com/cksource/umberto/compare/v10.1.2...v10.1.3) (March 10, 2026)
45
-
46
- ### Bug fixes
47
-
48
- * Fix regression introduced in `v10.1.2` affecting slugs generated for headings. Slugs generated now should be the same as in `v10.1.1` and previous versions of Umberto.
49
-
50
50
  ---
51
51
 
52
52
  To see all releases, visit the [release page](https://github.com/cksource/umberto/releases).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "umberto",
3
- "version": "10.4.0",
3
+ "version": "10.4.1",
4
4
  "description": "CKSource Documentation builder",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -163,8 +163,7 @@ export const buildDocumentation = options => {
163
163
  { name: 'CKEditor 5', slug: 'ckeditor5' },
164
164
  {
165
165
  name: 'CKEditor 5 LTS Edition',
166
- slug: 'ckeditor5',
167
- channel: 'lts-v47',
166
+ href: 'https://ckeditor.com/docs/ckeditor5/lts-v47/index.html',
168
167
  rel: 'nofollow'
169
168
  },
170
169
  { name: 'Cloud Services', slug: 'cs' },
@@ -52,7 +52,7 @@ mixin dropdown({ id: dropdownId, className, preferredDirection, contentId, updat
52
52
  //- +dropdown-item({ label: 'Item with icon', icon: 'github' })
53
53
  //- +dropdown-item({ label: 'Custom link', href: '/custom-link', rel: 'nofollow' })
54
54
  //- +dropdown-item({ label: 'With ID', itemId: 'special-item' })
55
- mixin dropdown-item({ itemId, href, rel, active = false, narrow, label, icon, value, projectSlug, projectChannel } = {})
55
+ mixin dropdown-item({ itemId, href, rel, active = false, narrow, label, icon, value, projectKey } = {})
56
56
  - const tag = href ? 'a' : 'div';
57
57
 
58
58
  li.c-dropdown__list-item
@@ -65,8 +65,7 @@ mixin dropdown-item({ itemId, href, rel, active = false, narrow, label, icon, va
65
65
  'c-dropdown__item--narrow': !!narrow
66
66
  },
67
67
  data-item-id=itemId,
68
- data-project-slug=projectSlug,
69
- data-project-channel=projectChannel,
68
+ data-project-key=projectKey,
70
69
  data-value=value,
71
70
  tabindex='-1'
72
71
  )
@@ -1,6 +1,19 @@
1
1
  mixin nav-project-select-dropdown()
2
2
  -
3
- const getProjectHref = project => relative_url( page.path, `${ project.slug }/${ project.channel || 'latest' }/index.html` );
3
+ const getProjectKey = pathname => {
4
+ const segments = pathname.split( '/' ).filter( Boolean );
5
+
6
+ if ( segments[ 0 ] === 'docs' ) {
7
+ segments.shift();
8
+ }
9
+
10
+ const [ slug, rawChannel ] = segments;
11
+ const channel = /^\d+\.\d+\.\d+$/.test( rawChannel || '' ) ? 'latest' : ( rawChannel || 'latest' );
12
+
13
+ return slug ? `/${ slug }/${ channel }` : '';
14
+ };
15
+ const getProjectHref = project => project.href || relative_url( page.path, `${ project.slug }/${ project.channel || 'latest' }/index.html` );
16
+ const getItemProjectKey = project => getProjectKey( project.href ? new URL( project.href ).pathname : `/${ project.slug }/${ project.channel || 'latest' }/index.html` );
4
17
  const currentProject = projectLocals && quickNavigationProjects && quickNavigationProjects.find(
5
18
  project => project.slug === projectLocals.projectSlug && !project.channel
6
19
  );
@@ -16,29 +29,36 @@ mixin nav-project-select-dropdown()
16
29
  ( project.channel || 'latest' ) === 'latest',
17
30
  href: getProjectHref( project ),
18
31
  rel: project.rel,
19
- projectSlug: project.slug,
20
- projectChannel: project.channel || 'latest'
32
+ projectKey: getItemProjectKey( project )
21
33
  })
22
34
 
23
35
  if currentProject
24
36
  script.
25
37
  (function() {
26
38
  const script = document.currentScript;
39
+ const getProjectKey = pathname => {
40
+ const segments = pathname.split( '/' ).filter( Boolean );
41
+
42
+ if ( segments[ 0 ] === 'docs' ) {
43
+ segments.shift();
44
+ }
45
+
46
+ const [ slug, rawChannel ] = segments;
47
+ const channel = /^\d+\.\d+\.\d+$/.test( rawChannel || '' ) ? 'latest' : ( rawChannel || 'latest' );
48
+
49
+ return slug ? `/${ slug }/${ channel }` : '';
50
+ };
27
51
 
28
52
  document.addEventListener( 'DOMContentLoaded', () => {
29
53
  const dropdown = script?.previousElementSibling;
30
- const currentProjectSlug = !{ JSON.stringify( currentProject.slug ) };
31
- const segments = window.location.pathname.split( '/' ).filter( Boolean );
32
- const slugIndex = segments.indexOf( currentProjectSlug );
54
+ const currentProjectKey = getProjectKey( window.location.pathname );
33
55
 
34
- if ( !dropdown || slugIndex < 0 ) {
56
+ if ( !dropdown || !currentProjectKey ) {
35
57
  return;
36
58
  }
37
59
 
38
- const rawChannel = segments[ slugIndex + 1 ];
39
- const currentChannel = /^\d+\.\d+\.\d+$/.test( rawChannel || '' ) ? 'latest' : ( rawChannel || 'latest' );
40
- const items = Array.from( dropdown.querySelectorAll( '.c-dropdown__item[data-project-slug][data-project-channel]' ) );
41
- const activeItem = items.find( item => item.dataset.projectSlug === currentProjectSlug && item.dataset.projectChannel === currentChannel );
60
+ const items = Array.from( dropdown.querySelectorAll( '.c-dropdown__item[data-project-key]' ) );
61
+ const activeItem = items.find( item => item.dataset.projectKey === currentProjectKey );
42
62
 
43
63
  if ( !activeItem ) {
44
64
  return;
@@ -3,7 +3,20 @@ include ./slide-parts
3
3
  mixin mobile-nav()
4
4
  -
5
5
  const currentProject = projectLocals && projectsData.find( project => project.slug === projectLocals.projectSlug );
6
- const getProjectHref = project => relative_url( page.path, `${ project.slug }/${ project.channel || 'latest' }/index.html` );
6
+ const getProjectKey = pathname => {
7
+ const segments = pathname.split( '/' ).filter( Boolean );
8
+
9
+ if ( segments[ 0 ] === 'docs' ) {
10
+ segments.shift();
11
+ }
12
+
13
+ const [ slug, rawChannel ] = segments;
14
+ const channel = /^\d+\.\d+\.\d+$/.test( rawChannel || '' ) ? 'latest' : ( rawChannel || 'latest' );
15
+
16
+ return slug ? `/${ slug }/${ channel }` : '';
17
+ };
18
+ const getProjectHref = project => project.href || relative_url( page.path, `${ project.slug }/${ project.channel || 'latest' }/index.html` );
19
+ const getItemProjectKey = project => getProjectKey( project.href ? new URL( project.href ).pathname : `/${ project.slug }/${ project.channel || 'latest' }/index.html` );
7
20
  let initialSlideId = projectLocals ? `project-${projectLocals.projectSlug}` : 'products';
8
21
 
9
22
  // If the page has a category, set the initial slide to that category's slide
@@ -30,37 +43,43 @@ mixin mobile-nav()
30
43
  label: project.name,
31
44
  active: true,
32
45
  targetSlideId: `project-${ project.slug }`,
33
- projectSlug: project.slug,
34
- projectChannel: project.channel || 'latest'
46
+ projectKey: getItemProjectKey( project )
35
47
  })
36
48
  else
37
49
  +mobile-nav-item({
38
50
  href: getProjectHref( project ),
39
51
  label: project.name,
40
52
  rel: project.rel,
41
- projectSlug: project.slug,
42
- projectChannel: project.channel || 'latest'
53
+ projectKey: getItemProjectKey( project )
43
54
  })
44
55
 
45
56
  if currentProject
46
57
  script.
47
58
  (function() {
48
59
  const script = document.currentScript;
60
+ const getProjectKey = pathname => {
61
+ const segments = pathname.split( '/' ).filter( Boolean );
62
+
63
+ if ( segments[ 0 ] === 'docs' ) {
64
+ segments.shift();
65
+ }
66
+
67
+ const [ slug, rawChannel ] = segments;
68
+ const channel = /^\d+\.\d+\.\d+$/.test( rawChannel || '' ) ? 'latest' : ( rawChannel || 'latest' );
69
+
70
+ return slug ? `/${ slug }/${ channel }` : '';
71
+ };
49
72
 
50
73
  document.addEventListener( 'DOMContentLoaded', () => {
51
74
  const mobileNav = script?.previousElementSibling;
52
- const currentProjectSlug = !{ JSON.stringify( currentProject.slug ) };
53
- const segments = window.location.pathname.split( '/' ).filter( Boolean );
54
- const slugIndex = segments.indexOf( currentProjectSlug );
75
+ const currentProjectKey = getProjectKey( window.location.pathname );
55
76
 
56
- if ( !mobileNav || slugIndex < 0 ) {
77
+ if ( !mobileNav || !currentProjectKey ) {
57
78
  return;
58
79
  }
59
80
 
60
- const rawChannel = segments[ slugIndex + 1 ];
61
- const currentChannel = /^\d+\.\d+\.\d+$/.test( rawChannel || '' ) ? 'latest' : ( rawChannel || 'latest' );
62
- const items = Array.from( mobileNav.querySelectorAll( '.c-mobile-nav__item[data-project-slug][data-project-channel]' ) );
63
- const activeItem = items.find( item => item.dataset.projectSlug === currentProjectSlug && item.dataset.projectChannel === currentChannel );
81
+ const items = Array.from( mobileNav.querySelectorAll( '.c-mobile-nav__item[data-project-key]' ) );
82
+ const activeItem = items.find( item => item.dataset.projectKey === currentProjectKey );
64
83
 
65
84
  if ( !activeItem ) {
66
85
  return;
@@ -32,7 +32,7 @@ mixin mobile-nav-heading({ href, label })
32
32
  iconPosition: 'right',
33
33
  })
34
34
 
35
- mixin mobile-nav-item({ href, label, rel, targetSlideId, target, active = false, projectSlug, projectChannel })
35
+ mixin mobile-nav-item({ href, label, rel, targetSlideId, target, active = false, projectKey })
36
36
  li
37
37
  if targetSlideId
38
38
  +button({
@@ -49,15 +49,13 @@ mixin mobile-nav-item({ href, label, rel, targetSlideId, target, active = false,
49
49
  'aria-controls'=`slide-${targetSlideId}`,
50
50
  'aria-expanded'='false',
51
51
  'aria-haspopup'='true',
52
- 'data-project-slug'=projectSlug,
53
- 'data-project-channel'=projectChannel
52
+ 'data-project-key'=projectKey
54
53
  )
55
54
  else
56
55
  a.b-link.c-mobile-nav__item(
57
56
  href=href,
58
57
  rel=rel,
59
- data-project-slug=projectSlug,
60
- data-project-channel=projectChannel,
58
+ data-project-key=projectKey,
61
59
  class=active ? 'is-active' : '',
62
60
  target=target
63
61
  )= label