Public previews in Hugo

Posted by ads' corner on Saturday, 2020-03-14
Posted in [Hugo][Online][Software]

Hugo is a static templating system. It is (mainly) used to deploy websites/blogs which don’t have and need dynamic content. The content of all pages is pre-generated, and the webserver delivers files from disk (or rather from cache, once files are loaded into memory). This approach allows for extremely fast websites, as no dynamic content is generated on every request.

While I know Hugo from work, I haven’t really used it for private projects - until recently. I have started a new project where I present interviews with people behind the PostgreSQL Project - and this is perfect for a static website. Interviews don’t change, once published.

There was just a little problem: every interview must be approved by the interviewed person. This requires a full preview, but one which does not show up on the main website, or the Sitemap, or the RSS feed. By default, even drafts show up in Sitemap and the RSS feed in Hugo.

Let’s start the basics: enabling the RSS feed and the Sitemap. This is done in config.toml:

rssLimit = 15

  changefreq = "weekly"
  filename = "sitemap.xml"
  priority = 0.5

RSS feed

In order for search engines and other tools to pick up the RSS feed, the URL should be added to the header of the page. That’s done in the theme, in layouts/partials/head.html:

{{ range .AlternativeOutputFormats -}}
    {{ printf `<link rel="%s" type="%s" href="%s" title="%s" />` .Rel .MediaType.Type .Permalink $.Site.Title | safeHTML }}
{{ end }}

This also needs a XML file which Hugo is using to generate the feed. By default Hugo is using an internal file, but if layouts/_default/rss.xml is present in the theme directory, this overrides the internal file.

{{- $pctx := . -}}
{{- if .IsHome -}}{{ $pctx = .Site }}{{- end -}}
{{- $pages := slice -}}
{{- if or $.IsHome $.IsSection -}}
{{- $pages = $pctx.RegularPages -}}
{{- else -}}
{{- $pages = $pctx.Pages -}}
{{- end -}}
{{- $drafts := where $pages ".Draft" false -}}
{{- $pages := $pages | intersect $drafts -}}
{{- $mainpages := where .Site.RegularPages "Type" "in" .Site.Params.mainSections -}}
{{- $pages := $pages | intersect $mainpages -}}
{{- $limit := .Site.Config.Services.RSS.Limit -}}
{{- if ge $limit 1 -}}
{{- $pages = $pages | first $limit -}}
{{- end -}}
{{- printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>" | safeHTML }}
<rss version="2.0" xmlns:atom="">
    <title>{{ if eq  .Title  .Site.Title }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{.}} on {{ end }}{{ .Site.Title }}{{ end }}</title>
    <link>{{ .Permalink }}</link>
    <description>Recent content {{ if ne  .Title  .Site.Title }}{{ with .Title }}in {{.}} {{ end }}{{ end }}on {{ .Site.Title }}</description>
    {{ with .Site.LanguageCode }}
    <language>{{.}}</language>{{end}}{{ with }}
    <managingEditor>{{.}}{{ with $ }} ({{.}}){{end}}</managingEditor>{{end}}{{ with }}
    <webMaster>{{.}}{{ with $ }} ({{.}}){{end}}</webMaster>{{end}}{{ with .Site.Copyright }}
    <copyright>{{.}}</copyright>{{end}}{{ if not .Date.IsZero }}
    <lastBuildDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</lastBuildDate>{{ end }}
    {{ with .OutputFormats.Get "RSS" }}
	{{ printf "<atom:link href=%q rel=\"self\" type=%q />" .Permalink .MediaType | safeHTML }}
    {{ end }}
    {{ range $pages }}
      <title>{{ .Title }}</title>
      <link>{{ .Permalink }}</link>
      <pubDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</pubDate>
      {{ with }}<author>{{.}}{{ with $ }} ({{.}}){{end}}</author>{{end}}
      <guid>{{ .Permalink }}</guid>
      <description>{{ .Summary | html }}</description>
    {{ end }}

The difference to the default rss.xml: this excludes all files from the feed where Draft is set to true, and it includes all pages which are used to build the main website (like “about”) and such. No need to have those in the feed.


Hugo also comes with an integraded template for a Sitemap. This template includes all pages, Drafts are not excluded. If a custom XML file is placed in layouts/sitemap.xml in the themes directory, this content will override the builtin sitemap.

{{ printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\" ?>" | safeHTML }}

<urlset xmlns=""
  {{ $allpages := .Data.Pages }}
  {{ $drafts := where .Data.Pages ".Draft" false }}
  {{ $pages := $allpages | intersect $drafts }}
  {{ range $pages }}
    <loc>{{ .Permalink }}</loc>{{ if not .Lastmod.IsZero }}
    <lastmod>{{ safeHTML ( .Lastmod.Format "2006-01-02T15:04:05-07:00" ) }}</lastmod>{{ end }}{{ with .Sitemap.ChangeFreq }}
    <changefreq>{{ . }}</changefreq>{{ end }}{{ if ge .Sitemap.Priority 0.0 }}
    <priority>{{ if (in .Params.categories "Interviews") }}0.8{{ else }}{{ .Sitemap.Priority }}{{ end }}</priority>{{ end }}{{ if .IsTranslated }}{{ range .Translations }}
                hreflang="{{ .Lang }}"
                href="{{ .Permalink }}"
                />{{ end }}
                hreflang="{{ .Lang }}"
                href="{{ .Permalink }}"
                />{{ end }}
  {{ end }}

For the Sitemap, all Drafts are excluded. But all main pages are included - the search engines are supposed to index those pages.


All in all these features are documented, however it seems that most websites just go with the default. On the other hand, it’s not hard to change Hugo to generate exactly the expected content. And I have a preview system, where I can send out a link without having this link show up in a search engine, or a RSS aggregator.

Categories: [Hugo] [Online] [Software]
Tags: [Blog] [Draft] [Hugo] [Interview] [Preview] [Rss] [Template] [Xml]