diff --git a/mkdocs/build.py b/mkdocs/build.py
index 86617977..1bc2c318 100644
--- a/mkdocs/build.py
+++ b/mkdocs/build.py
@@ -10,9 +10,8 @@ from jinja2.exceptions import TemplateNotFound
 from six.moves.urllib.parse import urljoin
 import jinja2
 import json
-import markdown
 
-from mkdocs import nav, search, toc, utils
+from mkdocs import nav, search, utils
 from mkdocs.relative_path_ext import RelativePathExtension
 import mkdocs
 
@@ -39,19 +38,8 @@ def convert_markdown(markdown_source, site_navigation=None, extensions=(), stric
     builtin_extensions = ['meta', 'toc', 'tables', 'fenced_code']
     mkdocs_extensions = [RelativePathExtension(site_navigation, strict), ]
     extensions = utils.reduce_list(builtin_extensions + mkdocs_extensions + user_extensions)
-    md = markdown.Markdown(
-        extensions=extensions,
-        extension_configs=extension_configs
-    )
-    html_content = md.convert(markdown_source)
-
-    # On completely blank markdown files, no Meta or tox properties are added
-    # to the generated document.
-    meta = getattr(md, 'Meta', {})
-    toc_html = getattr(md, 'toc', '')
 
-    # Post process the generated table of contents into a data structure
-    table_of_contents = toc.TableOfContents(toc_html)
+    html_content, table_of_contents, meta = utils.convert_markdown(markdown_source, extensions, extension_configs)
 
     return (html_content, table_of_contents, meta)
 
diff --git a/mkdocs/nav.py b/mkdocs/nav.py
index 1b8c6c48..85d33513 100644
--- a/mkdocs/nav.py
+++ b/mkdocs/nav.py
@@ -15,18 +15,37 @@ from mkdocs import utils, exceptions
 log = logging.getLogger(__name__)
 
 
-def filename_to_title(filename):
+def file_to_title(filename):
     """
     Automatically generate a default title, given a filename.
+
+    The method parses the file to check for a title, uses the filename
+     as a title otherwise.
     """
     if utils.is_homepage(filename):
         return 'Home'
 
+    try:
+        with open(filename, 'r') as f:
+            lines = f.read()
+            _, table_of_contents, meta = utils.convert_markdown(lines, ['meta', 'toc'])
+            if "title" in meta:
+                return meta["title"][0]
+            if len(table_of_contents.items) > 0:
+                return table_of_contents.items[0].title
+    except IOError:
+        # File couldn't be found - use filename as the title
+        # this is used in tests. We use the filename as the title
+        # in that case
+        pass
+
+    # No title found in the document, using the filename
     title = os.path.splitext(filename)[0]
     title = title.replace('-', ' ').replace('_', ' ')
-    # Captialize if the filename was all lowercase, otherwise leave it as-is.
+    # Capitalize if the filename was all lowercase, otherwise leave it as-is.
     if title.lower() == title:
         title = title.capitalize()
+
     return title
 
 
@@ -220,18 +239,18 @@ def _generate_site_navigation(pages_config, url_context, use_directory_urls=True
         # then lets automatically nest it.
         if title is None and child_title is None and os.path.sep in path:
             filename = path.split(os.path.sep)[-1]
-            child_title = filename_to_title(filename)
+            child_title = file_to_title(filename)
 
         if title is None:
             filename = path.split(os.path.sep)[0]
-            title = filename_to_title(filename)
+            title = file_to_title(filename)
 
         # If we don't have a child title but the other title is the same, we
         # should be within a section and the child title needs to be inferred
         # from the filename.
         if len(nav_items) and title == nav_items[-1].title == title and child_title is None:
             filename = path.split(os.path.sep)[-1]
-            child_title = filename_to_title(filename)
+            child_title = file_to_title(filename)
 
         url = utils.get_url_path(path, use_directory_urls)
 
diff --git a/mkdocs/utils.py b/mkdocs/utils.py
index 67e4356d..9149ec09 100644
--- a/mkdocs/utils.py
+++ b/mkdocs/utils.py
@@ -9,9 +9,15 @@ and structure of the site and pages in the site.
 
 import os
 import shutil
+import markdown
+import logging
+from mkdocs import toc
 
 from six.moves.urllib.parse import urlparse
 from six.moves.urllib.request import pathname2url
+
+log = logging.getLogger(__name__)
+
 try:
     from collections import OrderedDict
     import yaml
@@ -272,6 +278,34 @@ def path_to_url(path):
     return pathname2url(path)
 
 
+def convert_markdown(markdown_source, extensions=None, extension_configs=None):
+    """
+    Convert the Markdown source file to HTML content, and additionally
+    return the parsed table of contents, and a dictionary of any metadata
+    that was specified in the Markdown file.
+    `extensions` is an optional sequence of Python Markdown extensions to add
+    to the default set.
+    """
+    extensions = extensions or []
+    extension_configs = extension_configs or {}
+
+    md = markdown.Markdown(
+        extensions=extensions,
+        extension_configs=extension_configs
+    )
+    html_content = md.convert(markdown_source)
+
+    # On completely blank markdown files, no Meta or tox properties are added
+    # to the generated document.
+    meta = getattr(md, 'Meta', {})
+    toc_html = getattr(md, 'toc', '')
+
+    # Post process the generated table of contents into a data structure
+    table_of_contents = toc.TableOfContents(toc_html)
+
+    return (html_content, table_of_contents, meta)
+
+
 def get_theme_names():
     """Return a list containing all the names of all the builtin themes."""
 

If no title is provided in the mkdocs.yml use the title in the markdown file
As per the title, this came up in #308 but it's something I've wanted for a while.

diff --git a/mkdocs/tests/nav_tests.py b/mkdocs/tests/nav_tests.py
index cdd2bf9f..90fc4469 100644
--- a/mkdocs/tests/nav_tests.py
+++ b/mkdocs/tests/nav_tests.py
@@ -348,3 +348,16 @@ class SiteNavigationTests(unittest.TestCase):
 
         for page, expected_ancestor in zip(site_navigation.pages, ancestors):
             self.assertEqual(page.ancestors, expected_ancestor)
+
+    def test_file_to_tile(self):
+        title = nav.file_to_title("mkdocs/tests/resources/empty.md")
+        self.assertEqual(title, "Mkdocs/tests/resources/empty")
+
+        title = nav.file_to_title("mkdocs/tests/resources/with_title.md")
+        self.assertEqual(title, "Title")
+
+        title = nav.file_to_title("mkdocs/tests/resources/title_metadata.md")
+        self.assertEqual(title, "Metadata Title")
+
+        title = nav.file_to_title("mkdocs/tests/resources/no_title_metadata.md")
+        self.assertEqual(title, "Title")
diff --git a/mkdocs/tests/resources/empty.md b/mkdocs/tests/resources/empty.md
new file mode 100644
index 00000000..f208f991
--- /dev/null
+++ b/mkdocs/tests/resources/empty.md
@@ -0,0 +1,1 @@
+There's no title in this file.
diff --git a/mkdocs/tests/resources/no_title_metadata.md b/mkdocs/tests/resources/no_title_metadata.md
new file mode 100644
index 00000000..af50b648
--- /dev/null
+++ b/mkdocs/tests/resources/no_title_metadata.md
@@ -0,0 +1,4 @@
+author: I have no title in metadata
+
+# Title
+
diff --git a/mkdocs/tests/resources/title_metadata.md b/mkdocs/tests/resources/title_metadata.md
new file mode 100644
index 00000000..dd4c6f02
--- /dev/null
+++ b/mkdocs/tests/resources/title_metadata.md
@@ -0,0 +1,4 @@
+title: Metadata Title
+
+# Title
+
diff --git a/mkdocs/tests/resources/with_title.md b/mkdocs/tests/resources/with_title.md
new file mode 100644
index 00000000..e7f1dc2f
--- /dev/null
+++ b/mkdocs/tests/resources/with_title.md
@@ -0,0 +1,2 @@
+# Title
+

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest mock
apt-get update apt-get install -y gcc
3.9
requirements/project.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
click==8.1.8
exceptiongroup==1.2.2
ghp-import==2.1.0
importlib_metadata==8.6.1
iniconfig==2.1.0
Jinja2==3.1.6
livereload==2.7.1
Markdown==3.7
MarkupSafe==3.0.2
-e git+https://github.com/mkdocs/mkdocs.git@aa7dd95813f49ad187111af387fa3f427720184b#egg=mkdocs
mock==5.2.0
packaging==24.2
pluggy==1.5.0
pytest==8.3.5
python-dateutil==2.9.0.post0
PyYAML==6.0.2
six==1.17.0
tomli==2.2.1
tornado==6.4.2
zipp==3.21.0

name: mkdocs
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - click==8.1.8
      - exceptiongroup==1.2.2
      - ghp-import==2.1.0
      - importlib-metadata==8.6.1
      - iniconfig==2.1.0
      - jinja2==3.1.6
      - livereload==2.7.1
      - markdown==3.7
      - markupsafe==3.0.2
      - mock==5.2.0
      - packaging==24.2
      - pluggy==1.5.0
      - pytest==8.3.5
      - python-dateutil==2.9.0.post0
      - pyyaml==6.0.2
      - six==1.17.0
      - tomli==2.2.1
      - tornado==6.4.2
      - zipp==3.21.0
prefix: /opt/conda/envs/mkdocs

mkdocs/tests/nav_tests.py::SiteNavigationTests::test_file_to_tile
mkdocs/tests/nav_tests.py::SiteNavigationTests::test_ancestors mkdocs/tests/nav_tests.py::SiteNavigationTests::test_base_url mkdocs/tests/nav_tests.py::SiteNavigationTests::test_empty_toc_item mkdocs/tests/nav_tests.py::SiteNavigationTests::test_generate_site_navigation mkdocs/tests/nav_tests.py::SiteNavigationTests::test_generate_site_navigation_windows mkdocs/tests/nav_tests.py::SiteNavigationTests::test_indented_toc mkdocs/tests/nav_tests.py::SiteNavigationTests::test_indented_toc_missing_child_title mkdocs/tests/nav_tests.py::SiteNavigationTests::test_invalid_pages_config mkdocs/tests/nav_tests.py::SiteNavigationTests::test_nested_ungrouped mkdocs/tests/nav_tests.py::SiteNavigationTests::test_nested_ungrouped_no_titles mkdocs/tests/nav_tests.py::SiteNavigationTests::test_nested_ungrouped_no_titles_windows mkdocs/tests/nav_tests.py::SiteNavigationTests::test_relative_md_links_have_slash mkdocs/tests/nav_tests.py::SiteNavigationTests::test_simple_toc mkdocs/tests/nav_tests.py::SiteNavigationTests::test_walk_empty_toc mkdocs/tests/nav_tests.py::SiteNavigationTests::test_walk_indented_toc mkdocs/tests/nav_tests.py::SiteNavigationTests::test_walk_simple_toc
mkdocs/utils.py mkdocs/nav.py mkdocs/build.py
mkdocs/utils.py mkdocs/nav.py mkdocs/build.py