Publishable Stuff

Rasmus Bååth's Blog


Setting up plain markdown blogging in Hugo

2023-01-28

I recently spent a lot of time migrating this blog from being generated by Octopress (RIP) to the Hugo static site generator. This was fairly painful. Not because any of these frameworks are bad, but just because I also had to migrate all of Octopress’s quirks and special cases to Hugo (slightly different RSS formats, markdown engines, file name conventions, etc.). So, when migrating to Hugo I had two things in mind:

  1. To go back in time to tell young Rasmus to never jump on the static site generator train and just get a bog-standard WordPress blog.
  2. Lacking a working time machine, to rely on as few Hugo-specific features as possible to make any inevitable future migration less painful.

Specifically, I wanted to write my blog posts in plain markdown only, and not rely on Hugo shortcodes (a Hugo-specific syntax for generating custom html content in markdown). I also wanted each markdown post and its related resources (images, linked files, etc.) to live together in the same folder and not spread out with posts being in content/blog and images being over in static/images, as is the default. The benefit of a setup like this is that I can write markdown posts in anything (say in Rstudio, which works great as a markdown editor) without having to change any image paths or add short codes to get it to work in Hugo later. Here I’ll go through the problems that I needed to solve to get to this setup.

The Hugo and  Markdown logos

At first, I thought it would be easy. Hugo has a feature called Page Bundles that allows you to co-locate a blog post’s markdown file and any page “resources” (images, etc.) in the same folder. You can then refer to resources relative to the markdown file ([](my-image.png) instead of [](/blog/some-blogpost/my-image.png)). The problem is that the relative links only work for the actual post page. It does not work in other places the post could appear, such as the RSS feed or on the site’s front page.

To solve this we need to have Hugo rewrite all relative links to absolute links. This can be solved with custom Hugo shortcodes, but that’s the kind of non-markdown magic we want to avoid. Instead, we’ll use another kind of magic: Hugo’s Markdown Render Hooks. This feature allows you to write custom Hugo templates that will be run on any markdown links ([like this](example.txt)) and image links (![like this](example.png)). What we need these render hooks to do is:

In addition to this, I also wanted to be able to indicate if images should be shown in high resolution, that is, rendered at 50% of their size. Like this:

The Hugo and  Markdown logos, but smaller

This image should look sharper than the one above if you’re viewing this on a high-res screen, which is almost any screen in 2023. I wanted to be able to set the image scaling “globally” in the post frontmatter (img_scale: 0.50) but also set it for individual images (![](hugo-markdown.png "img_scale: 0.50")).

After battling with the Hugo templating language for quite some time, I finally got all of this to work. While the two render hooks (found, in full, below) turned out pretty messy, I’m still happy with the end result: I can write plain markdown blog posts, with the post and images living happily in the same folder and, when Hugo renders my site, links will still be correct both in the actual post page, but also on the front page and in the RSS feed.

The avoid-shortcodes-and-use-plain-markdown Hugo render hooks

The link render hook to be put in, say, layouts/_default/_markup/render-link.html:

<!-- 
  This link render hook makes sure that links that are relative to the markdown 
  file gets converted to being relative of the site root. This makes the links 
  work also on the frontpage and in RSS feeds. Adapted from here:
  https://github.com/bep/portable-hugo-links/blob/master/layouts/_default/_markup/render-link.html
-->

{{- if or (strings.HasPrefix .Destination "http://") (strings.HasPrefix .Destination "https://") -}}
  <!-- Urls starting with http/s are external and shouldn't be tampered with. -->
  <a href="{{ .Destination | safeURL }}">{{ .Text | safeHTML }}</a>
{{- else if strings.HasPrefix .Destination "/" -}}
  <!-- Urls starting with / are external and shouldn't be tampered with. -->
  <a href="{{ .Destination | safeURL }}">{{ .Text | safeHTML }}</a>
{{- else -}}
  <!-- We're likely dealing with a relative URL, we're going to try to find out 
       where it's pointing and we're going to rewrite it as an absolute URL-->  
  {{- $url := urls.Parse .Destination -}}
  {{- $resource := .Page.Resources.GetMatch $url.Path -}}
  {{- if and (not $resource) .Page.File -}}
    {{ $path := path.Join .Page.File.Dir $url.Path }}
    {{- $resource = resources.Get $path -}}
  {{- end -}}
  {{- with $resource -}}
    {{- $fragment := "" -}}
    {{- with $url.Fragment -}}{{- $fragment = printf "#%s" . -}}{{ end -}}
    {{- $link := printf "%s%s" .RelPermalink $fragment -}}
    <a href="{{ $link | safeURL }}">{{ $.Text | safeHTML }}</a>
  {{- else -}}
    <!-- We didn't figure out what this link is, so just using it as-is,
         even though it's likely broken... -->
    <a href="{{ .Destination | safeURL }}">{{ .Text | safeHTML }}</a>
  {{- end -}}
{{- end -}}

{{- /* Removes trailing whitespace */ -}}

The image link render hook to be put in, say, layouts/_default/_markup/render-image.html:

<!--
This image render hook does a couple of things:
* It makes sure that images links that are relative to the markdown file gets
  converted to being relative of the site root. This makes the images show up
  correctly on the frontpage and in RSS feeds. This uses code adapted from here:
  - https://github.com/bep/portable-hugo-links/blob/master/layouts/_default/_markup/render-image.html
  - https://github.com/zoni/obsidian-export/issues/8#issuecomment-774521792
* It allows one to set `img_scale: 0.5` in the frontmatter. This is useful to set 
  if a post contains high-res "retina" images that otherwise would be really large.
* It allows one to override any `img_scale` setting by abusing the image title
  like this: ![](path/to/super-high-res-image.jpeg "img_scale: 0.25"). The
  `img_scale: 0.25`part will also be removed from the image title.
-->

<!-- Is there an actual image at the image URL/.Destination? -->
{{- $img := .Page.Resources.GetMatch .Destination -}}
{{- if and (not $img) .Page.File -}}
  {{- $path := path.Join .Page.File.Dir .Destination -}}
  {{- $img = resources.Get $path -}}
{{- end -}}

<!-- Figuring out whether img_scale is set in the image title or 
     in the Page frontmatter. It's a fairly complicated process...  */ -->
{{- $img_scale := false -}}
{{- $title := .Title -}}
{{- $img_scale_regexp := `\s*img_scale:\s*([0-9]*[.])?[0-9]+\s*` -}}
{{- $title_img_scale_text := index (findRE $img_scale_regexp $title) 0 -}}
{{- if $title_img_scale_text -}}
  {{- $title = replace $title $title_img_scale_text "" -}}
  {{- $number_regexp := `([0-9]*[.])?[0-9]+` -}}
  {{- $img_scale = float (index (findRE $number_regexp $title_img_scale_text) 0 )  -}}
{{- else if isset .Page.Params "img_scale" -}}
  {{- $img_scale = .Page.Params.img_scale -}}
{{- end -}}

<!-- /* Finally rendering the actual img tag! */ -->

{{- if $img -}}
  {{- $is_raster_img := ne $img.MediaType.SubType "svg" -}}
  {{- $should_set_width := and $img_scale $is_raster_img -}}
  <img src="{{ $img.RelPermalink }}" {{if .Text}}alt="{{ .Text }}"{{end}} {{if $title}}title="{{$title}}"{{end}} {{ if $should_set_width }} width = "{{ mul $img.Width $img_scale }}" {{ end }}/>
{{- else -}}
  <img src="{{ .Destination | safeURL }}" {{if .Text}}alt="{{ .Text }}"{{end}} {{if $title}}title="{{$title}}"{{end}} />
{{- end -}}

{{- /* Removes trailing whitespace */ -}}
Posted by Rasmus Bååth | 2023-01-28 | Tags: Hugo