Repository: scientific-python/scientific-python-hugo-theme
Pull request #668: Fix the rendering of shortcuts with branch bundles, and improve shortcut header performance
Author: login: agriyakhetarpal
Labels: name: type: Enhancement
description: New feature or request
Created: 2025-03-04T23:12:53Z | Merged: 2025-03-18T23:21:24Z

## Description

The `shortcutDepth` parameter wasn't working correctly with branch bundles (`_index.md` files); while the navigation shortcuts would appear and function correctly for leaf bundles (`index.md` files), they were missing entirely for their counterparts. This is because of an extra div that wrapped `{{ .Content }}`. This pull request fixes the rendering of the shortcuts section/secondary sidebar for both types.

For example, the "User Guide" page is a part of a branch bundle, and currently has a specified shortcut depth of "2"

https://github.com/scientific-python/scientific-python-hugo-theme/blob/02d8fa7b526c449b610a2017cb0395573c9d03da/doc/content/user_guide/_index.md?plain=1#L1-L7

but the corresponding HTML page at https://theme.scientific-python.org/user_guide/ does not display the shortcuts (see below).

A few more ancillary changes were performed:
- A fix for missing headers on https://scipy.org/install – the `[role='tabpanel'] > h${i}` selector specifically targets headers that are direct children of tabpanels
- A performance optimisation where the `scrollHeaders` calculations are not made by returning early if shortcuts don't exist;
- allow the shortcuts to be adjusted dynamically if the contents change slightly after the page is loaded.

## Comparison

<details>
<summary>Before</summary>

<br>
<img width="1512" alt="image" src="https://github.com/user-attachments/assets/be4cbb4d-0f5b-4fdb-ad83-1efadbba654f" />
<br>

</details>

<details>
<summary>After</summary>

<br>
<img width="1512" alt="image" src="https://github.com/user-attachments/assets/6c98298b-76e6-4235-968c-a406b44b849a" />
<br>

</details>


## Additional context

https://gohugo.io/content-management/page-bundles/



Diff:
diff --git a/assets/js/shortcuts.js b/assets/js/shortcuts.js
index 74a43ff4..20894ff7 100644
--- a/assets/js/shortcuts.js
+++ b/assets/js/shortcuts.js
@@ -33,9 +33,12 @@ function scrollHeaders() {
   const headers = Array.from(
     document.querySelectorAll(":is(h1, h2, h3, h4, h5, h6)[id]"),
   );
-  const allShortcuts = Array.from(
-    document.querySelectorAll("#shortcuts > div"),
-  );
+  const shortcuts = document.getElementById("shortcuts");
+
+  // Exit early if shortcuts container doesn't exist
+  if (!shortcuts) return;
+
+  const allShortcuts = Array.from(shortcuts.querySelectorAll("div"));
 
   headers.map((currentSection) => {
     // get the position of the section
@@ -81,22 +84,38 @@ function remToPx(rem) {
 }
 
 function setupShortcuts(shortcutDepth = 2) {
+  // Find the shortcut target container in the sidebar
+  const shortcutsTarget = document.getElementById("shortcuts");
+
+  /*
+   * Exit early if shortcuts container doesn't exist
+   * This is important to avoid errors when the theme
+   * is used on pages that don't have the shortcuts sidebar
+   */
+  if (!shortcutsTarget) {
+    return;
+  }
+
   shortcutDepth += 1; // to account for the page title
 
-  // Build a class selector for each header type, and concatenate with commas
+  // Build a class selector for each header level
   let classes = "";
   for (let i = 2; i <= shortcutDepth; i++) {
     if (i != 2) {
       classes += ",";
     }
-    classes += " .content-container :not([role='tabpanel']) > h" + i;
+    classes += `.content-container > h${i}`;
   }
 
-  // Content Page Shortcuts
-  const shortcutsTarget = document.getElementById("shortcuts");
-  if (shortcutsTarget) {
-    const classElements = Array.from(document.querySelectorAll(classes));
+  const classElements = Array.from(document.querySelectorAll(classes));
+
+  // Only proceed if we found headers (to avoid creating an empty shortcuts section)
+  if (classElements.length > 0) {
     classElements.map((el) => {
+      if (!el.id) {
+        return;
+      }
+
       const title = el.innerHTML;
       const elId = el.id;
       // Gets the element type (e.g. h2, h3)
@@ -139,10 +158,13 @@ function setupShortcuts(shortcutDepth = 2) {
   const shortcuts = Array.from(
     document.querySelectorAll("#shortcuts div:not(#shortcuts-header)"),
   );
-  if (shortcuts.length == 0) {
-    const shortcutsContainer = document.getElementById("shortcuts-container");
-    if (shortcutsContainer) {
+  const shortcutsContainer = document.getElementById("shortcuts-container");
+
+  if (shortcutsContainer) {
+    if (shortcuts.length == 0) {
       shortcutsContainer.style.display = "none";
+    } else {
+      shortcutsContainer.style.display = ""; // make shortcuts display visible, if hidden
     }
   }
 
diff --git a/layouts/_default/single.html b/layouts/_default/single.html
index 8f8f0ad1..220cbcdd 100644
--- a/layouts/_default/single.html
+++ b/layouts/_default/single.html
@@ -6,9 +6,7 @@ <h1>{{ .Title }}</h1>
     {{ if .Params.Subtitle }}
     <h5 class="subtitle">{{ .Params.Subtitle }}</h5>
     {{ end }}
-    <div>
-      {{ .Content }}
-    </div>
+    {{ .Content }}
   </div>
   {{ partial "shortcuts.html" . }}
 </section>


Reviews:
user: login: agriyakhetarpal
body: Here's a self-review with a few comments to explain the changes better that I decided not to put inside in-line comments.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-04T23:21:10Z
user: login: stefanv
body: Thanks! Looks good overall, I just don't completely understand the modification made for leaf bundles.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-04T23:39:08Z
user: login: agriyakhetarpal
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-04T23:43:23Z
user: login: agriyakhetarpal
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-04T23:49:01Z
user: login: agriyakhetarpal
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-04T23:52:41Z
user: login: stefanv
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-04T23:55:13Z
user: login: agriyakhetarpal
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T00:20:27Z
user: login: stefanv
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T05:43:06Z
user: login: stefanv
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T05:43:56Z
user: login: agriyakhetarpal
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T11:02:01Z
user: login: agriyakhetarpal
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T12:29:10Z
user: login: stefanv
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T22:34:43Z
user: login: agriyakhetarpal
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T22:45:27Z
user: login: stefanv
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-07T08:59:41Z
user: login: agriyakhetarpal
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-07T12:31:17Z
user: login: agriyakhetarpal
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T11:52:44Z
user: login: stefanv
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T21:05:06Z
user: login: stefanv
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T21:21:55Z
user: login: agriyakhetarpal
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T21:22:25Z
user: login: agriyakhetarpal
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T21:24:35Z
user: login: stefanv
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T21:56:43Z
user: login: stefanv
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-18T23:21:03Z

Review comments:
diff_hunk: @@ -33,9 +33,12 @@ function scrollHeaders() {
   const headers = Array.from(
     document.querySelectorAll(":is(h1, h2, h3, h4, h5, h6)[id]"),
   );
-  const allShortcuts = Array.from(
-    document.querySelectorAll("#shortcuts > div"),
-  );
+  const shortcuts = document.getElementById("shortcuts");
+
+  // Exit early if shortcuts container doesn't exist
+  if (!shortcuts) return;
path: assets/js/shortcuts.js
user: login: agriyakhetarpal
body: This check is also repeated below; the difference in this context is that it exists purely as a performance optimization - there's no need for `scrollHeaders` to perform scroll calculations if there's no shortcuts container to update.
created_at: 2025-03-04T23:14:48Z
author_association: MEMBER
diff_hunk: @@ -81,6 +84,18 @@ function remToPx(rem) {
 }
 
 function setupShortcuts(shortcutDepth = 2) {
+  // Find the shortcut target container in the sidebar
+  const shortcutsTarget = document.getElementById("shortcuts");
+
+  /*
+   * Exit early if shortcuts container doesn't exist
+   * This is important to avoid errors when the theme
+   * is used on pages that don't have the shortcuts sidebar
+   */
+  if (!shortcutsTarget) {
+    return;
+  }
path: assets/js/shortcuts.js
user: login: agriyakhetarpal
body: This check sets up the shortcuts when the page first loads (once), differing from the above one as it prevents attempting to build the shortcuts secyion if there is nowhere to put them.
created_at: 2025-03-04T23:16:03Z
author_association: MEMBER
diff_hunk: @@ -144,6 +173,11 @@ function setupShortcuts(shortcutDepth = 2) {
     if (shortcutsContainer) {
       shortcutsContainer.style.display = "none";
     }
+  } else {
+    const shortcutsContainer = document.getElementById("shortcuts-container");
+    if (shortcutsContainer && shortcutsContainer.style.display === "none") {
+      shortcutsContainer.style.display = "";
+    }
path: assets/js/shortcuts.js
user: login: agriyakhetarpal
body: This `else` block might seem redundant, but it has been added to address dynamic content loading – i.e., if the content changes and headers are added after the initial moment the page is loaded, the shortcuts container needs to become visible (or be evaluated for visibility) again. We use a custom shortcode to fetch Markdown content from a raw GitHub URL downstream. This block makes it aware of such changes, instead of keeping the shortcut container hidden.
created_at: 2025-03-04T23:19:56Z
author_association: MEMBER
diff_hunk: @@ -92,11 +107,25 @@ function setupShortcuts(shortcutDepth = 2) {
     classes += " .content-container :not([role='tabpanel']) > h" + i;
   }
 
-  // Content Page Shortcuts
-  const shortcutsTarget = document.getElementById("shortcuts");
-  if (shortcutsTarget) {
-    const classElements = Array.from(document.querySelectorAll(classes));
+  /*
+   * Add selectors for branch bundles (_index.md files). In this case,
+   * branch bundles often have a different DOM structure than leaf bundles,
+   * so we need to include headers that are direct children of the content
+   * container here.
+   */
+  for (let i = 2; i <= shortcutDepth; i++) {
path: assets/js/shortcuts.js
user: login: stefanv
body: I'm struggling to see the difference between what's happening here and on line 103-107?
created_at: 2025-03-04T23:34:55Z
author_association: MEMBER
diff_hunk: @@ -144,6 +173,11 @@ function setupShortcuts(shortcutDepth = 2) {
     if (shortcutsContainer) {
       shortcutsContainer.style.display = "none";
     }
+  } else {
+    const shortcutsContainer = document.getElementById("shortcuts-container");
+    if (shortcutsContainer && shortcutsContainer.style.display === "none") {
+      shortcutsContainer.style.display = "";
+    }
path: assets/js/shortcuts.js
user: login: stefanv
body: Makes sense, although probably fine to simply do:

```
if (shortcutsContainer) {
  shortcutsContainer.style.display = ""; // make shortcuts display visible, if hidden
}
```
created_at: 2025-03-04T23:38:47Z
author_association: MEMBER
diff_hunk: @@ -92,11 +107,25 @@ function setupShortcuts(shortcutDepth = 2) {
     classes += " .content-container :not([role='tabpanel']) > h" + i;
   }
 
-  // Content Page Shortcuts
-  const shortcutsTarget = document.getElementById("shortcuts");
-  if (shortcutsTarget) {
-    const classElements = Array.from(document.querySelectorAll(classes));
+  /*
+   * Add selectors for branch bundles (_index.md files). In this case,
+   * branch bundles often have a different DOM structure than leaf bundles,
+   * so we need to include headers that are direct children of the content
+   * container here.
+   */
+  for (let i = 2; i <= shortcutDepth; i++) {
path: assets/js/shortcuts.js
user: login: agriyakhetarpal
body: Ah, lines 103–107 handle leaf bundles, while these lines handle branch bundles.
created_at: 2025-03-04T23:43:23Z
author_association: MEMBER
diff_hunk: @@ -144,6 +173,11 @@ function setupShortcuts(shortcutDepth = 2) {
     if (shortcutsContainer) {
       shortcutsContainer.style.display = "none";
     }
+  } else {
+    const shortcutsContainer = document.getElementById("shortcuts-container");
+    if (shortcutsContainer && shortcutsContainer.style.display === "none") {
+      shortcutsContainer.style.display = "";
+    }
path: assets/js/shortcuts.js
user: login: agriyakhetarpal
body: Done, thanks! baa71d49f3621eeb8a61968562b1eaf16ea141c0
created_at: 2025-03-04T23:49:01Z
author_association: MEMBER
diff_hunk: @@ -92,11 +107,25 @@ function setupShortcuts(shortcutDepth = 2) {
     classes += " .content-container :not([role='tabpanel']) > h" + i;
   }
 
-  // Content Page Shortcuts
-  const shortcutsTarget = document.getElementById("shortcuts");
-  if (shortcutsTarget) {
-    const classElements = Array.from(document.querySelectorAll(classes));
+  /*
+   * Add selectors for branch bundles (_index.md files). In this case,
+   * branch bundles often have a different DOM structure than leaf bundles,
+   * so we need to include headers that are direct children of the content
+   * container here.
+   */
+  for (let i = 2; i <= shortcutDepth; i++) {
path: assets/js/shortcuts.js
user: login: agriyakhetarpal
body: I added a small note highlighting the difference in f0370a38a1b257b435fa21b0f6d7e5e6ace85b4d. I hope that resolves it!
created_at: 2025-03-04T23:52:41Z
author_association: MEMBER
diff_hunk: @@ -92,11 +107,25 @@ function setupShortcuts(shortcutDepth = 2) {
     classes += " .content-container :not([role='tabpanel']) > h" + i;
   }
 
-  // Content Page Shortcuts
-  const shortcutsTarget = document.getElementById("shortcuts");
-  if (shortcutsTarget) {
-    const classElements = Array.from(document.querySelectorAll(classes));
+  /*
+   * Add selectors for branch bundles (_index.md files). In this case,
+   * branch bundles often have a different DOM structure than leaf bundles,
+   * so we need to include headers that are direct children of the content
+   * container here.
+   */
+  for (let i = 2; i <= shortcutDepth; i++) {
path: assets/js/shortcuts.js
user: login: stefanv
body: Still, could you explain the difference in selectors to me?
created_at: 2025-03-04T23:55:13Z
author_association: MEMBER
diff_hunk: @@ -92,11 +107,25 @@ function setupShortcuts(shortcutDepth = 2) {
     classes += " .content-container :not([role='tabpanel']) > h" + i;
   }
 
-  // Content Page Shortcuts
-  const shortcutsTarget = document.getElementById("shortcuts");
-  if (shortcutsTarget) {
-    const classElements = Array.from(document.querySelectorAll(classes));
+  /*
+   * Add selectors for branch bundles (_index.md files). In this case,
+   * branch bundles often have a different DOM structure than leaf bundles,
+   * so we need to include headers that are direct children of the content
+   * container here.
+   */
+  for (let i = 2; i <= shortcutDepth; i++) {
path: assets/js/shortcuts.js
user: login: agriyakhetarpal
body: Yes, sure. The idea is that `layouts/_default/single.html` is used for leaf bundles, and `layouts/partials/posts/list.html` for branch bundles.

For the leaf bundle:

https://github.com/scientific-python/scientific-python-hugo-theme/blob/02d8fa7b526c449b610a2017cb0395573c9d03da/layouts/_default/single.html#L3-L12

The content is expected to be inside an additional `<div>` wrapper. This means any headings in the Markdown content will be nested one level deeper than the container.

Meanwhile, the branch bundle

https://github.com/scientific-python/scientific-python-hugo-theme/blob/02d8fa7b526c449b610a2017cb0395573c9d03da/layouts/partials/posts/list.html#L31-L42

expects the `.Content` is rendered directly inside the `content-container` without an extra `<div>` wrapper. So, the headings from any Markdown content will be direct children of the container.

So, `.content-container :not([role='tabpanel']) > h${i}` finds headings that are direct children _of any element inside `.content-container`_ (as long as that element isn't a `tabpanel`, of course). However, it can't handle headings that the `.content-container > h${i}` selector can (for the branch bundle case), because this one is specifically targeting headings that are direct children of `.content-container` itself. I haven't fully worked out where the `tabpanel`s are inserted and how, though, but it is obvious that there is an effect.

That said, I think this could be normalised in a subsequent PR so that we won't need two selectors at all – but I'm worried I'll break the theme completely. 😅


created_at: 2025-03-05T00:20:27Z
author_association: MEMBER
diff_hunk: @@ -92,11 +107,25 @@ function setupShortcuts(shortcutDepth = 2) {
     classes += " .content-container :not([role='tabpanel']) > h" + i;
   }
 
-  // Content Page Shortcuts
-  const shortcutsTarget = document.getElementById("shortcuts");
-  if (shortcutsTarget) {
-    const classElements = Array.from(document.querySelectorAll(classes));
+  /*
+   * Add selectors for branch bundles (_index.md files). In this case,
+   * branch bundles often have a different DOM structure than leaf bundles,
+   * so we need to include headers that are direct children of the content
+   * container here.
+   */
+  for (let i = 2; i <= shortcutDepth; i++) {
path: assets/js/shortcuts.js
user: login: stefanv
body: Hrm, yeah, it seems like we just need to unify the two layout files?
created_at: 2025-03-05T05:43:06Z
author_association: MEMBER
diff_hunk: @@ -92,11 +107,25 @@ function setupShortcuts(shortcutDepth = 2) {
     classes += " .content-container :not([role='tabpanel']) > h" + i;
   }
 
-  // Content Page Shortcuts
-  const shortcutsTarget = document.getElementById("shortcuts");
-  if (shortcutsTarget) {
-    const classElements = Array.from(document.querySelectorAll(classes));
+  /*
+   * Add selectors for branch bundles (_index.md files). In this case,
+   * branch bundles often have a different DOM structure than leaf bundles,
+   * so we need to include headers that are direct children of the content
+   * container here.
+   */
+  for (let i = 2; i <= shortcutDepth; i++) {
path: assets/js/shortcuts.js
user: login: stefanv
body: > That said, I think this could be normalised in a subsequent PR so that we won't need two selectors at all – but I'm worried I'll break the theme completely. 😅

"How bad can it be" :sweat_smile: 
created_at: 2025-03-05T05:43:56Z
author_association: MEMBER
diff_hunk: @@ -92,11 +107,25 @@ function setupShortcuts(shortcutDepth = 2) {
     classes += " .content-container :not([role='tabpanel']) > h" + i;
   }
 
-  // Content Page Shortcuts
-  const shortcutsTarget = document.getElementById("shortcuts");
-  if (shortcutsTarget) {
-    const classElements = Array.from(document.querySelectorAll(classes));
+  /*
+   * Add selectors for branch bundles (_index.md files). In this case,
+   * branch bundles often have a different DOM structure than leaf bundles,
+   * so we need to include headers that are direct children of the content
+   * container here.
+   */
+  for (let i = 2; i <= shortcutDepth; i++) {
path: assets/js/shortcuts.js
user: login: agriyakhetarpal
body: > Hrm, yeah, it seems like we just need to unify the two layout files?

I think so, it should be doable, and will reduce the diff. Let me try that!
created_at: 2025-03-05T11:02:01Z
author_association: MEMBER
diff_hunk: @@ -81,22 +84,49 @@ function remToPx(rem) {
 }
 
 function setupShortcuts(shortcutDepth = 2) {
+  // Find the shortcut target container in the sidebar
+  const shortcutsTarget = document.getElementById("shortcuts");
+
+  /*
+   * Exit early if shortcuts container doesn't exist
+   * This is important to avoid errors when the theme
+   * is used on pages that don't have the shortcuts sidebar
+   */
+  if (!shortcutsTarget) {
+    return;
+  }
+
   shortcutDepth += 1; // to account for the page title
 
-  // Build a class selector for each header type, and concatenate with commas
+  /*
+   * Build selectors for both patterns:
+   * 1. Direct children of .content-container (for headers directly in the container)
+   * 2. Headers within wrappers inside .content-container (for when content-wrapper is used)
+   *
+   * This works with both DOM structures we might encounter, whether the templates
+   * have been updated to use content-wrapper or not (just in case).
+   */
   let classes = "";
   for (let i = 2; i <= shortcutDepth; i++) {
     if (i != 2) {
       classes += ",";
     }
-    classes += " .content-container :not([role='tabpanel']) > h" + i;
path: assets/js/shortcuts.js
user: login: agriyakhetarpal
body: It turns out that the `:not([role='tabpanel']` seems to be no longer needed. Looking at the git blame to see why it was added, I noticed your older comment from https://github.com/scientific-python/scientific-python-hugo-theme/pull/387#issuecomment-1796916855 and this was the commit from which introduced it: https://github.com/scientific-python/scientific-python-hugo-theme/pull/387/commits/84feef3e702fc2c50242dd61a364654255b65ee3, with the title "Ignore tab contents in shortcuts". The tab contents are not being included in the shortcuts now – unless there's something I'm missing?
created_at: 2025-03-05T12:29:09Z
author_association: MEMBER
diff_hunk: @@ -81,22 +84,49 @@ function remToPx(rem) {
 }
 
 function setupShortcuts(shortcutDepth = 2) {
+  // Find the shortcut target container in the sidebar
+  const shortcutsTarget = document.getElementById("shortcuts");
+
+  /*
+   * Exit early if shortcuts container doesn't exist
+   * This is important to avoid errors when the theme
+   * is used on pages that don't have the shortcuts sidebar
+   */
+  if (!shortcutsTarget) {
+    return;
+  }
+
   shortcutDepth += 1; // to account for the page title
 
-  // Build a class selector for each header type, and concatenate with commas
+  /*
+   * Build selectors for both patterns:
+   * 1. Direct children of .content-container (for headers directly in the container)
+   * 2. Headers within wrappers inside .content-container (for when content-wrapper is used)
+   *
+   * This works with both DOM structures we might encounter, whether the templates
+   * have been updated to use content-wrapper or not (just in case).
+   */
   let classes = "";
   for (let i = 2; i <= shortcutDepth; i++) {
     if (i != 2) {
       classes += ",";
     }
-    classes += " .content-container :not([role='tabpanel']) > h" + i;
path: assets/js/shortcuts.js
user: login: stefanv
body: The panel tabs are rendered as a div, but I don't know why they contained headings at some point.

I used this page to check that it works, and it seems fine: https://deploy-preview-668--scientific-python-hugo-theme.netlify.app/scipy/install/#installing-with-type-stubs

However, on that page I now see that the headers are not picked up.
created_at: 2025-03-05T22:34:43Z
author_association: MEMBER
diff_hunk: @@ -81,22 +84,49 @@ function remToPx(rem) {
 }
 
 function setupShortcuts(shortcutDepth = 2) {
+  // Find the shortcut target container in the sidebar
+  const shortcutsTarget = document.getElementById("shortcuts");
+
+  /*
+   * Exit early if shortcuts container doesn't exist
+   * This is important to avoid errors when the theme
+   * is used on pages that don't have the shortcuts sidebar
+   */
+  if (!shortcutsTarget) {
+    return;
+  }
+
   shortcutDepth += 1; // to account for the page title
 
-  // Build a class selector for each header type, and concatenate with commas
+  /*
+   * Build selectors for both patterns:
+   * 1. Direct children of .content-container (for headers directly in the container)
+   * 2. Headers within wrappers inside .content-container (for when content-wrapper is used)
+   *
+   * This works with both DOM structures we might encounter, whether the templates
+   * have been updated to use content-wrapper or not (just in case).
+   */
   let classes = "";
   for (let i = 2; i <= shortcutDepth; i++) {
     if (i != 2) {
       classes += ",";
     }
-    classes += " .content-container :not([role='tabpanel']) > h" + i;
path: assets/js/shortcuts.js
user: login: agriyakhetarpal
body: 🤔 I think that might have broken elsewhere, as https://scipy.org/install/ doesn't seem to pick up the headers either. Tabbing my way down that page, I go from the "SciPy user guide" link above the heading, and then directly to the "the ``scipy-stubs` GitHub repository" link under that heading.
created_at: 2025-03-05T22:45:27Z
author_association: MEMBER
diff_hunk: @@ -81,22 +84,49 @@ function remToPx(rem) {
 }
 
 function setupShortcuts(shortcutDepth = 2) {
+  // Find the shortcut target container in the sidebar
+  const shortcutsTarget = document.getElementById("shortcuts");
+
+  /*
+   * Exit early if shortcuts container doesn't exist
+   * This is important to avoid errors when the theme
+   * is used on pages that don't have the shortcuts sidebar
+   */
+  if (!shortcutsTarget) {
+    return;
+  }
+
   shortcutDepth += 1; // to account for the page title
 
-  // Build a class selector for each header type, and concatenate with commas
+  /*
+   * Build selectors for both patterns:
+   * 1. Direct children of .content-container (for headers directly in the container)
+   * 2. Headers within wrappers inside .content-container (for when content-wrapper is used)
+   *
+   * This works with both DOM structures we might encounter, whether the templates
+   * have been updated to use content-wrapper or not (just in case).
+   */
   let classes = "";
   for (let i = 2; i <= shortcutDepth; i++) {
     if (i != 2) {
       classes += ",";
     }
-    classes += " .content-container :not([role='tabpanel']) > h" + i;
path: assets/js/shortcuts.js
user: login: stefanv
body: Just checking to see if you'd like me to re-review, or whether you are looking into that issue first.
created_at: 2025-03-07T08:59:41Z
author_association: MEMBER
diff_hunk: @@ -81,22 +84,49 @@ function remToPx(rem) {
 }
 
 function setupShortcuts(shortcutDepth = 2) {
+  // Find the shortcut target container in the sidebar
+  const shortcutsTarget = document.getElementById("shortcuts");
+
+  /*
+   * Exit early if shortcuts container doesn't exist
+   * This is important to avoid errors when the theme
+   * is used on pages that don't have the shortcuts sidebar
+   */
+  if (!shortcutsTarget) {
+    return;
+  }
+
   shortcutDepth += 1; // to account for the page title
 
-  // Build a class selector for each header type, and concatenate with commas
+  /*
+   * Build selectors for both patterns:
+   * 1. Direct children of .content-container (for headers directly in the container)
+   * 2. Headers within wrappers inside .content-container (for when content-wrapper is used)
+   *
+   * This works with both DOM structures we might encounter, whether the templates
+   * have been updated to use content-wrapper or not (just in case).
+   */
   let classes = "";
   for (let i = 2; i <= shortcutDepth; i++) {
     if (i != 2) {
       classes += ",";
     }
-    classes += " .content-container :not([role='tabpanel']) > h" + i;
path: assets/js/shortcuts.js
user: login: agriyakhetarpal
body: Thanks for checking in. It's ready for review, but I'll mark it as a draft for now and see if I can get the unification to work, or split things in another PR if not.
created_at: 2025-03-07T12:31:17Z
author_association: MEMBER
diff_hunk: @@ -92,11 +107,25 @@ function setupShortcuts(shortcutDepth = 2) {
     classes += " .content-container :not([role='tabpanel']) > h" + i;
   }
 
-  // Content Page Shortcuts
-  const shortcutsTarget = document.getElementById("shortcuts");
-  if (shortcutsTarget) {
-    const classElements = Array.from(document.querySelectorAll(classes));
+  /*
+   * Add selectors for branch bundles (_index.md files). In this case,
+   * branch bundles often have a different DOM structure than leaf bundles,
+   * so we need to include headers that are direct children of the content
+   * container here.
+   */
+  for (let i = 2; i <= shortcutDepth; i++) {
path: assets/js/shortcuts.js
user: login: agriyakhetarpal
body: Resolving, this was handled with 4636a527b898668dab44faa8af9d56fcfb3a023f.
created_at: 2025-03-18T11:52:44Z
author_association: MEMBER
diff_hunk: @@ -98,15 +98,24 @@ function setupShortcuts(shortcutDepth = 2) {
 
   shortcutDepth += 1; // to account for the page title
 
-  // Build a class selector for each header type, and concatenate with commas
+  /*
+   * We have build selectors to find headers in different contexts.
path: assets/js/shortcuts.js
user: login: stefanv
body: ```suggestion
   * We build selectors to find headers in different contexts.
```
created_at: 2025-03-18T21:03:47Z
author_association: MEMBER
diff_hunk: @@ -98,15 +98,24 @@ function setupShortcuts(shortcutDepth = 2) {
 
   shortcutDepth += 1; // to account for the page title
 
-  // Build a class selector for each header type, and concatenate with commas
+  /*
+   * We have build selectors to find headers in different contexts.
+   * i. direct children of .content-container (main structure)
+   * i. headers within tabpanels (for tabbed content)
path: assets/js/shortcuts.js
user: login: stefanv
body: I thought these were the ones we explicitly avoided before? We don't want to link to the content of tabpanels!
created_at: 2025-03-18T21:04:08Z
author_association: MEMBER
diff_hunk: @@ -81,22 +84,47 @@ function remToPx(rem) {
 }
 
 function setupShortcuts(shortcutDepth = 2) {
+  // Find the shortcut target container in the sidebar
+  const shortcutsTarget = document.getElementById("shortcuts");
+
+  /*
+   * Exit early if shortcuts container doesn't exist
+   * This is important to avoid errors when the theme
+   * is used on pages that don't have the shortcuts sidebar
+   */
+  if (!shortcutsTarget) {
+    return;
+  }
+
   shortcutDepth += 1; // to account for the page title
 
-  // Build a class selector for each header type, and concatenate with commas
+  /*
+   * We have build selectors to find headers in different contexts.
+   * i. direct children of .content-container (main structure)
+   * i. headers within tabpanels (for tabbed content)
+   */
   let classes = "";
+
   for (let i = 2; i <= shortcutDepth; i++) {
     if (i != 2) {
       classes += ",";
     }
-    classes += " .content-container :not([role='tabpanel']) > h" + i;
path: assets/js/shortcuts.js
user: login: stefanv
body: Note the `:not`.
created_at: 2025-03-18T21:04:59Z
author_association: MEMBER
diff_hunk: @@ -89,14 +104,18 @@ function setupShortcuts(shortcutDepth = 2) {
     if (i != 2) {
       classes += ",";
     }
-    classes += " .content-container :not([role='tabpanel']) > h" + i;
path: assets/js/shortcuts.js
user: login: stefanv
body: Safer to leave this in?
created_at: 2025-03-18T21:21:51Z
author_association: MEMBER
diff_hunk: @@ -89,14 +104,18 @@ function setupShortcuts(shortcutDepth = 2) {
     if (i != 2) {
       classes += ",";
     }
-    classes += " .content-container :not([role='tabpanel']) > h" + i;
path: assets/js/shortcuts.js
user: login: agriyakhetarpal
body: Yes – let's bring it back
created_at: 2025-03-18T21:22:25Z
author_association: MEMBER
diff_hunk: @@ -89,14 +104,18 @@ function setupShortcuts(shortcutDepth = 2) {
     if (i != 2) {
       classes += ",";
     }
-    classes += " .content-container :not([role='tabpanel']) > h" + i;
path: assets/js/shortcuts.js
user: login: agriyakhetarpal
body: Ah, no, doing so removes the shortcuts from all pages, regardless of leaf or branch bundles.
created_at: 2025-03-18T21:24:35Z
author_association: MEMBER
diff_hunk: @@ -89,14 +104,18 @@ function setupShortcuts(shortcutDepth = 2) {
     if (i != 2) {
       classes += ",";
     }
-    classes += " .content-container :not([role='tabpanel']) > h" + i;
path: assets/js/shortcuts.js
user: login: stefanv
body: I'm confused; why would that be? We just want to exclude header children of tabpanels. Is there something wrong with the CSS rule?
created_at: 2025-03-18T21:56:43Z
author_association: MEMBER

PR comments:
user: login: netlify[bot]
created_at: 2025-03-04T23:13:09Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *scientific-python-hugo-theme* ready!
Built [without sensitive environment variables](https://docs.netlify.com/configure-builds/environment-variables/#sensitive-variable-policy)

|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 343cb67661fea3f16c88b13ac95a53c09c4f57ae |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/scientific-python-hugo-theme/deploys/67d9ff254a702e00086d5fac |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-668--scientific-python-hugo-theme.netlify.app](https://deploy-preview-668--scientific-python-hugo-theme.netlify.app) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTY2OC0tc2NpZW50aWZpYy1weXRob24taHVnby10aGVtZS5uZXRsaWZ5LmFwcCJ9.3DoIdml1Fo8E8iykEe93yv0qEdzu4IXKOf2HzhSq9P4)<br /><br />_Use your smartphone camera to open QR code link._</details> |
|<span aria-hidden="true">![Lighthouse](https://app.netlify.com/public/external-usage/git-comments/lighthouse-sm.png)</span><br />Lighthouse | 1 paths audited<br />**Performance**: 94<br />**Accessibility**: 100<br />**Best Practices**: 100<br />**SEO**: 91<br />**PWA**: -<br />[View the detailed breakdown and full score reports](https://app.netlify.com/sites/scientific-python-hugo-theme/deploys/67d9ff254a702e00086d5fac) |
---
<!-- [scientific-python-hugo-theme Preview](https://deploy-preview-668--scientific-python-hugo-theme.netlify.app) -->
_To edit notification comments on pull requests, go to your [Netlify site configuration](https://app.netlify.com/sites/scientific-python-hugo-theme/configuration/notifications#deploy-webhooks)._
author_association: NONE
user: login: agriyakhetarpal
created_at: 2025-03-04T23:21:35Z
body: The shortcuts are now working on https://deploy-preview-668--scientific-python-hugo-theme.netlify.app/user_guide/
author_association: MEMBER
user: login: agriyakhetarpal
created_at: 2025-03-05T12:21:41Z
body: This should be in a better state now. Trying to unify it more broke the navbar's styling, so here's an intermediate approach that I'm able to get to work with both styles of bundles. I'm happy to add more changes in a follow-up if this is acceptable to you. Ready for another look!
author_association: MEMBER
user: login: agriyakhetarpal
created_at: 2025-03-17T11:23:41Z
body: Hi @stefanv – I've examined the theme in a bit more detail, and I think this should be ready to go now. Instead of adding a `content-wrapper` div for all pages to normalise them, I've dropped the extra container for the branch bundles so that the layout is in line with the rest of the pages (which don't wrap the `{{ .Content }}` in an extra container), and I've kept the removal of `:not([role='tabpanel'])` from the shortcut selector. A combination of both, it turns out, was the problem's original cause.

I can confirm that <kbd>Tab</kbd>bing my way through both https://deploy-preview-668--scientific-python-hugo-theme.netlify.app/shortcodes/ and https://theme.scientific-python.org/shortcodes/ (i.e., pages there's a long-enough secondary sidebar to do so) is working identically.

author_association: MEMBER
user: login: stefanv
created_at: 2025-03-17T20:11:10Z
body: Thanks, @agriyakhetarpal.

- https://deploy-preview-668--scientific-python-hugo-theme.netlify.app/scipy/install/ still seems to not generate the correct sidebar
- The div drop is fine, I think, we must just double check that no existing CSS relies on that
author_association: MEMBER
user: login: agriyakhetarpal
created_at: 2025-03-18T11:52:15Z
body: > Thanks, @agriyakhetarpal.
> 
> * [deploy-preview-668--scientific-python-hugo-theme.netlify.app/scipy/install](https://deploy-preview-668--scientific-python-hugo-theme.netlify.app/scipy/install/) still seems to not generate the correct sidebar

Thanks, @stefanv – I noticed that https://deploy-preview-668--scientific-python-hugo-theme.netlify.app/scipy/install/ and https://scipy.org/install have the same problem (so it was a change unrelated to fixing the branch bundles), and I do have a fix – I just pushed it with this PR in ef7bf8f8bf1abc49bef41ded7caa66e93b6ebe02 as it's not a big addition.

It took me a while to get back to this PR; apologies!

> * The div drop is fine, I think, we must just double check that no existing CSS relies on that

I believe we should be fine. I checked most of the CSS we have, and it targets specific components of the theme, not the content inside the `content-container` directly. I also checked the kitchen sink and all other pages, and I didn't spot any difference upon inspecting, as that extra div only inherited pre-existing styles, not having its own.
author_association: MEMBER
user: login: agriyakhetarpal
created_at: 2025-03-18T12:19:15Z
body: https://deploy-preview-668--scientific-python-hugo-theme.netlify.app/scipy/install displays all headings in the shortcuts container now! :)

There is one more change needed, which would be out of scope with what I wanted to fix with this PR: if you were to click on the "Installing system-wide via a package manager" shortcut in the deployment link above, it would not lead to anything as the heading and those under it ("Fedora", "macOS", etc.) are behind a different tab that isn't active – the active one upon loading the page is "Project Based", so only headings under that tab are working unless you switch tabs in the contents view.

Either way, I'm interested in contributing further; so, would you be okay with the fix I have for that issue not being in this PR but in one that I put together soon after this is merged?
author_association: MEMBER
user: login: stefanv
created_at: 2025-03-18T21:05:49Z
body: > Either way, I'm interested in contributing further; so, would you be okay with the fix I have for that issue not being in this PR but in one that I put together soon after this is merged?

Totally fine with subsequent PRs, but I don't think tabpanel linking is something that should be allowed (it wasn't before).
author_association: MEMBER
user: login: stefanv
created_at: 2025-03-18T21:07:35Z
body: OK, had a look at the SciPy example, and now I see: they use tabpanels in a way that we didn't originally anticipate: they use it to switch a large part of the page. Originally, we thought of tabpanels more as a way to show different scripts, e.g., side-by-side.

So, for now, I think we stick with *not* having tabpanel headings in the sidebar. I apologize for that red herring.
author_association: MEMBER
user: login: stefanv
created_at: 2025-03-18T21:09:15Z
body: We are already following PDST styling, so the simple solution is to outline the tabpanel:

![2025-03-18_14:08:24](https://github.com/user-attachments/assets/6cf2bf58-495b-4368-9369-f4ab2e3a3d30)

Not necessary for this PR, of course.
author_association: MEMBER
user: login: agriyakhetarpal
created_at: 2025-03-18T21:16:56Z
body: Thanks for taking a deeper look. IIUC, that means I should revert my last commit, right? Also, it would also mean that my subsequent PR to handle active/inactive tabs in headings isn't needed either, so all good! :)
author_association: MEMBER
user: login: stefanv
created_at: 2025-03-18T21:19:18Z
body: Yes, correct! Although I think we should apply the styling change to make it clear to the user what's happening.
author_association: MEMBER
user: login: agriyakhetarpal
created_at: 2025-03-18T21:20:25Z
body: > Yes, correct! Although I think we should apply the styling change to make it clear to the user what's happening.

I'll take this up later in the day today! In a new PR, of course.
author_association: MEMBER
user: login: agriyakhetarpal
created_at: 2025-03-18T21:56:54Z
body: So, I got why it doesn't work: https://developer.mozilla.org/en-US/docs/Web/CSS/Descendant_combinator

If I were to add back and switch to `classes += " .content-container :not([role='tabpanel']) > h" + i;`, why it doesn't work is because of _one extra space_ which changes the meaning – how subtle!

The gist here is:

- `classes += " .content-container:not([role='tabpanel']) > h" + i;` works as it selects elements with class `content-container` that don't have `role="tabpanel"`, then finds heading elements that are direct children of those containers
- if you include a space between `.content-container` and `not([role='tabpanel'])`, it becomes a descendant combinator and selects any descendants of `.content-container` that don't have `role="tabpanel"`, then finds heading elements that are direct children of those elements. This won't work because I dropped the extra div that wrapped `{{ .Content }}` inside `content-container`.
author_association: MEMBER
user: login: agriyakhetarpal
created_at: 2025-03-18T22:03:10Z
body: Suggestions on the in-line comment I added about this are welcome. I think we should omit the bit about descendant combinators, but we can link to this PR if need be.
author_association: MEMBER
user: login: stefanv
created_at: 2025-03-18T23:16:58Z
body: I think the rule without the role was more correct, so reverted to that. We are looking for direct header children of the `.content-container`, so I don't think we explicitly need to exclude tabpanels.
author_association: MEMBER
user: login: stefanv
created_at: 2025-03-18T23:21:29Z
body: Thanks @agriyakhetarpal!
author_association: MEMBER