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:
- obsidian-git
- Automate pushing from my vault to a Github repo
- Templater
- 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.
- obsidian-export
- 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.
- make-index-files
- A small script I wrote to add missing
_index.md
files. See below. - Husky
- 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
I have a Hugo site as a GitHub repo. It’s a normal Hugo site, nothing special here.
In that repo, I have a
vault
folder, which contains my Obsidian vault. I point Obsidian at that vault for editing.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 myvault
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.
- Advanced -> Custom base path:
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/
This:
- blows away the existing site content
- converts the obsidian content in
vault/
to plain markdown incontent/
, 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.
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:
continue
index_path = dir / "_index.md"
if not index_path.exists():
index_path.write_text(f"---\ntitle: {dir.name.title()}\n---\n")
if __name__ == "__main__":
make_indexes(Path(sys.argv[1]))