Obsidian is the perfect Hugo CMS
TLDR: I publish my blog using Obsidian, a writer’s app for Markdown. It saves frontmatter tagged writing and images to my static site builder Hugo, where I can preview it locally on my Mac, or use git — even on iPad! — to deploy my site to Netlify. As a writer, this might be as good as it can get.
Steph Ango — kepano on X and the current CEO of Obsidian — posted a summary of his Obsidian → Jekyll → Github → site publishing workflow back in December, and it made me realize it was time to upgrade my “write in Obsidian, copy and paste markdown to Visual Studio Code, edit, git deploy to Hugo site” workflow needed an immediate upgrade.
a quick demo of how I edit my website with Obsidian 1.8
— kepano (@kepano) December 19, 2024
write in @obsdmd → auto-build jekyll → preview local site in web viewer tab → push to github → auto-deploy to live site
I've been making websites for twenty five years, and this is by far the nicest it's ever been pic.twitter.com/Dlw7Iorxop
My prior Hugo workflow: thank you Front Matter CMS
The unfortunately named Front Matter CMS is an excellent headless CMS. It integrates directly into Visual Studio Code, provides a very very good media manager, excellent taxonomy management, and is integrated into git for publishing. I have loved blogging with it.
However.
Working within VS Code is literally working in the code environment. For me, the experience of handling text, and thinking about writing, always seemed to take third seat to everything else. I want an writing and publishing environment where I’m far from the site code and templates, and as close to the content as I can be — that’s why I brainstorm, draft, and edit in Obsidian. I’ve been so close to the ideal workflow; Obsidian saves markdown files, which Hugo uses for content. I just needed to spend some time to sort it.
My Obsidian + Hugo workflow
- In Obsidian, I create a new Note, using the Templater community plugin. It creates the note with standard YAML front matter that I defined, in a named Hugo bundle folder.
- Edit the front matter with Obsidian’s beautiful Properties view.
- Write in Obsidian.
- Preview locally, with
hugo server
, when I’m writing on my laptop (preview not available on iPad). - Push to Gitlab, which triggers automatic deployments of my website to Netlify.
The process is so easy and fun, I’ve noticed an uptick in my writing and publishing already. Things just aren’t sitting around in draft mode as they were before, waiting for a edit round in VS Code.
How to integrate Obsidian as your Hugo CMS
Migrate legacy content file structure to use page bundles
There are many ways to handle markdown content in Hugo: I chose to use page bundles. This helps my experience in Obsidian, since each post will live in a folder, named for the article URL slug, and all its image assets will store there, too. To migrate my legacy content, I use the command line to create the folders and rename files:
find ./content/ -name "*.md" -not -name "index.md" -not -name "_index.md" -exec sh -c '
for file; do
dir=$(dirname "$file")
slug=$(basename "$file" .md)
mkdir -p "$dir/$slug"
mv "$file" "$dir/$slug/index.md"
echo "Migrated $file to $dir/$slug/index.md"
done
' sh {} +
Migrate legacy TOML to YAML
In Hugo, I preferred writing TOML. But Obsidian supports only YAML for front matter. No big deal: I won’t be seeing it much in Obsidian, anyway, because of the beautiful properties view.
hugo convert toYAML --output content_as_yaml --unsafe
After such a big change, I like to ensure things are as I expect. Let’s direct Hugo to use the new YAML files to build the site. In hugo.yaml configuration file, set
contentDir: content_as_yaml
and QA the site for any issues or errors.
Rename the Hugo content directory, and set it as an Obsidian vault
In my early prototyping, I was really annoyed that I couldn’t rename a vault in Obsidian to be semantically meaningful, without renaming the folder itself, which Hugo expected to be content
. That left me with a lot of vaults called “content” that were pointing to my test Hugo sites. But, of course, you can name your Hugo content folder anything you like, and then see that name in Obsidian!
So: rename content_as_yaml
to something meaningful as a Vault in Obsidian, set the contentDir:
parameter in hugo.yaml
, and in Obsidian, open it: manage vaults > open folder as vault > open new directory
Refine Obsidian behaviors
We’ll need to adjust a few default Obsidian configurations:
- Manage linking in standard markdown: Obsidian Settings > Files and links > Use Wikilinks : off
- new link format: relative path to file
- Since we’re using page bundles, set assets to the same directory: Default location for new attachments: same folder as current file
Configure Hugo’s link rendering
Next up, we’ll want to clean up links. Obsidian’s internal linking will include the ‘.md’ extension, but we’ll need clean, human readable links for Hugo. Happily, Hugo’s render-link.html provides a vast sea of flexibility in how links are rendered. Thanks to this post, I settled on the following:
{{- $url := urls.Parse .Destination -}}
{{- $scheme := $url.Scheme -}}
<a href="
{{- if eq $scheme "" -}}
{{- if strings.HasSuffix $url.Path ".md" -}}
{{- relref .Page .Destination | safeURL -}}
{{- else -}}
{{- .Destination | safeURL -}}
{{- end -}}
{{- else -}}
{{- .Destination | safeURL -}}
{{- end -}}"
{{- with .Title }} title="{{ . | safeHTML }}"{{- end -}}>
{{- .Text | safeHTML -}}
</a>
{{- /* whitespace stripped here to avoid trailing newline in rendered result caused by file EOL */ -}}
Ignore the obsidian workspace
Obsidian stores lots of information about its state that you don’t need to commit to your repository, so:
- Add
.obsidian/workspace.json
to your.gitignore
file. - git rm –cached .obsidian/workspace.json
Use Templater to generate Notes with YAML front matter and folder structure
Now, some work to make things easy. Given Hugo’s page bundles, I want Obsidian to prompt me for a URL slug, which it will use to create the folder, and to create an index.md file in that folder, and setup all my front matter for me.
This is surprisingly tricky, because Templater’s language is a bit tricky, and takes careful reading to understand when a file is created and how to move it. This works for me:
<%*
const folderName = await tp.system.prompt("Enter slug name:");
if (!folderName) {
new Notice("Folder creation canceled.");
return;
}
// Function to URLerize the folder name
const urlerize = (name) => {
return name
.toLowerCase() // Convert to lowercase
.replace(/[^a-z0-9\s-]/g, "") // Remove special characters
.trim() // Trim whitespace
.replace(/\s+/g, "-"); // Replace spaces with hyphens
};
// URLerize the folder name
const sanitizedFolderName = urlerize(folderName);
// Define the base path for folders
const basePath = "posts";
const newFolderPath = `${basePath}/${sanitizedFolderName}`;
// Create the folder
await app.vault.createFolder(newFolderPath);
// move the default untitled file
await tp.file.move(newFolderPath + '/index');
// Define YAML front matter
%>---
title:
slug: <% sanitizedFolderName %>
description:
tags:
date: <% tp.date.now("YYYY-MM-DD") %>
draft: true
image:
layout:
excludeSearch: false
unlisted: false
---
Let's write something great:
Have Hugo ignore the template folder
Hugo gets mad when it sees Templater code in the content directory, so ignore that file in Hugo’s configuration.
Write!
Go ahead and write in Obsidian. Changes are saved to the new Hugo content directory.
Push to Git, deploy to Netlify
When editing is done, push the changes via git to the repo; Netlify picks them up, builds the site asynchronously, and deploys it live.
Other approaches
This works perfectly for me, because I want a separate vault for my websites. Other folks have approached this problem from different angles. See:
- https://jacobian.org/til/hugo-obsidian/
- https://oscarmlage.com/posts/hugo-and-obsidian/
- https://quantick.dev/posts/obsidian-hugo/
Final refinements
- obsidian-git might make it even easier to
git add -a
and push to my repo - Kepano’s obsidian-permalink-opener might play a helpful role in editing
- For a recent work-remote stint where I don’t have my primary laptop, I’ve built a mobile iPad writing solution, too. I’ll document that soon.