When writing blog posts in Hugo, I often want to open the file that I’m currently editing in my browser, so I can preview how the post looks. It’d be nice to do this automatically from a vim shortcut. Fortunately, Hugo can introspect on the pages it generates, which allows us to programatically find the URL of the current page we’re editing.
Hugo’s CLI tool has a command that outputs all the pages it generates:
hugo list
. The two fields this command outputs that we care about are path
and permalink
: path
is the filesystem path of the content file (think
/content/path/to/my/post.md
) and permalink is the actual URL of the page that
Hugo generates.
$ hugo list all
path,slug,title,date,expiryDate,publishDate,draft,permalink
content/blog/2020-07-10-Vim-Tip-Open-Hugo-Page-in-Browser/index.md,,Vim Tip: Open Hugo Page in Browser,2020-07-10T08:38:03Z,0001-01-01T00:00:00Z,2020-07-10T08:38:03Z,false,https://benjamincongdon.me/blog/2020/07/10/Vim-Tip-Open-Hugo-Page-in-Browser/
content/archive.md,,Blog Archive,0001-01-01T00:00:00Z,0001-01-01T00:00:00Z,0001-01-01T00:00:00Z,false,https://benjamincongdon.me/blog/archive/
...
From the above, we see that the path of this post is
content/blog/2020-07-10-Vim-Tip-Open-Hugo-Page-in-Browser/index.md
, and its
permalink is
https://benjamincongdon.me/blog/2020/07/08/Vim-Tip-Open-Hugo-Page-in-Browser/
.
Only, this isn’t quite right for local development. When previewing posts
locally, we want a localhost base URL – usually something starting with
http://localhost:1313
. Fortunately, we can set the HUGO_BASEURL
environment
variable to make the permalinks work locally:
$ HUGO_BASEURL="http://localhost:1313" hugo list all
path,slug,title,date,expiryDate,publishDate,draft,permalink
content/blog/2020-07-10-Vim-Tip-Open-Hugo-Page-in-Browser/index.md,,Vim Tip: Open Hugo Page in Browser,2020-07-10T08:38:03Z,0001-01-01T00:00:00Z,2020-07-10T08:38:03Z,false,https://localhost:1313/blog/2020/07/10/Vim-Tip-Open-Hugo-Page-in-Browser/
...
In order to open the permalink
yielded by hugo list
, we have to extract it
from the list of results:
hugo list all | grep /path/to/page.md | head -n 1 | cut -d ',' -f 8
We use grep
to filter the results to the page we want to preview, head
to
ensure we only ever get 1 result, and cut
to extract the permalink
field.
Here’s what that looks like translated into vimscript:
let l:url = systemlist("hugo list all | grep " . l:contentpath . " | head -n 1 | cut -d ',' -f 8")[0]
Next, we have to do some scripting in Vim so that the hugo
CLI tool is
correctly run from the Hugo base directory (i.e. the directory that contains
your config.{toml,yaml,json}
). If hugo
isn’t run from the directory
containing your site’s configuration, hugo list
won’t work correctly. In the
code snippet below, HugoBaseDirectory()
recurses up the directory hierarchy of
the provided file until it finds something that contains a config file.
And here’s the final code:
let g:hugo_site_config = [ 'config.toml', 'config.yaml', 'config.json' ]
" The local Hugo server URL
let g:hugo_base_url = "http://localhost:1313/"
function! HugoBaseDirectory(filepath)
let l:mods = ':p:h'
let l:dirname = 'dummy'
while !empty(l:dirname)
let l:path = fnamemodify(a:filepath, l:mods)
let l:mods .= ':h'
let l:dirname = fnamemodify(l:path, ':t')
" Check if the parent of the content directory contains a config file.
let l:parent = fnamemodify(l:path, ":h")
if HugoConfigFile(l:parent) != ""
return l:parent
endif
endwhile
return ""
endfunction
function! HugoConfigFile(dir)
" :p adds the final path separator if a:dir is a directory.
let l:dirpath = fnamemodify(a:dir, ':p')
for config in g:hugo_site_config
let l:file = l:dirpath . config
if filereadable(l:file)
return l:file
endif
endfor
return ""
endfunction
function! PreviewHugoPage(filepath)
let l:fullpath = fnamemodify(a:filepath, ':p')
" Get Hugo's base directory
let l:basedir = HugoBaseDirectory(l:fullpath)
if l:basedir == ""
return ""
endif
" Get the path of the content file relative to Hugo's base directory
let l:contentpath = substitute(l:fullpath, l:basedir . '/', '', '')
" Huse `hugo list all` to find the URL of the content file
let l:url = systemlist("cd " . l:basedir . " && HUGO_BASEURL='" . g:hugo_base_url . "' hugo list all | grep " . l:contentpath . " | head -n 1 | cut -d ',' -f 8")[0]
exe system("open " . l:url)
endfunction
nmap <Leader>hp :call PreviewHugoPage(expand('%'))<cr>
Now, you can type ,hp
in vim while editing a Hugo content file, and that file
will automatically be opened in your browser for previewing!
The nice thing about making g:hugo_base_url
configurable is that you could
make a variant of this command that opens the deployed version of your content
post – you’d just have to make a function that altered g:hugo_base_url
to
point to your deployed Hugo site.
Attribution
Some of the Hugo config file code is adapted from vim-hugo-helper