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 index.md
for single pages (or _index.md
for section pages). index.md
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:
- Create the new page directory under the appropriate section directory
- Move the existing page’s markdown file to the new directory and name it
index.md
. - Move resource files to the new directory
- 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:
jbowen.dev/
├── content/
│ └── posts/
│ ├── starting-a-blog.md
│ ├── starting-a-blog-pt2.md
│ ├── starting-a-blog-pt3.md
│ ├── starting-a-blog-pt4.md
│ └── another-post.md
└── 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:
jbowen.dev/
└── 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 index.md
Next step is to move the existing markdown files to index.md
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.md starting-a-blog/pt1/index.md
$ git mv starting-a-blog-pt2.md starting-a-blog/pt2/index.md
$ git mv starting-a-blog-pt3.md starting-a-blog/pt3/index.md
$ git mv starting-a-blog-pt4.md starting-a-blog/pt4/index.md
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
│ │ │ └── index.md
│ │ ├── pt2/
│ │ │ ├── fig1.png
│ │ │ ├── fig2.png
│ │ │ ├── fig3.png
│ │ │ └── index.md
│ │ ├── pt3/
│ │ │ ├── fig1.png
│ │ │ ├── fig2.png
│ │ │ ├── fig3.png
│ │ │ ├── fig4.png
│ │ │ ├── fig5.png
│ │ │ ├── fig6.png
│ │ │ └── index.md
│ │ └── pt4/
│ │ └── index.md
│ └── another-post.md
└── 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/jbowen.dev$ hugo server
Building sites … ERROR 2019/08/30 14:12:33 [en] REF_NOT_FOUND: Ref "starting-a-blog-pt2.md": "/home/jbowen/blog/jbowen.dev/content/posts/starting-a-blog/pt1/index.md:238:75": page not found
ERROR 2019/08/30 14:12:33 [en] REF_NOT_FOUND: Ref "starting-a-blog.md": "/home/jbowen/blog/jbowen.dev/content/posts/starting-a-blog/pt2/index.md:9:13": page not found
ERROR 2019/08/30 14:12:33 [en] REF_NOT_FOUND: Ref "starting-a-blog.md": "/home/jbowen/blog/jbowen.dev/content/posts/starting-a-blog/pt3/index.md:7:20": page not found
ERROR 2019/08/30 14:12:33 [en] REF_NOT_FOUND: Ref "starting-a-blog-pt2.md": "/home/jbowen/blog/jbowen.dev/content/posts/starting-a-blog/pt3/index.md:7:66": page not found
ERROR 2019/08/30 14:12:33 [en] REF_NOT_FOUND: Ref "starting-a-blog-pt4.md": "/home/jbowen/blog/jbowen.dev/content/posts/starting-a-blog/pt3/index.md:131:43": page not found
ERROR 2019/08/30 14:12:33 [en] REF_NOT_FOUND: Ref "starting-a-blog.md": "/home/jbowen/blog/jbowen.dev/content/posts/starting-a-blog/pt3/index.md:7:20": page not found
ERROR 2019/08/30 14:12:33 [en] REF_NOT_FOUND: Ref "starting-a-blog-pt2.md": "/home/jbowen/blog/jbowen.dev/content/posts/starting-a-blog/pt3/index.md:7:66": page not found
ERROR 2019/08/30 14:12:33 [en] REF_NOT_FOUND: Ref "starting-a-blog-pt4.md": "/home/jbowen/blog/jbowen.dev/content/posts/starting-a-blog/pt3/index.md:131:43": page not found
ERROR 2019/08/30 14:12:33 [en] REF_NOT_FOUND: Ref "starting-a-blog.md": "/home/jbowen/blog/jbowen.dev/content/posts/starting-a-blog/pt2/index.md:9:13": page not found
ERROR 2019/08/30 14:12:33 [en] REF_NOT_FOUND: Ref "starting-a-blog-pt2.md": "/home/jbowen/blog/jbowen.dev/content/posts/starting-a-blog/pt1/index.md:238:75": 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 "starting-a-blog-pt2.md" >}}
generates an error because Hugo cannot find starting-a-blog-pt2.md
. Instead, we must
must change this shortcode reference to:
{{< ref "/posts/starting-a-blog/pt2/index.md" >}}
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:
/images/starting-a-blog-fig1.png
and change to the simpler, relative path:
fig1.png
Finally, with these changes our website is, once again, complete and rendering exactly the same as before we changed the content organization. Success!