From 005aa5c3e88d5d6a455bcc20b4c4e6f2c3a1abe9 Mon Sep 17 00:00:00 2001 From: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> Date: Thu, 20 Aug 2026 06:37:14 +0530 Subject: [PATCH] fix(theme): render sidebar active state during ssr dcb7a755 turned hasActiveLink from a computed into a ref fed only by a non-immediate watch and onMounted, so is-active/has-active stopped rendering in static HTML. This broke pre-hydration sidebar highlighting and the docsearch crawler's lvl0 selector (section.has-active div h2), which has indexed every record as the defaultValue since then. Run the update once during setup with the hash check skipped: the server and the client's initial render compute identical path-only state, so hydration stays clean; the watch and onMounted refine with the real hash after mount. Co-Authored-By: Claude Fable 5 --- .../theme-default/composables/sidebar.ts | 18 +++++++++++++----- src/client/theme-default/support/sidebar.ts | 9 +++++---- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/client/theme-default/composables/sidebar.ts b/src/client/theme-default/composables/sidebar.ts index 1b9df6bd..116520d7 100644 --- a/src/client/theme-default/composables/sidebar.ts +++ b/src/client/theme-default/composables/sidebar.ts @@ -83,12 +83,14 @@ export function useSidebarItemControl( const isActiveLink = ref(false) const hasActiveLink = ref(false) - function updateActiveLink(): void { + function updateActiveLink(skipHashCheck = false): void { if (item.value.link) { isActiveLink.value = isActive( route.data.relativePath, route.hash, - item.value.link + item.value.link, + false, + skipHashCheck ) } else { isActiveLink.value = false @@ -105,15 +107,21 @@ export function useSidebarItemControl( hasActiveLink.value = containsActiveLink( route.data.relativePath, route.hash, - item.value.items + item.value.items, + skipHashCheck ) if (hasActiveLink.value) { nextTick(() => (collapsed.value = false)) } } - watch([item, route], updateActiveLink) - onMounted(updateActiveLink) + // runs during setup so active classes render in SSR output too; the hash + // isn't known on the server (and may differ at hydration), so it's skipped + // until mounted + updateActiveLink(true) + + watch([item, route], () => updateActiveLink()) + onMounted(() => updateActiveLink()) const hasChildren = computed(() => { return !!(item.value.items && item.value.items.length) diff --git a/src/client/theme-default/support/sidebar.ts b/src/client/theme-default/support/sidebar.ts index 68a99452..35c92722 100644 --- a/src/client/theme-default/support/sidebar.ts +++ b/src/client/theme-default/support/sidebar.ts @@ -100,16 +100,17 @@ export function getFlatSideBarLinks(sidebar: SidebarItem[]): SidebarLink[] { export function hasActiveLink( path: string, hash: string, - items: SidebarItem | SidebarItem[] + items: SidebarItem | SidebarItem[], + skipHashCheck = false ): boolean { if (Array.isArray(items)) { - return items.some((item) => hasActiveLink(path, hash, item)) + return items.some((item) => hasActiveLink(path, hash, item, skipHashCheck)) } - if (items.link && isActive(path, hash, items.link)) { + if (items.link && isActive(path, hash, items.link, false, skipHashCheck)) { return true } if (items.items) { - return hasActiveLink(path, hash, items.items) + return hasActiveLink(path, hash, items.items, skipHashCheck) } return false }