Obsidian is the perfect Hugo CMS

Obsidian is the perfect Hugo CMS

March 7, 2025

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.

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

  1. 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.
  2. Edit the front matter with Obsidian’s beautiful Properties view.
  3. Write in Obsidian.
  4. Preview locally, with hugo server, when I’m writing on my laptop (preview not available on iPad).
  5. 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:

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.
Updated on