Script to Convert Posts to Hugo Page Bundles

Technology keeps moving but this post has not.

What you're about to read hasn't been updated in more than a year. The information may be out of date. Let me know if you see anything that needs fixing.

In case you missed the news, I recently migrated this blog from a site built with Jekyll to one built with Hugo. One of Hugo's cool features is the concept of Page Bundles, which bundle a page's resources together in one place instead of scattering them all over the place.

Let me illustrate this real quick-like. Focusing only on the content-generating portions of a Hugo site directory might look something like this:

 2├── content
 3│   └── post
 4│       ├──
 5│       ├──
 6│       └──
 7└── static
 8    └── images
 9        ├── logo.png
10        └── post
11            ├── first-post-image-1.png
12            ├── first-post-image-2.png
13            ├── first-post-image-3.png
14            ├── second-post-image-1.png
15            ├── second-post-image-2.png
16            ├── third-post-image-1.png
17            ├── third-post-image-2.png
18            ├── third-post-image-3.png
19            └── third-post-image-4.png

So the article contents go under site/content/post/ in a file called Each article may embed image (or other file types), and those get stored in site/static/images/post/ and referenced like ![Image for first post](/images/post/first-post-image-1.png). When Hugo builds a site, it processes the stuff under the site/content/ folder to render the Markdown files into browser-friendly HTML pages but it doesn't process anything in the site/static/ folder; that's treated as static content and just gets dropped as-is into the resulting site.

It's functional, but things can get pretty messy when you've got a bunch of image files and are struggling to keep track of which images go with which post.

Like I mentioned earlier, Hugo's Page Bundles group a page's resources together in one place. Each post gets its own folder under site/content/ and then all of the other files it needs to reference can get dropped in there too. With Page Bundles, the folder tree looks like this:

 2├── content
 3│   └── post
 4│       ├── first-post
 5│       │   ├── first-post-image-1.png
 6│       │   ├── first-post-image-2.png
 7│       │   ├── first-post-image-3.png
 8│       │   └──
 9│       ├── second-post
10│       │   ├──
11│       │   ├── second-post-image-1.png
12│       │   └── second-post-image-2.png
13│       └── third-post
14│           ├──
15│           ├── third-post-image-1.png
16│           ├── third-post-image-2.png
17│           ├── third-post-image-3.png
18│           └── third-post-image-4.png
19└── static
20    └── images
21        └── logo.png

Images and other files are now referenced in the post directly like ![Image for post 1](/first-post-image-1.png), and this makes it a lot easier to keep track of which images go with which post. And since the files aren't considered to be static anymore, Page Bundles enables Hugo to perform certain Image Processing tasks when the site gets built.

Anyway, I wanted to start using Page Bundles but didn't want to have to manually go through all my posts to move the images and update the paths so I spent a few minutes cobbling together a quick script to help me out. It's pretty similar to the one I created to help migrate images from Hashnode to my Jekyll site last time around - and, like that script, it's not pretty, polished, or flexible in the least, but it did the trick for me.

This one needs to be run from one step above the site root (../site/ in the example above), and it gets passed the relative path to a post (site/content/posts/ From there, it will create a new folder with the same name (site/content/posts/first-post/) and move the post into there while renaming it to (site/content/posts/first-post/

It then looks through the newly-relocated post to find all the image embeds. It moves the image files into the post directory, and then updates the post to point to the new image locations.

Next it updates the links for any thumbnail images mentioned in the front matter post metadata. In most of my past posts, I reused an image already embedded in the post as the thumbnail so those files would already be moved by the time the script gets to that point. For the few exceptions, it also needs to move those image files over as well.

Lastly, it changes the usePageBundles flag from false to true so that Hugo knows what we've done.

 2# Hasty script to convert a given standard Hugo post (where the post content and 
 3# images are stored separately) to a Page Bundle (where the content and images are
 4# stored together in the same directory). 
 6# Run this from the directory directly above the site root, and provide the relative 
 7# path to the existing post that needs to be converted.
 9# Usage: ./ vpotato/content/posts/
11inputPost="$1"                              # vpotato/content/posts/
12postPath=$(dirname $inputPost)              # vpotato/content/posts
13postTitle=$(basename $inputPost .md)        # hello-hugo
14newPath="$postPath/$postTitle"              # vpotato/content/posts/hello-hugo
15newPost="$newPath/"                 # vpotato/content/posts/hello-hugo/
17siteBase=$(echo "$inputPost" | awk -F/ '{ print $1 }')  # vpotato
18mkdir -p "$newPath"                         # make 'hello-hugo' dir
19mv "$inputPost" "$newPost"                  # move '' to 'hello-hugo/'
21imageLinks=($(grep -o -P '(?<=!\[)(?:[^\]]+)\]\(([^\)]+)' $newPost | grep -o -P '/images.*'))
22# Ex: '/images/posts/image-name.png'
23imageFiles=($(for file in ${imageLinks[@]}; do basename $file; done))
24# Ex: 'image-name.png'
25imagePaths=($(for file in ${imageLinks[@]}; do echo "$siteBase/static$file"; done))
26# Ex: 'vpotato/static/images/posts/image-name.png'
27for index in ${!imagePaths[@]}; do
28    mv ${imagePaths[index]} $newPath
29    # vpotato/static/images/posts/image-name.png --> vpotato/content/posts/hello-hugo/image-name.png
30    sed -i "s^${imageLinks[index]}^${imageFiles[index]}^" $newPost
33thumbnailLink=$(grep -P '^thumbnail:' $newPost | grep -o -P 'images.*')
34# images/posts/thumbnail-name.png
35if [[ $thumbnailLink ]]; then
36    thumbnailFile=$(basename $thumbnailLink)    # thumbnail-name.png
37    sed -i "s|thumbnail: $thumbnailLink|thumbnail: $thumbnailFile|" $newPost
38    # relocate the thumbnail file if it hasn't already been moved
39    if [[ ! -f "$newPath/$thumbnailFile" ]]; then
40        mv "$siteBase/static/$thumbnailLink" "$newPath"
41    done
43# enable page bundles
44sed -i "s|usePageBundles: false|usePageBundles: true|" $newPost