技术教程 给博客做了一轮大修:书架、中控台、访客信息一次搞定 Cheymin 2026-08-01 2026-08-01 又是折腾博客的一晚上 博客用 AnZhiYu 主题用了一阵子,零零散散积了一堆小毛病:关于页的「追番」我根本没看几部番,空荡荡的;中控台最新评论莫名消失;浅色模式下个人卡片文字全白看不见;页脚信息太单薄;签到图标还是原作者的「安知鱼」。
一狠心,全给收拾了。顺便把侧边栏访客欢迎信息、倒计时、友链朋友圈后端这几块也整理记录一下,免得下次升级主题又得重来一遍。
一、追番页改书架 为啥要改 关于页那个「爱好番剧」板块,我就看过两三部,挂在那儿特别空旷。又不想为了填满硬去看一堆番,干脆换成「书架」——书我倒是真看了不少。
怎么改的 数据全在 source/_data/about.yml 里,找到 comic 这一段,把标题和列表换掉就行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 comic: comic_tips: 爱好阅读 comic_title: 书架 comic_list: - name: 三体 href: https://book.douban.com/subject/2567698/ cover: /img/books/three-body-1.jpg - name: 三体II 黑暗森林 href: https://book.douban.com/subject/3066477/ cover: /img/books/three-body-2.jpg - name: 三体III 死神永生 href: https://book.douban.com/subject/5366477/ cover: /img/books/three-body-3.jpg - name: 流浪地球 href: https://book.douban.com/subject/3266609/ cover: /img/books/wandering-earth.jpg - name: 活着 href: https://book.douban.com/subject/4913064/ cover: /img/books/to-live.jpg
封面图我是从豆瓣下到本地 source/img/books/ 里的,为啥不放外链?因为豆瓣有防盗链,直接引用会裂图。下到本地最稳。
踩坑 别用主题默认的 cover 外链,豆瓣图片 Referer 一校验就 403,老老实实存本地。 字段名 comic 不能改,主题模板里写死了,只改里面的内容。 二、中控台最新评论回来了 啥情况 点开右下角中控台,以前会显示最新评论和最新文章,结果某天突然就没了,光剩个切换边栏和暗色模式的按钮。
根因 主题这块要单独开开关,newest_comments.enable 默认是关的。在 _config.anzhiyu.yml 里加上:
1 2 3 4 5 6 newest_comments: enable: true limit: 5 storage: 0.6 avatar: true
limit:显示几条,我设 5storage:缓存时长(小时),0.6 就是大约 36 分钟刷一次avatar:显示评论者头像评论数据是从 Twikoo 后端拉的,所以还得保证 twikoo.envId 配的地址能正常访问,不然就算开关开了也是空的。
验证后端 我直接 curl 了一下 Twikoo:
1 2 3 curl -X POST https://twikoo.346247.xyz/.netlify/functions/twikoo \ -H "Content-Type: application/json" \ -d '{"event":"getRecentComments","commentLength":5}'
返回 200 且有数据,说明后端没问题。所以之前「啥也没恢复」纯粹是因为部署还没推完,改完一定要硬刷新浏览器(Ctrl+Shift+R) ,主题的 localStorage 缓存特别顽固。
三、浅色模式个人卡片不空白了 啥情况 切到浅色模式,侧边栏个人卡片只剩头像和那个狗头状态图标,名字、描述、社交图标全看不见——不是没渲染,是字白色的,背景也是浅色,叠一起就「隐形」了。
根因 我之前为了统一卡片风格,用 CSS 把主题默认的渐变背景层(.card-info::before)隐藏了:
1 2 3 #aside-content > .card-widget .card-info ::before { display : none !important ; }
背景一没,原来设计在彩色背景上的白色文字就暴露问题了。
修复 在 _config.anzhiyu.yml 的 inject.head 里注入一段 CSS,浅色模式下把文字改成深色,深色模式保持白色:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #aside-content .card-info .author-info__name ,#aside-content .card-info .author-info__desc ,#aside-content .card-info #author-info__sayhi ,#aside-content .card-info .author-info__description ,#aside-content .card-info .card-info-social-icons i { color : var (--anzhiyu-fontcolor) !important ; } #aside-content .card-info #author-info__sayhi { background : var (--anzhiyu-card-op) !important ; } [data-theme="dark" ] #aside-content .card-info .author-info__name ,[data-theme="dark" ] #aside-content .card-info .author-info__desc ,[data-theme="dark" ] #aside-content .card-info #author-info__sayhi ,[data-theme="dark" ] #aside-content .card-info .author-info__description ,[data-theme="dark" ] #aside-content .card-info .card-info-social-icons i { color : var (--anzhiyu-white) !important ; }
用主题自带的 CSS 变量 --anzhiyu-fontcolor 和 --anzhiyu-white,这样不用自己定颜色,跟着主题走。
四、页脚补上框架信息 啥情况 页脚那栏就一个「2025-2026 By Cheymin」,右边空着。别人的博客这儿都挂着「Powered by Hexo · Theme · Hosted on Cloudflare」,我也想要。
怎么加 主题自带 footerBar 配置,在 _config.anzhiyu.yml 里:
1 2 3 4 5 6 7 8 9 10 11 12 13 footer: footerBar: enable: true authorLink: / linkList: - text: Hexo link: https://hexo.io/ - text: AnZhiYu link: https://docs.anheyu.com/ - text: Cloudflare link: https://www.cloudflare.com/ cc: enable: false
linkList 里的每一项就会渲染成页脚右侧的小链接,点开跳对应官网。cc(知识共享徽章)我关了,嫌乱。
五、签到图标换成自己的 啥情况 页脚那个签到图标原来写的是「安知鱼 下班啦」,鼠标悬浮提示「下班了就改….」。这是原作者的,得换成我自己的。
怎么改 在 _config.anzhiyu.yml 的 footer.runtime 里:
1 2 3 4 5 6 7 runtime: enable: true launch_time: 08 /20/2025 15 :20:50 work_img: /img/sign-work.svg work_description: Cheymin 上班摸鱼中... offduty_img: /img/sign-offduty.svg offduty_description: 放学了就该好好休息啦!
图标我一开始自己画了个圆角矩形 SVG,结果风格跟主题格格不入。后来学乖了,模仿主题原来 shields.io 徽章的风格,左侧图标+名字、右侧状态,只改字:
1 2 3 4 5 6 7 <svg xmlns ="http://www.w3.org/2000/svg" width ="160" height ="40" viewBox ="0 0 160 40" > <rect x ="0" y ="0" width ="160" height ="40" rx ="20" ry ="20" fill ="#FF7B3A" /> <rect x ="0" y ="0" width ="70" height ="40" rx ="20" ry ="20" fill ="#555" /> <text x ="35" y ="26" font-family ="sans-serif" font-size ="14" font-weight ="700" fill ="#fff" text-anchor ="middle" > Cheymin</text > <text x ="115" y ="26" font-family ="sans-serif" font-size ="14" font-weight ="700" fill ="#fff" text-anchor ="middle" > 放学啦</text > </svg >
悬浮提示就是 work_description / offduty_description 这俩字段,按当前时间自动切换上班/下班图标。
六、侧边栏访客欢迎信息 这块是我最满意的改动,每次有人进博客,侧边栏公告卡片里会显示「欢迎来自 XX 的朋友,距博主约 XXX km」。
效果 国内访客:欢迎来自「重庆市 重庆市」的朋友!距博主约 XX km 国外访客:带我去你的城市玩玩吧(高德查不到时降级显示) IP 地址默认模糊处理,鼠标悬浮才清楚显示,保护隐私 实现思路 整个组件就一个 JS 文件 source/js/card-welcome.js,挂在公告卡片的 #welcome-info 容器里。流程:
1 访客进站 → 取公网IP → 高德IP定位 → 算与博主距离 → 渲染欢迎语
1. 公告卡片留容器 在 _config.anzhiyu.yml 的公告卡片配置里,content 末尾留一个空 div:
1 2 3 4 5 6 aside: card_announcement: enable: true content: | 欢迎来到我的博客! <div id="welcome-info"></div>
JS 会往这个 #welcome-info 里塞内容。
2. 引入脚本 在 inject.bottom 里引入:
1 2 3 inject: bottom: - <script src="/js/card-welcome.js"></script>
3. 配置 API 密钥 脚本顶部有个配置对象,关键是高德 Web 服务 API 密钥:
1 2 3 4 5 6 7 8 9 window .IP_CONFIG = { API_KEY : '你的高德key' , BLOG_LOCATION : { lng : 106.66 , lat : 29.97 , }, CACHE_DURATION : 1000 * 60 * 60 , HOME_PAGE_ONLY : false , };
高德 key 去 高德开放平台 申请,选「Web 服务」类型,免费的,每天 30 万次额度个人博客根本用不完。
4. 核心:拿 IP + 定位 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 const fetchIpData = async ( ) => { const [ip, amapData] = await Promise .all ([ fetchClientIp (), (async () => { const response = await fetch ( `https://restapi.amap.com/v3/ip?key=${encodeURIComponent (IP_CONFIG.API_KEY)} ` ); const data = await response.json (); if (data.status !== '1' ) throw new Error (data.info || '高德 API 错误' ); return data; })(), ]); const rect = amapData.rectangle || '' ; let lng = 0 , lat = 0 ; if (rect) { const pts = rect.split (';' ); const [lng1, lat1] = pts[0 ].split (',' ).map (Number ); const [lng2, lat2] = pts[1 ].split (',' ).map (Number ); lng = (lng1 + lng2) / 2 ; lat = (lat1 + lat2) / 2 ; } return { data : { lng, lat, prov : amapData.province , city : amapData.city }, ip }; };
公网 IP 用了三个源并发抢答,谁先返回用谁,避免单源挂了整个卡片就废:
1 2 3 4 5 const sources = [ 'https://4.ipw.cn' , 'https://api.ipify.org?format=json' , 'https://api.ip.sb/ip' , ];
5. 算距离 用 Haversine 公式算访客和博主的球面距离:
1 2 3 4 5 6 7 8 9 10 11 12 const calculateDistance = (lng, lat ) => { if (!lng || !lat) return '??' ; const R = 6371 ; const rad = Math .PI / 180 ; const dLat = (lat - IP_CONFIG .BLOG_LOCATION .lat ) * rad; const dLon = (lng - IP_CONFIG .BLOG_LOCATION .lng ) * rad; const a = Math .sin (dLat / 2 ) ** 2 + Math .cos (IP_CONFIG .BLOG_LOCATION .lat * rad) * Math .cos (lat * rad) * Math .sin (dLon / 2 ) ** 2 ; return Math .round (R * 2 * Math .atan2 (Math .sqrt (a), Math .sqrt (1 - a))); };
踩坑 高德 API 返回的 province/city 有时是空数组 :查不到定位时高德会返回 [] 而不是空字符串,直接用会显示「来自 [] 的朋友」。得判断一下:Array.isArray(amapData.province) ? '' : amapData.province。国外 IP 查不到 :高德 IP 定位只支持国内,国外 IP 返回的 province/city 是空的。这种情况降级显示「带我去你的城市玩玩吧」,不显示距离,体验好一点。IP 混淆 HTTP/HTTPS :博客是 https 的,如果 IP 接口是 http 的会被浏览器拦截。三个源全换成 https 的才行。缓存 :每次进站都调高德 API 太浪费,用 localStorage 缓存 1 小时,同一个访客一小时内刷新页面直接读缓存。pjax 适配 :主题开了 pjax,页面切换不会重新执行脚本。得监听 pjax:complete 或者用 MutationObserver 监听 #welcome-info 是否被重新插入。七、侧边栏倒计时组件 公告卡片下面挂了个倒计时,显示今天/本周/本月/本年还剩多少,外加最近一个节假日倒计时。
效果 1 2 3 4 5 6 7 今日还剩 8 小时 [▓▓▓▓░░░░] 66% 本周还剩 3 天 [▓▓▓░░░░░] 42% 本月还剩 12 天 [▓▓░░░░░░] 60% 本年还剩 153 天 [▓▓▓▓░░░░] 58% 距国庆还有 45 天 2026-10-01
实现 两个文件:source/js/countdown.js(逻辑)+ source/css/countdown.css(样式),通过 source/_data/widget.yml 挂到侧边栏:
1 2 3 4 5 6 7 top: - class_name: card-countdown id_name: card-countdown name: 倒计时 icon: anzhiyu-icon-hourglass-start html: '<div id="countdown-timer"></div>'
主题会自动把这个 html 渲染进一个带图标的卡片容器,我只要管往 #countdown-timer 里塞内容就行。
1. 算今日/本周/本月/本年剩余 纯 JS 计算,不依赖任何接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 function calcProgress ( ) { var now = new Date (); var dayPassed = now.getHours () * 3600 + now.getMinutes () * 60 + now.getSeconds (); var dayRemaining = 86400 - dayPassed; var wd = now.getDay (); var weekPassed = (wd === 0 ? 6 : wd - 1 ); var weekRemaining = 6 - weekPassed; var monthTotal = new Date (now.getFullYear (), now.getMonth () + 1 , 0 ).getDate (); var monthRemaining = monthTotal - now.getDate () + 1 ; var yearStart = new Date (now.getFullYear (), 0 , 1 ); var yearEnd = new Date (now.getFullYear () + 1 , 0 , 1 ); var yearRemainingSec = (yearEnd - yearStart) / 1000 - (now - yearStart) / 1000 ; return { day : { remaining : Math .floor (dayRemaining / 3600 ), percentage : (dayPassed / 86400 ) * 100 , unit : "小时" }, week : { remaining : weekRemaining, percentage : ((weekPassed + 1 ) / 7 ) * 100 , unit : "天" }, month : { remaining : monthRemaining, percentage : ((now.getDate () - 1 ) / monthTotal) * 100 , unit : "天" }, year : { remaining : Math .floor (yearRemainingSec / 86400 ), percentage : ((now - yearStart) / (yearEnd - yearStart)) * 100 , unit : "天" }, }; }
2. 节假日数据 节假日不用自己维护,用 NateScarlet/holiday-cn 这个仓库,它自动同步国务院每年的放假安排,通过 jsDelivr 直接拉 JSON:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 var HOLIDAY_CN_BASE = "https://cdn.jsdelivr.net/gh/NateScarlet/holiday-cn@master/" ;function fetchNextHoliday (cb ) { var year = new Date ().getFullYear (); var urls = [HOLIDAY_CN_BASE + year + ".json" ]; if (new Date ().getMonth () >= 10 ) urls.push (HOLIDAY_CN_BASE + (year + 1 ) + ".json" ); var allDays = []; urls.forEach (function (url ) { fetch (url).then (r => r.json ()).then (data => { allDays = allDays.concat (data.days ); }).then (() => { cb (resolveNextHoliday (allDays, new Date ())); }); }); }
3. 找最近一个假期 数据里 isOffDay: true 的就是放假日,但要把连续的放假日归并成一个假期,取第一天作为倒计时目标:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 function resolveNextHoliday (days, now ) { days.sort ((a, b ) => new Date (a.date ) - new Date (b.date )); var groups = [], current = null ; for (var i = 0 ; i < days.length ; i++) { var d = days[i]; if (!d.isOffDay ) continue ; var t = new Date (d.date + "T00:00:00" ).getTime (); if (current && t - current.endTime === 86400000 ) { current.endTime = t; } else { current = { startTime : t, endTime : t, days : [d] }; groups.push (current); } } for (var j = 0 ; j < groups.length ; j++) { if (groups[j].startTime >= now.getTime ()) { var diffDays = Math .ceil ((groups[j].startTime - new Date (now.getFullYear (), now.getMonth (), now.getDate ())) / 86400000 ); return { name : groups[j].days [0 ].name , date : groups[j].days [0 ].date , diffDays : diffDays }; } } return null ; }
踩坑 节假日在 JSON 里是按天列的,不是按假期 :国庆 7 天会有 7 条记录。一开始我没合并,显示「距国庆还有 7 天」「距国庆还有 6 天」……一串,蠢死了。归并成一段取第一天才对。跨年问题 :12 月查下一年的 JSON,不然 1 月初查本年会查不到春节(春节数据在上一年的 JSON 里)。pjax 刷新 :跟访客卡片一样,监听 pjax:complete 重新渲染。另外用 setInterval 每分钟刷新一次进度条,不然进度条永远不动。八、友链朋友圈后端 主题自带的「朋友圈」页面能聚合所有友链博客的最新文章,但要自己部署后端抓取。
为啥要搞 手动点开每个友链博客看更新太累,朋友圈把大家的 atom.xml 抓过来按时间排序,一屏看完,爽。
部署后端 用的是 HexoCircle 的后端(Vercel 部署版),fork 一份改配置就行。关键配置项是友链列表,从博客的 /link/ 页面自动抓。
后端跑起来后会暴露一个 API,我部署在 https://friend.346247.xyz/。
主题配置 在 _config.anzhiyu.yml 里:
1 2 3 4 5 friends_vue: enable: true vue_js: /js/fcircle_vue.js apiurl: https://friend.346247.xyz/
vue_js:前端渲染脚本,放本地 source/js/fcircle_vue.js,避免每次都从 CDN 拉apiurl:后端地址,末尾要带斜杠 ,不带的话前端拼接 URL 会 404踩坑 apiurl 斜杠 :跟 Qexo 的配置一样,这里必须带斜杠,前端是字符串拼接不是 URL 对象,apiurl + "api/..." 不带斜杠就变成 friend.346247.xyzapi/...。Vercel 函数超时 :免费版函数执行上限 10 秒,友链一多抓不完。解决办法是后端定时抓取缓存到 KV,前端只读缓存数据,不实时抓。友链格式要求 :后端靠解析友链页面的 HTML 拿博客地址,友链的 atom.xml 路径得规范(一般是 /atom.xml)。有些友链博客没开 RSS 或者路径不对,就抓不到,会出现在「无法抓取」列表里,正常现象。CORS :后端 API 要允许博客域名跨域,不然前端 fetch 会被浏览器拦。Vercel 函数响应头加 Access-Control-Allow-Origin: * 最省事。最后说一句 这一轮折腾下来最大的感受是:能改配置就别改源码,能用主题变量就别写死颜色 。
top_img: false 一行配置搞定标题的事,别用 CSS 硬藏footerBar.linkList 加链接,别去改 footer.pug 模板颜色用 var(--anzhiyu-xxx),浅色深色自动适配,省得自己写两套 唯一动源码的就是补回误删的组件(比如音乐馆切换按钮),这种改动记得记一笔,下次主题升级好对照着改回去。
至于访客信息和倒计时这种自写组件,关键是把数据源选对、把缓存做好、把 pjax 适配好,剩下的就是体力活了。
大概就这样,博客终于清爽了,收工。