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:

params:
  potw_img_url: https://photos.smugmug.com/photos/i-TkDZjHx/0/ce3d02d6/XL/i-TkDZjHx-XL.jpg
  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.
  potw_gallery_link:  https://pix.puddingtime.org/Uploads/n-47GBfb/i-TkDZjHx

… then pull use a partial:

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

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:

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

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_img_url: https://photos.smugmug.com/photos/i-87Bm3V2/0/ecba06f4/XL/i-87Bm3V2-XL.jpg
potw_alt: Wheatpaste of a tv mounted on a human body giving a thumbs up. The TV reads "Profit from the Panic"
potw_gallery_link:  https://pix.puddingtime.org/Picture-of-the-Week/n-D5HJ4W/i-87Bm3V2

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>

<figure>
  <a href="{{ .Page.Params.potw_gallery_link  }}" alt="{{ .Page.Params.potw_alt }}">
    <img src="{{ .Page.Params.potw_img_url }}" />
  </a>
<figcaption>
{{ .Page.Params.title }}
</figcaption>
</figure>
</div>
{{ 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
else 
  content_type  :json
  @recents.first.to_json
end

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 = {}

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

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

  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
    end
  end

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

end.parse!

if options[:test] == true
  endpoint = 'http://localhost:4567/potw?json=1'
else
  endpoint = 'https://imgup.puddingtime.org/potw?json=1'
end

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

date = Date.today.strftime("%Y-%m-%d")
long_date = Time.now.strftime("%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.")
end


uri = URI.parse(endpoint)
request = Net::HTTP::Get.new(uri)
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|
  http.request(request)
end

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

potw_post = <<-POTW
#{yaml}

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

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

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.