Appearance
VitePress拓展功能,添加文章随机推荐功能
VitePress 默认提供了简洁清爽的文档体验,但在作为静态博客时,为了增加页面的内链密度、提升用户停留时长与 SEO 权重,给每篇文章底部增加一个**“内容推荐”**模块是非常关键的扩展功能。
本文介绍如何在不侵入修改现有 Markdown 源码的前提下,通过 VitePress 的扩展能力,实现一个纯静态、对 SEO 极其友好的随机推荐文章功能。
环境版本说明
在开始之前,先记录本文实践所基于的核心依赖版本信息:
- VitePress:
1.6.4 - Vue:
3.5.40 - Node.js:
>= 18.0.0
一、 预期效果与设计目标
- 位置与外观:放置在每篇文章的结尾(“上一页 / 下一页”导航栏的下方),支持响应式布局(PC 端 2 列卡片,手机端自适应 1 列);
- 智能排除:推荐列表必须自动排除当前正在阅读的文章本身;
- 构建期全量随机(Build-Time Static Random):
- 不采用客户端运行期 JS 动态打乱,而是在
npm run build打包瞬间,在 Node.js 端为全站每一篇文章独立生成固化的随机推荐清单; - 输出纯静态 HTML 与 JSON,对搜索引擎爬虫(Googlebot)完全透明,无任何运行时性能损耗;
- 每次重新触发打包发布,全站各文章的内链推荐关系重新全量随机打乱。
- 不采用客户端运行期 JS 动态打乱,而是在
二、 实现思路
整个方案通过 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 / 概念 | 作用与使用场景 |
|---|---|
createContentLoader | VitePress 的构建期数据加载 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-after、doc-before、doc-footer-before 等),允许在不改动任何 .md 文章的情况下全局注入自定义 Vue 组件。 |