Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/about/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 10 additions & 1 deletion mkdocs/commands/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down
23 changes: 23 additions & 0 deletions mkdocs/structure/nav.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
48 changes: 48 additions & 0 deletions mkdocs/tests/build_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
37 changes: 36 additions & 1 deletion mkdocs/tests/structure/nav_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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")
Loading