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 透明代理