Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions assets/js/critical/js-detect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Swap the `no-js` marker class on the root element for `js` as early as possible, so
// stylesheets can key no-JS fallbacks (such as the sidebar collapse trail) off `no-js`.
(function () {
var el = document.documentElement
el.classList.remove('no-js')
el.classList.add('js')
}())
97 changes: 97 additions & 0 deletions assets/js/critical/sidebar-active.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Client-side sidebar active-item highlighting. The sidebar HTML is rendered once per
// (section, version, language, variant) and cached, so the server no longer emits per-page
// `active` classes or an expanded collapse trail; this script applies them from the current
// location. It runs in the critical bundle so the highlight lands before first paint.
(function () {
'use strict'

function normalize (path) {
try { path = decodeURI(path) } catch { /* keep the raw path */ }
if (path.length > 1) path = path.replace(/\/+$/, '')
return path || '/'
}

function findBestMatch (nav, pathname) {
var best = null
var bestLength = -1
var bestExact = false
nav.querySelectorAll('a[href]').forEach(function (link) {
var href = link.getAttribute('href')
if (!href || href.charAt(0) === '#') return
var url
try { url = new URL(href, window.location.origin) } catch { return }
if (url.origin !== window.location.origin) return
var path = normalize(url.pathname)
if (path === pathname) {
if (!bestExact) {
best = link
bestLength = path.length
bestExact = true
}
} else if (!bestExact && link.getAttribute('data-sidebar-match') === 'prefix') {
var prefix = path === '/' ? '/' : path + '/'
if (pathname.indexOf(prefix) === 0 && path.length > bestLength) {
best = link
bestLength = path.length
}
}
})
return { link: best, exact: bestExact }
}

function initNav (nav, pathname) {
var match = findBestMatch(nav, pathname)
var link = match.link
if (!link) return

nav.classList.add('sidebar-no-transition')
link.classList.add('active')
link.setAttribute('aria-current', match.exact ? 'page' : 'true')

// Expand the collapse trail: the matched group's own collapse (when the link is a group
// title) plus every collapse ancestor within this nav instance.
var own = null
var group = link.closest('.sidebar-item-group')
if (group && group.parentElement) {
own = group.parentElement.querySelector(':scope > .collapse')
}
var trail = own ? [own] : []
var ancestor = link.closest('.collapse')
while (ancestor && nav.contains(ancestor)) {
trail.push(ancestor)
ancestor = ancestor.parentElement && ancestor.parentElement.closest('.collapse')
}
trail.forEach(function (el) {
el.classList.add('show')
if (el.id) {
nav.querySelectorAll('[data-bs-target="#' + el.id + '"]').forEach(function (toggle) {
toggle.setAttribute('aria-expanded', 'true')
})
}
})
nav.offsetHeight // force reflow before re-enabling transitions
nav.classList.remove('sidebar-no-transition')

// Clicking the current group's title toggles its collapse instead of re-navigating.
if (own && match.exact) {
link.addEventListener('click', function (event) {
if (typeof bootstrap === 'undefined' || !bootstrap.Collapse) return
event.preventDefault()
bootstrap.Collapse.getOrCreateInstance(own).toggle()
})
}
}

function init () {
var pathname = normalize(window.location.pathname)
document.querySelectorAll('[data-sidebar-nav]').forEach(function (nav) {
initNav(nav, pathname)
})
}

if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init)
} else {
init()
}
}())
8 changes: 8 additions & 0 deletions assets/scss/components/_sidebar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,14 @@ html.sidebar-pre-collapsed .sidebar-collapsible .sidebar-brand {
}
}

// No-JS fallback: the collapse trail and active highlight are applied client-side by
// sidebar-active.js, and Bootstrap's collapse toggles require JavaScript. Without JavaScript
// (the `no-js` class set in baseof.html is swapped for `js` by critical/js-detect.js) render
// every sidebar group expanded instead.
.no-js .sidebar .collapse {
display: block;
}

// Collapsible sidebar
.sidebar-no-transition,
.sidebar-no-transition * {
Expand Down
10 changes: 10 additions & 0 deletions data/structures/link.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ arguments:
optional: true
comment: Skip internal link validation (use for dynamic content).
release: v2.0.0
icon-mode:
type: string
optional: true
group: partial
release: v3.3.0
comment: >-
Per-call rendering mode of the external-link cue icon (one of `symbols`,
`svg`, or `webfonts`), passed through to the icon partial. Cached
contexts such as the sidebar pass `svg` to avoid per-page sprite
registration; defaults to the globally configured icon mode.
# deprecated arguments
destination:
type: string
Expand Down
18 changes: 18 additions & 0 deletions data/structures/sidebar.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,24 @@ arguments:
comment: >-
Version of the sidebar navigation, used to define the base URL of
generated links, together with the page's section.
baseURL:
type: string
optional: true
group: partial
release: v3.3.0
comment: >-
Base URL of the site for the current language, used to resolve relative
menu links. Pass it explicitly (see `page/sidebar-cached.html`) so the
renderer performs no per-page Scratch reads; defaults to `/`.
filename:
type: string
optional: true
group: partial
release: v3.3.0
comment: >-
Path of the data file that defines the sidebar menu, used in error
messages only. Passed explicitly by `page/sidebar-cached.html` so the
renderer performs no per-page Scratch reads.
auto-generate:
type: bool
optional: true
Expand Down
2 changes: 1 addition & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export default [
}
},
{
files: ['assets/js/sidebar-toggle.js'],
files: ['assets/js/sidebar-toggle.js', 'assets/js/critical/sidebar-active.js'],
languageOptions: {
globals: {
bootstrap: 'readonly'
Expand Down
4 changes: 4 additions & 0 deletions exampleSite/hinode.work.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/FortAwesome/Font-Awesome v0.0.0-20260715180930-14c65a3747d0 h1:4+bB2ojsMTsQroHtQr1FWy2gxFzpBn5hISldeRfxF+o=
github.com/FortAwesome/Font-Awesome v0.0.0-20260715180930-14c65a3747d0/go.mod h1:IUgezN/MFpCDIlFezw3L8j83oeiIuYoj28Miwr/KUYo=
github.com/cloudcannon/bookshop/hugo/v3 v3.17.1 h1:weTVWBamjQHMIp/oYTFsPwRzzhWrZA6JO43QnxI1kxw=
github.com/cloudcannon/bookshop/hugo/v3 v3.17.1/go.mod h1:s7mIonDhtsLcn10ZKuVXyqd6BDHI8vT1WQhZw8rPfY8=
github.com/cloudcannon/bookshop/hugo/v3 v3.18.2 h1:j3XUvvuCv/7SfGKzd7gzb3WEgs1DurqTRDe7gdMAAvU=
Expand All @@ -14,6 +16,8 @@ github.com/gethinode/mod-fontawesome/v2 v2.1.2 h1:v1aHhbLLwe/05zRHnx9qGqh6b3toDz
github.com/gethinode/mod-fontawesome/v2 v2.1.2/go.mod h1:zukv88wXqquEvTJJ9mWWk8Ia+9INnA41wYqusf2RcHA=
github.com/gethinode/mod-fontawesome/v3 v3.1.3 h1:xlwmdgulIV/IMj5I1/fH4tPM1/AU0OYZ4vepQiJbOYA=
github.com/gethinode/mod-fontawesome/v3 v3.1.3/go.mod h1:+E6jSVNv7h6oXw4Wm1XRq8HBEB2WrvxCPkp6u4vY5xo=
github.com/gethinode/mod-fontawesome/v6 v6.1.0 h1:hvhBAHuICZYAiQdmcd+qtLRoiJAIgU7/5sEbs1GoVW4=
github.com/gethinode/mod-fontawesome/v6 v6.1.0/go.mod h1:875OBk5te+KBJmr0PdSkFcduYnsKCuHWfcaqChh9NGo=
github.com/gethinode/mod-utils/v4 v4.12.0 h1:5sSfYIxZCeQbXLoZdS//rl6thwLwtXuvM0ujaWKyPmc=
github.com/gethinode/mod-utils/v4 v4.12.0/go.mod h1:bYmvRdAo4ICy5MpSGafDvO4p5bTDpsDKFCPL3bH0mN4=
github.com/gethinode/mod-utils/v6 v6.5.0/go.mod h1:E5tO9w3VKaidJpu1nI8zAKmh0bddFHOIIQnudAaXQTs=
Expand Down
4 changes: 3 additions & 1 deletion layouts/_partials/assets/link.html
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@
{{- end -}}

{{- if $cue -}}
{{- $suffix := partial "assets/icon.html" (dict "page" $args.page "icon" $externalLink "class" "fa-2xs" "spacing" false) -}}
{{- $iconArgs := dict "page" $args.page "icon" $externalLink "class" "fa-2xs" "spacing" false -}}
{{- with $args.iconMode }}{{ $iconArgs = merge $iconArgs (dict "mode" .) }}{{ end -}}
{{- $suffix := partial "assets/icon.html" $iconArgs -}}
{{- $text = printf "%s %s" $text $suffix | safeHTML -}}
{{- end -}}
{{- else -}}
Expand Down
Loading