Skip to content

Setting up a blog

Material for MkDocs makes it very easy to build a blog, either as a sidecar to your documentation or standalone. Focus on your content while the engine does all the heavy lifting, automatically generating archive and category indexes, post slugs, configurable pagination and more.


Check out our blog, which is created with the new built-in blog plugin!

Configuration

Built-in blog plugin

Sponsors only · insiders-4.23.0 · Plugin · Experimental

The built-in blog plugin adds support for building a blog from a folder of posts, which are annotated with dates and other structured data. First, add the following lines to mkdocs.yml:

plugins:
  - blog

If you need to be able to build your documentation with and without Insiders, please refer to the built-in plugins section to learn how shared configurations help to achieve this.

By default, the built-in blog plugin assumes that your blog is hosted inside the blog subfolder of your documentation (this is configurable). Next, you need to create the following structure:

.
├─ docs/
  └─ blog/
     ├─ posts/
     └─ index.md
└─ mkdocs.yml

Since the built-in blog plugin auto-generates archive and category indexes, it must know where to add those to the navigation. Thus, make sure to add a blog/index.md file in mkdocs.yml:

nav:
  - Blog:
    - blog/index.md # (1)!
  1. Within this file, you can specify the title of your blog, which is then picked up and used by the built-in blog plugin:

    # Blog
    

The following configuration options are available:

enabled

Default: true – This option specifies whether the plugin is enabled when building your project. If you want to speed up local builds, you can use an environment variable:

plugins:
  - blog:
      enabled: !ENV [CI, false]
blog_dir

Default: blog – This option specifies the folder where your posts and metadata live. The name of the folder will also be included in the generated URLs as a prefix to all blog-related pages. If you want to build a standalone blog, change it to .:

plugins:
  - blog:
      blog_dir: path/to/folder
plugins:
  - blog:
      blog_dir: .

The path must be defined relative to docs_dir.

blog_toc

Default: false – This option specifies whether indexes include a table of contents with all post titles on the right side as an overview:

plugins:
  - blog:
      blog_toc: true

Note that this setting is also used as the default value for archive_toc and categories_toc, unless those settings are explicitly defined.

The built-in blog plugin has dozens of options that allow for advanced configuration. It's a good idea to start writing your first post, and come back here later for fine-tuning the output.


Posts

The following configuration options are available for posts:

post_date_format

Default: long – This option specifies the date format that is used when posts are rendered. Under the hood, the built-in blog plugin leverages Babel to render dates locale-aware using the configured site language. The following formats are supported:

plugins:
  - blog:
      post_date_format: full
plugins:
  - blog:
      post_date_format: long
plugins:
  - blog:
      post_date_format: medium
plugins:
  - blog:
      post_date_format: short

Note that depending on the site language, formats might look different for other languages. Additionally, Babel supports a pattern syntax which allows for custom formats.

post_url_date_format

Default: yyyy/MM/dd – This option specifies the date format that is used in the URL of the post. The format string must adhere to Babel's pattern syntax. Some examples:

plugins:
  - blog:
      post_url_date_format: yyyy/MM/dd
plugins:
  - blog:
      post_url_date_format: yyyy/MM
plugins:
  - blog:
      post_url_date_format: yyyy

If you want to exclude the date altogether, e.g. when your blog features mostly evergreen content, you can remove the date placeholder from the format string (see below).

post_url_format

Default: {date}/{slug} – This option specifies the format string that is used for the URL of the post. The following placeholders are currently supported:

  • categories – Replaced with the post's slugified categories.

  • date – Replaced with the post's date, as configured in post_url_date_format.

  • slug – Replaced with a slug generated from the post's title.

  • file – Replaced with the post's file name.

plugins:
  - blog:
      post_url_format: "{date}/{slug}"
plugins:
  - blog:
      post_url_format: "{slug}"

If you remove the date placeholder, make sure that post URLs don't collide with other the URLs of other pages added to the blog section, as this leads to undefined behavior.

post_url_max_categories

Default: 1 – This option specifies the number of categories that are included in the URL if the categories placeholder is part of post_url_format. If a post is assigned to multiple categories, they are joined with /:

plugins:
  - blog:
      post_url_format: "{categories}/{slug}"
      post_url_max_categories: 2
post_slugify

Default: headerid.slugify – This option specifies which function to use for generating URL-compatible slugs from post titles. Python Markdown Extensions comes with several Unicode-aware slug functions which should be a good choice for non-ASCII languages:

plugins:
  - blog:
      post_slugify: !!python/object/apply:pymdownx.slugs.slugify
        kwds:
          case: lower
plugins:
  - blog:
      post_slugify: !!python/object/apply:pymdownx.slugs.slugify
post_slugify_separator

Default: - – This option specifies the separator which is used by the slug function. By default, a hyphen is used, but it can be changed to any string, including the empty string:

plugins:
  - blog:
      post_slugify_separator: "-"
post_excerpt

Default: optional – This option specifies whether post excerpts should be considered being optional or required by the built-in blog plugin when generating indexes. If excerpts are required, the plugin terminates with an error if a post doesn't define an excerpt:

plugins:
  - blog:
      post_excerpt: optional
plugins:
  - blog:
      post_excerpt: required
post_excerpt_max_authors

Default: 1 – This option specifies the number of authors rendered in post excerpts. While each post may be written by multiple authors, this setting allows to limit the display to just a few or even a single author, or disable authors in excerpts altogether:

plugins:
  - blog:
      post_excerpt_max_authors: 2
plugins:
  - blog:
      post_excerpt_max_authors: 0
post_excerpt_max_categories

Default: 5 – This option specifies the number of categories rendered in post excerpts. While each post may be assigned to multiple categories, the built-in blog plugin can be instructed to only show the first n categories to keep it short and concise:

plugins:
  - blog:
      post_excerpt_max_categories: 2
plugins:
  - blog:
      post_excerpt_max_categories: 0
post_excerpt_separator

Default: <!-- more --> – This option specifies the separator the built-in blog plugin will look for in a post's content when generating post excerpts. All content after the separator is not considered to be part of the excerpt.

post_readtime

Default: true – This option specifies whether the built-in blog plugin should compute the reading time of a post automatically, which is then rendered in post excerpts, as well as in the posts themselves. If you want to disable reading time computation, add:

plugins:
  - blog:
      post_readtime: false
post_readtime_words_per_minute

Default: 265 – This option specifies the number of words that a reader is expected to read per minute when computing the reading time of a post. If you feel that estimation is not quite right, you can fine-tune reading time computation with the following setting:

plugins:
  - blog:
      post_readtime_words_per_minute: 265

Archive

The following configuration options are available for archive index generation:

archive

Default: true – This option specifies whether the built-in blog plugin should generate archive indexes. An archive indexes shows all posts for a specific interval (e.g. year, month, etc.) in reverse chronological order. If you want to disable archive index generation, add:

plugins:
  - blog:
      archive: false
archive_name

Default: automatically set – This option specifies the title of the archive section which the built-in blog plugin will generate and add to the navigation. If this setting is omitted, it's sourced from the translations, falling back to English. Change it with:

plugins:
  - blog:
      archive_name: Archive
archive_date_format

Default: yyyy – This option specifies the date format that is used when archive indexes are rendered. The format string must adhere to Babel's pattern syntax. Popular settings are:

plugins:
  - blog:
      archive_date_format: yyyy
plugins:
  - blog:
      archive_date_format: MMMM yyyy
archive_url_date_format

Default: yyyy – This option specifies the date format that is used in the archive index URL. The format string must adhere to Babel's pattern syntax. Some examples:

plugins:
  - blog:
      archive_url_date_format: yyyy
plugins:
  - blog:
      archive_url_date_format: yyyy/MM
archive_url_format

Default: archive/{date} – This option specifies the format string that is used for the URL of the archive index, and can be used to localize the URL:

plugins:
  - blog:
      archive_url_format: "archive/{date}"
plugins:
  - blog:
      archive_url_format: "{date}"
archive_toc

Default: false – This option specifies whether an archive index includes a table of contents with all post titles on the right side as an overview:

plugins:
  - blog:
      archive_toc: true

Categories

The following configuration options are available for category index generation:

categories

Default: true – This option specifies whether the built-in blog plugin should generate category indexes. A category index shows all posts for a specific category in reverse chronological order. If you want to disable category index generation, add:

plugins:
  - blog:
      categories: false
categories_name

Default: automatically set – This option specifies the title of the category section which the built-in blog plugin will generate and add to the navigation. If this setting is omitted, it's sourced from the translations, falling back to English. Change it with:

plugins:
  - blog:
      categories_name: Categories
categories_url_format

Default: category/{slug} – This option specifies the format string that is used for the URL of a category index, and can be used to localize the URL:

plugins:
  - blog:
      categories_url_format: "category/{slug}"
plugins:
  - blog:
      categories_url_format: "{slug}"
categories_slugify

Default: headerid.slugify – This option specifies which function to use for generating URL-compatible slugs from categories. Python Markdown Extensions comes with several Unicode-aware slug functions which should be a good choice for non-ASCII languages:

plugins:
  - blog:
      categories_slugify: !!python/object/apply:pymdownx.slugs.slugify 
        kwds:
          case: lower
plugins:
  - blog:
      categories_slugify: !!python/object/apply:pymdownx.slugs.slugify
categories_slugify_separator

Default: - – This option specifies the separator which is used by the slug function. By default, a hyphen is used, but it can be changed to any string, including the empty string:

plugins:
  - blog:
      categories_slugify_separator: "-"
categories_allowed

Default: none – This option specifies the categories that are allowed to be used in posts. If this setting is omitted, the built-in blog plugin will not check category names. Use this option to define a list of categories in order to catch typos:

plugins:
  - blog:
      categories_allowed:
        - General
        - Search
        - Performance
categories_toc

Default: false – This option specifies whether a category index includes a table of contents with all post titles on the right side as an overview:

plugins:
  - blog:
      categories_toc: true

Pagination

The following configuration options are available for index pagination:

pagination

Default: true – This option specifies whether the built-in blog plugin should paginate the index. The index shows all posts in reverse chronological order, which can be many. If you want to disable index pagination, add:

plugins:
  - blog:
      pagination: false
pagination_per_page

Default: 10 – This option specifies the number of posts rendered on a single index page. If more posts are found, they are assigned to a 2nd page, and so on. If you have large post excerpts, it might be a good idea to reduce the number of posts per page:

plugins:
  - blog:
      pagination_per_page: 5
pagination_url_format

Default: page/{page} – This option specifies the format string that is used for the URL of the paginated index, and can be used to localize the URL:

plugins:
  - blog:
      pagination_url_format: "page/{page}"
plugins:
  - blog:
      pagination_url_format: "{page}"
pagination_template

Default: ~2~ – This option specifies the format string that is provided to the paginate module, which allows to customize how pagination is constructed. Popular choices:

plugins:
  - blog:
      pagination_template: "~2~"
plugins:
  - blog:
      pagination_template: "$link_first $link_previous ~2~ $link_next $link_last"
plugins:
  - blog:
      pagination_template: "$link_previous $page $link_next"

The paginate module exposes the following placeholders:

  • $first_page – number of first reachable page
  • $last_page – number of last reachable page
  • $page – number of currently selected page
  • $page_count – number of reachable pages
  • $items_per_page – maximal number of items per page
  • $first_item – index of first item on the current page
  • $last_item – index of last item on the current page
  • $item_count – total number of items
  • $link_first – link to first page (unless this is first page)
  • $link_last – link to last page (unless this is last page)
  • $link_previous – link to previous page (unless this is first page)
  • $link_next – link to next page (unless this is last page)
pagination_keep_content

Default: false – This option specifies whether paginated index pages should inherit the custom content from the index page, i.e. the content of blog/index.md:

plugins:
  - blog:
      pagination_keep_content: true

Authors

The following configuration options are available for author info:

authors

Default: true – This option specifies whether the built-in blog plugin should generate author info. If it is enabled, the plugin will look up authors in a file called .authors.yml and include authors in indexes and in posts. If you want to disable this behavior, add:

plugins:
  - blog:
      authors: false
authors_file

Default: .authors.yml – This option specifies the name of the file where the authors for your posts resides. The default settings assumes that the file is called .authors.yml (mind the . at the beginning):

plugins:
  - blog:
      authors_file: .authors.yml

The path must be defined relative to blog_dir. Also see the section on adding authors.

Drafts

The following configuration options are available for drafts:

draft

Default: false – This option specifies whether the built-in blog plugin should also include posts marked as drafts when the site is being built. Including draft posts might be desired in deploy previews, which is why it exists in the first place:

plugins:
  - blog:
      draft: true
plugins:
  - blog:
      draft: false
draft_on_serve

Default: true – This option specifies whether posts marked as drafts should be included when previewing your site with mkdocs serve. By default, drafts are rendered when previewing, but skipped when the site is being built:

plugins:
  - blog:
      draft_on_serve: true
draft_if_future_date

Default: false – This option specifies whether the built-in blog plugin should mark posts with a future date as drafts. When the date passed today, the post is automatically unmarked and included when the site is being built:

plugins:
  - blog:
      draft_if_future_date: true

RSS

Sponsors only · insiders-4.23.0 · Plugin

The built-in blog plugin integrates seamlessly with the RSS plugin, which provides a simple way to add an RSS feed to your blog (or to your whole documentation). Install it with pip:

pip install mkdocs-rss-plugin

Then, add the following lines to mkdocs.yml:

plugins:
  - rss:
      match_path: blog/posts/.* # (1)!
      date_from_meta:
        as_creation: date
      categories:
        - categories
        - tags # (2)!
  1. The RSS plugin allows to filter for URLs to be included in the feed. In this example, only blog posts will be part of the feed.

  2. If you want to include a post's categories as well as its tags in the feed, add both categories and tags here.

The following configuration options are supported:

enabled

Default: true – This option specifies whether the plugin is enabled when building your project. If you want to speed up local builds, you can use an environment variable:

plugins:
  - rss:
      enabled: !ENV [CI, false]
match_path

Default: .* – This option specifies which pages should be included in the feed. For example, to only include blog posts in the feed, use the following regular expression:

plugins:
  - rss:
      match_path: blog/posts/.*
date_from_meta

Default: none – This option specifies which front matter property should be used as a creation date of a page in the feed. It's recommended to use the date property:

plugins:
  - rss:
      date_from_meta:
        as_creation: date
categories

Default: none – This option specifies which front matter properties are used as categories as part of the feed. If you use categories and tags, add both with the following lines:

plugins:
  - rss:
      categories:
        - categories
        - tags
comments_path

Default: none – This option specifies the anchor at which comments for a post or page can be found. If you've integrated a comment system, add the following lines:

plugins:
  - rss:
      comments_path: "#__comments"

Material for MkDocs will automatically add the necessary metadata to your site which will make the RSS feed discoverable by browsers and feed readers. Note that the RSS plugin comes with several other configuration options. For further information, see the documentation.

Usage

Writing your first post

After you've successfully set up the built-in blog plugin, it's time to write your first post. The plugin doesn't assume any specific directory structure, so you're completely free in how you organize your posts, as long as they are all located inside the posts directory:

.
├─ docs/
  └─ blog/
     ├─ posts/
       └─ hello-world.md # (1)!
     └─ index.md
└─ mkdocs.yml
  1. If you'd like to arrange posts differently, you're free to do so. The URLs are built from the format specified in post_url_format and the titles and dates of posts, no matter how they are organized inside the posts directory.

Create a new file called hello-world.md and add the following lines:

---
draft: true # (1)!
date: 2022-01-31
categories:
  - Hello
  - World
---

# Hello world!
...
  1. If you mark a post as a draft, a red marker appears next to the post date on index pages. When the site is built, drafts are not included in the output. This behavior can be changed, e.g. for rendering drafts when building deploy previews.

When you spin up the live preview server, you should be greeted by your first post! You'll also realize, that archive and category indexes have been automatically generated for you.

Adding an excerpt

The blog index, as well as archive and category indexes can either list the entire content of each post, or excerpts of posts. An excerpt can be created by adding a <!-- more --> separator after the first few paragraphs of a post:

# Hello world!

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod
nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor
massa, nec semper lorem quam in massa.

<!-- more -->
...

When the built-in blog plugin generates all indexes, the content before the excerpt separator is automatically extracted, allowing the user to start reading a post before deciding to jump in.

Adding authors

In order to add a little more personality to your posts, you can associate each post with one or multiple authors. First, create the .authors.yml file in your blog directory, and add an author:

squidfunk:
  name: Martin Donath
  description: Creator
  avatar: https://github.com/squidfunk.png

The .authors.yml file associates each author with an identifier (in this example squidfunk), which can then be used in posts. The following properties are available for each author:

name

Default: none · Required – This property must define a name for the author. The name is displayed in the left sidebar of each post as part of the author info.

description

Default: none · Required – This property can be used to add a short description for the author, e.g. the role or profession of the author, or any other title.

avatar

Default: none · Required – This property must point to a valid image URL, internal or external, and is used as part of posts and excerpts as the author's avatar.

Now, you can assign one or more authors to a post by referencing their identifiers in the front matter of the Markdown file under the authors property. For each author, a small profile is rendered in the left sidebar of each post, as well as in post excerpts on index pages:

---
date: 2022-01-31
authors:
  - squidfunk
    ...
---

# Hello world!
...

Adding categories

Categories are an excellent way for grouping your posts thematically on dedicated index pages. This way, a user interested in a specific topic can explore all of your posts on this topic. Make sure categories are enabled and add them to the front matter categories property:

---
date: 2022-01-31
categories:
  - Hello
  - World
---

# Hello world!
...

If you want to save yourself from typos when typing out categories, you can define your desired categories in mkdocs.yml as part of the categories_allowed configuration option. The built-in blog plugin will stop the build if a category is not found within the list.

Adding tags

Besides categories, the built-in blog plugin also integrates with the built-in tags plugin. If you add tags in the front matter tags property as part of a post, the post is linked from the tags index:

---
date: 2022-01-31
tags:
  - Foo
  - Bar
---

# Hello world!
...

As usual, the tags are rendered above the main headline and posts are linked on the tags index page, if configured. Note that posts are, as pages, only linked with their titles.

Related links offer the perfect way to prominently add a further reading section to your post that is included in the left sidebar, guiding the user to other destinations of your documentation. Use the front matter links property to add related links to a post:

---
date: 2022-01-31
links:
  - setup/setting-up-site-search.md#built-in-search-plugin
  - insiders/index.md#how-to-become-a-sponsor
---

# Hello world!
...

You can use the exact same syntax as for the nav section in mkdocs.yml, which means you can set explicit titles for links, add external links and even use nesting:

---
date: 2022-01-31
links:
  - setup/setting-up-site-search.md#built-in-search-plugin
  - insiders/index.md#how-to-become-a-sponsor
  - Nested section:
    - External link: https://example.com
    - setup/setting-up-site-search.md
---

# Hello world!
...

If you look closely, you'll realize that you can even use an anchor to link to a specific section of a document, extending the possibilities of the nav syntax in mkdocs.yml. The built-in blog plugin resolves the anchor and sets the title of the anchor as a subtitle of the related link.

Note that all links must be relative to docs_dir, as is also the case for the nav setting.

Linking from and to posts

While post URLs are dynamically computed, the built-in blog plugin ensures that all links from and to posts and a post's assets are correct. If you want to link to a post, just use the path to the Markdown file as a link reference (links must be relative):

[Hello World!](blog/posts/hello-world.md)

Linking from a post to a page, e.g. the index, follows the same method:

[Blog](../index.md)

All assets inside the posts directory are copied to the blog/assets folder when the site is being built. Of course, you can also reference assets from posts outside of the posts directory. The built-in blog plugin ensures that all links are correct.

Setting the reading time

When enabled, the readtime package is used to compute the expected reading time of each post, which is rendered as part of the post and post excerpt. Nowadays, many blogs show reading times, which is why the built-in blog plugin offers this capability as well.

Sometimes, however, the computed reading time might not feel accurate, or result in odd and unpleasant numbers. For this reason, reading time can be overridden and explicitly set with the front matter readtime property for a post:

---
date: 2022-01-31
readtime: 15
---

# Hello world!
...

This will disable automatic reading time computation.

Setting defaults

If you have a lot of posts, it might feel redundant to define all of the above for each post. Luckily, the built-in meta plugin allows to set default front matter properties per folder. You can group your posts by categories, or authors, and add a .meta.yml file to set common properties:

.
├─ docs/
  └─ blog/
     ├─ posts/
     ├─ .meta.yml # (1)!
     └─ index.md
└─ mkdocs.yml
  1. As already noted, you can also place a .meta.yml file in nested folders of the posts directory. This file then can define all front matter properties that are valid in posts, e.g.:

    authors:
      - squidfunk
    categories:
      - Hello
      - World
    

Note that order matters – the built-in meta plugin must be defined before the blog plugin in mkdocs.yml, so that all set defaults are correctly picked up by the built-in blog plugin:

plugins:
  - meta
  - blog

Lists and dictionaries in .meta.yml files are merged and deduplicated with the values defined for a post, which means you can define common properties in .meta.yml and then add specific properties or overrides for each post.

Adding pages

Besides posts, it's also possible to add static pages to your blog by listing the pages in the nav section of mkdocs.yml. All generated indexes are included after the last specified page. For example, to add a page on the authors of the blog, add the following to mkdocs.yml:

nav:
  - Blog:
    - blog/index.md
    - blog/authors.md
      ...

Customization

Custom index pages

insiders-4.24.0 · Experimental

If you want to add custom content to automatically generated archive and category indexes, e.g. to add a category description prior to the list of posts, you can manually create the category page in the same location where the built-in blog plugin would create it:

.
├─ docs/
  └─ blog/
     ├─ category/
       └─ hello.md # (1)!
     ├─ posts/
     └─ index.md
└─ mkdocs.yml
  1. The easiest way is to first add the category to the blog post, then take the URL generated by the built-in blog plugin and create the file at the corresponding location in the blog_dir folder.

    Note that the shown directory listing is based on the default configuration. If you specify different values for the following options, be sure to adjust the path accordingly:

You can now add arbitrary content to the newly created file, or set specific front matter properties for this page, e.g. to change the page description:

---
description: Nullam urna elit, malesuada eget finibus ut, ac tortor.
---

# Hello
...

All post excerpts belonging to the category are automatically appended.

Overriding templates

The built-in blog plugin is built on the same basis as Material for MkDocs, which means you can override all templates used for the blog by using theme extension as usual.

The following templates are added by the built-in blog plugin:


Last update: April 14, 2023
Created: April 14, 2023