Repository: mkdocs/mkdocs

Issue:
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.


Discussion:

Patch:
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."""
 


Test patch:
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
+