Skip to content

Hierarchical Yaml Config #2218

@waylan

Description

@waylan

In the real world, many users have bits and pieces of various MkDocs sites which are shared. They have shared content among multiple different sites. Or they have multiple sites which all come under the same organization and share much of the same config with only the content being different. Or they may have some combination of the two. Often times, they may even include the source of all of their sites under a single repo.

However, MkDocs requires that each 'site' be completely defined in its own standalone config file. The assumption is that each site is in its own repo. There are no options to include or inherit options from other config files. Then the build command must be run either with the config file explicitly named or from a unique subdir. To edit/change a single option across all sites, the option must be manually edited in every individual config file.

Naturally, many users have developed various hacks to work around those limitations. And every time we want to move forward with certain types of changes, those hacks always get in the way. Therefore, I propose we officially support some type of hierarchical system which can pull config options from multiple files together for a single site. Unfortunately, there is no single obvious way forward here as YAML provides little out-of-the-box in this regard. Therefore I have outlined multiple different possible solutions below.

To be clear, I am not promising that we will implement anything proposed here. The goal of this discussion is to explore and narrow the options down to determine if we can settle on a single workable solution. The conclusion could be that we won't do anything. Obviously input from users who have real world experience with the situations described above would be most helpful here.

PyYAML's built-in inheritance

The PyYAML lib, which we use to parse the config files, has limited support for inheritance.

A config file might look like this:

dev: &default
    site_name: My Site
    theme:
        name: material
        # complex theme config here...

production:
    <<: *default
    site_url: 'https://example.com'

files:
    <<: *default
    site_url: ''
    use_directory_urls: false

Of course, with that config, we obviously want to use a different one of the configs for the dev server, the production website and files which are distributed for browsing on the file system. We would need provide some extra commandline flag to specify which one to use. But we might also define some defaults in the config file as well. Perhaps something like this:

defaults:
    - command: serve
      use: dev
    - command: deploy
      use: production
    - command: build
      use: files

Then, MkDocs would check the defaults and compare that against the command used to determine which of the configs to use. That seems simple enough to implement and requires no extra dependencies. However, I don;t really think it is the right solution. In addition to requiring us to add another commandline flag, there are a number of limitations.

All separate configs must be defined in the same file. We do not get deep merging of nested key-value pairs. We could only override or add root level items. In other words, to override one item three levels deep into the theme config would require redefining the entire theme config. The same goes for a multilevel nav. This StackOverflow question explores workarounds for these limitations.

File include

A simple implementation of this was proposed and rejected in #942. However, I think it is worthy of reevaluation. Of note is the pyyaml-include library, which provides a ready-made constructor. We would only need to add that constructor to the existing loader. To implement the same config as above, one might do:

theme-option.yml

name: material
# complex theme config here...

site-name-option.yml

"My Site"

dev.yml

site_name: !include site-name-option.yml
theme: !include theme-option.yml

production.yml

site_url: 'https://example.com'
site_name: !include site-name-option.yml
theme: !include theme-option.yml

file.yml

site_url: ''
use_directory_urls: false
site_name: !include site-name-option.yml
theme: !include theme-option.yml

To call this, you would need to explicitly provide one of dev.yml, production.yml or file.yml to the build command. There is no obvious way to define defaults here other than naming one of the files mkdocs.yml.

While this is very powerful, it requires a lot of files to be useful. As there is no inheritance, every root level key needs to be defined in every config as either a hard value or as an included value.

It seems to me that this might be more useful in stitching together a complex navigation (where each subdir might have its own nav.yml file and all are included in the base config), but is not really useful for the rest of the the config.

File merge

I found at least two libraries which will deep merge multiple YAML files together into a single config: yamlreader and hiyapyco. They both take a list of YAML files, and then deep merge them in order.

Deep merging allows a user to redefine one obscure theme config option three levels deep, or insert a new subsection in the nav multiple levels down. However, the user needs to pass a list of config files in order every time. Perhaps something like:

mkdocs build -f foo.yml -f bar.yml -f baz.yml

Mixing up the order would break/change the config in possibly unintended ways. There is no way to define the hierarchy in the config files themselves. While this has the potential to be very powerful, I don't find if particularly compelling from a usability perspective.

File inheritance

We could implement our own inheritance scheme. A YAML file would define its parent. That parent would then be parsed, and the current file would then get deep merged with the parent. Of course, when the parent was loaded, it was recursively loaded in the same way so that there could be any number of levels of inheritance. Some example libraries which might be useful for the merging include jsonmerge, mergedeep, and deep_merge (not a comprehensive list). jsonmerge has an interesting option of being able to define a scheme so that certain keys can be merged in different ways that others. The other solutions all provide a single merging behavior for everything.

There is no existing implementation that I could find so we would be inventing our own syntax here. I'm not sure how best to indicate that a file inherits from another so I went with !inheritsfrom filename.yml (I also considered !parent filename.yml). Suggestions are welcome. In any event, the same config as above might be defined like this:

dev.yml

site_name: My Site
theme:
    name: material
    # complex theme config here...

production.yml

!inheritsfrom dev.yml
site_url: 'https://example.com'
theme: some: deep: option: "new value"

Note the override of theme.some.deep.option which would only override that value and not any of the rest of the deeply nests structure. See the docs for the libs linked above for details.

files.yml

!inheritsfrom production.yml
site_url: ''
use_directory_urls: false

As with the "file include" proposal above, to call this, you would need to explicitly provide one of dev.yml, production.yml or file.yml to the build command. There is no obvious way to define defaults here other than naming one of the files mkdocs.yml.

While this seems like the best option to me, it also has the largest barrier in that we would be building and maintaining it ourselves. We would prefer to use something which was developed and maintained as a separate library.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions