When I started this blog, I used Typora for writing blog posts, but then I started using Emacs (Spacemacs) for both programming and writing. This blog is powered by Hugo, and you can use it by running commands on a terminal emulator, but it is not elegant. I found a recipe that integrates Hugo into Emacs, borrowed some of its ideas and code, and implemented my own solution. This blog post describes my integration.

Project directory

I put my blog repository at ~/blog. In order to be able to create a blog post from outside the project, I have to let Emacs know the location:

(setq hugo-project-directory "~/blog")

I also defined a function to get the location of my blog:

(defun get-hugo-project-directory ()
  (if (and (boundp 'hugo-project-directory) (stringp hugo-project-directory))
      hugo-project-directory
    (let ((dir (read-directory-name "Your hugo project directory: ")))
      (setq hugo-project-directory dir)
      dir)))

Creating a new post

Without Emacs, you would create a new draft using hugo new command:

hugo new post/sample.md

If this command runs successfully, it will return the following output:

/home/arch/blog/content/post/sample.md created

Otherwise, it prints an error message.

This command can be wrapped with the following Emacs Lisp function:

(defun hugo-create-content (path)
  (let* ((default-directory (get-hugo-project-directory))
         (output (process-lines "hugo" "new" path)))
    (if (and output (listp output))
        (let ((s (car output)))
          (if (string-match "^\\(.+\\) created$" s)
              (match-string 1 s)
            (:error output)))
      (:error "empty output from hugo new"))))

This function returns the file path of a new file if successful, and (:error MESSAGE) otherwise.

I implemented a function to create a post interactively:

(defun hugo-create-and-edit-content (path &optional title)
  (let ((command-result (hugo-create-content path)))
    (if (stringp command-result)
        (progn
         (find-file command-result)
         ;;; replace the title
         (if title
             (progn
               (goto-char (point-min))
               (search-forward-regexp "^title:.*$")
               (replace-match (concat "title: " title))))
         (end-of-buffer)
         command-result)
        (message (cdr command-result)))))
(defun hugo-build-filename-from-title (title)
  (lexical-let
      ((funcs '(downcase
                (lambda (s) (replace-regexp-in-string "[^-[:alnum:]]" "" s))
                (lambda (s) (replace-regexp-in-string "\s" "-" s)))))
    (reduce 'funcall funcs :from-end t :initial-value title)))

(defun hugo-new-post (&optional title)
  (interactive)
  (let* ((title (if title title (read-from-minibuffer "Title of the blog post: ")))
         (filename-from-user (read-from-minibuffer
                              "File name (without 'post/'): "
                              (hugo-build-filename-from-title title)))
         (path (concat "post/" (file-name-sans-extension filename-from-user) ".md")))
    (hugo-create-and-edit-content path title)))

hugo-new-post function takes a title and a file name from the mini-buffer, creates a new post, and replaces its title automatically.

Publishing a post (undrafting)

To undraft a post, you would run the following command:

hugo undraft PATH

I defined a function wrapping this command to undraft the current buffer:

(defun hugo-undraft-projectile ()
  (interactive)
  (let* ((filename (file-truename (buffer-file-name)))
         (project-root (projectile-project-root))
         (post-path (file-relative-name filename project-root)))
    (when (not (string-suffix-p ".md" filename))
      (error "not a markdown file"))
    (when (string-prefix-p ".." post-path)
      (error (concat "failed to resolve the post path: " post-path
                     " in project root " project-root)))
    (let* ((default-directory project-root)
           (output (process-lines "hugo" "undraft" "--verbose" post-path)))
      (if (listp output)
          (message (concat "Post undrafted: " (car output)))))))

This function retrieves the relative path of the current buffer from the project directory ((projectile-project-root), which returns normally the root of a Git repository), and passes it to hugo undraft command.

Listing drafts

You can get a list of drafts using hugo list drafts command, but I didn’t have to wrap this command. My deft setup displays the draft statuses on blog posts, and I can get almost the result I want by typing draft: true in deft:

Search drafts using deft

Starting and stopping a server

You can start/stop the development server of Hugo with Emacs, as described in this blog post. I made the following changes to the original implementation:

  • Retrieve the project directory from projectile so that you can run a server on any projects.
  • Separate start and stop functions. I prefer this design, as I don’t remember whether a server is running or not
    • If a server is running, the start function reopens the site
  • No `-d dev’ option, as I am using continuous integration to publish the site

It is implemented as follows:

(setq hugo-server-buffer-name "*hugo-server*")

(defun hugo-get-server-process ()
  (let ((proc (get-buffer-process hugo-server-buffer-name)))
    (if (and proc (process-live-p proc))
        proc
        nil)))

(defun hugo-server-start-projectile (&optional arg)
  (interactive "P")
  (if (hugo-get-server-process)
      (message "Hugo server is already running")
    (let ((default-directory (projectile-project-root)))
      (start-process "hugo" hugo-server-buffer-name "hugo" "server" "--buildDrafts" "--watch")
      (message "Started Hugo server")))
  (unless arg (browse-url "http://localhost:1313/")))

(defun hugo-server-stop ()
  (interactive)
  (let ((proc (hugo-get-server-process)))
    (if proc
        (progn
          (interrupt-process proc)
          (message "Stopped Hugo server"))
      (message "Hugo server is not running"))))

The entire code

It is available on GitHub.