Page Bundles in Hugo

Page bundles are available in hugo (a static web site site generator which is great fun) for quite a while now.

I always wanted to store files (assets and images, etc…) which belongs to an article in the same directory as the article itself - or at least in a sibling directory down below the content tree. In the very 1st versions of Hugo this was not possible and all related content needed to be stored in the /static directory. But now, since the introduction of “page bundles” life should be much easier now - let’s take a look!

Leave and branch bundles

What I discovered by try and error so far is that there is a difference between the usage of _index.md and index.md.

The former is related to a directory structure called “branch bundles” and the latter is meant to be “leave bundles”.

Hugo uses different rendering defaults for both:

  • “branch bundles” are rendered via “list.html” and
  • “leave bundles” are rendered via “page.html”

I also found out that only for “page bundles” the .Page.Resource is available.

It becomes quite clear when going through the hugo documentation at: https://gohugo.io/content-management/page-bundles/#readout

Leave bundles

As I want to keep things simple, looks like for now the leave bundle is the way to go. I’d like to reorganize content, so that I only need a few more page bundle related shortcodes to be able to reference files stored as “page resources”.

For example, file and directory storage might look similar to:

	├── webdevelopment/
	│   │
	│   └──19-12-09_resource_bundle/
	│      │
	│      ├── assets
	│      │   ├── article.pdf
	│      │   └── another-article.pdf
	│      │
	│      ├── images
	│      │   ├── some-image.png
	│      │   ├── image1.jpg
	│      │   ├── image2.png
	│      │   └── image3.md
	│      │
	│      └── index.md

The shortcode (shortcodes/pagebundle/get_assets.html) I came across to access files from sibling subdirectories “assets” and “images” are as following:

{{ $dir := (.Get "dir") }}
{{ if eq $dir "" }} {{ $dir = "assets" }} {{ end }}
	get all files that matches the directory name
	which is passed as argument dir="" to the shortcode:
{{ $assets := .Page.Resources.Match (printf "%s/*" $dir) }}
{{ $assetdir := (printf "%s/%s" (.Page.Path | path.Dir) $dir) }}
{{ $page_resources := .Page.Resources.ByType "image" }}

<table class="table table-responsive table-condensed">
	<th><span style="padding-left:20px"></th>
	<th>Size <small>/ byte</small>:</th>
	{{- range $assets -}}
		<!-- took me a while to figure this out: -->
		{{ $fname := . | path.Base }}
		{{ $fstat := os.Stat (printf "%s/%s" $assetdir $fname ) }}
			<!-- if the actual file is an image,
				we'd like to show a small thumbnail image...  -->
			{{ if ($page_resources.GetMatch (printf "*%s*" . )) }}
				<img src="{{ .Resize "60x60" }}" width="60px">
			{{ else if eq (. | path.Ext ) ".pdf" }}
				<i class="icon-file-pdf1" style="font-size:22px;color:Orange" ></i>
			{{ else }}
				<i class="icon-line-file" style="font-size:22px;color:DarkGrey" ></i>
			{{ end }}

			<a href="{{ .RelPermalink }}"> {{ . | path.Base }} </a>
		<td> {{ $fstat.Size }} </td>
	{{- end -}}

Note: Hugo was telling me that .Page.Dir will be deprecated in the future and so, this is what I came across as a possible replacement:

  • deprecated: .Page.Dir
  • replacement: .Page.Path | path.Dir

Using the shortcode in markdown is pretty straight forward, it just takes one line to display all files stored in the “assets” sub-directory. Please also note that for the attachements the file size is also listed in the last column. Check out the code in the shortcut as this -at least for me- was not so straight forward and took me quite a while to figure it out.

Shortcode usage:

## Attachements:

<!-- shortcode with named argument: -->
{{/* pagebundle/get_assets dir="assets" */}}

<!-- Please note: the "/* ... *\/"  is just a notation for a comment.
     If this code should be processed as usual, the "< ... >" notation needs to be used instead. -->


Name:Size / byte:

Being even more adventurous we’d like to create an image gallery as a shortcode command…

The gallery hereby takes all images from a given “images” sub-directory down below the content directory. The image dir is given as an argument to the shortcode. This way multiple image galleries can be added to the markdown easily - yes, I like it.

Something like:

{{/* pagebundle/gallery dir="images" */}}

And finally here is the shortcode for the image gallery as an example. You might need to tweak classes, etc. to bring this into life, the main intention of this example is to show the usage of the .Page.Resources functionality in conjunction with page bundles:

{{ $dir := (.Get "dir") }}
{{ $background := .Get "background" }}

	get all files that matches the directory name
	which is passed as argument dir="" to the shortcode:

{{ $assets := .Page.Resources.Match (printf "%s/*" $dir) }}
{{ $assetdir := (printf "%s/%s" (.Page.Path | path.Dir) $dir) }}
{{ $page_resources := .Page.Resources.ByType "image" }}

<div class="col_full clearfix">
	<div class="card" style="padding: 5px 5px 5px 5px; {{- with $background -}} background: {{ $background }}; {{- end -}}">
		<div class="masonry-thumbs grid-6" data-big="3" data-lightbox="gallery">
			{{- range $assets -}}
				<a href="{{ .RelPermalink }}" data-lightbox="gallery-item" title="{{ . | path.Base }}">
					<img class="image_fade" src="{{ .RelPermalink }}" alt="{{ . | path.Base }}">
			{{- end -}}

All images and attachements used here are just to build up this examples.


Page bundles offer a great way to organize content in a most logical way.


  • Cleaner and more logical organization of content related data.
  • As a side effect, information is kept together in one place (or better: directory structure) and this might also help to migrate data in the future more easier.
  • It also allows me to move back and forth articles between Markdown2Go and Hugo quite easily.


One think I like to mention is the upload performance of a fully rendered site onto the web server‘s share via ftp. I discovered that when managing big sized files aside in page bundles, the upload takes significantly longer.

Let me explain:

Hugo requires to render articles if either the content has changed or maybe the layout (template) has changed. In this case the file representation on the disk gets a new timestamp. For the moment there is nothing wrong with this logic, that’s the nature of things.

On the other hand, the timestamp for files organized in the /static folder does not need to be changed that often. These files are not affected when changing the content or the template.

As a side effect, i discovered that bundle assets need to uploaded all the time when using the ftp client.

And so ftp transfer times are significantly longer when using page bundles especially when there are big sized files available. I am therefore considering to use page bundles only for images and small supplementary files and not for big sized file attachements.