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/: 
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.
Copyright statement:
- All content that is not sourced is original., please do not reprint without authorization (because the typesetting is often disordered after reprinting, the content is uncontrollable, and cannot be continuously updated, etc.);
- For non-profit purposes, to deduce any content of this blog, please give the relevant webpage address of this site in the form of 'source of original text' or 'reference link' (for the convenience of readers).
See Also:
- Hugo adds the last modified time at the end of the article
- Adding a Back to Top Feature to the Website
- Hugo Custom Fonts
- Hugo static site access Adsense ads
- How to rotate webp images on Mac
- Website navigation bar prevents insertion of Adsense automatic ads
- Batch conversion of Simplified Chinese articles to Traditional Chinese
- Batch Convert PNG Images to WebP
- Trying to Develop a Small Game Using Godot on Linux
- Arch linux dae Transparent Proxy