Hugo's Complete AVIF Conversion Guide

By the time you read this article, most of the images on this site, except for some logos, thumbnails, and icons (which I personally think are unsuitable), have been switched from png, jpg, jpeg, WebP to AVIF.

AVIF (AV1 Image File Format) is a high-efficiency image format based on AV1 encoding, open and royalty-free. It represents the best balance of "compression efficiency + modern features + open ecosystem," significantly superior to traditional formats such as JPEG, WebP, and PNG.

In plain terms: it saves bandwidth, loads faster for users, and may be beneficial for SEO.

Times are changing. We used to use WebP and thought it was quite good, but now we have to switch. Even the nearly 80-year-old Donald Trump is busy grabbing mines, oil, and land for the US; why can't he optimize his website's image format? It benefits both himself and others.

Let's see the results first—very significant! Size differences of the same file in different formats:

1➜ ls -al GPXSee.png
2-rw-r--r-- 1 mephisto mephisto 802779 Dec 10 20:35 GPXSee.png
3➜ ls -al GPXSee.webp
4-rw-r--r-- 1 mephisto mephisto 126820 Dec 10 20:35 GPXSee.webp
5➜ mogrify -format avif GPXSee.png
6➜ ls -lhrS --time-style=+"" GPXSee.*
7-rw-r--r-- 1 mephisto mephisto 103K  GPXSee.avif
8-rw-r--r-- 1 mephisto mephisto 124K  GPXSee.webp
9-rw-r--r-- 1 mephisto mephisto 784K  GPXSee.png

The original images on this site are generally in PNG format, from the default output of various screenshot software. I always try my best to keep the original image and a small compressed image. Previously, it was PNG and WebP, but obviously now it will be changed to PNG and AVIF. WebP may be removed later.

The original image has the clearest effect, as a backup in case of unforeseen circumstances.

1. How to convert formats

  • Single image conversion
1magic input.png output.avif
  • Batch conversion

Batch operations should be done with caution, you should know that! , after all, there are many affected images. Mogrify will keep the original images (whether the behavior is consistent across different Linux distributions has not been tested).

Before deciding to start, it is best to create a temporary directory and put various images in it to test whether it is consistent with expectations.

1# jpg/jpeg/png/webp → avif
2mogrify -format avif _.jpg _.jpeg _.png _.webp

Both magick and mogrify come from the imagemagick package, which is undeniably powerful.

1➜ ~ pacman -Qo $(which magick)
2/usr/bin/magick is owned by imagemagick 7.1.2.12-2
3➜ ~ pacman -Qo $(which mogrify)
4/usr/bin/mogrify is owned by imagemagick 7.1.2.12-2
  • Programming Conversion

I had AI write a Golang program for me, and after a preliminary review, it meets my needs.

Requirements:

  • Convert image formats, prioritizing PNG > JPG > JPEG > WebP, because I know my image sources are in this order; if PNG is available, use that source for conversion and compression first.

  • Help me modify image links in Markdown

  1package main
  2
  3import (
  4	"flag"
  5	"fmt"
  6	"os"
  7	"os/exec"
  8	"path/filepath"
  9	"regexp"
 10	"strings"
 11)
 12
 13// Define source file priority: png > jpg > jpeg > webp
 14var srcPriorities = []string{"png", "jpg", "jpeg", "webp"}
 15
 16// convertToAVIF finds source files by priority and converts them to AVIF (supports deep directories)
 17// relDir: The subdirectory of the image relative to imagesDir (e.g., travel/wugongshan)
 18func convertToAVIF(imagesDir, relDir, filename string) (bool, error) {
 19	var srcPath string
 20	// 1. Concatenate subdirectory paths
 21	fullRelDir := filepath.Join(imagesDir, relDir)
 22
 23	// 2. Find source files by priority
 24	for _, ext := range srcPriorities {
 25		candidate := filepath.Join(fullRelDir, fmt.Sprintf("%s.%s", filename, ext))
 26		if _, err := os.Stat(candidate); err == nil {
 27			srcPath = candidate
 28			break
 29		}
 30	}
 31
 32	// No available source file
 33	if srcPath == "" {
 34		return false, fmt.Errorf("No available source file (png/jpg/jpeg/webp)")
 35	}
 36
 37	// 3. Target AVIF file path (preserve original directory structure)
 38	avifPath := filepath.Join(fullRelDir, fmt.Sprintf("%s.avif", filename))
 39
 40	// Check if AVIF already exists to avoid duplicate conversion
 41	if _, err := os.Stat(avifPath); err == nil {
 42		fmt.Printf("ℹ️ AVIF file already exists, skip: %s\n", avifPath)
 43		return true, nil
 44	}
 45
 46	// 4. Convert to AVIF using ffmpeg (compatible with libsvtav1 encoder for Arch Linux)
 47	cmd := exec.Command(
 48		"ffmpeg",
 49		"-i", srcPath, // Source file
 50		"-c:v", "libsvtav1", // Arch Linux compatible AVIF encoder
 51		"-crf", "30", // Quality parameter (0-63, lower value for better quality)
 52		"-preset", "6", // Speed ​​preset (0-13, lower value for slower speed/higher quality)
 53		"-g", "240", // GOP size, for image conversion
 54		"-pix_fmt", "yuv420p", // Color format, for compatibility
 55		"-y", // Overwrite existing files (checked previously, this is a fallback)
 56		avifPath,
 57	)
 58
 59	// Perform conversion and capture output
 60	output, err := cmd.CombinedOutput()
 61	if err != nil {
 62		return false, fmt.Errorf("Conversion failed: %v, Output: %s", err, string(output))
 63	}
 64
 65	fmt.Printf("✅ Conversion successful: %s → %s\n", srcPath, avifPath)
 66	return true, nil
 67
 68}
 69
 70// replaceMDLinks Replace image links in Markdown files (supports deep directories)
 71func replaceMDLinks(mdPath, imagesDir string) (bool, error) {
 72	// Read Markdown content
 73	content, err := os.ReadFile(mdPath)
 74	if err != nil {
 75		return false, fmt.Errorf("Failed to read MD file: %v", err)
 76	}
 77	oldContent := string(content)
 78
 79	// Regular expression matches all levels of image links under /images/: ![xxx](/images/any path/filename.ext)
 80	// Key modification: ([^/.]+/)* matches any level of subdirectories (e.g., travel/wugongshan/)
 81	pattern := `(!\[[^\]]*\]\(/images/(([^/.]+/)*)([^/.]+))\.(webp|jpg|jpeg|png)(\))`
 82	re := regexp.MustCompile(pattern)
 83
 84	// Replace with .avif links
 85	newContent := re.ReplaceAllString(oldContent, "${1}.avif${6}")
 86
 87	// No content change
 88	if newContent == oldContent {
 89		return false, nil
 90	}
 91
 92	// Write the replaced content
 93	err = os.WriteFile(mdPath, []byte(newContent), 0644)
 94	if err != nil {
 95		return false, fmt.Errorf("Failed to write MD file: %v", err)
 96	}
 97
 98	fmt.Printf("✅ MD link replacement complete: %s\n", mdPath)
 99	return true, nil
100}
101
102func main() {
103	// Command line arguments
104	imagesDir := flag.String("images", "./images", "image root directory path")
105	mdDir := flag.String("md", ".", "Markdown file directory path")
106	flag.Parse()
107
108	// 1. Verify if the image directory exists
109	if _, err := os.Stat(*imagesDir); os.IsNotExist(err) {
110		fmt.Printf("❌ Image directory does not exist: %s\n", *imagesDir)
111		os.Exit(1)
112	}
113
114	// 2. Recursively scan all levels of the image directory and extract the files to be converted (keeping relative paths)
115	fmt.Println("===== Start converting images to AVIF =====")
116	type imageItem struct {
117		relDir   string // Subdirectory relative to imagesDir (e.g., travel/wugongshan)
118		filename string // Filename without extension (e.g., IMG_2150)
119	}
120
121	var imageItems []imageItem
122	// Deduplication mapping: key = relDir/filename, to avoid duplicate processing
123	processed := make(map[string]bool)
124
125	err := filepath.Walk(*imagesDir, func(path string, info os.FileInfo, err error) error {
126		if err != nil {
127			return err
128		}
129
130		// Skip directories
131		if info.IsDir() {
132			return nil
133		}
134
135		// Filter non-target format files
136		ext := strings.TrimPrefix(filepath.Ext(path), ".")
137		isTargetExt := false
138		for _, e := range srcPriorities {
139			if ext == e {
140				isTargetExt = true
141				break
142			}
143		}
144
145		if !isTargetExt {
146			return nil
147		}
148
149		// Calculate relative path and filename
150		relPath, err := filepath.Rel(*imagesDir, path)
151		if err != nil {
152			fmt.Printf("⚠️ Cannot calculate relative path: %s, skip\n", path)
153			return nil
154		}
155
156		// Calculate: relPath = relDir/filename.ext → relDir + filename
157		relDir := filepath.Dir(relPath)                                  // Relative subdirectories (e.g.) travel/wugongshan)
158		baseName := filepath.Base(relPath)                               // Filename with extension (e.g., IMG_2150.webp)
159		filename := strings.TrimSuffix(baseName, filepath.Ext(baseName)) // Filename without extension
160
161		// Deduplication: Avoid processing the same file (different extensions) repeatedly
162		key := filepath.Join(relDir, filename)
163		if processed[key] {
164			return nil
165		}
166		processed[key] = true
167
168		imageItems = append(imageItems, imageItem{
169			relDir:   relDir,
170			filename: filename,
171		})
172		return nil
173	})
174
175	if err != nil {
176		fmt.Printf("❌ Failed to scan image directory: %v\n", err)
177		os.Exit(1)
178	}
179
180	// Batch convert images (supports deep directories)
181	convertCount := 0
182	for _, item := range imageItems {
183		success, err := convertToAVIF(*imagesDir, item.relDir, item.filename)
184		if success {
185			convertCount++
186		} else {
187			fullPath := filepath.Join(item.relDir, item.filename)
188			fmt.Printf("❌ Skip %s: %v\n", fullPath, err)
189		}
190	}
191
192	// 3. Replace image links in Markdown (supports deep directories)
193	fmt.Println("\n===== Start replacing MD image links =====")
194	mdCount := 0
195	modifiedMdCount := 0
196	err = filepath.Walk(*mdDir, func(path string, info os.FileInfo, err error) error {
197		if err != nil {
198			return err
199		}
200		// Only process .md files
201		if !info.IsDir() && filepath.Ext(path) == ".md" {
202			mdCount++
203			modified, err := replaceMDLinks(path, *imagesDir)
204			if err != nil {
205				fmt.Printf("❌ Failed to process MD file %s: %v\n", path, err)
206			} else if modified {
207				modifiedMdCount++
208			} else {
209				fmt.Printf("ℹ️ No links to be replaced, skip: %s\n", path)
210			}
211		}
212		return nil
213	})
214
215	if err != nil {
216		fmt.Printf("❌ Scanning MD directory failed: %v\n", err)
217		os.Exit(1)
218	}
219
220	// 4. Output summary information
221	fmt.Println("\n===== Summary of processing completed =====")
222	fmt.Printf("📸 Image conversion: %d files were found, %d AVIF files were successfully converted\n", len(imageItems), convertCount)
223	fmt.Printf("📝 MD replacement: %d MD files were scanned, %d links of files were successfully replaced\n", mdCount, modifiedMdCount)
224}

The AI is quite powerful; after a few simple interactions, it can generate code. Compile into an executable file img-convert-replace

1go build -o img-convert-replace img-convert-replace.go

Example of a custom path (e.g., image directory is /home/you/static/images, md file directory is /home/you/mds)

1./img-convert-replace -images /home/you/static/images -md /home/you/mds

Friendly reminder: the above program is not suitable for other scenarios; the logic is different for each site. For example, switching between AVI and FPS on Bilibili would definitely be a large project.

If your needs are simple, then you don't need to write a program; using the command line is quite convenient. If there is a lot of additional conditional logic, then you need to write a program.

2. Verification

After the conversion, check with git status and git diff to see if there are any problems. Better safe than sorry.

Hugo sites are all static, so even if they crash, you can roll back using git.

3. Adding Caching and Anti-Hotlinking to the Web Server

Hugo static sites are usually used with Nginx and Caddy to provide services. You can configure image caching and anti-hotlinking measures.

I use caddy, the diff example is as follows:

 1➜ git diff 7127 203d
 2diff --git a/Caddyfile b/Caddyfile
 3index 4c99e16..674de8b 100644
 4--- a/Caddyfile
 5+++ b/Caddyfile
 6@@ -86,13 +86,13 @@ mephisto.cc {
 7
 8        @static {
 9                file
10-               path *.ico *.css *.js *.gif *.jpg *.jpeg *.png *.svg *.webp *.avif
11+               path *.ico *.css *.js *.gif *.jpg *.jpeg *.png *.svg *.webp
12        }
13        header @static Cache-Control max-age=31536000
14
15        @invalid_referer {
16                file
17-               path *.webp *.png *.avif
18+               path *.webp *.png
19                not header Referer https://mephisto.cc*
20        }

This is a basic server with slow internet speed; it's only good for honest people, not malicious ones.

Lastmod: Friday, January 16, 2026

See Also:

Translations: