Making a Picture of the Week feature on Hugo (Updated)

ยท 859 words ยท 5 minute read

Updated: See the last section

I wanted to take advantage of the flexibility I gave myself with Hugo to have a “Picture of the Week” (PotW) feature on my new site. It took a few iterations to get it to where I liked it, and there are some things about Hugo I learned along the way, but it’s done enough for now.

The basic idea is that I want to take advantage of the streamlined upload and metadata workflow I’ve set up between Lightroom and imgup to share photos without a lot of repeating myself when it comes to writing titles, alt text, etc.

First try ๐Ÿ”—

My first iteration was to keep the data for a featured picture in the site config, like this:

  potw_caption: Manzanita Beach at Dawn
  potw_alt: A rocky beach at dawn with hills and a mountain shrouded in mist. A person in a red jacket looks over the ocean.

… then pull use a partial:

<div class='front-image'>
  <a href="{{ .Site.Params.potw_gallery_link  }}" alt="{{ .Site.Params.potw_alt }}">
    <img src="{{ .Site.Params.potw_img_url }}" />
Picture of the Week: {{ .Site.Params.potw_caption }}

That worked fine for my purposes, but I didn’t like all the clicking to get SmugMug photo info into the config, and I hated the idea of touching my config to do a simple task.

It also had no memory of previous images and I preferred the idea of building a list of these items over time to make a specific PotW page, or to just have them in the record.

What I’ve settled on now is a little less clicky, and I’ve got an idea for how to make it even less clicky.

Second try ๐Ÿ”—

I started by setting up a specific PotW album in SmugMug. Anyone with the URL can see it, but it’s not exposed right now. I upload photos to there.

Next, I made a shortcode that can talk to metadata stored in a page:

  <a href="{{ $.Page.Params.potw_gallery_link  }}" alt="{{ $.Page.Params.potw_alt }}">
    <img src="{{ $.Page.Params.potw_img_url }}" />
{{ $.Page.Params.title }}

As you can see, the frontmatter needs to have a few extra bits in it:

title:  'Picture of the Week: Profit from the Panic'
potw_alt: Wheatpaste of a tv mounted on a human body giving a thumbs up. The TV reads "Profit from the Panic"

date: 2023-02-01T20:10:15-0800
tags: ['potw','photography']
draft: false

imgup’s PotW page gives me the YAML frontmatter:

A screenshot of a browser showing a page that has YAML snippets next to thumbnails of photos.

I can copy and paste it into a PotW post along with the shortcode and it’s ready to go.

Pulling it into the front page is just a partial that pulls in the most recent item tagged potw:

{{ range first 1 .Site.Taxonomies.tags.potw }}
<div class='front-image'>
<h3>Picture of the Week</h3>

  <a href="{{ .Page.Params.potw_gallery_link  }}" alt="{{ .Page.Params.potw_alt }}">
    <img src="{{ .Page.Params.potw_img_url }}" />
{{ .Page.Params.title }}
{{ end }}

What’s next ๐Ÿ”—

It was quick and easy to just copy a route in imgup to make the PotW page, but it’s still manual and clicky to make a PotW post. So my next step will be to hang an endpoint off imgup that automates the process of getting the most recent PotW gallery image and its metadata so I don’t have to write the posts at all.

Update ๐Ÿ”—

Sinatra made it easy to add a tiny bit of logic to the existing PotW route to make it send back JSON:

unless json == true
  haml :potw
  content_type  :json

A simple script hits the endpoint, grabs the JSON, and writes it out to a file:

##!/usr/bin/env ruby

require 'net/http'
require 'uri'
require 'json'
require 'date'
require 'slugify'
require 'yaml'
require 'optparse'

# set default tags for each entry. The "tags" option allows you to add more
tags = ['photography', 'potw']

options = {} do |parser|
  parser.on("-t", "--test", "Changes the endpoint to localhost:4567") do |o|
    options[:test] = true

  parser.on("-o", "--overwrite", "Danger: Overwrite the existing potw if there's a conflict.") do |o|
    options[:overwrite] = true

  parser.on("-T", "--tags TAGS", "Comma-delimited tags for the post, e.g. 'banana,apple,pear'") do |o|
    options[:tags] = o.split(',')
    options[:tags].each do |t|
      tags << t

  parser.on("-h", "--help", "Get help.") do |o|
    puts parser


if options[:test] == true
  endpoint = 'http://localhost:4567/potw?json=1'
  endpoint = ''

site_posts_dir = "~/src/simple/content/posts/"

date ="%Y-%m-%d")
long_date ="%Y-%m-%dT%H:%M:%S%z")
title = date + '-potw'

slug = title.slugify
filename =  slug + '.md'
file_path = File.expand_path(site_posts_dir + filename)

if File.exists?(file_path) && options[:overwrite] != true
  abort("*** Error: #{file_path} already exists. Exiting.")

uri = URI.parse(endpoint)
request =
request.basic_auth(ENV['IMGUP_USER'], ENV['IMGUP_PASS'])

req_options = {
  use_ssl: uri.scheme == "https",

response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|

json = JSON.parse(response.body)
yaml = YAML.dump(json)

potw_post = <<-POTW

date: #{long_date}
tags: #{tags}
categories: ['photography']
draft: true

# escaped  to keep hugo from parsing this as a shortcode: 
# {\{< potw >}}


File.write(file_path, potw_post)

`open #{file_path}`

Much more turnkey than it was, and with a little more logic I might be able to build it into a pre-build script of some kind to completely automate the whole thing.