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:
If this is done, have a look at #318, as this will be even more an issue then.

I still don't entirely get the motivation for not wanting to maintain a properly listed index. It's essentially zero amount of _real_ work/overhead.

A few reasons.
- Keep the config file as minimal as possible (and remove duplication).
- Reduce steps for new users, list all the pages vs list all the pages and add/check the titles (for example, when I done DRF, this would have really helped).
- We already guess the title based on the filename, we should try somewhere more useful first.

This one...

> We already guess the title based on the filename, we should try somewhere more useful first.

Is a very good reason, agreed.

Which one would take precedence over the other? I suggest the following:
title from mkdocs.yml > title from md file (meta) > title from filename (if all else fails)

Would we want to implement that in `_generate_site_navigation()`[here](https://github.com/tomchristie/mkdocs/blob/master/mkdocs/nav.py#L212)?

Either this:

```
mkdocs.yml > md title > filename title
```

Or this:

```
mkdocs.yml > md title > [no title]
```

Could be okay.

I'd vote for:

```
mkdocs.yml > md title > filename title
```

That looks like a reasonable place to do it, I've not had a look at this yet.

+1 for this, it would save on a fair bit of duplication. I'd also prefer as above:

```
mkdocs.yml > md title > filename title
```

From a discussion with @d0ugal on irc:

Update `filename_to_title` [nav.py](https://github.com/mkdocs/mkdocs/blob/master/mkdocs/nav.py#L17). Use python-markdown lib to parse the markdown and find if there's a title in the markdown file. Return it if there is, otherwise return the filename.

@ugocupcic we should also rename `filename_to_title` to something like `file_to_title` as it wont just be looking at the filename anymore.

Hmm, so if @ugocupcic's suggestion is implemented, for any file which does not have a title defined in the config, that file is parsed by Markdown twice then? That seems like a bad idea to me.

I have seen other projects address this issue by creating an instance of a class for each document (let's say a class named `Page`). That class holds the raw markdown, the meta-data, and the HTML as attributes on the class. Once all Page instances are created, you also have the navigation data and can then built the site by looping through Page instances (rather than the file system). This results in only one pass through the file system and only one pass through Markdown for each page. The downside is that the entire site it held in memory (in both Markdown and HTML) at once, which could be a problem for large sites.

This is why I have recommended at various times in the past that the TOC extension not be used. If a separate meta-data script was used, then it could extract the meta-data without parsing the Markdown when building the Nav and still be used when parsing the actual Markdown. Something like [docdata](https://github.com/waylan/docdata) might be a good way to go (or your own simple in-house solution).

@waylan I agree to all of the above, the only issue is the time required to do a large refactor like this.

It is only my todolist and I've been pondering how best to work towards something like this incrementally.


Resolving patch:
diff --git a/mkdocs/build.py b/mkdocs/build.py
--- a/mkdocs/build.py
+++ b/mkdocs/build.py
@@ -10,9 +10,8 @@
 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
--- a/mkdocs/nav.py
+++ b/mkdocs/nav.py
@@ -15,18 +15,37 @@
 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
--- a/mkdocs/utils.py
+++ b/mkdocs/utils.py
@@ -9,9 +9,15 @@
 
 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
--- a/mkdocs/tests/nav_tests.py
+++ b/mkdocs/tests/nav_tests.py
@@ -348,3 +348,16 @@ def test_ancestors(self):
 
         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
--- /dev/null
+++ b/mkdocs/tests/resources/empty.md
@@ -0,0 +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
--- /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
--- /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
--- /dev/null
+++ b/mkdocs/tests/resources/with_title.md
@@ -0,0 +1,2 @@
+# Title
+