DEV Community

Brian P. Hogan
Brian P. Hogan

Posted on • Originally published at bphogan.com

Creating Unlisted Content in Hugo

You may want pages that are available via a direct link, but not displayed in list pages, related content, or feeds. By default, Hugo collects all content into collections that themes use to build list pages.

You can do this by adding a new piece of front matter to your pages, and then altering your themes to filter those pages out. In this tutorial you'll see exactly what queries to change, and how to change them, so you can publish your own unlisted content.

Filtering Unlisted Pages From Queries

Creating unlisted pages involves two steps: adding the front matter field, and modifying any iterations in your theme to ignore them.

In this example, we'll use unlisted: true in the front matter for a page. Create a new page called unlisted.md using the hugo command:

$ hugo new unlisted.md
Enter fullscreen mode Exit fullscreen mode

Open contents/unlisted.md in your editor and change the front matter to include a new unlisted: true field. And make sure that draft is false so Hugo generates the page:

---
title: "{{ replace .Name "-" " " | title }}"
date: {{ .Date }}
draft: false
unlisted:true
Enter fullscreen mode Exit fullscreen mode

You'll add this new field to any pages you wish to hide.

Now locate your template that lists pages in your site. The default one is usually located in your theme folder in layouts/_default/list.html. You'll find logic that looks like this:

 <ul>
    {{ range .Pages }}
      <li>
        <a href="{{ .RelPermalink }}">{{ .Title }}</a>
      </li>
    {{ end }}
  </ul>

Enter fullscreen mode Exit fullscreen mode

This logic iterates over all the Pages in the site. It might say RegularPages or .Site.RegularPages, depending on who created the theme.

To filter out the unlisted pages, change the range query to use a where function to filter out any pages where the unlisted field is set to true:

 <ul>
    {{ range (where .Pages ".Params.unlisted" "!=" "true") }}
      <li>
        <a href="{{ .RelPermalink }}">{{ .Title }}</a></li>
    {{ end }}
  </ul>
Enter fullscreen mode Exit fullscreen mode

If you have more specific templates, you'll need to change the scopes there as well. For example, a template that iterates through posts might have a range query that looks for a type of posts, rather than all pages on the site. Here's an example of that:

{{ range (where .Site.RegularPages "Type" "in" "posts").ByDate.Reverse }}
Enter fullscreen mode Exit fullscreen mode

Hugo's query language doesn't support AND operations, so you'll have to nest the where functions to ensure that both conditions apply:

{{ range (where (where .Site.RegularPages "Type" "posts") ".Params.unlisted" "!=" "true").ByDate.Reverse }}
Enter fullscreen mode Exit fullscreen mode

Some themes display the most recent blog posts on the site's home page. In your site's index.html template, you might have a snippet that pulls in the most recent posts, like this:

{{ range first 3 (where .Site.RegularPages "Type" "in" "posts").ByDate.Reverse }}
Enter fullscreen mode Exit fullscreen mode

This is another place you'll want to add the nested where function to filter out the unlisted content:

{{ range first 3 (where (where .Site.RegularPages "Type" "posts") ".Params.unlisted" "!=" "true").ByDate.Reverse }}
Enter fullscreen mode Exit fullscreen mode

You'll want to carry these changes through to any other specific list templates in your theme, including ones for tag lists and category lists.

Once you've adjusted your general range queries, you'll want to address pagination and related content.

Removing Unlisted Pages from Pagination and Related Content

If you're paginating your content, you'll use the same approach you've used before. A paginator for posts in your current theme may look like this:

{{ $paginator := .Paginate (where .Pages "Type" "posts") }}
Enter fullscreen mode Exit fullscreen mode

Add the unlisted filter with a nested where:

{{ $paginator := .Paginate (where (where .Pages "Type" "posts") ".Params.unlisted" "!=" "true") }}
Enter fullscreen mode Exit fullscreen mode

Finally, Hugo has a built-in feature for displaying related content on your pages. You may have a partial that looks like this:

{{ $related := .Site.RegularPages.Related . | first 5 }}
{{ with $related }}
<h3>You might also be interested in...</h3>
  <ul>
    {{ range .ByDate.Reverse }}
      <li><a href="{{ .RelPermalink }}">{{ .Title }}</a> (Published {{ .PublishDate.Format "January 2, 2006" }})</li>
     {{ end }}
  </ul>
{{ end }}

Enter fullscreen mode Exit fullscreen mode

To ensure that the unlisted content is filtered out, wrap the .Site.RegularPages.Relatedfunction with a where clause:

{{ $related := (where (.Site.RegularPages.Related . ) ".Params.unlisted" "!=" "true") | first 5 }}
Enter fullscreen mode Exit fullscreen mode

Now the related content excludes the unlisted content. But you might be exposing that content in your feeds.

Fixing Your RSS Feed

You've excluded your unlisted content from your pages, but don't forget about your site's RSS feed. Hugo has a default RSS template that you can override, but if you don't, Hugo uses this built-in default. That means you may accidentally expose your unlisted content through your feeds.

To override this file, create the filelayouts/_default/rss.xml in your site or in your theme and modify it to exclude the unlisted pages just like you've done with other queries.

The default template creates a $pages variable and populates it based on various conditions, such as whether this is the home page, or if the content should be limited to a certain number of records:

{{- $pctx := . -}}
{{- if .IsHome -}}{{ $pctx = .Site }}{{- end -}}
{{- $pages := slice -}}
{{- if or $.IsHome $.IsSection -}}
{{- $pages = $pctx.RegularPages -}}
{{- else -}}
{{- $pages = $pctx.Pages -}}
{{- end -}}
{{- $limit := .Site.Config.Services.RSS.Limit -}}
{{- if ge $limit 1 -}}
{{- $pages = $pages | first $limit -}}
{{- end -}}
Enter fullscreen mode Exit fullscreen mode

The quickest way to hide the unlisted pages is to apply the where clause after the limit:

{{- if ge $limit 1 -}}
{{- $pages = $pages | first $limit -}}
{{- end -}}
{{- $pages = (where $pages ".Params.unlisted" "!=" "true") -}}
Enter fullscreen mode Exit fullscreen mode

Be sure to use the {{- -}} notation here to suppress space before and after the line.

The entire RSS feed template with the changes looks like this:

{{- $pctx := . -}}
{{- if .IsHome -}}{{ $pctx = .Site }}{{- end -}}
{{- $pages := slice -}}
{{- if or $.IsHome $.IsSection -}}
{{- $pages = $pctx.RegularPages -}}
{{- else -}}
{{- $pages = $pctx.Pages -}}
{{- end -}}
{{- $limit := .Site.Config.Services.RSS.Limit -}}
{{- if ge $limit 1 -}}
{{- $pages = $pages | first $limit -}}
{{- end -}}
{{- $pages = (where $pages ".Params.unlisted" "!=" "true") -}}
{{- printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>" | safeHTML }}
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <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>
    <generator>Hugo -- gohugo.io</generator>{{ with .Site.LanguageCode }}
    <language>{{.}}</language>{{end}}{{ with .Site.Author.email }}
    <managingEditor>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</managingEditor>{{end}}{{ with .Site.Author.email }}
    <webMaster>{{.}}{{ with $.Site.Author.name }} ({{.}}){{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 }}
    <item>
      <title>{{ .Title }}</title>
      <link>{{ .Permalink }}</link>
      <pubDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</pubDate>
      {{ with .Site.Author.email }}<author>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</author>{{end}}
      <guid>{{ .Permalink }}</guid>
      <description>{{ .Summary | html }}</description>
    </item>
    {{ end }}
  </channel>
</rss>
Enter fullscreen mode Exit fullscreen mode

If you have other alternative formats, like a JSON feed, be sure to exclude the content in a similar way. And remember to add the unlisted: true entry to your front matter.

Conclusion

You can now add a new front matter field to your content and have it excluded from Hugo's generated content lists, but still have the pages published so you can provide people with direct links to the content.

Remember that in order for this to work, you'll need to change any place that iterates over your content, including lists for alternative content types, such as your site's automatically-generated sitemap or XML feed. Check and double check your site to ensure your unlisted content is truly unlisted before you publish.

Like this post? Support my writing by purchasing one of my books about software development.

Top comments (1)

Collapse
 
evn profile image
Evan B

Wonderful. Thank you for this post!