Skip to content

VitePress拓展功能,添加文章随机推荐功能

VitePress 默认提供了简洁清爽的文档体验,但在作为静态博客时,为了增加页面的内链密度、提升用户停留时长与 SEO 权重,给每篇文章底部增加一个**“内容推荐”**模块是非常关键的扩展功能。

本文介绍如何在不侵入修改现有 Markdown 源码的前提下,通过 VitePress 的扩展能力,实现一个纯静态、对 SEO 极其友好的随机推荐文章功能


环境版本说明

在开始之前,先记录本文实践所基于的核心依赖版本信息:

  • VitePress: 1.6.4
  • Vue: 3.5.40
  • Node.js: >= 18.0.0

一、 预期效果与设计目标

  1. 位置与外观:放置在每篇文章的结尾(“上一页 / 下一页”导航栏的下方),支持响应式布局(PC 端 2 列卡片,手机端自适应 1 列);
  2. 智能排除:推荐列表必须自动排除当前正在阅读的文章本身;
  3. 构建期全量随机(Build-Time Static Random)
    • 不采用客户端运行期 JS 动态打乱,而是在 npm run build 打包瞬间,在 Node.js 端为全站每一篇文章独立生成固化的随机推荐清单;
    • 输出纯静态 HTML 与 JSON,对搜索引擎爬虫(Googlebot)完全透明,无任何运行时性能损耗;
    • 每次重新触发打包发布,全站各文章的内链推荐关系重新全量随机打乱。

二、 实现思路

整个方案通过 VitePress 官方推荐的三层扩展结构实现:

+---------------------------------------------------------------+
|  1. 数据层 (posts.data.js)                                      |
|     利用 createContentLoader 在构建期扫描 markdown 提取元数据    |
|     在 Node.js 端用 Fisher-Yates 算法为每个 slug 计算专属推荐字典|
+-------------------------------+-------------------------------+
                                |
                                v
+---------------------------------------------------------------+
|  2. 组件层 (RecommendPosts.vue)                                |
|     依据当前页面 slug 查表,配合 withBase() 补全 base 路径输出卡片|
+-------------------------------+-------------------------------+
                                |
                                v
+---------------------------------------------------------------+
|  3. 主题布局层 (.vitepress/theme/index.js)                    |
|     利用 layout 插槽 ('doc-after') 全局无侵入注入推荐组件     |
+-------------------------------+-------------------------------+

三、 核心代码实现

1. 数据层:docs/.vitepress/theme/posts.data.js

利用数据加载器在打包时提取元数据并生成随机字典:

javascript
import { createContentLoader } from 'vitepress'

function getSlug(page) {
  const src = page.file || page.url || ''
  return (src.split(/[/\\]/).pop() || '').replace(/\.(md|html)$/, '')
}

export default createContentLoader('posts/*.md', {
  transform(raw) {
    const all = raw.map(page => ({
      slug: getSlug(page),
      title: page.frontmatter.title,
      desc: page.frontmatter.description || '',
      link: page.url
    }))

    const recommendationsMap = {}

    // 构建期 Node.js 端为每一篇文章独立做 Fisher-Yates 洗牌
    all.forEach(current => {
      const others = all.filter(item => item.slug !== current.slug)
      const shuffled = [...others]
      for (let i = shuffled.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]
      }
      recommendationsMap[current.slug] = shuffled.slice(0, 6)
    })

    return { all, recommendationsMap }
  }
})

2. 组件层:docs/.vitepress/theme/RecommendPosts.vue

组件纯静态读取数据,通过 withBase 补全基准路径:

vue
<template>
  <div v-if="recommendations.length > 0" class="recommendations-container">
    <h3 class="recommendations-title">💡 内容推荐</h3>
    <div class="recommendations-grid">
      <a 
        v-for="post in recommendations" 
        :key="post.link" 
        :href="withBase(post.link)" 
        class="recommendation-card"
      >
        <div class="recommendation-card-title">{{ post.title }}</div>
        <div class="recommendation-card-desc">{{ post.desc }}</div>
      </a>
    </div>
  </div>
</template>

<script setup>
import { computed } from 'vue'
import { useData, withBase } from 'vitepress'
import { data } from './posts.data.js'

const { page } = useData()

function getCurrentSlug() {
  const relPath = page.value.relativePath || ''
  return (relPath.split(/[/\\]/).pop() || '').replace(/\.md$/, '')
}

const recommendations = computed(() => {
  if (!data?.recommendationsMap) return []
  const slug = getCurrentSlug()
  return data.recommendationsMap[slug] || []
})
</script>

3. 主题层:docs/.vitepress/theme/index.js

扩展 VitePress 默认主题,在插槽注入:

javascript
import DefaultTheme from 'vitepress/theme'
import { h } from 'vue'
import RecommendPosts from './RecommendPosts.vue'

export default {
  extends: DefaultTheme,
  Layout() {
    return h(DefaultTheme.Layout, null, {
      // 在文章内容及上下页导航下方无侵入注入推荐组件
      'doc-after': () => h(RecommendPosts)
    })
  }
}

四、 VitePress 核心 API 总结

API / 概念作用与使用场景
createContentLoaderVitePress 的构建期数据加载 API,用于在打包阶段扫描 Markdown 文件并抽取 frontmatter 元数据,避免将未用到的源文件打包进前端产物。
useData()VitePress 核心组合式 API,useData().page.value.relativePath 可稳定获取当前渲染文章相对于 docs/ 源码目录的相对路径与文件名。
withBase()路径助手 API,自动为绝对路径或相对路径补全在 config.mjs 中设置的 base 前缀(如 /blog/zh/),防止生产环境链接 404。
Layout Slots (doc-after)VitePress 提供的默认主题布局插槽(包含 doc-afterdoc-beforedoc-footer-before 等),允许在不改动任何 .md 文章的情况下全局注入自定义 Vue 组件。

💡 内容推荐

NameSilo 坑人!Cloudflare 11 美元的 .net 域名它卖 16 美元,老站长避坑吐槽
吐槽长期使用 NameSilo 遭遇的价格被坑体验,对比 Cloudflare Registrar 的批发成本价及 .net/.io 域名的选购避坑指南。
普通人建网站和博客的最佳选择:零技术门槛的建站神器 Publii
没有编程基础和技术背景的普通人,想搭一个属于自己的个人博客或企业展示网站?本文介绍一款完全免费、像写 Word 一样建站的软件 Publii,帮你零成本、极简搭建与维护网站。
谷歌反重力怎样查看剩余额度?(各版本准确操作指南)
专门针对谷歌反重力(Google Antigravity)的不同形态版本,详细拆解 IDE 编辑器版本的准确查看剩余额度步骤与 Gemini 3.6 升级体验。
Parcel 2 致命 Bug:link hreflang 构建后变成 Hash 字符串导致谷歌多语言页面大量 404 不收录
做多语言网站 SEO 时,在 HTML 中配置 link hreflang 是标准操作。但 Parcel 2 构建时会将 hreflang 的 href 误判为资源依赖并改写为带有 Hash 的 404 路径,导致 Google 爬虫抓取失败、GSC 大量报错。本文深入剖析此 Bug 及绕过方案。
还学编程吗?别学了,AI编程已经堵住软件开发的路了,但也有机会
深度剖析 AI 编程时代的行业剧变。为什么传统的软件开发之路被堵住了?新人如何通过 AI 全栈出海与个人产品化迎来新机会?
Parcel v2 构建导致 SVG 自定义属性丢失 Bug 踩坑与绕过方案
在 HTML 中结合 Vue.js 进行渐进式开发时,使用 Parcel v2 打包工具容易遇到 <svg> 标签上的 Vue 指令(如 v-if、v-show)及自定义属性在打包后被擦除丢失的问题。本文详细分析原因并给出最佳绕过解决方案。