Technology / Software / 

18 Jul 2022

Obsidian to Web with `mkdocs-material`

This seems like the easiest route to a nice-looking website, aside from just paying the Obsidian folks for their built-in sync and publish features (which I still might end up doing).


First, I created a Dockerfile: (don’t copy this, it’s wrong; see the corrected one below)

Dockerfile FROM squidfunk/mkdocs-material ARG WITH_PLUGINS=true ENV PYTHONDONTWRITEBYTECODE=1 COPY ./Technotes /docs WORKDIR /docs RUN mkdocs build

Then, I created a minimal mkdocs.yml to configure the site itself:

YAML site_name: Kadin's Technotes site_url: site_author: site_dir: site theme: name: material

And then I kicked off docker build . and crossed my fingers.

Aaaaand… immediately I hit my first problem. The machine I was using was still i386, and it appears that mkdocs-material is only distributed for x86_64. Whoops. Guess it’s really past time to upgrade the old home server. So I moved to my Linux laptop, which is 64-bit clean.

Second problem – a minor one, this time. After installing Docker, you need to do a couple of things: first, you need to make sure your user is in the correct group (sudo usermod -aG docker username, after which you need to log out and then back in); second, you have to make sure the Docker daemon is actually running (duh), which is just a matter of sudo service docker start. Now the real troubleshooting can begin!


My first semi-successful run of docker build . ended with this error:

Config value: 'docs_dir'. Error: The path /docs/docs isn't an existing directory.

It would appear that the docs_dir value isn’t being specified correctly. For some reason, it’s looking for /docs/docs inside the container, which doesn’t exist. Perhaps I don’t / shouldn’t change the WORKDIR into /docs in the Dockerfile? Let’s try commenting that out and seeing what happens…

Same error. Okay, so something more subtle is going on with docs_dir, apparently.

Time to consult the docs. Here’s what they have to say about docs_dir:

The directory containing the documentation source markdown files. This can either be a relative directory, in which case it is resolved relative to the directory containing your configuration file, or it can be an absolute directory path from the root of your local file system.

It would seem that docs_dir is being resolved relative to the mkdocs.yml file, which in my case is inside /Technotes, causing it to not find the Markdown content. I think.

I decided to make the following adjustments to mkdocs.yml:

YAML # Input director for Markdown content docs_dir: . # Output directory for static HTML site_dir: ../site

And that got us a much more understandable error message:

Config value: 'docs_dir'. Error: The 'docs_dir' should not be the parent directory of the config file. Use a child directory instead so that the 'docs_dir' is a sibling of the config file.

Apparently mkdocs just really doesn’t like what I’m trying to do, with the YAML config file in the same directory as the content. Okay, fine, you win. I had hoped to have separate YAML files in each top-level content directory, so that I could build multiple sites from the same Obsidian vault, but maybe that’s not worth it.

So we just revert the dumb changes, git mv that sucker up a directory, so it’s in the top-level Vault directory right next to the Dockerfile, and then do some adjustment to our Dockerfile so that we make sure we copy it into the container. Oh, and we also need to get rid of the WORKDIR /docs line, because mkdocs really, really wants to be run in the parent directory of both the input and output. Again, fine. Sometimes it’s best to let the Wookie win, or in this case, the software written by people who know more than me about this stuff.

And what do you know, it works! Step 7/7 : RUN mkdocs build ---> Running in 3d4a44aa4d88 INFO - Cleaning site directory INFO - Building documentation to directory: /site INFO - Documentation built in 0.92 seconds Removing intermediate container 3d4a44aa4d88 ---> 52299ee963ab Successfully built 52299ee963ab

Neato. Now, we just need to figure out how to get our artifacts (the generated static HTML) out of the Docker container and onto the host’s filesystem, so we can actually do something with it. And then we can start making the site look decent.

Except… it seems like Docker really isn’t designed to work this way? It looks like there is a way to get the files out, via a “multistage build” process, but that feels like I’m somehow misusing the tool. This doesn’t seem like it’s exactly an edge case or anything. (Goes back to Googling.)

It looks like the right way to do things is with a shared volume (see this tutorial). But for now, let’s just get the site up and running in a browser. This required a bit more tweaking to the Dockerfile, in order to replace the RUN command with a proper ENTRYPOINT so we could execute mkdocs -v serve and not just mkdocs build.

Here’s the command that actually got things going:

Bash $ docker build -t mkdocs-test:v1 . [...] Successfully built a03707ebb56a Successfully tagged mkdocs-test:v1 $ docker run -it -p 8000:8000 mkdocs-test:v1 -v serve --dev-addr= [... lots and lots of debug output ...]

With this, I was able to go to localhost:8000 in Chrome and see my site. Hooray.

Cosmetic Setup

Okay, now it’s time to make this thing start looking decent. All the cosmetic stuff is controlled by mkdocs.yml (which in my case is actually called technotes-mkdocs.yml and then renamed as it’s copied into the Docker container).

Everything in mkdocs.yml is pretty well documented, but there are some hard limitations that I’m a bit disappointed by. In particular, I’m not in love with the left-side navigation pane: ![[mkdocs-material-nav_2022-07-26.png]] It seems like a slightly odd ordering, doesn’t it? That’s because it’s in alphanumeric order by file name. Not document title. The “Notes on Bluetooth trackers” doc is at the top, because its actual filename starts with “bluetooth”. Not ideal, for my purposes.

And MkDocs only has two ways of managing the nav tree: you can either let MkDocs build it automatically from the input files, or you can specify it manually in the YAML. But if you specify any part of it in the YAML configuration, that’s all you get – there’s no way to say “lay out these 5 files manually, then put the rest down here in filename order”.

Sample Configuration Files

Here’s stripped-down versions of my working configuration files.


```Dockerfile FROM squidfunk/mkdocs-material ARG WITH_PLUGINS=true ENV PYTHONDONTWRITEBYTECODE=1

COPY ./Technotes /docs COPY ./technotes-mkdocs.yml /mkdocs.yml


RUN pip install mkdocs-roamlinks-plugin

ENTRYPOINT [“mkdocs”] ``` This throws some pip warnings about running as root, but in general it seems to function fine.

MkDocs YAML (mkdocs.yml)

```YAML site_name: Kadin’s Technotes site_url: site_author: site_description: >- Technical notes and projects. copyright: Copyright © 2022 Kadin (

site_dir: site

theme: name: material locale: en features: - navigation.instant # makes internal links faster - navigation.tracking # updates address bar to anchors palette: - scheme: slate - primary: blue grey - accent: cyan

markdown_extensions: - footnotes - pymdownx.arithmatex: generic: true - pymdownx.tasklist: custom_checkbox: true - def_list - pymdownx.critic - pymdownx.caret - pymdownx.keys - pymdownx.mark - pymdownx.tilde - toc: permalink: true

use_directory_urls: false

plugins: - roamlinks # for internal Obsidian links ``` Subject to the comments about flexibility above, this seems to work well.