-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Summary
The sidebar currently relies on raw DOM manipulation (using document.createElement, insertBefore, etc.) to inject project and version selectors. This approach is fragile, causes issues with Nextra integration, and should be replaced with proper React patterns.
Problem Description
From components/DocsWrapper.tsx, the current implementation uses:
useEffect(() => {
if (versions.length === 0) return;
const sidebar = document.querySelector("aside.nextra-sidebar");
if (!sidebar || sidebar.querySelector(".version-selector-container"))
return;
const projDiv = document.createElement("div");
projDiv.className = "project-selector-container";
const verDiv = document.createElement("div");
verDiv.className = "version-selector-container";
sidebar.insertBefore(verDiv, sidebar.firstChild);
sidebar.insertBefore(projDiv, sidebar.firstChild);
setProjectContainer(projDiv);
setVersionContainer(verDiv);
}, [versions]);This approach has several issues:
- Fragile - Depends on specific Nextra internal class names (
aside.nextra-sidebar) - Race conditions - Timing issues with Nextra's rendering
- No TypeScript safety - Direct DOM manipulation bypasses React's type checking
- Hydration issues - Can cause mismatches between server and client
- Maintenance burden - Breaks when Nextra updates their class names or structure
- Testing difficulty - Harder to test and debug
Nextra v4 Sidebar API Research
Investigation of Nextra v4's API reveals that the Layout component does not provide sidebar content injection props:
| Feature | Available? |
|---|---|
sidebar.header / sidebarHeader prop |
No |
sidebar.footer / sidebarFooter prop |
No |
| Sidebar slot or render prop | No |
SidebarProvider / SidebarContext export |
No |
| Sidebar customization hooks | Read-only (useThemeConfig()) |
The Layout sidebar prop only accepts:
sidebar?: {
autoCollapse?: boolean;
defaultMenuCollapseLevel?: number;
defaultOpen?: boolean;
toggleButton?: boolean;
}This confirms why DOM manipulation was used — Nextra v4 does not offer a supported way to inject custom content into the sidebar.
Available Nextra APIs
While there's no sidebar injection API, Nextra does provide:
normalizePages()fromnextra— Processes aPageMapItem[]into structured sidebar data (directories, files, active items). This is the same function Nextra uses internally to build its sidebar.useThemeConfig()fromnextra-theme-docs— Read-only access to theme configuration.useConfig()fromnextra-theme-docs— Access to current page context includinghideSidebar.
Proposed Solution
Option 1: Custom Layout with normalizePages() (Recommended)
Replace Nextra's Layout with a custom layout that uses normalizePages() for sidebar data but renders a custom sidebar component with project/version selectors built in:
import { normalizePages } from 'nextra';
function CustomDocsLayout({ children, pageMap }) {
const pathname = usePathname();
const { docsDirectories, activeIndex } = normalizePages({
list: pageMap,
route: pathname,
});
return (
<div className="flex">
<aside className="w-64 shrink-0 border-r">
<ProjectSelector /> {/* Direct React rendering */}
<VersionSelector /> {/* Direct React rendering */}
<SidebarNavigation items={docsDirectories} />
</aside>
<main className="flex-1">{children}</main>
</div>
);
}Pros:
- Full control over sidebar content and layout
- No DOM manipulation
- Type-safe, proper React patterns
- Uses Nextra's official
normalizePages()API for sidebar data
Cons:
- Must reimplement sidebar features: collapsible folders, active state highlighting, mobile overlay, keyboard navigation, scroll persistence
- Significant effort — Nextra's sidebar is non-trivial
- Risk of losing polish that Nextra's built-in sidebar provides
Mitigation: The codebase already has a shadcn sidebar.tsx component (components/ui/sidebar.tsx) with SidebarProvider, collapsible groups, and mobile sheet support. This can be used as the foundation.
Option 2: Fork/Extend Nextra's Sidebar Component
If Nextra's sidebar source is accessible:
- Copy Nextra's sidebar component implementation
- Add a
headerslot for project/version selectors - Use this forked version instead of Nextra's built-in
Pros: Preserves all existing sidebar behavior while adding injection points.
Cons: Maintenance burden of keeping the fork in sync with Nextra updates.
Option 3: Keep Portal Pattern, Improve Robustness (Pragmatic)
If a full rewrite is too costly, improve the current approach:
- Add retry logic with
MutationObserverinstead of one-shotquerySelector - Use a more stable selector (data attribute if possible)
- Add error boundaries and fallback UI
- Document the fragility
Pros: Minimal effort, fixes immediate reliability issues.
Cons: Still fragile, still DOM manipulation.
Recommendation
Option 1 is recommended for long-term maintainability, using the existing shadcn sidebar.tsx as a foundation. However, this is a significant piece of work that affects mobile (issue #3), nested folders (issue #2), and page meta (issue #1).
Consider Option 3 as an interim fix if other issues are blocked by sidebar reliability problems.
Requirements
- Remove document.createElement - No raw DOM manipulation in components
- Proper React patterns - Use state, props, and React rendering
- Maintain functionality - Project and version selectors still work
- Preserve sidebar features - Collapsible folders, active state, mobile overlay, keyboard nav
- Type safety - Full TypeScript support
- Reliable - No race conditions or timing issues
Files Likely Affected
components/DocsWrapper.tsx- RemoveuseEffectDOM manipulation, removecreatePortalpatternapp/[project]/docs/[version]/layout.tsx- Switch from NextraLayoutto custom layout (if Option 1)components/ui/sidebar.tsx- Already exists as shadcn sidebar, can be foundation for custom sidebar- New components needed:
- Custom docs layout wrapper
- Sidebar navigation component (using
normalizePages()data)
Dependencies
- This issue affects issue Support nested folders in documentation sidebar #3 (mobile) since the mobile sidebar behavior depends on how the sidebar is implemented
- Should be coordinated with issues Phase 2 - tuning up our docs and docs website #1 and Implement sidebar and page meta controls #2 (page meta and nested folders) since sidebar rendering changes affect both
Migration Plan
- Build custom sidebar component using shadcn
sidebar.tsxas foundation - Integrate
normalizePages()for page tree data - Add project/version selectors as direct React children
- Create custom docs layout that replaces Nextra's
Layoutfor doc pages - Test with all projects, versions, and on mobile
- Remove old DOM manipulation code from
DocsWrapper - Verify no regressions in sidebar behavior (collapsing, active states, etc.)
Success Criteria
- No
document.createElementorinsertBeforecalls - No
document.querySelectorfor Nextra internal class names - Project selector renders via React, not DOM injection
- Version selector renders via React, not DOM injection
- Sidebar folders are collapsible
- Active page is highlighted in sidebar
- Mobile sidebar works (overlay/drawer)
- No hydration mismatches
- Full TypeScript support