跳到主内容

一文搞懂 VitePress 自定义主题:插槽、暗黑模式与全文搜索 ​

为什么不建议从零写一套 VitePress 主题 ​

很多团队第一次搭建文档站时,都会在两个方向之间犹豫:要么完全使用默认主题,后期发现品牌区、公告条、页脚备案信息无处安放;要么直接复制整套默认主题源码改造,结果 VitePress 升级后冲突不断。

更稳妥的方式是继承默认主题并做增量扩展。VitePress 默认主题已经内置响应式布局、侧边栏、上一页/下一页、本地搜索和暗黑模式切换。我们只需要通过 .vitepress/theme/index 导出一个主题对象,包装默认的 Layout,再借助命名插槽注入自定义内容。这样既保留官方能力,又能控制升级成本。

本文用一个可直接落地的最小文档站,串起三个高频需求:

  1. 使用 Layout 插槽扩展全站导航、文档头部和页脚;
  2. 正确接管暗黑模式,避免首屏闪烁与样式丢失;
  3. 配置本地全文搜索,并说明中文分词、排除目录和热更新边界。

VitePress 自定义主题的整体结构 ​

默认主题的扩展点可以分成三层:配置层负责导航、侧边栏和搜索选项;主题层负责注册布局、全局组件与样式;插槽层负责在固定位置插入 UI。理解这三层的边界,能避免把业务逻辑写进错误位置。

config 配置层导航 / 搜索 / 外观theme 主题层继承 Layout / 全局样式Slots 插槽层公告 / 页脚 / 增强输出:可升级的品牌文档站暗黑模式 + 全文搜索 + 自定义区块

建议的目录结构如下:

text
.docs/
├─ .vitepress/
│  ├─ config.ts
│  └─ theme/
│     ├─ index.ts
│     ├─ custom.css
│     └─ components/
│        ├─ SiteBanner.vue
│        └─ SiteFooter.vue
└─ guide/
   └─ index.md

方案对比:继承默认主题还是完整自定义 ​

方案优点代价适用场景
直接使用默认主题零主题代码,升级最轻松品牌定制能力有限内部文档、开源项目
继承并包装 Layout保留搜索、导航、暗色模式,插槽扩展简单需要理解插槽命名大多数团队文档站
完整自定义主题UI 自由度最高要自行实现路由、目录、搜索入口等能力营销官网与文档深度融合

本文选择第二种。它的关键不是覆盖默认主题,而是把默认 Layout 当作组件继续使用。

步骤一:包装默认 Layout 并注册主题 ​

先创建主题入口。下面示例同时引入自定义样式、全局组件,并通过插槽渲染公告条和页脚。

typescript
// .vitepress/theme/index.ts
import DefaultTheme from 'vitepress/theme'
import type { Theme } from 'vitepress'
import SiteBanner from './components/SiteBanner.vue'
import SiteFooter from './components/SiteFooter.vue'
import './custom.css'

export default {
  extends: DefaultTheme,
  enhanceApp({ app }) {
    // 注册可在 Markdown 中直接使用的全局组件
    app.component('SiteBanner', SiteBanner)
  },
  Layout() {
    return h(
      DefaultTheme.Layout,
      null,
      {
        // 导航栏上方的全站公告
        'layout-top': () => h(SiteBanner),
        // 文档内容底部的自定义页脚
        'doc-footer-before': () => h(SiteFooter)
      }
    )
  }
} satisfies Theme

上面的 h 需要从 Vue 引入:

typescript
import { h } from 'vue'

如果你更喜欢 SFC 写法,也可以新建 CustomLayout.vue,在模板中使用 <Layout> 与具名插槽,再在 index.ts 中把它作为 Layout 导出。插槽名称要以当前 VitePress 版本文档为准,常见的有 nav-bar-content-before、aside-outline-before、doc-before、doc-after、layout-bottom 等。

步骤二:用插槽扩展公告条与页脚 ​

公告条通常需要可关闭,并把关闭状态保存在本地。注意不要在服务端渲染阶段直接访问 localStorage,否则构建时可能报 localStorage is not defined。

vue
<!-- .vitepress/theme/components/SiteBanner.vue -->
<script setup lang="ts">
import { ref, onMounted } from 'vue'

const STORAGE_KEY = 'docs-banner-hidden-v1'
const visible = ref(false)

onMounted(() => {
  visible.value = localStorage.getItem(STORAGE_KEY) !== '1'
})

function closeBanner() {
  visible.value = false
  localStorage.setItem(STORAGE_KEY, '1')
}
</script>

<template>
  <div v-if="visible" class="site-banner">
    <span>新版搜索已支持中文标题与正文检索,立即按 Ctrl/⌘ + K 体验。</span>
    <button type="button

<!-- 内容间隙推广位:正文之后内联渲染,不遮挡阅读;变现开关关闭时零渲染零占位 -->
<MonetizeSlot position="inline" domain="frontend" />

<!-- 免责声明:文字与 data/site.config.yaml 的 disclaimer 保持完全一致,不得删改 -->
::: warning 免责声明
本工具与文章内容仅用于学习测试场景,优化建议仅供参考,请勿直接用于生产环境,由此产生的风险自行承担。
:::