主题介绍
该主题框架由 imsyy 制作,由于存在某些BUG 特性 ,特写一篇文章修改。
特性 1 搜索数据库修改
由于 DocSearch 的数据库配置没有加到配置中,需要在对应的位置修改为自己的数据库。
// .vitepress/theme/components/Search.vue
// 修改为自己的数据库配置特性 2 右键复制地址 undefined
右键复制本页地址显示复制成功,实际粘贴出来是 undefined。
// .vitepress/theme/components/RightMenu.vue
const pageLink = theme.value.site + router.route.path;
// 修改为:
const pageLink = theme.value?.siteMeta?.site + router.route.path;特性 3 点击 ArticleGPT 的文章摘要跳转 404
由于自部署的主题中可能没有作者原始的文章,ArticleGPT 的点击事件会跳转到无效路径。
// .vitepress/theme/components/Aside/Widgets/ArticleGPT.vue
// 移除该点击事件:
@click="router.go('/posts/2024/0218')"特性 4 主页边栏 GitHub 和邮箱写死
主页边栏的社交链接需要手动在组件中修改。
// .vitepress/theme/components/Aside/Widgets/Hello.vue
// 在该文件中修改对应的 GitHub 地址和邮箱特性 5 复制链接访问失败 (Nginx 重写)
当你部署完静态站点后,直接访问带有路径的 URL 可能会失败(404)。如果你的服务器基于 Nginx,需要添加以下配置,其余方式见 官方文档。
# nginx 站点配置
try_files $uri $uri.html $uri/ =404;原理说明:当访问 /0615 时,Nginx 会依次查找 0615 文件、0615.html 文件以及 0615/ 目录。由于静态部署的路径实际为 0615.html,该配置能确保顺利完成转发。
特性 6 无法访问 RSS 和 Sitemap
如果你发现无法正常获取 rss.xml 或 sitemap.xml,请确保开启了正确的 Gzip 配置:
# nginx 配置
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;特性 7 文章更新时间错乱与过期提示误判
由于 .gitignore 原本排除了 posts/* 目录,导致 VitePress 无法读取文章的 Git 历史提交时间作为更新时间,只能退而求其次读取本地文件的物理修改时间。当文件被解压或重新上传时,所有文章的修改时间都被统一重置为了错误的解压时间。此外,原主题的过期提醒直接咬死 Frontmatter 中的创建日期(date),导致老文章即使重新修改也会顽固触发过期弹窗。
为了解决该链条式问题,我进行了如下重构修复:
- 移出
.gitignore限制:将posts/重新纳入 Git 版本管理。 - 多重兜底时间提取机制:修改
getPostData.mjs,将更新时间的抓取权重调整为:🥇 手动指定的 Frontmatter 中的updated属性 -> 🥈 本地磁盘物理修改时间 -> 🥉 文章发布时间。 - 过期时间计算与文案重构:修改
Post.vue运行时组件,在计算过期警告时,以最终的更新时间戳finalLastUpdated代替原有的发布时间,并修正过期文案为“最近更新于”。 - 统一本地时间解析(解决时区偏差):为了消除不同服务器/浏览器在构建或运行时因对
YYYY-MM-DD的 UTC 解析差异导致的更新日期偏差(如“今日更新显示为1天前”),引入parseLocalDate并将日期格式统一化。
// .vitepress/theme/utils/getPostData.mjs
// 重构更新时间抓取权重,并增加本地时间解析 parseLocalDate 规避时区差
const parseLocalDate = (dateVal) => {
if (!dateVal) return 0;
if (dateVal instanceof Date) {
const y = dateVal.getUTCFullYear();
const m = String(dateVal.getUTCMonth() + 1).padStart(2, '0');
const d = String(dateVal.getUTCDate()).padStart(2, '0');
return new Date(`${y}/${m}/${d}`).getTime();
}
if (typeof dateVal === "number") {
return dateVal;
}
const dateStr = dateVal.toString();
if (dateStr.includes("T")) {
return new Date(dateStr).getTime();
}
const formattedStr = dateStr.replace(/-/g, "/");
return new Date(formattedStr).getTime();
};
const { title, date, updated, categories, description, tags, top, cover, hidden } = data;
// 优先采用 updated 字段并调用 parseLocalDate 锁死历史物理修改戳
const lastUpdateTime = updated
? parseLocalDate(updated)
: (mtimeMs || (date ? parseLocalDate(date) : birthtimeMs));
const expired = Math.floor(
(new Date().getTime() - lastUpdateTime) / (1000 * 60 * 60 * 24),
);// .vitepress/theme/views/Post.vue
// 重构前端过期天数与更新时间的判定逻辑
import { formatTimestamp, parseLocalDate } from "@/utils/helper";
const finalLastUpdated = computed(() => {
if (frontmatter.value?.updated) {
return parseLocalDate(frontmatter.value.updated);
}
return page.value.lastUpdated || postMetaData.value?.lastModified;
});
const expiredDays = computed(() => {
const lastUpdated = finalLastUpdated.value;
if (!lastUpdated) return 0;
return Math.floor((Date.now() - lastUpdated) / (1000 * 60 * 60 * 24));
});特性 8 折叠代码块 (::: details) 展开时首行被标题栏遮挡
由于全局样式中配置了折叠框最后一项子元素的覆盖样式 .details > :last-child { padding: 16px; },当在折叠框内放置代码块 div[class*="language-"] 时,代码块原本为顶部绝对定位标题栏(.lang,高度为 36px)预留的 padding-top: 36px 会被强行覆盖缩减为 16px。这导致绝对定位的标题栏直接覆盖了代码的第一行内容。
通过在 SCSS 样式表中增加特定的过滤规则,即可完美复原代码块的顶部填充间距:
// .vitepress/theme/style/post.scss
// 在 .details > :last-child 的末尾追加针对代码块的过滤和内边距复原
> :last-child {
padding: 16px;
font-size: 1rem;
border-radius: 0 0 12px 12px;
border: 1px solid var(--main-card-border);
background-color: var(--main-card-background);
border-top: none;
// 针对嵌套代码块,复原 top padding 并清除外边距
&[class*="language-"] {
padding: 36px 0 0 0;
margin: 0;
}
}特性 9 外链中转与跳转保护(Redirect)在客户端失效与 Hydration 竞态冲突
由于原生的外链中转逻辑仅仅在 VitePress 静态构建阶段(SSR)通过 transformHtml 挂载 Cheerio 替换静态 HTML 中含有 target="_blank" 的链接。但是在浏览器中,由于 Vue 客户端激活 (Hydration) 机制,虚拟 DOM 的原始直链配置会将已劫持的 href 重新强行覆盖还原;同时,动态单页路由切换(SPA Navigation)与异步 API 渲染(例如 LinkCard 动态拉取站点数据后组件二次渲染)也会覆盖 DOM 的 href 导致中转失效。
为了彻底解决这一“猫鼠游戏”式的重写竞态,我将跳转安全逻辑完美合流,实现了服务端与响应式组件的源头双重闭环防线:
- 客户端全局路由拦截:在全站根组件
App.vue中加装watch监听route.path。通过nextTick结合延时对渲染出的全部 DOM 外链做客户端级的 Base64 劫持(原作者在initTools.mjs中被注释掉的重定向监听现已被完全打通激活)。 - LinkCard 响应式计算源头拦截:由于
LinkCard.vue自带异步渲染重绘,在此类响应式组件内部,直接将:href绑定为全新构建的redirectUrl计算属性,支持跨平台(支持 Node.js 服务端构建与 window 客户端渲染)无缝 Base64 编码,实现源头阻断,不受任何二次重绘影响。
// .vitepress/theme/components/Tags/LinkCard.vue
// 内部添加响应式计算属性 redirectUrl,安全兼容 SSR 与 CSR 环境的 Base64 编码
const redirectUrl = computed(() => {
if (isOutLink.value && theme.value?.jumpRedirect?.enable) {
try {
const originalUrl = props.url;
const encoded = typeof window !== "undefined" && typeof window.btoa === "function"
? window.btoa(originalUrl)
: Buffer.from(originalUrl, "utf-8").toString("base64");
return `/redirect?url=${encoded}`;
} catch (e) {
console.error("LinkCard 编码失败:", e);
return props.url;
}
}
return props.url;
});// .vitepress/theme/App.vue
// 加装全局路由监听器,杜绝 client-side 页面切换对 Markdown 传统超链接的还原
watch(
() => route.path,
() => {
nextTick(() => {
setTimeout(() => {
jumpRedirect(null, theme.value, true);
}, 300);
});
},
{ immediate: true },
);
