Random Stock Images

Header image for Random Stock Images

Assigning a random stock image to each post as the site is generated.

6 minute reading time of 1244 words (inc code) Code available at codeberg.org and the last commit was


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”:

  - scope:
      path: "assets/images/stock"
      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.'
      # 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 {
        # 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
          # and get broader
          elsif category_matches.length > 0
            post.data["image"] = category_matches.sample.relative_path
      # do the same for the pages (in my case, specifically the auto archive pages)
      @pages = site.pages
      @pages.each {
        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("-")
          # 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
      # 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 }

    def get_matches(post, key)
      # get only the images that have any of the array elements present in the path
      return @images.select {
        Array(post.data[key]).any? {
          |element| img.relative_path.include?(element)
    def add_general(item)
      # ignore any pages like robots.txt
      if item.data["title"].nil? and item["type"].nil?
      # 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


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.

Reply via email