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 组件。

💡 内容推荐

VitePress 是用来做什么的?聊聊它的适用场景、优势与局限
VitePress 是基于 Vite 和 Vue 3 的超高速 SSG 静态网站生成器。本文详细解读 VitePress 用来做什么、适合哪些场景(博客、笔记、文档),以及哪些场景不适合使用它。
2026 年做网站还能赚钱吗?国内与海外市场对比及 AI 产品新机遇
深入分析 2026 年做网站的商业现状。为什么国内内容站变现维艰?为什么海外市场与 AI 产品(图片/视频生成、写作辅助)蕴含巨大机会?
Rspack 也许是最符合需求的前端构建工具
在 Webpack 速度见顶、Vite 面对旧项目与 ES5 老浏览器兼容较为吃力的今天,基于 Rust 的 Rspack 凭极速的编译性能、原生的 ES5 输出支持以及对 Webpack 生态的高度兼容,成为了很多企业级项目的最优构建方案。
普通人建网站和博客的最佳选择:零技术门槛的建站神器 Publii
没有编程基础和技术背景的普通人,想搭一个属于自己的个人博客或企业展示网站?本文介绍一款完全免费、像写 Word 一样建站的软件 Publii,帮你零成本、极简搭建与维护网站。
OPC(一人公司)应该选择做什么?独立开发者的赛道选择与商业路径分析
探讨一人公司(OPC)在当前时代下的商业模式与赛道选型。深入剖析 App 出海、网站出海、自媒体博主、游戏攻略站等不同方向的优劣与长板打造。
VitePress 跨页面/页脚链接点击触发 404 Bug 排查与终极解决方法
在基于 VitePress 搭建静态博客或文档站点时,配置了 base 路径后在页脚或页面中添加外部/跨目录链接容易触发 404 错误。本文记录 VitePress v1.6.4 下此 Bug 的产生根源、对 SEO 的影响以及终极解决方案。