Creating Unlisted Content in Hugo
Tags: webdev, html, hugo
Published: 2020-08-12 14:33:02

---
title: "Creating Unlisted Content in Hugo"
published: true
description: In this tutorial you'll create unlisted pages in a Hugo site.
canonical_url: https://bphogan.com/2020/08/11/2020-08-11-creating-unlisted-content-in-hugo
tags: webdev,html,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:

```bash
$ hugo new unlisted.md
```

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:

```md
---
title: "{{ replace .Name "-" " " | title }}"
date: {{ .Date }}
draft: false
unlisted:true
```

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:

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

```

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`:

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

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:

```html
{{ range (where .Site.RegularPages "Type" "in" "posts").ByDate.Reverse }}
```

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

```html
{{ range (where (where .Site.RegularPages "Type" "posts") ".Params.unlisted" "!=" "true").ByDate.Reverse }}
```

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:

```html
{{ range first 3 (where .Site.RegularPages "Type" "in" "posts").ByDate.Reverse }}
```

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

```html
{{ range first 3 (where (where .Site.RegularPages "Type" "posts") ".Params.unlisted" "!=" "true").ByDate.Reverse }}
```

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:

```html
{{ $paginator := .Paginate (where .Pages "Type" "posts") }}
```

Add the `unlisted` filter with a nested `where`:

```html
{{ $paginator := .Paginate (where (where .Pages "Type" "posts") ".Params.unlisted" "!=" "true") }}
```

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

```html
{{ $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 }}

```

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

```html
{{ $related := (where (.Site.RegularPages.Related . ) ".Params.unlisted" "!=" "true") | first 5 }}
```

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](https://github.com/gohugoio/hugo/blob/master/tpl/tplimpl/embedded/templates/_default/rss.xml) 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 file` layouts/_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:

```html
{{- $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 -}}
```

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

```html
{{- if ge $limit 1 -}}
{{- $pages = $pages | first $limit -}}
{{- end -}}
{{- $pages = (where $pages ".Params.unlisted" "!=" "true") -}}
```

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:

```html
{{- $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>
```

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](https://bphogan.com/books).