How I’m Hosting My Images For Free

Since GitHub blocks files larger than 100 MiB, I needed to find a way to host my website’s images (and possibly other large media). From a quick Reddit search, it seems that Cloudinary is among the best free solutions. It has quite a generous free tier, and I can easily transform my images by adjusting the Cloudinary URL—which is really convenient.

For example, this is the link to this post’s banner picture:

https://res.cloudinary.com/sarahmak/image/upload/w_400/c_limit/dpr_auto,f_auto,q_auto/cloudinary_pffmx9
The Cloudinary logo.

But by adding the URL transformations e_colorize,co_purple/ after /upload/, the URL is now:

https://res.cloudinary.com/sarahmak/image/upload/e_colorize,co_purple/c_limit,w_200/dpr_auto,f_auto,q_auto/cloudinary_pffmx9

Which turns the entire image purple:

An entirely purple image.

There are also URL transformations that allow me to load images faster by compressing the images, and resizing them based on the layout width of the page.

Unfortunately, Cloudinary’s documentation isn’t very clear, so it took me a while to figure out how to use it.

Figuring Out How To Use Cloudinary With Hugo and GitHub and Netlify

Currently, my website uses Hugo to generate the site, GitHub to manage my code, and Netlify to deploy it. I first tried to learn how to integrate Cloudinary into these tools—but it turned out that I didn’t need to integrate them at all.

Some Hugo-Cloudinary tutorials are for making shortcodes (Hugo’s version of templates) for Cloudinary images, while others are for adding Cloudinary images as paths instead of URLs. I didn’t use these in the end—they make it more difficult to edit the image URLs andapply Cloudinary’s URL transformations.

I didn’t want to use the Cloudinary plugin in Netlify either. It scans for image files in the repo, then redirects them to Cloudinary URLs—but after the file uploads, it doesn’t seem that I can easily apply URL transformations either.

After this research, I realised that I could just use a simpler method: just upload the images to Cloudinary, then use the images’ URLs in my webpages.

At first, I uploaded the images by dragging and dropping them into the Cloudinary Media Library webpage, but I realised that I could use the Cloudinary API, and just make a Python script for this! In one click, I could losslessly compress the images using pingo, upload the images to Cloudinary, then copy the image URLs to my clipboard.

Here’s my Python script, and what you need to do if you want to use it!

My Python Script for Uploading Images to Cloudinary

Prerequisites

  1. Download pingo and add it to your PATH.1 (We’re using the pingo CLI program, not the Python module!)

  2. Install the cloudinary and pyperclip Python modules. Run these in the terminal:

    pip3 install cloudinary
    
    pip3 install pyperclip
    
  3. Make a Cloudinary account.

  4. Follow these three steps to set up the cloudinary-core JavaScript library with your website. You need this for the transformations w_auto and dpr_auto to work later, so Cloudinary can generate images based on the layout width and device pixel ratio (DPR).2 (Also, JavaScript code should be placed at the bottom of the final HTML file!)

The Script

This is the Python script that I’m using:

import cloudinary
from cloudinary.uploader import upload
import os
import subprocess
import pyperclip
import os
from dotenv import load_dotenv

# Compresses, uploads all files in /temp to Cloudinary, then copies their URLs to the clipboard
# Cloudinary media library: https://console.cloudinary.com/console/c-f1d8a656afdacd8cf5216bb8d215f4/media_library/search?q=

image_folder = "XXX" # Replace with image folder path
shortcode = True  # True: copies the cl-figure shortcode with the URL to the clipboard. Else, copy the URL only.

load_dotenv()

cloudinary.config(
    cloud_name=os.getenv('CLOUDINARY_CLOUD_NAME'),
    api_key=os.getenv('CLOUDINARY_API_KEY'),
    api_secret=os.getenv('CLOUDINARY_API_SECRET')
)

cloud_name = os.getenv('CLOUDINARY_CLOUD_NAME')

# Ensure the temp folder exists
if not os.path.exists(image_folder):
    os.makedirs(image_folder)
    print(f"Created directory: {image_folder}")

# Compress all images in folder
try:
    subprocess.run(f'pingo -lossless -s4 "{image_folder}"', check=True)
    print(f"Completed image compression in folder: {image_folder}")
except subprocess.CalledProcessError as e:
    print(f"Error during compression: {e}")
    exit(1)

# Get a list of all image files in the folder
image_files = [
    f for f in os.listdir(image_folder) if os.path.isfile(os.path.join(image_folder, f))
]

if not image_files:
    print(f"No image files found in directory: {image_folder}")
    exit(1)

# Upload each image to Cloudinary
for image_file in image_files:
    image_path = os.path.join(image_folder, image_file)
    try:
        response = upload(image_path)
        if response.get("public_id"):
            public_id = response["public_id"]
            url = f"https://res.cloudinary.com/{cloud_name}/image/upload/c_limit,w_auto/dpr_auto,f_auto,q_auto/{public_id}"

            if shortcode:
                cl_figure_url = 'cl-figure data-src="{url}" alt="" caption="" class="center-text" loading="lazy" width=""'.format(
                    url=url
                ) # in my actual script I wrap cl_figure_url with {{{{ }}}} but that causes a formatting issue in this code block
                pyperclip.copy(cl_figure_url)
                print(f"Copied {url} to clipboard as {cl_figure_url}.")
            else:
                pyperclip.copy(url)
                print(f"Copied {url} to clipboard.")
        else:
            print(f"Failed to upload {image_file}. Error: {response.get('error')}")
    except Exception as e:
        print(f"Error uploading {image_file}: {e}")

To use this, create a .env file with your Cloudinary info, like this:

CLOUDINARY_CLOUD_NAME=XXX
CLOUDINARY_API_KEY=XXX
CLOUDINARY_API_SECRET=XXX

Then replace XXX with the relevant information.

I use the transformations c_limit,w_auto/dpr_auto,f_auto,q_auto to make my images load faster. This is what they do:

  • c_limit scales the image down, so it takes up as much area as possible within the page width available.
  • w_auto automatically adjusts the width available for the image in the containing element.
  • / separates the transformations.
  • dpr_auto automatically adjusts the resolution based on the device’s device pixel ratio.
  • f_auto automatically adjusts the image’s format to one with a smaller file size.
  • q_auto automatically lowers the image quality slightly while minimising the file size.

There are other methods to use images in my website, but with the Python script, this process isn’t too much of a hassle, and lets me load images in my site quickly—and for free!

References

  1. https://damien.co/blog/2020-06-16-move-hugo-images-to-cloud-performance-boost/
  2. https://cloudinary.com/documentation/responsive_client_side_js

  1. Download pingo.exe from the official website, place it in a folder (that you won’t delete), then add the folder to your PATH↩︎

  2. There are some downsides to using the cloudinary-core JS library: with the JavaScript, the image load start is delayed, and paint times are longer. You can use client hints instead, but it only works for Chromium browsers. ↩︎