11import { defineConfig } from "vitepress" ;
2+ import { VitePWA } from "vite-plugin-pwa" ;
23import { buildRSS } from "./rss" ;
34import { zh } from "./locales/zh" ;
45import { en } from "./locales/en" ;
@@ -12,6 +13,10 @@ import { zhHk } from "./locales/zh-hk";
1213// undefined → all 5 (dev mode only, needs lots of RAM)
1314const BUILD_MODE = process . env . BUILD_MODE as "main" | "zh-variants" | undefined ;
1415
16+ // ── Analytics (Umami) ────────────────────────────────────────────────────────
17+ // Only injected in production builds to avoid polluting analytics with dev traffic.
18+ const IS_PRODUCTION = process . env . NODE_ENV === "production" ;
19+
1520const allLocales = {
1621 root : { ...zh } ,
1722 en : { ...en } ,
@@ -61,6 +66,13 @@ export default defineConfig({
6166 { property : "og:url" , content : "https://fonghehe.github.io/blog/" } ,
6267 ] ,
6368 [ "meta" , { property : "og:locale" , content : "zh_CN" } ] ,
69+ [
70+ "meta" ,
71+ {
72+ property : "og:image" ,
73+ content : "https://fonghehe.github.io/blog/icons/icon.svg" ,
74+ } ,
75+ ] ,
6476 [ "meta" , { name : "twitter:card" , content : "summary_large_image" } ] ,
6577 [ "meta" , { name : "twitter:title" , content : "前端成长记录" } ] ,
6678 [
@@ -71,10 +83,36 @@ export default defineConfig({
7183 "一个前端工程师从 2018 年开始的学习与成长记录。1200+ 篇深度文章,涵盖框架原理、工程化实践与前沿探索。" ,
7284 } ,
7385 ] ,
86+ [
87+ "meta" ,
88+ {
89+ name : "twitter:image" ,
90+ content : "https://fonghehe.github.io/blog/icons/icon.svg" ,
91+ } ,
92+ ] ,
93+ // Umami analytics — only injected in production builds
94+ ...( IS_PRODUCTION
95+ ? ( [
96+ [
97+ "script" ,
98+ {
99+ defer : "" ,
100+ src : "https://cloud.umami.is/script.js" ,
101+ "data-website-id" : "4d310e6b-2d69-42aa-aaa2-c788a664d3c5" ,
102+ } ,
103+ ] ,
104+ ] as [ string , Record < string , string > ] [ ] )
105+ : [ ] ) ,
74106 ] ,
75107 sitemap : {
76108 hostname : "https://fonghehe.github.io/blog/" ,
77109 } ,
110+ transformHead ( { pageData } ) {
111+ const canonical = `https://fonghehe.github.io/blog/${ pageData . relativePath } `
112+ . replace ( / i n d e x \. m d $ / , "" )
113+ . replace ( / \. m d $ / , ".html" ) ;
114+ return [ [ "link" , { rel : "canonical" , href : canonical } ] ] ;
115+ } ,
78116 locales : allLocales ,
79117 themeConfig : {
80118 search : {
@@ -88,14 +126,16 @@ export default defineConfig({
88126 markdown : {
89127 // 完全替换 shiki — 用轻量 highlighter 直接输出 <pre><code>
90128 // shiki 是构建最大瓶颈:加载语法文件 + token 化 7000 篇文章的代码块
129+ // 语法高亮由客户端 highlight.js 接管(见 theme/index.ts)
91130 highlight ( code , lang ) {
92131 const escaped = code
93132 . replace ( / & / g, "&" )
94133 . replace ( / < / g, "<" )
95134 . replace ( / > / g, ">" )
96135 . replace ( / \{ \{ / g, "{{" )
97136 . replace ( / \} \} / g, "}}" ) ;
98- return `<pre class="language-${ lang } "><code>${ escaped } </code></pre>` ;
137+ const cls = lang ? ` class="language-${ lang } "` : "" ;
138+ return `<pre class="language-${ lang } "><code${ cls } >${ escaped } </code></pre>` ;
99139 } ,
100140 config ( md ) {
101141 // 转义非代码区域的 {{ }} 防止 Vue 模板编译报错
@@ -132,6 +172,62 @@ export default defineConfig({
132172 }
133173 } ,
134174 vite : {
175+ plugins : [
176+ VitePWA ( {
177+ // Only register service worker in the main build
178+ disable : BUILD_MODE === "zh-variants" ,
179+ registerType : "autoUpdate" ,
180+ outDir : ".vitepress/dist" ,
181+ injectRegister : "script-defer" ,
182+ manifest : {
183+ name : "前端成长记录" ,
184+ short_name : "前端记录" ,
185+ description :
186+ "一个前端工程师从 2018 年开始的学习与成长记录,1200+ 篇深度文章" ,
187+ theme_color : "#3c8772" ,
188+ background_color : "#ffffff" ,
189+ display : "standalone" ,
190+ start_url : "/blog/" ,
191+ scope : "/blog/" ,
192+ icons : [
193+ {
194+ src : "/blog/icons/icon.svg" ,
195+ sizes : "any" ,
196+ type : "image/svg+xml" ,
197+ purpose : "any maskable" ,
198+ } ,
199+ ] ,
200+ } ,
201+ workbox : {
202+ globPatterns : [ "**/*.{js,css,html,ico,png,svg,woff2}" ] ,
203+ // Exclude local search indexes (5-6 MB each) from precache — served via NetworkFirst at runtime
204+ globIgnores : [ "**/chunks/@localSearchIndex*" ] ,
205+ navigateFallback : null ,
206+ runtimeCaching : [
207+ {
208+ // Search indexes are large and change on every build; fetch fresh when online
209+ urlPattern : / @ l o c a l S e a r c h I n d e x / ,
210+ handler : "NetworkFirst" ,
211+ options : {
212+ cacheName : "search-index" ,
213+ expiration : { maxEntries : 10 } ,
214+ } ,
215+ } ,
216+ {
217+ urlPattern : / ^ h t t p s : \/ \/ f o n t s \. ( g o o g l e a p i s | g s t a t i c ) \. c o m \/ .* / i,
218+ handler : "CacheFirst" ,
219+ options : {
220+ cacheName : "google-fonts" ,
221+ expiration : {
222+ maxEntries : 20 ,
223+ maxAgeSeconds : 60 * 60 * 24 * 365 ,
224+ } ,
225+ } ,
226+ } ,
227+ ] ,
228+ } ,
229+ } ) ,
230+ ] ,
135231 build : {
136232 reportCompressedSize : false ,
137233 chunkSizeWarningLimit : 4000 ,
0 commit comments