Marking pages as deleted in Hugo

Sometimes you may want to delete some content you’ve published, for various reasons. Deleting your content in Hugo is really easy: you just delete1 the source files for the pages you no longer need and re-compile your site as usual. However, if you want to be a good IndieWeb citizen, you’d better be a little more graceful when removing content.

First of all, for the content that used to be available but no longer is, the 404 Not Found that your Hugo site returns by default for any non-existent (also drafted out or expired) page is not the proper status code. The proper status code for this case is 410 Gone, and if you use webmentions (like a self-respecting IndieWeb citizen should), this code is what the spec says you should use. You also should re-send your webmentions from the removed page, if there were any.

Setting status codes for pages is the webserver’s job, but you can set Hugo up in a way to facilitate this. Here’s what I’ve done for this site, that is being served using Apache:

Apache uses .htaccess files to control the ways the content is served. The fact that an .htaccess file can be placed in any directory to control how the contents of that particular directory are served fits just fine into Hugo’s model. In my site’s config.toml I declared a new output format:

...
[outputs]
    page = ["HTML", "htaccess"]
...
[outputFormats]
  [outputFormats.htaccess]
    mediaType = "text/htaccess"
    baseName = ""
    isPlainText = false
    notAlternative = true
...

and the created a simple /layouts/_default/single.htaccess:

{{- with .Params.deleted -}}Redirect 410 "/{{- $.File.Dir -}}"{{- printf "\n" -}}
{{- end -}}

This way if a source file has

deleted = true

in its front matter, an .htaccess file gets created in the corresponding subdirectory, and this file instructs Apache to give a 410 Gone response for both the deleted page and any assets that may have been bundled with it (images and such).

Naturally, the tool I wrote for outgoing webmentions detects such cases and re-sends webmentions for newly deleted pages.

This is only half the setup, though. I also needed to make sure the deleted pages don’t show up as “next” and “previous” links on existing pages. Of course, I didn’t want them on the main page, in RSS, and in the posts list of the site, either. This turned out to be a little bit more tricky, but doable.

The RSS part was relatively simple since I already had a custom /layouts/_default/rss.xml (based on Hugo’s built-in one). I only needed to add

{{- $pages = where $pages "Params.deleted" "!=" true -}}

to the end of the set-up section. The same could have been done for the partial that generates “next” and “previous” links, but doing the double-where for every post in this blog appeared to take too much time, so I ended up writing a partial:

{{- $pages := where (where .RegularPages "Type" "in" .Params.mainSections) "Params.deleted" "!=" true -}}
{{- return $pages -}}

This partial is called as

(partialCached "non-deleted-pages.html" .Site)

from both “next-previous” partial and main page’s paginator code, and this way selection of the non-deleted pages only happens once2 instead of 1500+ times.

As for the posts lists, I wanted to see the deleted pages (and have a clear indication that they were deleted) in my development environment, i.e. when I run hugo server on a local machine. So the corresponding partial ended up looking like this:

{{ range . }}{{- if and (eq .Params.deleted true) (eq hugo.Environment "development") -}}<strike>{{- end -}}{{ if or (ne .Params.deleted true) (eq hugo.Environment "development") }}
<li>
...
</li>{{- if and (eq .Params.deleted true) (eq hugo.Environment "development") -}}</strike>{{- end -}}
{{ end }}{{ end }}

All this was, admittedly, quite some hassle to figure out, but now that I did it all works as expected with nothing to worry about3. Since the author of the theme I’m using, htr3n, is somewhat busy right now, and thus the upstreaming of my IndieWeb-related changes is stalled for the time being, I figured I’d just post this part here.


  1. You may want to keep the content for yourself, in which case you can mark the posts as drafts or exclude them from the compilation process by other means (for example, make them expired). Of course, if you use Git to control your site, people with access to your Git repository will still be able to see the content you’ve “deleted”. Also, your content may have already been saved elsewhere (in the Wayback Machine, for instance), but all that is outside of this post’s scope. ↩︎

  2. Once for each language, that is, of which there are currently two: English and Russian. ↩︎

  3. Seemingly. Comments about how this design is flawed and where it needs improvements will be highly appreciated. ↩︎

Comments are webmentions.