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)