diff --git a/docs/about/release-notes.md b/docs/about/release-notes.md index 4a4117d0..44a3c6c9 100644 --- a/docs/about/release-notes.md +++ b/docs/about/release-notes.md @@ -23,6 +23,10 @@ mkdocs, version 1.7.0 from /path/to/mkdocs (Python 3.12) Update documentation to fix broken link anchors. #62 +### Added + +* Auto-generated section titles now use the index page's title instead of the raw directory name when an index page exists. #54 + ## Version 1.7.3 (2026-05-09) ### Fixed diff --git a/mkdocs/commands/build.py b/mkdocs/commands/build.py index 0df8e90e..37f02ff2 100644 --- a/mkdocs/commands/build.py +++ b/mkdocs/commands/build.py @@ -20,7 +20,11 @@ get_files, set_exclusions, ) -from mkdocs.structure.nav import Navigation, get_navigation +from mkdocs.structure.nav import ( + Navigation, + get_navigation, + set_section_titles_from_index_pages, +) from mkdocs.structure.pages import Page from mkdocs.utils import DuplicateFilter # noqa: F401 - legacy re-export from mkdocs.utils import templates @@ -351,6 +355,11 @@ def build( + "\n - ".join(excluded) ) + # If the navigation was auto-generated, update section titles to use the + # index page's title instead of the raw directory name. + if config.get("nav") is None: + set_section_titles_from_index_pages(nav.items) + # Run `env` plugin events. env = config.plugins.on_env(env, config=config, files=files) diff --git a/mkdocs/structure/nav.py b/mkdocs/structure/nav.py index 3ebe0f47..1f911a60 100644 --- a/mkdocs/structure/nav.py +++ b/mkdocs/structure/nav.py @@ -265,3 +265,26 @@ def _add_previous_and_next_links(pages: list[Page]) -> None: zipped = zip(bookended[:-2], pages, bookended[2:]) for page0, page1, page2 in zipped: page1.previous_page, page1.next_page = page0, page2 + + +def set_section_titles_from_index_pages(items: list[StructureItem]) -> None: + """ + For auto-generated navigation, update section titles to use the index + page's title instead of the directory name. + + When `nav` is not explicitly configured, MkDocs generates section names + from directory names (e.g. "about" for an "about/" directory). If the + section contains an index page (e.g. "about/index.md"), this function + uses that page's title as the section title instead. + + This only applies when `page.title` is not None (i.e. the page has + been read/rendered, so its title is known from metadata or headings). + """ + for item in items: + if not isinstance(item, Section): + continue + for child in item.children: + if isinstance(child, Page) and child.is_index and child.title is not None: + item.title = child.title + break + set_section_titles_from_index_pages(item.children) diff --git a/mkdocs/tests/build_tests.py b/mkdocs/tests/build_tests.py index dae3b9f3..f4bd0913 100644 --- a/mkdocs/tests/build_tests.py +++ b/mkdocs/tests/build_tests.py @@ -1068,6 +1068,54 @@ def test_site_dir_contains_stale_files(self, site_dir): def test_not_site_dir_contains_stale_files(self, site_dir): self.assertFalse(build.site_directory_contains_stale_files(site_dir)) + @tempdir( + files={ + "index.md": "# Home Page", + "about/index.md": "# About Us", + "about/license.md": "# License", + } + ) + @tempdir() + def test_auto_nav_section_titles_use_index_page_titles(self, site_dir, docs_dir): + captured: dict = {} + + def on_nav(nav, config, files): + captured["nav"] = nav + return nav + + cfg = load_config(docs_dir=docs_dir, site_dir=site_dir) + cfg.plugins.events["nav"] += [on_nav] + build.build(cfg) + + about_section = captured["nav"].items[1] + self.assertEqual(about_section.title, "About Us") + + @tempdir( + files={ + "index.md": "# Home Page", + "about/index.md": "# About Us", + "about/license.md": "# License", + } + ) + @tempdir() + def test_explicit_nav_section_titles_are_preserved(self, site_dir, docs_dir): + captured: dict = {} + + def on_nav(nav, config, files): + captured["nav"] = nav + return nav + + cfg = load_config( + docs_dir=docs_dir, + site_dir=site_dir, + nav=[{"My Custom Section": ["about/index.md", "about/license.md"]}], + ) + cfg.plugins.events["nav"] += [on_nav] + build.build(cfg) + + section = captured["nav"].items[0] + self.assertEqual(section.title, "My Custom Section") + class _TestPreprocessor(markdown.preprocessors.Preprocessor): def __init__(self, base_path: str) -> None: diff --git a/mkdocs/tests/structure/nav_tests.py b/mkdocs/tests/structure/nav_tests.py index 4f020ded..1a9b9aa5 100644 --- a/mkdocs/tests/structure/nav_tests.py +++ b/mkdocs/tests/structure/nav_tests.py @@ -4,7 +4,12 @@ import unittest from mkdocs.structure.files import File, Files, set_exclusions -from mkdocs.structure.nav import Section, _get_by_type, get_navigation +from mkdocs.structure.nav import ( + Section, + _get_by_type, + get_navigation, + set_section_titles_from_index_pages, +) from mkdocs.structure.pages import Page from mkdocs.tests.base import dedent, load_config @@ -608,3 +613,33 @@ def test_get_by_type_nested_sections(self): files = Files(fs) site_navigation = get_navigation(files, cfg) self.assertEqual(len(_get_by_type(site_navigation, Section)), 2) + + def test_smart_section_titles_from_index_pages(self): + """Section titles use index page titles instead of directory names.""" + cfg = load_config(site_url="http://example.com/") + fs = [ + "index.md", + "about/index.md", + "about/license.md", + "api-guide/index.md", + "api-guide/running.md", + ] + files = Files( + [File(s, cfg.docs_dir, cfg.site_dir, cfg.use_directory_urls) for s in fs] + ) + site_navigation = get_navigation(files, cfg) + + # Before update: sections use directory names + about_section = site_navigation.items[1] + api_section = site_navigation.items[2] + self.assertEqual(about_section.title, "About") + self.assertEqual(api_section.title, "Api guide") + + # Set titles on index pages (as if they were read/rendered) + about_section.children[0].title = "About This Project" + api_section.children[0].title = "API Reference" + + set_section_titles_from_index_pages(site_navigation.items) + + self.assertEqual(about_section.title, "About This Project") + self.assertEqual(api_section.title, "API Reference")