Having setup my new blogging platform to work to my satisfaction, one thing is of course missing: Integration into Emacs. So I went ahead and added a couple of convenience functions that allow me to quickly create new entries, publish the blog, and run the dev server from inside Emacs. And seeing as I now have a blog to post this kind of stuff to, I thought I’d share my solution.
Creating new posts
First up is a function to create new postings easily. This looks like the following:
(setq hugo-base-dir "~/git/blog/"
hugo-buffer "*hugo*")
(defun hugo-new-post ()
(interactive)
(let* ((title (read-from-minibuffer "Title: "))
(filename (concat "post/"
(read-from-minibuffer "Filename: "
(replace-regexp-in-string "-\\.md" ".md"
(concat (downcase
(replace-regexp-in-string "[^a-z0-9]+" "-"
title))
".md")))))
(path (concat hugo-base-dir "content/" filename)))
(if (file-exists-p path)
(message "File already exists!")
(hugo-command "new" filename)
(find-file path)
(hugo-replace-key "title" title)
(goto-char (point-max))
(save-buffer))))
This function asks for the title of the new post, generates a filename
for it by lower-casing the title and replacing all special characters
with hyphens, then runs hugo new
to create the posting. Finally, the
file is opened and the correct title is inserted in place of the one
Hugo generates from the file name.
The above function uses a couple of helper functions:
(defun hugo-command (&rest args)
(let ((default-directory (expand-file-name hugo-base-dir)))
(apply 'call-process "hugo" nil hugo-buffer t args)))
(defun hugo-replace-key (key val)
(save-excursion
(goto-char (point-min))
; quoted value
(if (and (re-search-forward (concat key " = \"") nil t)
(re-search-forward "[^\"]+" (line-end-position) t))
(or (replace-match val) t) ; ensure we return t
; unquoted value
(when (and (re-search-forward (concat key " = ") nil t)
(re-search-forward ".+" (line-end-position) t))
(or (replace-match val) t)))))
Publishing the blog
Publishing the blog is quite simple:
(defun hugo-publish ()
(interactive)
(let* ((default-directory (concat (expand-file-name hugo-base-dir) "/")))
(when (call-process "bash" nil hugo-buffer t "./upload.sh")
(message "Blog published"))))
This simply runs the upload.sh
script I already created to publish the
blog. That script is itself quite simple and looks like this:
#!/bin/bash
cd ~/git/blog
rm -rf public
hugo -d public
# gzip files so nginx can serve the compressed versions
find public \( -name '*.js' -or -name '*.css' -or -name '*.svg' -or -name '*.html' \) -exec gzip -k9 '{}' \;
rsync -rtpl --delete public/ kau:/srv/http/blog/
Undrafting posts
When a post is ready to be published, its draft mode needs to be unset. Hugo includes a command for this, but we can do the same from Elisp:
(defun hugo-undraft ()
(interactive)
(when (and (hugo-replace-key "date" (iso-timestamp))
(hugo-replace-key "draft" "false"))
(save-buffer)
(message "Removed draft status and updated timestamp")))
(defun iso-timestamp ()
(concat (format-time-string "%Y-%m-%dT%T")
((lambda (x) (concat (substring x 0 3) ":" (substring x 3 5)))
(format-time-string "%z"))))
Running the Hugo dev server
Hugo has the ability to run as a web server on the local machine in watch mode, where it will watch for changes in the source files and rebuild whenever a change is detected. This is useful for immediately seeing the effects of editing when composing posts. This server can be run from inside Emacs via the following function:
(defun hugo-server (&optional arg)
(interactive "P")
(let* ((default-directory (concat (expand-file-name hugo-base-dir) "/"))
(proc (get-buffer-process hugo-buffer)))
(if (and proc (process-live-p proc))
(progn (interrupt-process proc)
(message "Stopped Hugo server"))
(start-process "hugo" hugo-buffer "hugo" "server" "--buildDrafts" "--watch" "-d" "dev")
(message "Started Hugo server")
(unless arg
(browse-url "http://localhost:1313/")))))
This simply spawn the Hugo server process and attaches it to the defined
Hugo buffer – or kills it if it is already running. The server is run
with the --buildDrafts --watch -d dev
command line options, which
causes draft posts to also be built, turns on watch mode, and uses a
separate directory for the compiled files (so as not to interfere with
publishing). If run without a prefix argument, this function will also
open a browser pointing to the Hugo localhost server.
Keybindings
The above functions can be bound to suitable keys. I simply use:
(define-key my-keys-minor-mode-map (kbd "C-c C-h n") 'hugo-new-post)
(define-key my-keys-minor-mode-map (kbd "C-c C-h p") 'hugo-publish)
(define-key my-keys-minor-mode-map (kbd "C-c C-h s") 'hugo-server)
(define-key my-keys-minor-mode-map (kbd "C-c C-h u") 'hugo-undraft)