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.
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:
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.
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.