From 254627024e7c359480ffb82f2792bdf3548b67e6 Mon Sep 17 00:00:00 2001 From: Joseph Bayly Date: Fri, 16 Jan 2026 15:23:13 -0500 Subject: [PATCH 1/2] feat(website): enhanced control over navbar and sidebar titles (#13477) - Allow `navbar.title` and `sidebar.title` to be `boolean` or `string`. - Fix bug where disabling navbar title erroneously hid sidebar title. - Support custom titles for navbar and sidebar. - Add smoke tests for title control options. - Update schema documentation for titling options. --- src/project/types/book/book-config.ts | 1 - .../types/website/website-navigation.ts | 71 +++++++-- .../projects/website/templates/navbrand.ejs | 4 +- .../projects/website/templates/sidebar.ejs | 16 +- src/resources/schema/definitions.yml | 4 +- .../issue-13477/book-sidebar-logo/.gitignore | 2 + .../issue-13477/book-sidebar-logo/_quarto.yml | 9 ++ .../issue-13477/book-sidebar-logo/index.qmd | 3 + .../issue-13477/book-sidebar-logo/logo.png | 1 + .../issue-13477/custom-titles/.gitignore | 2 + .../issue-13477/custom-titles/_quarto.yml | 11 ++ .../issue-13477/custom-titles/index.qmd | 4 + .../navbar-false-sidebar-undefined/.gitignore | 2 + .../_quarto.yml | 10 ++ .../navbar-false-sidebar-undefined/index.qmd | 4 + .../navbar-true-sidebar-true/.gitignore | 2 + .../navbar-true-sidebar-true/_quarto.yml | 11 ++ .../navbar-true-sidebar-true/index.qmd | 4 + .../websites/issue-13477/no-titles/.gitignore | 2 + .../issue-13477/no-titles/_quarto.yml | 11 ++ .../websites/issue-13477/no-titles/index.qmd | 4 + tests/smoke/website/issue-13477.test.ts | 144 ++++++++++++++++++ 22 files changed, 291 insertions(+), 31 deletions(-) create mode 100644 tests/docs/books/issue-13477/book-sidebar-logo/.gitignore create mode 100644 tests/docs/books/issue-13477/book-sidebar-logo/_quarto.yml create mode 100644 tests/docs/books/issue-13477/book-sidebar-logo/index.qmd create mode 100644 tests/docs/books/issue-13477/book-sidebar-logo/logo.png create mode 100644 tests/docs/websites/issue-13477/custom-titles/.gitignore create mode 100644 tests/docs/websites/issue-13477/custom-titles/_quarto.yml create mode 100644 tests/docs/websites/issue-13477/custom-titles/index.qmd create mode 100644 tests/docs/websites/issue-13477/navbar-false-sidebar-undefined/.gitignore create mode 100644 tests/docs/websites/issue-13477/navbar-false-sidebar-undefined/_quarto.yml create mode 100644 tests/docs/websites/issue-13477/navbar-false-sidebar-undefined/index.qmd create mode 100644 tests/docs/websites/issue-13477/navbar-true-sidebar-true/.gitignore create mode 100644 tests/docs/websites/issue-13477/navbar-true-sidebar-true/_quarto.yml create mode 100644 tests/docs/websites/issue-13477/navbar-true-sidebar-true/index.qmd create mode 100644 tests/docs/websites/issue-13477/no-titles/.gitignore create mode 100644 tests/docs/websites/issue-13477/no-titles/_quarto.yml create mode 100644 tests/docs/websites/issue-13477/no-titles/index.qmd create mode 100644 tests/smoke/website/issue-13477.test.ts diff --git a/src/project/types/book/book-config.ts b/src/project/types/book/book-config.ts index 23541182060..39e639e5160 100644 --- a/src/project/types/book/book-config.ts +++ b/src/project/types/book/book-config.ts @@ -191,7 +191,6 @@ export async function bookProjectConfig( // if we have a top-level 'contents' or 'appendix' fields fold into sidebar site[kSiteSidebar] = site[kSiteSidebar] || {}; const siteSidebar = site[kSiteSidebar] as Metadata; - siteSidebar[kSiteTitle] = siteSidebar[kSiteTitle] || book?.[kSiteTitle]; siteSidebar[kSidebarLogo] = siteSidebar[kSidebarLogo] || book?.[kSidebarLogo]; siteSidebar[kSidebarLogoHref] = siteSidebar[kSidebarLogoHref] || book?.[kSidebarLogoHref]; diff --git a/src/project/types/website/website-navigation.ts b/src/project/types/website/website-navigation.ts index 30ed38da25f..2d8d561d44a 100644 --- a/src/project/types/website/website-navigation.ts +++ b/src/project/types/website/website-navigation.ts @@ -1009,13 +1009,15 @@ async function sidebarsEjsData(project: ProjectContext, sidebars: Sidebar[]) { async function sidebarEjsData(project: ProjectContext, sidebar: Sidebar) { sidebar = ld.cloneDeep(sidebar); + // ensure title is present + sidebar.title = await sidebarTitle(sidebar, project) as string | undefined; + // if the sidebar has a title and no id generate the id if (sidebar.title && !sidebar.id) { sidebar.id = asHtmlId(sidebar.title); } - // ensure title and search are present - sidebar.title = await sidebarTitle(sidebar, project) as string | undefined; + // ensure search is present const searchOpts = await searchOptions(project); sidebar.search = sidebar.search !== undefined ? sidebar.search @@ -1251,7 +1253,8 @@ async function navbarEjsData( }; // if there is no navbar title and it hasn't been set to 'false' // then use the site title - if (!data.title && data.title !== false) { + const navbarTitle = data.title as unknown as string | boolean | undefined; + if (navbarTitle === true || (!navbarTitle && navbarTitle !== false)) { data.title = websiteTitle(project.config); } data.title = data.title || ""; @@ -1479,21 +1482,57 @@ function looksLikeShortCode(href: string) { async function sidebarTitle(sidebar: Sidebar, project: ProjectContext) { const { navbar } = await websiteNavigationConfig(project); - if (sidebar.title) { - // Title was explicitly set - return sidebar.title; - } else if (!sidebar.logo) { - if (!navbar) { - // If there isn't a logo and there isn't a sidebar, use the project title - return websiteTitle(project.config); - } else { - // The navbar will display the title - return undefined; - } - } else { - // There is a logo, just let the logo appear + + const sidebarTitleValue = sidebar.title as unknown as string | boolean | undefined; + const projectTitle = websiteTitle(project.config); + + // 1. "text": include the custom text + if (typeof sidebarTitleValue === "string") { + return sidebarTitleValue; + } + + // 2. false: no title + if (sidebarTitleValue === false) { return undefined; } + + // 3. true: includes the website/book title + if (sidebarTitleValue === true) { + return projectTitle; + } + + // 4. undefined (title line not included) + // include website/book title if and only if (no title is included in navbar AND no logo is included in the sidebar) + // Exception: for books, a sidebar logo does not hide the title. + + const hasLogo = (logo?: any) => { + if (!logo) return false; + if (typeof logo === "string") return true; + return !!(logo.light || logo.dark || logo.path); + }; + + const hasSidebarLogo = hasLogo(sidebar.logo); + + let navbarHasAnyTitle = false; + if (navbar) { + const navbarTitleValue = navbar.title as unknown as string | boolean | undefined; + if (navbarTitleValue !== false) { + navbarHasAnyTitle = true; + } + } + + const isBook = project.config?.project?.[kProjectType] === "book"; + + if (!navbarHasAnyTitle) { + // Navbar has no title. + // Rule: show sidebar title if no logo in sidebar. + // Exception: books ignore the sidebar logo constraint. + if (!hasSidebarLogo || isBook) { + return projectTitle; + } + } + + return undefined; } async function websiteHeadroom(project: ProjectContext) { diff --git a/src/resources/projects/website/templates/navbrand.ejs b/src/resources/projects/website/templates/navbrand.ejs index 11c0775e94f..51ca70d9485 100644 --- a/src/resources/projects/website/templates/navbrand.ejs +++ b/src/resources/projects/website/templates/navbrand.ejs @@ -1,6 +1,6 @@ -<% if (navbar.title || navbar.logo) { %> +<% if (navbar.title || (navbar.logo && (navbar.logo.light || navbar.logo.dark))) { %>