From c7d411d4540f00b1851f1923dff0fafdeceb2d00 Mon Sep 17 00:00:00 2001 From: Jamie Henson Date: Fri, 12 Jun 2026 15:14:04 +0100 Subject: [PATCH] fix(nav): render top-level pricing nav links as clickable links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SectionNav rendered every top-level nav entry as a static heading and only made content interactive when it had a `pages` array. Top-level link-only entries — the per-product pricing pages (Pub/Sub, Chat, Spaces, LiveObjects, LiveSync) — therefore showed as dead headings with no way to reach the page. Render a top-level entry with a `link` and no `pages` as a clickable link instead, with active-state highlighting and the existing language-param preservation. Add a test covering the new behaviour. DX-1417 Co-Authored-By: Claude Opus 4.8 (1M context) --- src/components/Layout/LeftSidebar.test.tsx | 21 ++++++++++++ src/components/Layout/LeftSidebar.tsx | 39 ++++++++++++++++++++-- 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/src/components/Layout/LeftSidebar.test.tsx b/src/components/Layout/LeftSidebar.test.tsx index 4bd7374109..850ecb4e89 100644 --- a/src/components/Layout/LeftSidebar.test.tsx +++ b/src/components/Layout/LeftSidebar.test.tsx @@ -126,4 +126,25 @@ describe('LeftSidebar', () => { // Should not be inside an accordion trigger button expect(sectionHeading.closest('button')).toBeNull(); }); + + it('renders a top-level entry with a link but no child pages as a clickable link', () => { + mockUseLayoutContext.mockReturnValue({ + activePage: { + page: { name: 'Postgres database connector', link: '/docs/livesync/postgres' }, + tree: [{ index: 6, page: { name: 'Ably LiveSync', link: '/docs/livesync' } }], + languages: [], + language: 'javascript', + product: 'liveSync', + template: null, + hasProductBar: false, + }, + }); + + render(); + + // "LiveSync pricing" is a top-level link, so it should render as an anchor, not a heading + const pricingLink = screen.getByText('LiveSync pricing').closest('a'); + expect(pricingLink).not.toBeNull(); + expect(pricingLink).toHaveAttribute('href', '/docs/livesync/pricing'); + }); }); diff --git a/src/components/Layout/LeftSidebar.tsx b/src/components/Layout/LeftSidebar.tsx index 1ba1b4a066..fbaaffa024 100644 --- a/src/components/Layout/LeftSidebar.tsx +++ b/src/components/Layout/LeftSidebar.tsx @@ -58,6 +58,8 @@ const accordionTriggerClassName = cn( const accordionLinkClassName = 'pl-3 py-1'; +const sectionHeadingClassName = 'ui-text-label2 font-bold text-neutral-1300 dark:text-neutral-000 pb-2 pt-5 pl-3 pr-2'; + const iconClassName = 'text-neutral-1300 dark:text-neutral-000 transition-transform'; const ChildAccordion = ({ content, tree }: { content: (NavProductPage | NavProductContent)[]; tree: number[] }) => { @@ -193,16 +195,47 @@ const ChildAccordion = ({ content, tree }: { content: (NavProductPage | NavProdu /** Render top-level nav sections as static headings with their content always expanded. */ const SectionNav = ({ content, tree }: { content: (NavProductPage | NavProductContent)[]; tree: number[] }) => { + const { activePage } = useLayoutContext(); + const location = useLocation(); + const searchParams = useMemo(() => new URLSearchParams(location.search), [location.search]); + return (
{content.map((page, index) => { const hasDeeperLayer = 'pages' in page && page.pages; + // A top-level entry with a link but no child pages (e.g. a product's pricing page) + // is a destination, not a section, so render it as a clickable link rather than a + // static heading. + if (!hasDeeperLayer && 'link' in page && page.link) { + const isSelected = page.link === activePage.page.link; + + return ( + + {page.name} + {page.external && ( + + )} + + ); + } + return (
-
- {page.name} -
+
{page.name}
{hasDeeperLayer && }
);