Shared Physics
by Roman Kudryashov
Shared Physics

How I Built A Simple Image-Gallery Static Site Generator

Published on 9 min read

TLDR: I recently built a static site generator that scratched quite a few itches for me, not least of all being 'learning python'. The project was relatively straightforward: you can put your photos into a folder structure, run the code, and out pops a simple, static HTML/CSS site.

I recently built a static site generator that scratched quite a few itches for me, not least of all being 'learning python'.

The project's goal was straightforward: put your photos into a folder structure, run the code, and get a simple, static HTML/CSS site that you can host as a github page (or anywhere, really) for showcasing your photos (or any other kinds of images, portfolios, comics, etc).

In this post, I'm going to take the code apart & write through what I did + how it works, talking mostly through the logic and decisions.

  • The full code is available here if you'd like to fork it and make your own version
  • My project write up is here
  • You can demo the site here

Getting Started

The code runs via a shell script named Build.sh. This runs in the terminal and executes a sequence of python scripts that go through your folders, optimize the images, and then writes HTML snippets that are compiled into a full website at the end. Build.sh looks like this:

#!/bin/sh

# Read the images directory and wrap the images in HTML
printf "\033[0;32m  Starting Build Process \033[0m\n" 

# Read the images directory and wrap the images in HTML
printf "\033[0;32m  ... Building Your Gallery... \033[0m\n" 
cd images
python3 buildImageList.py
python3 optimizations.py
python3 buildImageList-withsubdirectory.py

# Combine various html snippets into one core index.html file
printf "\033[0;32m  ... Compiling HTML files... \033[0m\n" 
cd ../snippets/
python3 buildIndex.py

# Go back and launch the page locally for testing
cd ../
python3 open.py

# Just a tooltip to let you know the script is running.
printf "\033[0;32m  All good! Launching site. \033[0m\n" 

Note, I added a lot of comments in my code to keep it clear and readable. I also like leaving tooltips for the build process so I can see if the code gets hung up somewhere.  

Also – if you're running this for the first time, you'll need to adjust your permissions to execute the file by running chmod +x Build.sh and pressing enter. You only need to do this once.

Anyway, let's take it section by section:

Step 1: Enter the "images" directory.
Step 2: Run the buildImagelist.py code
Step 3: Run the optimizations.py code
Step 4: Run the buildImagelist-withsubdirectoy.py code
Step 5: Enter the "snippets" directory
Step 6: Run the buildIndex.py code
Step 7: Go back to the index and run open.py to open to website when it has compiled.


Building the First Image List

This builds the "homepage" list of images:

#!/usr/bin/env python3

import os
from os import listdir
import codecs

#path = "../Snippets/"
dirs = sorted(os.listdir())

def main():
    writeFile= open("../snippets/5-body-images.html","w+")
    startImg = """<img class=\"img-large\" src=\"images/"""
    endImg = """\">"""

    for file in dirs:
        img_list = [".jpg",".JPG",".JPEG",".jpeg",".png",".PNG"] # list of image files
        if file.endswith(tuple(img_list)): # Reading only image files
            # This is a loop for each file.
            # It reads each file and reads the content to the writeFile, then adds a line break ("\n")
            writeFile.write(startImg)
            writeFile.write(file)
            writeFile.write(endImg)
            writeFile.write("\r\n")
    writeFile.close()

if __name__== "__main__":
    main()

I wrote this early on as a bit of copypasta when I was trying to figure out how Python works. I might go back and clean up the functions at some point, but the logic is:

  • Defining a few functions and variables (particularly startImg and endImg which are html wrappers I want to add before and after each image file to get them to show up in html.
  • Then reading the directory for a predefined list of supported image types
  • For each image, I run them through a loop at add some code to them. Then, for each loop, I append that to a clean .html snippet file for later use.

The end result is an html file that might look something like this:

<img class="img-large" src="images/austin-wade-beHyzISRtEY-unsplash.jpg">
<img class="img-large" src="images/harley-davidson-npZR4G67ev8-unsplash.jpg">
<img class="img-large" src="images/jorge-gardner-IHYtzp6Fe3M-unsplash.jpg">
<img class="img-large" src="images/kate-smr-Ye3VhmjdbXg-unsplash.jpg">
<img class="img-large" src="images/kate-smr-rSzOCn4CEfw-unsplash.jpg">
<img class="img-large" src="images/tim-trad-67IDWWYCltI-unsplash.jpg">

If you want to manually create a homepage with images and copy without autogenerating it like this, feel free to disregard this entire file – just delete it from the process, and update the  /snippets/5-body-images.html manually.


Optimizing Images

Once the homepage is built, I move on to optimizing all of the images in the subdirectories. TBH, I should be optimizing the homepage images too, but I started with the assumption that the hompage would be handled separately from everything else.

The optimizations are handled by the optimizations.py file:

#!/usr/bin/env python3
import os
from os import walk
from os import listdir
import subprocess
import codecs
import urllib.parse
from urllib.parse import urlparse
import shutil

dirs = sorted(next(os.walk('.'))[1]) # This will get all of the subdirectories in a folder
#dirs = sorted(os.listdir()) # This will get all of the files and subdirectories in a folder

# print (dirs) # Test output

for folders in dirs:

    folders_string = folders.replace (' ', r'\ ') # This ensures that when we call a subprocess, the filepath is correctly passed for terminal.

    images = sorted(os.listdir(folders))
    #print (images) # Test output

    # Managing subdirectories for image optimizations:
    #    If the path exists, delete it (and contents) to start afresh
    #    Else, recreate it.
    if os.path.exists(folders + '/optimised'):
        shutil.rmtree(folders + '/optimised')
        os.makedirs(folders + '/optimised')
    if not os.path.exists(folders + '/optimised'):
        os.makedirs(folders + '/optimised')

    # Call ImageMagick
    #     This first syncs the images to a new folder for optimization
    #     Then we run the ImageMagick mogrify command to process the images down to a specifc size and optimizes weight.
    os.system ('rsync -avr --exclude=\'*/\' --exclude=\'*.html\' --exclude=\'*.txt\' --include=\'*.{jpg,JPG,jpeg,JPEG,png,PNG}\' ' + folders_string + '/* ' + folders_string + '/optimised/') 
    os.system ('magick mogrify -density 72 -thumbnail 1100x1100 -filter Triangle -define filter:support=2 -unsharp 0.25x0.08+8.3+0.045 -dither None -posterize 136 -quality 82 -define jpeg:fancy-upsampling=off -define png:compression-filter=5 -define png:compression-level=9 -define png:compression-strategy=1 -define png:exclude-chunk=all -interlace none -colorspace sRGB ' + folders_string + '/optimised/*.{jpg,JPG,jpeg,JPEG,PNG,png}')

The logic here is a bunch of nested loops: we get a list of folders, then for each folder we get a list of items, and then for each item we run optimizations on it.

Additionally,  we're creating an "optimizations" folder in each subdirectory - I want to be really careful about (a) keeping all of the original files intact (generally a good practice), and (b) being able to call on them if needed (aka full size view), and (c) being able to restart the process without degradations (if we run the optimizations on the original files, we'll eventually get to a point of losing too much quality).

I hacked the optimizations themselves to be run outside of the python script. This isn't a best practice; there's a Pillow python module that handles image optimizations, but I opted for the more-familiar-to-me shell-run ImageMagick. (I was also able to leverage some optimization tests and scripts I had created back when I was focused on ecomm optimizations (my testing is here: https://gist.github.com/sharedphysics/6bc4a42844a39a90be403e018bc122c9).

This means that if you don't have ImageMagick already installed, you'll need to do that now. You can use homebrew for this: brew install ImageMagick (and installing homebrew is outside the scope of this walkthrough).

Lastly, I want to note that the optimizations still need some more fiddling-around-with... they're either much better on .jpg files than on .png files, or I'm losing a lot of data going from Adobe RGB to the more web-friendly sRGB.


Building Subdirectories

Once the optimizations are run, we're off to the races with the rest of the site with buildImagelist-withsubdirectories.py:

#!/usr/bin/env python3

import os
from os import walk
from os import listdir
import subprocess
import codecs
import urllib.parse
from urllib.parse import urlparse
import shutil
from shutil import copy


dirs = sorted(next(os.walk('.'))[1]) # This will get all of the subdirectories in a folder
#dirs = sorted(os.listdir()) # This will get all of the files and subdirectories in a folder

# print (dirs) # Test to see What

# Define and create the hompage-based sidebar URLs based on subdirectories folder:
createSidebar = open("../snippets/3-sidebar-content.html","w+")
for folders in dirs:
    print (folders) # Test output
    urlEncodeFolders = urllib.parse.quote(folders) # Encode folder paths to work as URLs
    buildURL = """<a href=\"./images/""" + urlEncodeFolders + """/index.html\">""" + folders + """</a><br>"""
    createSidebar.write(buildURL + "\r\n")
createSidebar.close()    

# This will create the proper sidebar url structure for the sidebar in subdirectories
for folders in dirs:
    createSubDirSidebar = open("./"+folders+"/3-sidebar-content.txt","w+")
    '''# WIP: Creating a recursive list of folders. Probably should use create a list before I go into the foreach, but will see!
    urlEncodeSubDirFolders = urllib.parse.quote(folders) # Encode folder paths to work as URLs
    buildSubDirURL = """<a href=\"../../""" + urlEncodeSubDirFolders + """/\">""" + folders + """</a><br>"""
    createSubDirSidebar.write(buildSubDirURL + "\r\n")    
    '''
    createSubDirSidebar.write("<a href=\"../../index.html\">< Back</a><br><br>" + "\r\n")    
    createSubDirSidebar.close()


    # This will create the images list for subdirectories
    images = sorted(os.listdir(folders))
    createSubDirImagesList = open("./"+folders+"/5-body-images.txt","w+")
    createSubDirImagesList.write("<br>")
    for imagefiles in images:
        img_list = [".jpg",".JPG",".JPEG",".jpeg",".png",".PNG"] # list of image files
        if imagefiles.endswith(tuple(img_list)): # Reading only image files
            print (imagefiles) # Test output
            urlEncodeImages = urllib.parse.quote(imagefiles) # Encode folder paths to work as URLs
            createSubDirImagesList.write("""<a href=\"""" + urlEncodeImages + """\">""" + """<img class=\"img-large\" src=\"./optimised/""" + urlEncodeImages + """\"></a>""" + "\r\n")
    createSubDirImagesList.close()


    # This will copy all of the key html snippet files to the subdirecty folder to build the pages
    shutil.copy("../snippets/1-header.html", folders+"/1-header.txt")
    shutil.copy("../snippets/2-sidebar-start.html", folders+"/2-sidebar-start.txt")
    shutil.copy("../snippets/4-body-start.html", folders+"/4-body-start.txt")
    shutil.copy("../snippets/6-body-end.html", folders+"/6-body-end.txt")

    # This will compile the snippets into a index.html file
    writeIndex = open("./"+folders+"/index.html","w+")
    txtfileslist = sorted(os.listdir(folders))
    for txtfiles in txtfileslist:
        print (txtfiles)
        if txtfiles.endswith(".txt"): # Reading only .txt files
            readFile = codecs.open("./"+folders+"/"+txtfiles, 'rb', encoding='utf-8')
            writeIndex.write(readFile.read() + "\n") 
    writeIndex.close()

A few things happen here:

  • We use the same process as before to identify all of the subdirectories, then all of the files in them.
  • Then we use that list of subdirectories to build the sidebar navigation on the homepage.
  • Then we create and write to a file similar to the homepage list of images. The major difference here is that we're showcasing the optimized files we just created, and linking those to the original files for anyone who wants to see the full quality/full size pictures.
  • Next we take some of the snippets from the ../snippets/ directory and add them here. One set of standardized files ends up containing all of the HTML and CSS. I'm not worried about reusability here because the files are comparatively tiny. If I had a larger bootstrap framework (or something similar), then I'd probably reference it seperately so that the file only needs to be referenced once. But as it stands, my stylesheet is three div and link styles.
  • Then we build an index.html per subdirectory that compiles all of the "snippets" that we've been using into a single file. You'll notice that the snippets are labeled with "1-",  "2-", "3-" and so forth: this is a quick hack to convey the order that they should be compiled it. Rather than hardcoding the names and order of compilation, I have full flexibility in the snippets directory to fool around with additions/removals to the code. It's not a foolproof system, but it has been very handy in enabling further hackyness... For example, if I wanted to add some pre-header code, I can just create and throw a .html file into the directory. This is how I added "support" for introductions/closing notes in each directory.
  • Lastly, we're sending various print commands to the terminal to let the user know exactly what's going on at each step.

Compiling the Index

Now that we have the subdirectory sidebar for navigation, and the core images to be featured on the homepage, I compile the final index:

#!/usr/bin/env python3

import os
from os import listdir
import codecs

#path = "../Snippets/"
dirs = sorted(os.listdir())

def main():
	writeFile= open("../index.html","w+")
	for file in dirs:
		if file.endswith(".html"): # Reading only .html files
			# This is a loop for each file.
			# It reads each file and reads the content to the writeFile, then adds a line break ("\n")
			readFile=codecs.open(file, 'rb', encoding='utf-8')
			writeFile.write(readFile.read()) 
			writeFile.write("\n") 
	writeFile.close()

if __name__== "__main__":
	main()

This is pretty straightforward: I walk through the files in the snippets folder to build the index.


Done

The final step is to run the open.py script to open the index file in your preferred browser:

#!/usr/bin/env python3
import os
import webbrowser

webbrowser.open('file://' + os.path.realpath("index.html"))

This lets you know that the build process is done, and lets you explore your images as a standalone page locally as well. You can take your full file directory and host it on a personal site, or use Github pages (instructions here) to host it for free there.

Opening it also lets you quickly troubleshoot if something went wrong during the process.


... and that's it! The logic for building your own gallery-like static site generator is pretty straightforward. Feel free to clone A Simple Gallery as the start of your own generator, including adding different themes (modifying the snippets) or changing your optimizations.

Thanks for reading

Was this useful? Interesting? Have something to add? Let me know.

Seriously, I love getting email and hearing from readers. Shoot me a note at hi@romandesign.co and I promise I'll respond.


You can also sign up for email or RSS updates when there's a new post.

Thanks,
Roman