Random Stock Images
Assigning a random stock image to each post as the site is generated.
Intro
For most of the posts I write, I don’t have a specific image that would naturally belong to it (like this one!) but I still want something that isn’t purely plain text - I get enough of that in a terminal.
Instead, the design for this site uses a bunch of stock images for the header images that I’ve downloaded, and prefixed with one of the tags and/or categories that I use (like code-pexels-blah-blah.webp
or painting-pexels-etc-etc.webp
). I’ll be able to use that prefix as a quick way of assigning it to a page.
First step is to define a default value in the site’s _config.yml
file which recognises those images as “stock”:
defaults:
- scope:
path: "assets/images/stock"
values:
stock: true
That’s the setup done, and I was initially content with that - I could get use Liquid tags to pull out a random image when building the site, using something like:
{% assign images = site.static_files | where_exp: "item", "item.stock and item.path contains 'something'" %}
{% assign header_image = images | map: "path" | sample: 1 %}
But I then wanted to have the post remember its image - pretty much just so that I could refer to it on the home page’s “latest post” splash as well as its own page - and that wouldn’t be possible by the time Liquid gets the site data, I needed to have it assigned to the post earlier.
The Plugin
So, one more plugin is called for:
require "jekyll"
module Jekyll
class StockImagesExtra < Jekyll::Generator
safe true
def generate(site)
# get a list of all the static files we've marked as stock images
@images = site.static_files.reject { |file| file.data.empty? or not file.data.key?("stock") or file.data["stock"] != true }
if @images.length == 0
Jekyll.logger.warn 'StockImagesExtra: No stock images defined in your static files.'
return
end
# we'll use these later for posts we don't have a better match for
# start with the same pool, and remove images we find out are a tag/category match to each
@general_images = @images
# loop over the posts and try matching each
@posts = site.posts.docs
@posts.each {
|post|
# remove matches so we'll be left with a "general" pool at the end
tag_matches = get_matches(post, "tags")
category_matches = get_matches(post, "category")
@general_images -= tag_matches + category_matches
# have we already manually assigned an image to this post in its front matter?
if not post.data.key?("image") or post.data["image"] == ""
# try to auto assign one, starting as specific as possible
if tag_matches.length > 0
post.data["image"] = tag_matches.sample.relative_path
next
# and get broader
elsif category_matches.length > 0
post.data["image"] = category_matches.sample.relative_path
next
end
end
}
# do the same for the pages (in my case, specifically the auto archive pages)
@pages = site.pages
@pages.each {
|page|
if page.is_a?(Jekyll::Archives::Archive)
type = page["type"]
name = page.instance_variable_get("@title")
if name.respond_to?(:flatten)
# the title is a hash of the date (year, plus month and day if enabled)
name = name.flatten.join("-")
end
# save that info and we can get the matches as before
page.data[type] = name
matches = get_matches(page, type)
if matches.length > 0
@general_images -= matches
page.data["image"] = matches.sample.relative_path
end
end
}
# if we've not removed everything from the pool, assign them to the leftovers
if @general_images.length > 0
@posts.each { |post| add_general(post) }
@pages.each { |page| if (page.is_a?(Jekyll::Archives::Archive) or page.is_a?(Jekyll::Page)) then add_general(page) end }
end
end
private
def get_matches(post, key)
# get only the images that have any of the array elements present in the path
return @images.select {
|img|
Array(post.data[key]).any? {
|element| img.relative_path.include?(element)
}
}
end
def add_general(item)
# ignore any pages like robots.txt
if item.data["title"].nil? and item["type"].nil?
return
# otherwise pick a general image for this page/post
elsif not item.data.key?("image") or item.data["image"] == ""
item.data["image"] = @general_images.sample.relative_path
end
end
end
end
Now when I generate the site, without having to add front matter to every single post or page, I can simply use {{ page.image }}
in my templates and rely on it being appropriate for a quick header image.
Note that it does mean the pagination uses the same image for each page - which could be fixed by passing the rest of the matched array as well (and then altering the pagination plugin to pick from that) - but as I’m unsure if that’s a bug or a feature, I’ll leave it for now.