Using Page Bundles to Organize Resources

Last year, Hugo introduced the concept of page bundles. Page bundles give website authors a new way to organize all the resources (.md files, images, etc…) of a page together. In this article, I show how I transitioned some of my existing posts to use page bundles without changing the final website layout and appearance.

Page Bundles

Before introducing page bundles, resources (most notably images) that are specific to a page (eg. a single blog post), needed to be stored within the /static directory along with all the other resource files for the entire website. Of course, images could be stored within any level of subdirectory of /static to allow for some degree of organization, but it is still less than ideal.

With the launch of page bundles in version 0.32 of Hugo, authors can now include resources within a per-page directory within the appropriate section directory under content. So now all the images (or any other page-specific files) can be stored together in the same directory.

So how do page bundles work? They are simply a directory under /content that contains a file named for single pages (or for section pages). holds the main content for the page and all the additional files in that directory (or child subdirectories) can be referenced by a relative URL from that file.

Migrating Existing Content to Page Bundles

Moving existing content to use page bundles is relatively straightforward:

  1. Create the new page directory under the appropriate section directory
  2. Move the existing page’s markdown file to the new directory and name it
  3. Move resource files to the new directory
  4. Fix any previous references to page resources to match new relative location

It should be noted that there is nothing requiring existing content to be moved to page bundles in order to use the feature with new content. Page bundles and traditional content organization can happily coexist within the same website.

Migration Example

To give an example of migrating to page bundles, I will show how I reorganized my four part series on starting this blog to leverage this feature.

Initial Layout

The initial layout of my posts followed the standard protocol of one Markdown file per post and keeping all the images in /static/images/ as shown below:
├── content/
│   └── posts/
│       ├──
│       ├──
│       ├──
│       ├──
│       └──
└── static/
    └── images/
        ├── avatar.jpg
        ├── starting-a-blog-fig1.png
        ├── starting-a-blog-pt2-fig1.png
        ├── starting-a-blog-pt2-fig2.png
        ├── starting-a-blog-pt2-fig3.png
        ├── starting-a-blog-pt3-fig1.png
        ├── starting-a-blog-pt3-fig2.png
        ├── starting-a-blog-pt3-fig3.png
        ├── starting-a-blog-pt3-fig4.png
        ├── starting-a-blog-pt3-fig5.png
        └── starting-a-blog-pt3-fig6.png

As you can see, even for a small blog with a few posts, the standard organization rules can lead to complexity. Note how easy it is to misread -pt2- and -pt3- among all the repeated image files and imagine how this will only get worse as more posts (and more images) are added in the future.

Create Directory Layout

The first step is to create the new directory structure for our page bundles. As page bundles can lie at any level of a directory tree beneath /content I decided to group all four posts within a single directory and then have individual child directories for each part:
└── content/
    └── posts
        └── starting-a-blog/
            ├── pt1/
            ├── pt2/
            ├── pt3/
            └── pt4/

With a little shell expansion magic, this can be done with a single command:

$ mkdir -p  content/posts/starting-a-blog/pt[1234]

Move Post(s) to

Next step is to move the existing markdown files to in our newly created directories. This could be done with the standard mv command on Unix (or by drag-and-drop within the system GUI), but since I have already committed these files to git, a far better option is to use the git mv command:

git mv <src> <dst>

The benefits of this command over standard mv, is that it will automatically mark the files as renamed within git and stage them for commit as if it had run git add on the newly moved files.

Moving our markdown files:

$ cd content/posts
$ git mv starting-a-blog/pt1/
$ git mv starting-a-blog/pt2/
$ git mv starting-a-blog/pt3/
$ git mv starting-a-blog/pt4/

Move Resources to Page Bundle Directory

Similar to above, we use git mv to move and rename the image files, this time from the root directory:

$ git mv static/images/starting-a-blog-fig1.png content/posts/starting-a-blog/pt1/fig1.png
$ git mv static/images/starting-a-blog-pt2-fig1.png content/posts/starting-a-blog/pt2/fig1.png
$ git mv static/images/starting-a-blog-pt2-fig2.png content/posts/starting-a-blog/pt2/fig2.png
$ git mv static/images/starting-a-blog-pt2-fig3.png content/posts/starting-a-blog/pt2/fig3.png
$ git mv static/images/starting-a-blog-pt3-fig1.png content/posts/starting-a-blog/pt3/fig1.png
$ git mv static/images/starting-a-blog-pt3-fig2.png content/posts/starting-a-blog/pt3/fig2.png
$ git mv static/images/starting-a-blog-pt3-fig3.png content/posts/starting-a-blog/pt3/fig3.png
$ git mv static/images/starting-a-blog-pt3-fig4.png content/posts/starting-a-blog/pt3/fig4.png
$ git mv static/images/starting-a-blog-pt3-fig5.png content/posts/starting-a-blog/pt3/fig5.png
$ git mv static/images/starting-a-blog-pt3-fig6.png content/posts/starting-a-blog/pt3/fig6.png

After the above commands, our directory layout now looks like the following:

├── content
│   └── posts
│       ├── starting-a-blog/
│       │   ├── pt1/
│       │   │   ├── fig1.png
│       │   │   └──
│       │   ├── pt2/
│       │   │   ├── fig1.png
│       │   │   ├── fig2.png
│       │   │   ├── fig3.png
│       │   │   └──
│       │   ├── pt3/
│       │   │   ├── fig1.png
│       │   │   ├── fig2.png
│       │   │   ├── fig3.png
│       │   │   ├── fig4.png
│       │   │   ├── fig5.png
│       │   │   ├── fig6.png
│       │   │   └──
│       │   └── pt4/
│       │       └──
│       └──
└── static/
    └── images/
        └── avatar.jpg

As you can see, the file organization is much clearer now and will make things much easier to manage going forward.

Fix References

While the above organization is clearer, there is one problem … it won’t build properly due to broken references with the unmodified markdown files, as seen by the errors reported when trying to run the Hugo test server:

jbowen@maranello:~/blog/$ hugo server
Building sites … ERROR 2019/08/30 14:12:33 [en] REF_NOT_FOUND: Ref "": "/home/jbowen/blog/": page not found
ERROR 2019/08/30 14:12:33 [en] REF_NOT_FOUND: Ref "": "/home/jbowen/blog/": page not found
ERROR 2019/08/30 14:12:33 [en] REF_NOT_FOUND: Ref "": "/home/jbowen/blog/": page not found
ERROR 2019/08/30 14:12:33 [en] REF_NOT_FOUND: Ref "": "/home/jbowen/blog/": page not found
ERROR 2019/08/30 14:12:33 [en] REF_NOT_FOUND: Ref "": "/home/jbowen/blog/": page not found
ERROR 2019/08/30 14:12:33 [en] REF_NOT_FOUND: Ref "": "/home/jbowen/blog/": page not found
ERROR 2019/08/30 14:12:33 [en] REF_NOT_FOUND: Ref "": "/home/jbowen/blog/": page not found
ERROR 2019/08/30 14:12:33 [en] REF_NOT_FOUND: Ref "": "/home/jbowen/blog/": page not found
ERROR 2019/08/30 14:12:33 [en] REF_NOT_FOUND: Ref "": "/home/jbowen/blog/": page not found
ERROR 2019/08/30 14:12:33 [en] REF_NOT_FOUND: Ref "": "/home/jbowen/blog/": page not found
Total in 15 ms
Error: Error building site: logged 10 error(s)

These errors are the result of the various cross-links between the different posts. With the organization change, the old shortcode ref command:

{{< ref ""  >}}

generates an error because Hugo cannot find Instead, we must must change this shortcode reference to:

{{< ref "/posts/starting-a-blog/pt2/" >}}

After making these changes, Hugo is able to build the website without reported errors. However, reviewing the test site generated by hugo server will show a bunch of broken image links. Similar to above, we need to find references to the prior image locations of the form:


and change to the simpler, relative path:


Finally, with these changes our website is, once again, complete and rendering exactly the same as before we changed the content organization. Success!