Image upload with render hooks

Something I still miss about the old, Ghost-powered version of this blog is the ease of adding images to posts. My favourite thing about Ghost was its two-paned markdown editor, which handled image uploads like this:

  1. In the editor pane, type the Markdown syntax for displaying an image but do not specify a path, i.e. ![]().
  2. In the preview pane, this empty image syntax produces a box onto which you can drag an image file.
  3. Once dragged, the image file will be uploaded and its location filled in, producing something like ![](/content/images/2024/10/myimage.png).

My Hugo blog, being a static site, does not have a post editor. I write posts in a text editor and must manually place any images I want to include somewhere in Hugo’s static directory (or a page bundle, but I’ve never gotten into the habit of using those). This is not by any means a huge burden, but it requires fiddling with file managers and/or terminal and thus takes me out of the writing flow.

I’ve tried a few different static site CMSs, from self-hosted like Netlify CMS (latterly Decap) to online services like Forestry (since discontinued in favour of TinaCMS) and even editor plugins like Front Matter for VS Code. While I appreciate the cleverness of these solutions, none of them really stuck. Ultimately, they all had too many features that I didn’t need and required me to write posts in something other than Vim, which I could never get into the habit of doing – especially when the alternative presented was a slightly souped-up <textarea>. And in any case, I didn’t have a great need for the other CMS stuff – I’m happy typing hugo new instead of clicking a button that says New Post and I’ll gladly fill in the TOML frontmatter without the help of drop-downs or toggle switches. But I did want a more integrated way to upload images.

In my first post in this series, I talked about how Hugo’s new Markdown render hooks allowed me to replace a whole bunch of common shortcodes with slightly more complex Markdown. Shortly after writing that, I started wondering if I could use render hooks to do more complex things, like syntax highlighting for languages not supported by Hugo’s built-in highlighter Chroma. Answer: it could, sort of. With that accomplished, I wanted to take things further still. Could I use an image render hook to enable Ghost-style image uploads?

The answer is yes, and it’s only a little bit convoluted.

The answer is yes, and it’s only a little bit convoluted.

Here’s what I came up with:

How it works:

  1. My theme’s image render hook includes some code to show an upload box if a given image’s path is empty and Hugo is running in the development environment.
  2. My theme’s base template includes some JavaScript for handling image dragging and uploading, only included if Hugo is running in the development environment.
  3. I’ve written a basic NodeJS app that lives in my site’s base directory, and which receives image uploads and saves them to an appropriate directory (static/content/images/YYYY/MM/imagename.jpg).
  4. After a successful upload, the uploader JavaScript replaces the box with the image and copies the image’s path to the clipboard. The included image is shown partially transparent to indicate that is temporary, and I still need to modify the post Markdown.
  5. I paste the image path into my post Markdown. When the post is saved, Hugo detects a change and reloads the page.

The only appreciable different between this and my old Ghost editor is that filling in the path requires a manual paste step. If anything, that gives it more flexibility.

To avoid having to start up two different webservers whenever I want to edit my blog, I also wrote a Bash script, blog.sh, which runs both hugo server and nodejs uploader.js. Any arguments passed to the script are forwarded to the former command, so I can still use --buildDrafts and --buildFuture.

I’ve made the code available in this repository. I’ve done my best to package it as a theme component, though it doesn’t fit perfectly into that frame. I haven’t done much to make it very general or user-friendly, as it is designed first and foremost for my own use. The following additional features might be nice to have:

  • Visual feedback to confirm that the image path has been copied to the clipboard.
  • Support for saving images to page bundles.
  • Processing of image uploads (compression, conversion, etc).
  • Image uploads from a file picker.

If you’d like to be able to upload more than just images, you should be able to do it by removing the content validation in uploader.js.

That’s it for render hooks, at least until Hugo releases footnote hook support.


similar posts
webmentions(?)