4 minutes
Markdown Website Workflow Part 1: Hugo & Obsidian
Note: I use the hello-friend-ng theme for my site - this solution may work with other themes, but I haven’t tested it.
This post assumes familiarity with Hugo.
Background
When I decided to move my personal website to Hugo, one of my main motivations was being able to write content more easily. Instead of handling front-end formatting, I could simply write Markdown posts and let Hugo deal with the rest.
I hadn’t used a dedicated Markdown editor before, and wanted to find one that would meet my specific needs. My search for an editor eventually led me to adopt Obsidian as my main note-taking app.
It seemed like the perfect choice to use Obsidian to edit my site content. However, I wanted a degree of automation that the default setup would not provide.
Goals
I had some key objectives for this setup:
- Rapid side-by-side editing, letting me see the final version as I write
- Drag-and-drop image support, no manually typing image names
- No centralized image storage - images should be associated with their posts
- Management of basic Hugo functions (starting the development server, building the site) from within Obsidian
To achieve this, I used a combination of Hugo configuration changes, Obsidian extensions, and Batch scripting.
Implementation
Hugo
All of the files Obsidian needs to access are kept in the /content
directory. Each post is a folder, with an index.md
file containing the post content. The images for each post are inside the folder. My file structure looks like this:
└── content
└── posts
└── this-is-a-post
├── index.md
├── image1.png
├── image2.png
└── etc...
The only necessary configuration change in Hugo is modifying hugo.toml
to ignore the /config
directory in content, which will be important later.
# put in the head of hugo.toml:
ignoreFiles = ["content/config/*"]
(There are different ways to do this if you are using YAML or JSON.)
Obsidian
Note that all of the extensions listed here are available in Obsidian’s Community Plugins search, nothing needs to be downloaded externally.
Setting up the Obsidian configuration requires:
- Opening the
/content
folder as an Obsidian vault - Image drag-and-drop (native settings + paste-image-rename)
- Adding frontmatter automatically (Templater)
- Starting the development server and building the site (script-launcher)
I also chose to set up some quality-of-life improvements:
- Editing scripts from within Obsidian (obsidian-code-files)
- Custom sorting of my content folders (obsidian-custom-sort)
- Shortcuts for accessing my server management scripts (Commander)
Adding the vault
To access the site’s files in Obsidian, I simply open the /content
folder as an existing Obsidian vault. (The extra files Obsidian adds do not interfere with Hugo’s ability to parse the folder.)
Image drag-and-drop and renaming
To meet my goal of avoiding central image storage, under Files and Links, I set Obsidian to store attachments in the post folder:
The default pasted image name format has a space in it, which doesn’t work with Hugo. To fix this, I use paste-image-rename
, with the following settings:
This configuration ensures that each image has a unique, time-searchable name. Images can be manually renamed after pasting if desired.
Frontmatter automation
Hugo requires specific frontmatter in the document. This is generated automatically when using hugo new
on the command line, but not when Obsidian makes a new file.
To add this automatically, I use Templater
. This extension lets me use custom JavaScript to define templates for new files.
First, I set /config/templates
to the template folder. Next, I make a template note called hugo_frontmatter
, which contains the following template:
---
title: "<% tp.file.folder() %>"
date: <%tp.file.creation_date("YYYY-MM-DDTHH:mm:ssZ")%>
draft: true
toc: false
images:
tags:
- untagged
---
Launching scripts
When working with Hugo, I frequently have to run commands like hugo server
to load a local testing environment, or hugo
to build the site. While I could open a terminal every time, running scripts from Obsidian is more convenient.
I set up script-launcher
for each script:
Editing scripts in Obsidian
By default, Obsidian won’t let you edit scripts directly. I can get around this with obsidian-code-files
by adding bat
to the list of supported filetypes in the extension.
Custom sorting
To order my folders the way I want, I use obsidian-custom-sort
. This is a very configurable extension, but here I just pass it a list of my desired sorting order via the /config/sortspec
file:
---
sorting-spec: |-
target-folder: /
about
projects
email
posts
config
---
Shortcuts for scripts
To run the scripts from a sidebar shortcut, I use Commander
:
Clicking the new button opens a menu with all of my script-launcher
scripts.
Scripts
These scripts are stored in /content/config/scripts
for script-launcher
to access.
Due to how script-launcher
handles running scripts, I wrote each of these scripts to open as a separate window - otherwise, their processes can never be closed without quitting Obsidian.
REM runHugoServerDraft.bat
@echo off
start cmd.exe /k hugo server -D -s C:\path\to\your\hugo\folder
REM runHugoServer.bat
@echo off
start cmd.exe /k hugo server -s C:\path\to\your\hugo\folder
REM hugoBuild.bat
@echo off
start cmd.exe /c hugo -s C:\path\to\your\hugo\folder