Jacob Kaplan-Moss

Today I Learned…

Publishing an Obsidian vault with Hugo

Here’s one way I figured out to publish an Obsidian vault as a static website using Hugo. This is a hard way: the easy way is to spend $100/yr on Obsidian Publish.

My requirements

I want to:

  • Edit my content in Obsidian.
  • Design and publish the web version with Hugo. I want to use Hugo but only because it’s the tool I know. I’m not sure it’s actually the best tool for this – as you’ll see below it has some warts that makes this more difficult than it might with other static site generators. But I know Hugo pretty well, and am happy enough with it despite its warts.
  • Not have to worry about correct Hugo frontmatter (dates, titles, etc). I can edit these in Obsidian via its Properties feature, but I want that to be optional. I want to be able to work on stuff in Obsidian and not thing about the Hugo part at all (unless I want to).
  • Publish automatically. I don’t want an explicit “commit” or “push” or “publish” step; I just want to write and have stuff appear on the web.

The components I chose

The various pieces I knitted together to make this happen are:

Automate pushing from my vault to a Github repo
More powerful templates in Obsidian. I’m not using this very heavily yet, but it’s the tool I’ll use to ensure consistent frontmatter when and if I want to start using it more.
Converts vault documents from Obsidian’s Markdown variant into “plain” markdown. Also adds empty frontmatter where it’s missing to prevent Hugo from complaining about files without frontmatter.
A small script I wrote to add missing _index.md files. See below.
Manage a pre-commit hook to run obsidian-export on commit. There are a million other ways to manage pre-commit hooks, Husky is nice enough. I already need a working NPM setup because I’m using Tailwind and PostCSS, so using something from the Node ecosystem here isn’t a problem.
Cloudflare Pages
Where I’m publishing. I have no love for Cloudflare but their product is fast and free and stable, and I don’t dislike their CEO quite enough to have cause to look elsewhere.

Plus of course Hugo, Obsidian, Github…

How it fits together

  1. I have a Hugo site as a GitHub repo. It’s a normal Hugo site, nothing special here.

  2. In that repo, I have a vault folder, which contains my Obsidian vault. I point Obsidian at that vault for editing.

  3. obsidian-git is set up to auto-commit and auto-push changes to the vault. The key settings are:

    • Advanced -> Custom base path: ... This is the key setting that tells obsidian-git that my vault folder is actually a subdirectory of the root git repo. Without this setting, pushes will fail.
    • Backup interval: 5 minutes. Nothing special about 5 minutes, but this setting has to be set to something greater than 0 for auto-commits to happen.
  4. A pre-commit script (managed by Husky) that converts the vault:

    rm -rf content/*
    bin/obsidian-export vault/ content/ --frontmatter always
    bin/make-index-files content/
    git add -A content/


    • blows away the existing site content
    • converts the obsidian content in vault/ to plain markdown in content/, adding frontmatter
    • makes missing _index.md files, see below
    • adds the converted content/ dir to the in-flight git commit

    Thus, when obsidian-git auto-commits, this script is run, and a converted site it pushed to github.

  5. Cloudflare pages is configured to watch this repo, and build it as a Hugo site.

I could have chosen to run the conversion steps as part of the Cloudflare build. There’s no particular reason I did it as a pre-commit hook, but I do like that it means I can very easily publish this anywhere else.

Adding missing _index.md files

The last missing piece for me was adding missing _index.md files. This isn’t necessary if you don’t have multiple levels of content nesting, but I do, and so Hugo needs a little extra help here.

Hugo only partially translates your source content hierarchy into a site hierarchy. Hugo considers any top-level directories as “sections” and creates hierarchy for these, but doesn’t automatically do anything with deeper nesting. That is, if you have content/aaa/one.md and content/bbb/two.md, Hugo gets this, and your built site will have aaa and bbb sections, and your two documents will have URLs like example.com/aaa/one/ and exmaple.com/bbb/two/. But, if you then add content/aaa/ccc/three.md, that document will be considered part of the aaa section, and will have a URL like example.com/aaa/three.md – the ccc subdirectory vanishes!

The way you tell Hugo that you want to preserve this deeper nesting is by adding an _index.md file. This forces Hugo to consider that directory a “section”. So, if you add content/aaa/ccc/_index.md, Hugo now considers aaa/ccc a “section”, and your three.md document will be at example.com/aaa/ccc/three/.

I don’t want to have to think about this, so I wrote this little script to do it as part of the pre-commit hook:

#!/usr/bin/env python

import sys
from pathlib import Path

def make_indexes(containing_dir: Path):
    for dir, _, _ in containing_dir.walk():
        # don't create a top-level _index
        if dir == containing_dir:

        index_path = dir / "_index.md"
        if not index_path.exists():
            index_path.write_text(f"---\ntitle: {dir.name.title()}\n---\n")

if __name__ == "__main__":