Slugifying this blog's headings

A recurring concern I have with this website (or blog) is to [make sure links don't die]({{< relref "this-will-deteriorate-quickly" >}}). One of the measures I take is to never change a URL once it is published. I mean, changing the heading and the content is possible, but not the URL.

How is it done? As previously mentioned1, [^fn:2], this website is built with a mix of Org Mode and Hugo. In this case, the Hugo feature I'm using is the slug attribute on the page's front matter. This basically overrides the file path that would be on the URL with a string of your choice.

Let's imagine you were writing plain Markdown files and thought a title was not adequate anymore, but the content was still sound. This how this article would look like:

+++
title = "Slugifying this blog's headings"
slug = "slugifying-this-blog-s-headings"
date = 2023-05-17
+++

<!-- content -->

Here, the slug would keep the URL the same even if the heading changed. In my case, this is how this article's portion looks like in Org:

** Slugifying this blog's headings                             :emacs:shell:
:PROPERTIES:
:EXPORT_DATE: 2023-05-17
:EXPORT_FILE_NAME: slugifying-this-blog-s-headings
:EXPORT_HUGO_CUSTOM_FRONT_MATTER: :slug slugifying-this-blog-s-headings
:END:

# content

Okay, but how to create the slugs? This can't be a manual task[^fn:3] as it is too error prone, I can make typos easily on bigger headings! Then I thought: "I'm already inside GNU Emacs, why not use it?". The idea is really simple, I select the heading text and run a function me/slugify that will copy the slugified version on my clipboard.

{{< alert class="info" >}} I might write a function that populates the heading plus properties automatically for me... in the future... {{< /alert >}}

The first step I took was to search on the internet some bash script that I could just glue (😜) together with Emacs Lisp. I ended up finding this gist comment that had a really straightforward command:

# note that the `echo` and last `tr` was added by me
echo "Slugifying this blog's headings" \
| iconv -t ascii//TRANSLIT  \
| sed -E -e 's/[^[:alnum:]]+/-/g' -e 's/^-+|-+$//g' \
| tr '[:upper:]' '[:lower:]' \
| tr -d '\n'
slugifying-this-blog-s-headings

This is exactly what I was looking for, but how to make this an Emacs Lisp function I can call? Note that I know barely nothing about Emacs Lisp, the only thing I know here is that I want an interactive command. In this sense, we need to declare a function with these properties/steps:

  • is interactive
  • gets the selected region
  • slugifies the selected region
  • copies the sligfied text to clipboard
  • notifies us somehow that it was successful

After doing some C-h f and online research, I found out the functions I needed:

(defun gluer/slugify ()
  (interactive)
  ;; only run this if there's an active region
  (when (use-region-p)
    ;; `let*` bounds the variables sequentially, useful to use a
    ;; previously declared variable
    (let* (
	   ;; stores the selected region on the `str` variable
	   (str (buffer-substring (region-beginning) (region-end)))
	   ;; interpolates the selected region on the `echo` command
	   ;; that will pipe it to the rest of the chain
	   (cmd (format "echo %s | iconv -t ascii//TRANSLIT | sed -E -e 's/[^[:alnum:]]+/-/g' -e 's/^-+|-+$//g' | tr '[:upper:]' '[:lower:]' | tr -d '\n'" str))
	   ;; stores the command output on the `slug` variable
	   (slug (shell-command-to-string cmd)))
      ;; copies the slug to the clipboard
      (kill-new slug)
      ;; diplays the slug on the bottom of the screen
      (message "copied '%s' to the clipboard" slug))))

Selecting the region and running M-x gluer/slugify gets us a nice slugified string on our clipboard! But it's not over yet!

The funny part was that I didn't do the most obvious thing: search for a "slug" function on GNU Emacs itself! Well, guess what? ox-hugo has a function called org-hugo-slug (!!!) that does exactly what I want...

(org-hugo-slug STR &optional ALLOW-DOUBLE-HYPHENS)

Convert string STR to a ‘slug’ and return that string.

Let's give it a try and adapt our previous one:

(defun gluer/slugify ()
  (interactive)
  (when (use-region-p)
    (let* ((str (buffer-substring (region-beginning) (region-end)))
	   (slug (org-hugo-slug str)))
      (kill-new slug)
      (message "copied '%s' to the clipboard" slug))))

This is way better than what I previously had. It's cleaner, doesn't add weird shell strings and uses the same mechanism behind the website generation.

Lesson learned: use the describe-* functions (in this case, describe-function) on GNU Emacs, they are there for a reason!

  1. [How this website is built - Nix and ox-hugo]({{< relref "how-this-website-is-built-nix-ox-hugo" >}}) [^fn:2]: [Why I use Nix and make(1) to develop]({{< relref "developing-with-nix-and-make" >}}) [^fn:3]: Although it has been a manual task for as long as this article. 😁