Emacs' Builtin Templating

I use Hugo for blogging. It statically generates the site, which means I need to type up a markdown file for every post. This should be so easy that I can do it at a moment’s notice, without thought. But there’s a bit of formatting that goes into the headers for such a file, so let’s streamline the process using Emacs.

There are a lot of templating options for Emacs, but my philosophy with almost all software is that you should use built-in functionality if it covers your needs, resorting to external projects only when the functionality gain outweighs the ongoing cost of maintaining a dependency on the external project. Luckily, Emacs has a couple of built-in systems that make this task easy.

For this task, we’ll use the Skeleton language in conjunction with abbrevs. Skeleton allows us to define functions that automatically insert the template text, and abbrevs allow us to invoke those functions when we type short codes in the buffer.

First, the skeleton:

(define-skeleton new-blog-post-skeleton
      ; Description of skeleton
      "Create a new blog post for Hugo"
      ; placeholder, since we're using skeleton-read directly
      ""
      ; Remaining lines are just concatenated together
      "---\nlayout: post\ntitle: \""
      ; Forms are evaluated and the result included
      (skeleton-read "Title: ")
      "\"\ndate: "
      (skeleton-read "Date: " (format-time-string "%Y-%m-%d")))
      "\n---\n\n")

This uses skeleton in a bit of an unconventional way, since it inlines the skeleton-read function so we can write a template that contains multiple values. With that caveat out of the way, the code is actually pretty simple. The first three arguments are fixed:

  1. The name of the skeleton being defined.
  2. A description of the skeleton
  3. A prompt used to gather input

We disregard (3) here since we use skeleton-read directly. Any argument beyond the first three arguments is just concatenated together to get the final template. Strings are handled as-is, and forms are evaluated, with their result (a string) concatenated like a normal string would be.

In this case, we use the form skeleton-read, which is a function that prompts for user input, accepting an argument for the prompt text, and an optional second argument for the default text to be used. We leverage the second argument to default to today’s date, which we can fetch and format using format-time-string.

Now that we’ve defined the skeleton, we just have to find an easy way to activate this function. Enter abbrev mode. We can use the short code =nbp= (for New Blog Post) to activate the skeleton code:

(define-abbrev global-abbrev-table "nbp"
  "" 'new-blog-post-skeleton)

(setq default-abbrev-mode t)