We need to see your repo or a repo which reproduces the problem with code you can share.
hereās a code snippet that should be easily reproducable:
{{ with .Params.thumbnail }}
{{ $thumbnail := $.Resources.GetMatch . }}
{{ if $thumbnail }}
{{ $title := $.Title | urlize }}
{{ range $index, $size := $.Site.Params.thumbnail_sizes.sizes }}
{{ $prefix := index $.Site.Params.thumbnail_sizes.prefixes $index }}
{{ range $format := $.Site.Params.post_headers.formats }}
{{ $resizedImage := $thumbnail.Resize $size }}
{{ with $resizedImage | resources.Copy (print "optimized/" $title "-" $prefix "." $format) }}
{{ $resizedImage = . }}
{{/*
[ {{ $resizedImage.RelPermalink }} ]
[ {{ (print $prefix "_" $format) }} ]
{{ $.Scratch.Set (print $prefix "_" $format) $resizedImage }}
{{ $fn := (print $prefix "_" $format) }}
{{ $.Scratch.Set (print $prefix "_" $format) $resizedImage }}
*/}}
{{ end }}
{{ end }}
{{ end }}
{{ end }}
{{ end }}
create any page bundle, in the front matter put thumbnail = āany bundle image resourceā
you can place this in the config.toml
[Params.post_headers]
formats = ["jpg", "webp", "png"]
[Params.thumbnail_sizes]
sizes = ["1280x720", "640x360", "320x180", "160x90"]
prefixes = ["TL", "TM", "TS", "TES"] # Thumbnail Large, Thumbnail Medium, Thumbnail Small, Thumbnail Extra Small
[Params.header_sizes]
sizes = ["1280x", "640x", "320x", "160x"]
prefixes = ["HL", "HM", "HS", "HES"] # Header Large, Header Medium, Header Small, Header Extra Small
it will generate the images, in the script even copy the files to the correct place but fails to be able to access anything from the scrach file outside of the script.
just try generating images, rename and copy them. Thereās so many bugs in this area of the code. If this code example is too difficult, thereās no need to see the repo. This is the simplest example youāll have.
Isnāt the purpose of hugo to pre-generate a site, this issue is such a major show problem because it seems thereās no passage without building outside tools to preprocess the image and possibly run hugo afterwards.
It honestly cannot be this arduous to get a static site generate to images. A whole weekend worth of work trying different things all for them to fail because of random edge cases that are not documented.
What does that mean? What is the āscratch fileā, what do you need it for, and why would the template (thereās no script) access it.
What does that mean? Does the code not run? Does it generate error messages? Does it generate wrong images?
There many people out there that have no problems generating images in the appropriate sizes and formats with the names they want (Iām one of them). That might be an indication that the problem in this case is not Hugo.
Show your code. All of it, not only those randomly selected pieces that you think are somehow relevant. A git clone
command is a lot easier than trying to stitch together something from the frustratred remarks youāre leaving here.
the code sample to resize the image is a partial. create a partial with the code, call the partial.
thereās two ways forward.
- duplicate this code all over the project
- try to create a partial with the code, the code works within the partial but you cannot access any of the variables set within the partial outside of the partial.
Try returning an object, using scratch space, passing context around.
They all fail
Have the partial return a string that you can use in your figure
or img
. Thatās what I do (kind of)
it does not workā¦
look at the code sample above, i print the RelPermalink in the partial, it works.
i set scratch name, check it right below the partial code in the template, nil
import re
import os
import sys
import yaml
import toml
import json
import hashlib
import argparse
from PIL import Image
from PIL import ImageOps
import pillow_avif
content_dir = None
output_dir = None
folder_fingerprints = {}
# Configuration
config = {
"thumbnail_sizes": ["1280x720", "640x360", "320x180", "160x90"],
"header_sizes": ["1280x", "640x", "320x", "160x"],
"formats": ["jpeg", "webp", "png", "avif"],
"thumbnail_postfixes": ["TL", "TM", "TS", "TES"],
"header_postfixes": ["HL", "HM", "HS", "HES"]
}
def parse_front_matter(file_content):
# Check for YAML front matter
if file_content.startswith('---'):
return yaml.safe_load(file_content.split('---')[1])
# Check for TOML front matter
elif file_content.startswith('+++'):
return toml.loads(file_content.split('+++')[1])
# Check for JSON front matter
elif file_content.startswith('{') and file_content.endswith('}'):
return json.loads(file_content)
else:
return None
def urlize_test(title):
return title.lower().replace(" ", "-").replace(":", "")
def urlize(s):
# Convert to lowercase
s = s.lower()
# Keep only alphanumeric characters and spaces
s = re.sub(r'[^a-z0-9 ]', '', s)
# Replace spaces with hyphens
s = s.replace(' ', '-')
return s
def generate_filename_fingerprint(filename):
return hashlib.sha256(filename.encode()).hexdigest()
def generate_fingerprint(file_path):
with open(file_path, 'rb') as f:
return hashlib.sha256(f.read()).hexdigest()
def has_file_changed(file_path, stored_fingerprint_path, original_fingerprint):
if not os.path.exists(stored_fingerprint_path):
print(f"file {file_path} has changed\n")
return True
with open(stored_fingerprint_path, 'r') as f:
stored_fingerprint = f.read().strip()
print(f"Stored Fingerprint: {stored_fingerprint}")
print(f"Original Fingerprint: {original_fingerprint}")
return stored_fingerprint != original_fingerprint
def process_image_test(image_path, image_type, title):
global output_dir
global content_dir
sizes = config[f"{image_type}_sizes"]
postfixes = config[f"{image_type}_postfixes"]
for index, size in enumerate(sizes):
postfix = postfixes[index]
for format in config["formats"]:
# Construct the new filename
new_filename = f"{title}-{postfix}.{format}"
# Print the details
print(f"\n")
print(f"{os.path.basename(image_path)}")
print(f"directory: {os.path.dirname(image_path)}")
print(f"Processing {image_path}")
print(f"{os.path.abspath(image_path)})")
print(f"New filename: {new_filename}")
# For now, let's use the new filename as the fingerprint (you can replace this with a real fingerprinting method later)
fingerprint = generate_filename_fingerprint(new_filename)
print(f"Fingerprint: {fingerprint}")
print(f"Output directory: {output_dir}")
output_file = f"{output_dir}/{new_filename}"
print(f"Output file: {output_file}")
def process_image(image_path, image_type, title):
global output_dir
global folder_fingerprints
sizes = config[f"{image_type}_sizes"]
postfixes = config[f"{image_type}_postfixes"]
folder_path = os.path.dirname(image_path)
if folder_path not in folder_fingerprints:
# Generate a fingerprint for the original image
folder_fingerprints[folder_path] = generate_fingerprint(image_path)
original_fingerprint = folder_fingerprints[folder_path]
for index, size in enumerate(sizes):
postfix = postfixes[index]
for format in config["formats"]:
# Construct the new filename
new_filename = f"{title}-{postfix}.{format}"
output_file = f"{output_dir}/{new_filename}"
fingerprint_file = f"{output_file}.hash"
print(f"fingerprint_file {fingerprint_file}")
# Check if the file has changed
if has_file_changed(image_path, fingerprint_file, original_fingerprint):
# Open the image
with Image.open(image_path) as img:
# Resize the image
if image_type == "thumbnail":
# Smart fill for thumbnail sizes
width, height = map(int, size.split('x'))
img_resized = ImageOps.fit(img, (width, height), Image.LANCZOS)
elif image_type == "header":
# Resize based on max width for header sizes
width = int(size[:-1]) # Remove the trailing 'x' and convert to int
aspect_ratio = img.width / img.height
height = int(width / aspect_ratio)
img_resized = img.resize((width, height))
# img = img.resize((width, height))
# Save the image in the desired format
img_resized.save(output_file, format=format.upper())
# Store the fingerprint of the new image
with open(fingerprint_file, 'w') as f:
f.write(original_fingerprint)
print(f"Processed and saved: {output_file}")
else:
print(f"File {output_file} has not changed. Skipping...")
def main():
global output_dir
global content_dir
parser = argparse.ArgumentParser(description="Image Preprocessor for Hugo")
parser.add_argument("--content-dir", required=True, help="Directory to crawl for content")
parser.add_argument("--output-dir", required=True, help="Directory where processed images will be saved")
args = parser.parse_args()
content_dir = os.path.abspath(args.content_dir) # Convert to absolute path
output_dir = os.path.abspath(args.output_dir) # Convert to absolute path
print(f"Content Directory: {content_dir}")
print(f"Output Directory: {output_dir}")
for root, _, files in os.walk(content_dir):
for file in files:
if file.endswith(".md"):
with open(os.path.join(root, file), 'r') as f:
content = f.read()
front_matter = parse_front_matter(content)
if front_matter:
raw_title = front_matter.get('title', '')
title = urlize(raw_title)
thumbnail = front_matter.get('thumbnail')
if thumbnail:
if isinstance(thumbnail, list):
for image in thumbnail:
full_image_path = os.path.join(root, image)
if not os.path.exists(full_image_path):
print(f"Missing file: {full_image_path}")
continue
process_image(full_image_path, "thumbnail", title)
process_image(full_image_path, "header", title)
else:
full_image_path = os.path.join(root, thumbnail)
if not os.path.exists(full_image_path):
print(f"Missing file: {full_image_path}")
continue
process_image(full_image_path, "thumbnail", title)
process_image(full_image_path, "header", title)
if __name__ == "__main__":
main()
this code can preprocess images, i have some hardcoded values in here to make it a standalone project.
you might need to install the pillow_avif pluginf or python to get avif format to work.
thereās rudamentary fingerprints to avoid reprocessing images if the main file hasnāt changed.
I agree with you regarding SEO. But having fixed file name (not random) also helps in combating one of the biggest problems on the internet: dead/broken links.
If someone shares a link to an image from my site on a forum, I guess it will turn into a broken link when I update my site or make changes to how I handle image resizing.