diff --git a/mkdocs/structure/pages.py b/mkdocs/structure/pages.py
index 74fe872a..e8eaf61c 100644
--- a/mkdocs/structure/pages.py
+++ b/mkdocs/structure/pages.py
@@ -96,6 +96,19 @@ def __repr__(self):
meta: MutableMapping[str, Any]
"""A mapping of the metadata included at the top of the markdown page."""
+ @property
+ def content_title(self) -> str | None:
+ """
+ The title of the page extracted from the first `
` heading in the page content.
+
+ This is `None` until the page has been rendered. Unlike `title`,
+ this does not fall back to any other source — it reflects exactly
+ the `` text from the rendered Markdown, with HTML tags stripped
+ and entities unescaped. It is useful for plugins that need the raw
+ heading text as it appears in the page body.
+ """
+ return self._title_from_render
+
@property
def url(self) -> str:
"""The URL of the page relative to the MkDocs `site_dir`."""
diff --git a/mkdocs/tests/structure/page_tests.py b/mkdocs/tests/structure/page_tests.py
index 53af9c75..29bcf5ee 100644
--- a/mkdocs/tests/structure/page_tests.py
+++ b/mkdocs/tests/structure/page_tests.py
@@ -329,6 +329,17 @@ def test_page_title_from_markdown(self):
self.assertEqual(pg.title, "Welcome to MkDocs")
pg.render(cfg, Files([fl]))
self.assertEqual(pg.title, "Welcome to MkDocs")
+ self.assertEqual(pg.content_title, "Welcome to MkDocs")
+
+ def test_content_title_property(self):
+ """content_title is None before rendering, set after."""
+ cfg = load_config()
+ fl = File("testing.md", cfg.docs_dir, cfg.site_dir, cfg.use_directory_urls)
+ pg = Page(None, fl, cfg)
+ pg.read_source(cfg)
+ self.assertIsNone(pg.content_title)
+ pg.render(cfg, Files([fl]))
+ self.assertEqual(pg.content_title, "Welcome to MkDocs")
def _test_extract_title(self, content, expected, extensions={}):
md = markdown.Markdown(