文章摘要
加载中...|
此内容根据文章生成,并经过人工审核,仅用于文章内容的解释与总结

主题介绍

该主题框架由 imsyy 制作,由于存在某些BUG 特性 ,特写一篇文章修改。

特性 1 搜索数据库修改

由于 DocSearch 的数据库配置没有加到配置中,需要在对应的位置修改为自己的数据库。

javascript
// .vitepress/theme/components/Search.vue
// 修改为自己的数据库配置

docsearchdocsearch

特性 2 右键复制地址 undefined

右键复制本页地址显示复制成功,实际粘贴出来是 undefined。

javascript
// .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 的点击事件会跳转到无效路径。

javascript
// .vitepress/theme/components/Aside/Widgets/ArticleGPT.vue
// 移除该点击事件:
@click="router.go('/posts/2024/0218')"

特性 4 主页边栏 GitHub 和邮箱写死

主页边栏的社交链接需要手动在组件中修改。

javascript
// .vitepress/theme/components/Aside/Widgets/Hello.vue
// 在该文件中修改对应的 GitHub 地址和邮箱

侧边栏侧边栏

特性 5 复制链接访问失败 (Nginx 重写)

当你部署完静态站点后,直接访问带有路径的 URL 可能会失败(404)。如果你的服务器基于 Nginx,需要添加以下配置,其余方式见 官方文档

nginx
# nginx 站点配置
try_files $uri $uri.html $uri/ =404;

原理说明:当访问 /0615 时,Nginx 会依次查找 0615 文件、0615.html 文件以及 0615/ 目录。由于静态部署的路径实际为 0615.html,该配置能确保顺利完成转发。

特性 6 无法访问 RSS 和 Sitemap

如果你发现无法正常获取 rss.xmlsitemap.xml,请确保开启了正确的 Gzip 配置:

nginx
# 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),导致老文章即使重新修改也会顽固触发过期弹窗。

为了解决该链条式问题,我进行了如下重构修复:

  1. 移出 .gitignore 限制:将 posts/ 重新纳入 Git 版本管理。
  2. 多重兜底时间提取机制:修改 getPostData.mjs,将更新时间的抓取权重调整为:🥇 手动指定的 Frontmatter 中的 updated 属性 -> 🥈 本地磁盘物理修改时间 -> 🥉 文章发布时间。
  3. 过期时间计算与文案重构:修改 Post.vue 运行时组件,在计算过期警告时,以最终的更新时间戳 finalLastUpdated 代替原有的发布时间,并修正过期文案为“最近更新于”。
  4. 统一本地时间解析(解决时区偏差):为了消除不同服务器/浏览器在构建或运行时因对 YYYY-MM-DD 的 UTC 解析差异导致的更新日期偏差(如“今日更新显示为1天前”),引入 parseLocalDate 并将日期格式统一化。
javascript
// .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),
);
javascript
// .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 样式表中增加特定的过滤规则,即可完美复原代码块的顶部填充间距:

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 导致中转失效。

为了彻底解决这一“猫鼠游戏”式的重写竞态,我将跳转安全逻辑完美合流,实现了服务端与响应式组件的源头双重闭环防线:

  1. 客户端全局路由拦截:在全站根组件 App.vue 中加装 watch 监听 route.path。通过 nextTick 结合延时对渲染出的全部 DOM 外链做客户端级的 Base64 劫持(原作者在 initTools.mjs 中被注释掉的重定向监听现已被完全打通激活)。
  2. LinkCard 响应式计算源头拦截:由于 LinkCard.vue 自带异步渲染重绘,在此类响应式组件内部,直接将 :href 绑定为全新构建的 redirectUrl 计算属性,支持跨平台(支持 Node.js 服务端构建与 window 客户端渲染)无缝 Base64 编码,实现源头阻断,不受任何二次重绘影响。
javascript
// .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;
});
javascript
// .vitepress/theme/App.vue
// 加装全局路由监听器,杜绝 client-side 页面切换对 Markdown 传统超链接的还原
watch(
  () => route.path,
  () => {
    nextTick(() => {
      setTimeout(() => {
        jumpRedirect(null, theme.value, true);
      }, 300);
    });
  },
  { immediate: true },
);
评论 隐私政策