Hugo全站AVIF记
当你读到这篇文章的时候,本站除了一些 logo、thumbnail、icon(个人觉得不适合),绝大部分图片已完成从 png、jpg、jpeg、WebP 等到 AVIF 的切换。
所谓 AVIF(AV1 Image File Format)是基于 AV1 编码、开放免版税的高效图像格式,是当前“压缩效率 + 现代特性 + 开放生态”的最佳平衡方案,显著优于 JPEG、WebP、PNG 等传统格式。
说人话是:节约带宽、用户加载快、可能利于 SEO。
时代在变化,以前用 Webp,觉得已经很不错了,如今又得切换。年近八旬的老人唐纳德·特朗普都在为美国四处抢矿、抢油、抢地盘,给自己网站优化下图片方案有何不可?利己利人。
先看疗效,非常显著!同一个文件,不同格式的体积差别:
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
本站原始图片一般是 png 格式,来自各种截图软件的默认输出,我总是尽力保留原始图片和一个小体积压缩图片,之前是 png 和 webp,很明显现在要换成 png 和 AVIF 了,webp 后续可能考虑删除。
原始图片效果最清晰,以防不时之需,兜底的。
1. 如何转换格式
- 单张转换
1magick input.png output.avif
- 批量转换
批量操作要小心,你应该懂的!,毕竟受响图片多,mogrify 会保留原始图片(各个 Linux 发行版是否行为一致未测试)。
决定动手前,最好自己创建个临时目录,放入各种图片测试下,看看是否和预期一致。
1# jpg/jpeg/png/webp → avif
2mogrify -format avif *.jpg *.jpeg *.png *.webp
magick 和 mogrify都来自 imagemagick 包,不得不说真的很强大。
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
- 写程序转换
我让 AI 帮我写了个 Golang 程序,初步审核了下,符合自己需求。
需求:
- 转换图片格式,优先级 png > jpg > jpeg > webp,因为我知道自己的图片源是这个顺序,有 png 优先使用 png 源转换压缩。
- 帮忙修改 Markdown 中的图片连接
1package main
2
3import (
4 "flag"
5 "fmt"
6 "os"
7 "os/exec"
8 "path/filepath"
9 "regexp"
10 "strings"
11)
12
13// 定义源文件优先级:png > jpg > jpeg > webp
14var srcPriorities = []string{"png", "jpg", "jpeg", "webp"}
15
16// convertToAVIF 按优先级查找源文件并转换为 AVIF(支持深层目录)
17// relDir: 图片相对于 imagesDir 的子目录(如 travel/wugongshan)
18func convertToAVIF(imagesDir, relDir, filename string) (bool, error) {
19 var srcPath string
20 // 1. 拼接子目录路径
21 fullRelDir := filepath.Join(imagesDir, relDir)
22
23 // 2. 按优先级查找源文件
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 // 无可用源文件
33 if srcPath == "" {
34 return false, fmt.Errorf("无可用源文件(png/jpg/jpeg/webp)")
35 }
36
37 // 3. 目标 AVIF 文件路径(保持原目录结构)
38 avifPath := filepath.Join(fullRelDir, fmt.Sprintf("%s.avif", filename))
39
40 // 检查 AVIF 是否已存在,避免重复转换
41 if _, err := os.Stat(avifPath); err == nil {
42 fmt.Printf("ℹ️ AVIF 文件已存在,跳过: %s\n", avifPath)
43 return true, nil
44 }
45
46 // 4. 使用 ffmpeg 转换为 AVIF(适配 Arch Linux 的 libsvtav1 编码器)
47 cmd := exec.Command(
48 "ffmpeg",
49 "-i", srcPath, // 源文件
50 "-c:v", "libsvtav1", // Arch Linux 兼容的 AVIF 编码器
51 "-crf", "30", // 质量参数(0-63,值越小质量越好)
52 "-preset", "6", // 速度预设(0-13,值越小速度越慢/质量越高)
53 "-g", "240", // GOP size,适配图片转换
54 "-pix_fmt", "yuv420p", // 色彩格式,保证兼容性
55 "-y", // 覆盖已存在的文件(前面已检查,此处为兜底)
56 avifPath,
57 )
58
59 // 执行转换并捕获输出
60 output, err := cmd.CombinedOutput()
61 if err != nil {
62 return false, fmt.Errorf("转换失败: %v, 输出: %s", err, string(output))
63 }
64
65 fmt.Printf("✅ 转换成功: %s → %s\n", srcPath, avifPath)
66 return true, nil
67}
68
69// replaceMDLinks 替换 Markdown 文件中的图片链接(支持深层目录)
70func replaceMDLinks(mdPath, imagesDir string) (bool, error) {
71 // 读取 Markdown 内容
72 content, err := os.ReadFile(mdPath)
73 if err != nil {
74 return false, fmt.Errorf("读取 MD 文件失败: %v", err)
75 }
76 oldContent := string(content)
77
78 // 正则匹配 /images/ 下所有层级的图片链接:
79 // 关键修改:([^/.]+/)* 匹配任意层级的子目录(如 travel/wugongshan/)
80 pattern := `(!\[[^\]]*\]\(/images/(([^/.]+/)*)([^/.]+))\.(webp|jpg|jpeg|png)(\))`
81 re := regexp.MustCompile(pattern)
82
83 // 替换为 .avif 链接
84 newContent := re.ReplaceAllString(oldContent, "${1}.avif${6}")
85
86 // 无内容变化
87 if newContent == oldContent {
88 return false, nil
89 }
90
91 // 写入替换后的内容
92 err = os.WriteFile(mdPath, []byte(newContent), 0644)
93 if err != nil {
94 return false, fmt.Errorf("写入 MD 文件失败: %v", err)
95 }
96
97 fmt.Printf("✅ MD 链接替换完成: %s\n", mdPath)
98 return true, nil
99}
100
101func main() {
102 // 命令行参数
103 imagesDir := flag.String("images", "./images", "图片根目录路径")
104 mdDir := flag.String("md", ".", "Markdown 文件目录路径")
105 flag.Parse()
106
107 // 1. 验证图片目录是否存在
108 if _, err := os.Stat(*imagesDir); os.IsNotExist(err) {
109 fmt.Printf("❌ 图片目录不存在: %s\n", *imagesDir)
110 os.Exit(1)
111 }
112
113 // 2. 递归扫描图片目录所有层级,提取待转换文件(保留相对路径)
114 fmt.Println("===== 开始转换图片为 AVIF =====")
115 type imageItem struct {
116 relDir string // 相对于 imagesDir 的子目录(如 travel/wugongshan)
117 filename string // 无后缀的文件名(如 IMG_2150)
118 }
119 var imageItems []imageItem
120 // 去重映射:key = relDir/filename,避免重复处理
121 processed := make(map[string]bool)
122
123 err := filepath.Walk(*imagesDir, func(path string, info os.FileInfo, err error) error {
124 if err != nil {
125 return err
126 }
127 // 跳过目录
128 if info.IsDir() {
129 return nil
130 }
131
132 // 过滤非目标格式文件
133 ext := strings.TrimPrefix(filepath.Ext(path), ".")
134 isTargetExt := false
135 for _, e := range srcPriorities {
136 if ext == e {
137 isTargetExt = true
138 break
139 }
140 }
141 if !isTargetExt {
142 return nil
143 }
144
145 // 计算相对路径和文件名
146 relPath, err := filepath.Rel(*imagesDir, path)
147 if err != nil {
148 fmt.Printf("⚠️ 无法计算相对路径: %s, 跳过\n", path)
149 return nil
150 }
151 // 拆分:relPath = relDir/filename.ext → relDir + filename
152 relDir := filepath.Dir(relPath) // 相对子目录(如 travel/wugongshan)
153 baseName := filepath.Base(relPath) // 带后缀的文件名(如 IMG_2150.webp)
154 filename := strings.TrimSuffix(baseName, filepath.Ext(baseName)) // 无后缀文件名
155
156 // 去重:避免同一文件(不同后缀)被重复处理
157 key := filepath.Join(relDir, filename)
158 if processed[key] {
159 return nil
160 }
161 processed[key] = true
162
163 imageItems = append(imageItems, imageItem{
164 relDir: relDir,
165 filename: filename,
166 })
167 return nil
168 })
169
170 if err != nil {
171 fmt.Printf("❌ 扫描图片目录失败: %v\n", err)
172 os.Exit(1)
173 }
174
175 // 批量转换图片(支持深层目录)
176 convertCount := 0
177 for _, item := range imageItems {
178 success, err := convertToAVIF(*imagesDir, item.relDir, item.filename)
179 if success {
180 convertCount++
181 } else {
182 fullPath := filepath.Join(item.relDir, item.filename)
183 fmt.Printf("❌ 跳过 %s: %v\n", fullPath, err)
184 }
185 }
186
187 // 3. 替换 Markdown 中的图片链接(支持深层目录)
188 fmt.Println("\n===== 开始替换 MD 图片链接 =====")
189 mdCount := 0
190 modifiedMdCount := 0
191 err = filepath.Walk(*mdDir, func(path string, info os.FileInfo, err error) error {
192 if err != nil {
193 return err
194 }
195 // 仅处理 .md 文件
196 if !info.IsDir() && filepath.Ext(path) == ".md" {
197 mdCount++
198 modified, err := replaceMDLinks(path, *imagesDir)
199 if err != nil {
200 fmt.Printf("❌ 处理 MD 文件失败 %s: %v\n", path, err)
201 } else if modified {
202 modifiedMdCount++
203 } else {
204 fmt.Printf("ℹ️ 无需要替换的链接,跳过: %s\n", path)
205 }
206 }
207 return nil
208 })
209
210 if err != nil {
211 fmt.Printf("❌ 扫描 MD 目录失败: %v\n", err)
212 os.Exit(1)
213 }
214
215 // 4. 输出汇总信息
216 fmt.Println("\n===== 处理完成汇总 =====")
217 fmt.Printf("📸 图片转换:共找到 %d 个待处理文件,成功转换 %d 个 AVIF 文件\n", len(imageItems), convertCount)
218 fmt.Printf("📝 MD 替换:共扫描 %d 个 MD 文件,成功替换 %d 个文件的链接\n", mdCount, modifiedMdCount)
219}
AI 还挺厉害的,简单交互几次后,就能生成代码。
编译成可执行文件 img-convert-replace
1go build -o img-convert-replace img-convert-replace.go
自定义路径示例(比如图片目录是 /home/you/static/images md 文件目录是 /home/you/mds)
1./img-convert-replace -images /home/you/static/images -md /home/you/mds
友情提示,上述程序不适应于其它场景,每个站点逻辑不一样。比如 B 站这种切换 AVIF,肯定是个大工程。
如果你的需求很简单,那就不用写程序,用命令行处理也挺方便的,额外判断逻辑多就需要写程序处理。
2. 核验
转换完成后,git status、git diff 查看下,看看是否有问题,小心驶得万年船。
hugo 站点都是静态的,万一真的搞崩了,也能 git 回滚。
3. webserver 加上缓存仿盗链等
hugo 静态站点通常配合 Nginx、Caddy 来对外提供服务,可以配置下图片缓存和防盗链。
我用 caddy,diff 示例如下:
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 }
丐版服务器小水管没办法,防君子不防小人。
版权申明:
- 未标注来源的内容全部为原创,未经授权请勿转载(因转载后排版往往错乱、内容不可控、无法持续更新等);
- 非营利为目的,演绎本博客任何内容,请以'原文出处'或者'参考链接'等方式给出本站相关网页地址(方便读者)。
相关文章:
- Hugo文末添加最后修改时间
- 网站添加回到顶部功能
- Hugo自定义字体
- Hugo静态站点接入Adsense广告
- Mac下如何旋转webp图片
- 网站导航栏防止插入Adsense自动广告
- 简体文章批量转换为繁体
- PNG图片批量转换为webp
- Linux下尝试使用Godot开发小游戏
- Arch linux dae 透明代理