Hugo and Emacs
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:
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.