Skip to content

Top menu from pages - #2904

Open
lmiq wants to merge 56 commits into
JuliaDocs:masterfrom
lmiq:top_menu_from_pages
Open

lmiq wants to merge 56 commits into
JuliaDocs:masterfrom
lmiq:top_menu_from_pages

Conversation

@lmiq

@lmiq lmiq commented Mar 9, 2026

Copy link
Copy Markdown

This is a variation of #2866 where instead of adding a top_menu vector that replaces the pages vector, a

top_menu=true

option is added to makedocs, which will interpret the first layer of the documentation menu as the top menu.

if top_menu=false the common (current - no top-menu) behavior is preserved.

This is a nice alternative for the interface, indeed.

@mortenpi

lmiq and others added 30 commits January 27, 2026 10:45
Implements a new `top_menu` keyword argument for `makedocs` that enables
upper-level organization of documentation with a horizontal navigation bar
at the top of the page.

## New Features

- **`top_menu` keyword**: Accepts a vector of section pairs, where each
  section has a title and a nested page structure identical to the `pages`
  argument format
- **Section-based navigation**: Each section maintains its own navigation
  tree in the sidebar, switching automatically when clicking between sections
- **Theme-aware styling**: Top menu bar uses sidebar theme variables for
  consistent appearance across all themes (light, dark, catppuccin variants)

## Implementation Details

- Added `TopMenuSection` struct in documents.jl to hold section metadata
- Extended `walk_navpages` to support section-specific navlist population
- Added `render_top_menu` and helper functions in HTMLWriter.jl
- Created new SCSS styles in _topmenu.scss with responsive mobile support
- Active section highlighted with bottom border indicator

## Documentation & Tests

- Added comprehensive documentation in makedocs.jl keyword docs
- Added user guide section in docs/src/man/guide.md
- Added unit tests for TopMenuSection and walk_navpages
- Added example build in test/examples with multi-section structure

## Behavior Notes

- First section in top_menu determines the landing page
- Pages should be unique across sections (warns on duplicates)
- Fully backward compatible - existing docs without top_menu unchanged
- Verify top_menu_sections are created correctly
- Verify section titles and navlists
- Verify first_page is set for each section
- Verify HTML files are generated in correct locations
- Verify redirect index.html is generated at root
- Verify top menu elements appear in generated HTML
- Verify has-top-menu class is applied
Use joinpath for cross-platform path comparison in first_page assertions
lmiq added 3 commits April 17, 2026 13:51
Done — top_menu is now a keyword argument of HTML (e.g. format = HTML(top_menu = true)) and has been removed from makedocs. The documentation and the example in test/examples/make.jl have been updated accordingly.

2. Removing top_menu logic from the builder pipeline

Done — the builder pipeline now always follows the original code path, with no awareness of top_menu. TopMenuSection was moved to HTMLWriter.jl, and a build_top_menu_sections function derives the per-section navtrees and navlists directly from the already-built global doc.internal.navtree at render time. The navlist keyword argument that had been added to walk_navpages was also removed.

3. Removing the redirect index.html

Done — the redirect generation has been removed entirely. The file structure is now unchanged when top_menu is enabled; the top bar is purely a per-page HTML addition. The landing page warning was also restored to its original unconditional form, and the redirect-related test assertions were dropped.

4. Accessing top_menu via ctx in render_html

Done — the top_menu keyword argument has been removed from render_html. The function now takes navnode as a positional argument and calls render_top_menu(ctx, navnode) internally, so the top menu is derived from ctx rather than passed in from the call site.
@lmiq
lmiq marked this pull request as draft April 17, 2026 17:49
@lmiq

lmiq commented Apr 17, 2026

Copy link
Copy Markdown
Author

Incorporated the changes suggested above.

I'm still testing and fixing bugs.

@github-actions github-actions Bot added Status: Waiting for Review The issue or PR needs feedback by a Documenter team member and removed Status: Waiting for Author The issue or pull request needs some action by its author labels Apr 17, 2026
lmiq added 3 commits April 17, 2026 14:56
…g name and text via new(TEXT, text), leaving attributes and nodes as #undef. When navitem() returns DOM.Node("") (an empty text node because no nav items are visible), render_sidebar at line 1521 tries push!(menu.attributes, ...) and hits the uninitialized field.

The fix initializes all four fields: new(TEXT, text, Attributes(), Node[]).
…vtree (children) is empty — like "Home" => "index.md" which is a leaf node with no children — it now falls back to the full ctx.doc.internal.navtree instead of returning an empty list. This means the sidebar will show all top-level menu entries when viewing index.md, rather than being empty.
…s navtree will be [top_node] instead of []. This means:

The top menu shows "Home", "Getting started", "Structure" as before
When viewing index.md, the sidebar shows "Home" as a single entry with its in-page headings (h2s etc.)
When viewing pages under "Getting started", the sidebar shows that section's pages
The UndefRefError from the previous conversation is also gone since we no longer produce an empty text node for the menu
@lmiq

lmiq commented Apr 17, 2026

Copy link
Copy Markdown
Author

Seems to be working:

Test pages:

https://m3g.github.io/FundamentosDMC.jl/dev
(does not have a root index.md - picks the first one as landing page, redirecting).

https://m3g.github.io/PDBTools.jl/dev/
(this one has a single index.md at the top level)

https://m3g.github.io/ComplexMixtures.jl/dev/
(this one has a single index.md file - but not at the top level)

When there is no index.md file at the top level, we are redirecting. The issue with avoiding the redirection is that relative paths might break if we move to the top level the index.md file - particularly those within raw html blocks.

I think that if there is no root/index.md file, just creating one that redirects to the first "index.md" file found in the pages tree is a good alternative. If there is a root/index.md file, then no redirection occurs.

@lmiq
lmiq marked this pull request as ready for review April 17, 2026 18:24
lmiq and others added 5 commits June 23, 2026 15:12
# Conflicts:
#	assets/html/themes/catppuccin-frappe.css
#	assets/html/themes/catppuccin-latte.css
#	assets/html/themes/catppuccin-macchiato.css
#	assets/html/themes/catppuccin-mocha.css
#	assets/html/themes/documenter-dark.css
#	assets/html/themes/documenter-light.css
@fingolfin

Copy link
Copy Markdown
Member

Claude AI and me just resolved the merge conflicts (by running make themes), and had a look. Here are the unfiltered review remarks made by Claude:


Now the review. I built the example docs locally and poked at a few edge cases; findings below, roughly in order of severity.

Blockers

1. The docs describe the wrong API. top_menu is an option of HTML(), but docs/src/man/guide.md and the CHANGELOG entry both call it a makedocs keyword, and the guide's example passes top_menu = true straight to makedocs — which won't work. The HTML docstring gets it right.

Related design question: should it be on HTML at all? top_menu reinterprets the meaning of pages, which is format-agnostic, so the LaTeX writer will silently ignore it and flatten everything. Putting it on makedocs would at least make the scope explicit.

2. The new tests don't run.

  • test/examples/tests.jl uses with_logger, ConsoleLogger and Logging.Warn, but that file never imports Logging → UndefVarError as soon as the testset is reached.
  • @test_nowarn makedocs_with_topmenu(["Empty Section" => []]) doesn't just warn, it throws: BoundsError from first(ctx.doc.internal.navlist) in render_sidebar (HTMLWriter.jl:1496). This one reproduces on master without top_menu too, so it's a pre-existing bug rather than a regression — but the test asserts the opposite of what happens.
  • The "invalid top_menu entry (not a Pair)" test is actually exercising the pre-existing "…" is not an existing page! error, not any top_menu-specific validation.

3. prev/next still cross section boundaries. The guide says "The sidebar's previous/next navigation stays within each section", but .prev/.next are still populated from the global doc.internal.navlist in builder_pipeline.jl, untouched by this PR. In the example build, guide/index.html (first page of section 2) gets « Installation, pointing back into section 1. Either implement per-section chaining or drop the claim.

4. hide() is ignored by the top menu. build_top_menu_sections never looks at nn.visible. With

pages = ["Sec A" => ["index.md"], hide("Sec B" => "other.md", ["hidden.md"])]

"Sec B" shows up in the top bar and its dropdown lists the hidden page.

5. No validation of the pages shape. The docs require "Title" => [...] entries, but nothing enforces it. With a flat pages = ["index.md", "Other" => "other.md"], title = something(top_node.title_override, "") produces an empty label and you get <a class="docs-top-menu-link" href><span…>▾</span></a> in the output — silently. This should be an error.

6. CI formatting. biome check fails on assets/html/js/sidebar.js:65 (missing trailing comma after decodeURIComponent(...)).

7. CHANGELOG. The entry links [#2866] (the superseded PR) and there's no matching [#2866]: link definition, so the changelog check will fail. Should be [#2904].

Should fix

8. The DOM.jl change looks like leftover debugging. Node(text::AbstractString) = new(TEXT, text, Attributes(), Node[]) adds two allocations to every text node of every page in every build. I reverted it locally and the html-topmenu example still builds fine — is there a case that actually needs it?

9. Dead code: find_navnode_for_page (HTMLWriter.jl:1203) is never called.

10. Per-page linear scans. render_top_menu and get_section_navtree each walk every section's navlist looking for the current page, on every page render. A Dict{String,Int} page → section index built once in build_top_menu_sections would replace both.

11. The redirect index.html is a separate feature riding along. It hand-writes a meta-refresh page and suppresses the "src/index.md missing" warning, but only when top_menu is on, and it picks "first path ending in index.md" as the target, which is fairly arbitrary. Worth splitting out and discussing on its own.

12. A section node that has both a page and children loses its own page. navtree = isempty(top_node.children) ? [top_node] : top_node.children drops top_node from the sidebar and dropdown, but first_page (the section's link target) still points at it — so clicking the section title can land you on a page that isn't in its own sidebar.

13. Unrelated churn, worth dropping to keep the diff reviewable: the (src === nothing) || push!(…) → if rewrite plus a stray blank line in builder_pipeline.jl, two blank lines in documents.jl, and docs/Project.toml gaining LiveServer and a reformatted [sources] entry.

14. The html-topmenu example build passes warnonly = true, which hides whatever the feature warns about.

Minor

  • Dropdowns are display: none !important on touch, so on mobile a section is only reachable via its first page. Reasonable degradation, but not mentioned in the docs.
  • No ARIA attributes on the dropdowns, and the caret is a literal ▾ character rather than the icon font used elsewhere in the theme.
  • @test occursin("getting-started", page_content) duplicates an assertion made a few lines above.
  • mktempdir() in the edge-case helper is never cleaned up, and import Markdown is unused.

Overall the layout and CSS side looks solid and the generated markup is clean — most of what's above is documentation drift, missing validation, and tests that were never executed.

lmiq and others added 2 commits September 29, 2026 13:26
* Document `top_menu` as an option of `Documenter.HTML` (guide and
  CHANGELOG), move the CHANGELOG entry to Unreleased and link JuliaDocs#2904.
* Validate the shape of `pages`: every top-level entry must have a
  section title.
* Respect `hide`: hidden sections are not shown in the top menu, and
  hidden pages are not listed in the dropdowns.
* Keep the page of a section that is a page with sub-pages in the
  section's sidebar.
* Keep the previous/next page links within each section.
* Map pages to sections once, instead of scanning all sections for
  every rendered page; remove the unused `find_navnode_for_page`.
* Drop the redirect `index.html` feature, to be discussed separately.
* Use the icon font for the dropdown caret and add ARIA attributes.
* Revert unrelated changes (DOM.jl, builder_pipeline.jl, documents.jl,
  docs/Project.toml) and fix the Biome formatting of sidebar.js.
* Fix the tests: move the edge-case tests to unit tests in
  test/topmenu.jl, remove `warnonly` from the example build, and cover
  hidden sections, page + sub-pages sections and prev/next links.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The HTML docstring is also rendered in docs/src/lib/internals/writers.md,
where a bare `makedocs` can not be resolved from the HTMLWriter module.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@lmiq

lmiq commented Sep 29, 2026

Copy link
Copy Markdown
Author

This PR now addresses the review of the top_menu feature (JuliaDocs/Documenter.jl#2904). It targets top_menu_from_pages2 (#3), so the diff only contains the review fixes. Once #3 and this PR are merged into top_menu_from_pages, the changes will show up in #2904.

Blockers

  1. Docs described the wrong API. The guide and the CHANGELOG now say that top_menu is an option of Documenter.HTML, and the guide example uses format = Documenter.HTML(top_menu = true).
    On the design question: I kept top_menu on HTML. A top bar with dropdowns is an HTML presentation feature, and for other writers the current structure is already right: LaTeX renders the first layer of pages as parts/chapters, which is the same hierarchy. The guide now says explicitly that other writers ignore the option. If we want this at the makedocs level later, it can be added without breaking HTML(top_menu = true).
  2. The new tests did not run.
    • The Logging problem is gone: the edge cases are now unit tests in test/topmenu.jl, and the duplicate-page warning is checked with @test_logs.
    • The empty-section test now uses an empty section next to a regular one, which is the top_menu-specific case. A build where every section is empty still hits the pre-existing BoundsError in render_sidebar, which also happens on master without top_menu. It is left for a separate fix.
    • The invalid-entry test now uses an existing page (pages = ["index.md"]). That gets past the "is not an existing page" check and reaches the new top_menu validation (item 5).
  3. prev/next crossed section boundaries. The footer now uses prev_next_navnodes, which keeps the previous/next links within the page's section when top_menu is on. NavNode.prev/.next are not modified, so other writers are unaffected. The example build tests that the first page of "User Guide" has no « link and the last page of "Getting Started" has no » link.
  4. hide() was ignored. TopMenuSection now stores visible. Sections hidden with hide("Title" => "page.md") are left out of the top bar, and hidden entries are left out of the dropdowns. In the reported example, hide("Sec B" => "other.md", ["hidden.md"]), the root stays visible (that is what hide(root, children) means), so "Sec B" is still in the top bar. But hidden.md is no longer listed in its dropdown; see also item 12.
  5. pages shape was not validated. Every top-level entry must now have a title. A bare "index.md" (or hide("index.md")) throws an error saying that the entry must be a "Section Title" => pages pair. "Title" => "page.md" is still accepted as a single-page section.
  6. Biome. Added the trailing comma in sidebar.js; make check-js passes.
  7. CHANGELOG. The entry now links [#2904] and adds the link definition. It was also sitting inside the already-released v1.17.0 section after the master merges; it is now under "Unreleased".

Should fix

  1. DOM.jl is reverted to master; the example and the full test suite pass without the change.
  2. Dead code: find_navnode_for_page is removed.
  3. Per-page linear scans: build_top_menu_sections now returns a page => section index Dict, stored in HTMLContext. The active-section lookup, the sidebar tree (get_section_navtree) and prev/next all use it through top_menu_section.
  4. The redirect index.html is removed, together with the suppression of the "src/index.md missing" warning and _find_first_index_page. A top_menu site now needs a root index.md, like any other site. The example build moves its landing page to src.topmenu/index.md. The redirect can be proposed separately.
  5. A section with a page and children lost its page. A section whose top node has a page now shows that node in the sidebar and dropdown, with its children nested under it; only title-only "Title" => [...] sections show their children directly. The example has a hide("Reference" => "reference/index.md", ["reference/extra.md"]) section to cover this.
  6. Unrelated churn: builder_pipeline.jl, documents.jl and docs/Project.toml are reverted to master.
  7. warnonly = true is removed from the html-topmenu example build, which builds without errors.

Minor

  • The guide now mentions that dropdowns are not shown on touch/small screens, where a section is reached through its title.
  • The caret now uses the Font Awesome chevron (\f078) through CSS, as the sidebar does, and is aria-hidden. The top <nav> has an aria-label, and the active section link has aria-current. Themes are recompiled.
  • The duplicated occursin("getting-started", ...) assertion is gone. The example tests now check the top menu <nav> and the sidebar <ul> separately, instead of searching the whole page.
  • The remaining end-to-end makedocs edge-case test uses mktempdir() do ... end, and the unused import Markdown is removed.

Testing

  • Pkg.test(): all top_menu tests pass (unit tests in test/topmenu.jl and the HTML: top_menu example testset). The only failures are three tests that compare against the JuliaDocs/Documenter.jl git remote (HTML: repo-* and two issue links in xrefs to Main), which fail in a checkout of a fork regardless of this change.
  • make check-js passes; Julia files are formatted with Runic.

AI disclosure: this change was prepared with the help of Claude Code (Claude Opus 5.5), and reviewed by me.

🤖 Generated with Claude Code

@lmiq
lmiq marked this pull request as ready for review September 29, 2026 17:38

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Format: HTML Related to the default HTML output Status: Waiting for Review The issue or PR needs feedback by a Documenter team member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants