{T}

自定义

自定义主题

可以通过创建 .vitepress/theme/index.js.vitepress/theme/index.ts 文件 (即“主题入口文件”) 来启用自定义主题:

sh
.
├─ docs                # 项目根目录
│  ├─ .vitepress
│  │  ├─ theme
│  │  │  └─ index.js   # 主题入口
│  │  └─ config.js     # 配置文件
│  └─ index.md
└─ package.json

当检测到存在主题入口文件时,VitePress 总会使用自定义主题而不是默认主题

主题接口

VitePress 自定义主题是一个对象,该对象具有如下接口:

ts
interface Theme {
  /**
   * 每个页面的根布局组件
   * @required
   */
  Layout: Component
  /**
   * 增强 Vue 应用实例
   * @optional
   */
  enhanceApp?: (ctx: EnhanceAppContext) => Awaitable<void>
  /**
   * 扩展另一个主题,在我们的主题之前调用它的 `enhanceApp`
   * @optional
   */
  extends?: Theme
}

interface EnhanceAppContext {
  app: App // Vue 应用实例
  router: Router // VitePress 路由实例
  siteData: Ref<SiteData> // 站点级元数据
}

主题入口文件需要将主题作为默认导出来导出:

ts
// .vitepress/theme/index.js

// 可以直接在主题入口导入 Vue 文件。VitePress 已预先配置 @vitejs/plugin-vue
import Layout from './Layout.vue'

export default {
  Layout,
  enhanceApp({ app, router, siteData }) {
    // ...
  }
}

默认导出是自定义主题的唯一方式,并且只有 Layout 属性是必须的。所以从技术上讲,一个 VitePress 主题可以是一个单独的 Vue 组件。在组件内部,它的工作方式就像是一个普通的 Vite + Vue 3 应用。注意:主题还需要保证 SSR 兼容

构建布局

最基本的布局组件需要包含一个 content 组件:

html
<!-- .vitepress/theme/Layout.vue -->
<template>
  <h1>Custom Layout!</h1>

  <!-- 此处将渲染 markdown 内容 -->
  <Content />
</template>

上面的布局只是将每个页面的 markdown 渲染为 HTML。添加的第一个改进是处理 404 错误:

Vue SFC
<script setup>
import { useData } from 'vitepress'
const { page } = useData()
</script>

<template>
  <h1>Custom Layout!</h1>

  <div v-if="page.isNotFound">
    Custom 404 page!
  </div>
  <Content v-else />
</template>

useData() 提供所有的运行时数据,以便根据不同条件渲染不同的布局。通过利用这个数据,可以让用户单独控制每个页面的布局。例如,用户可以指定一个页面是否使用特殊的主页布局:

markdown
---
layout: home
---

并且可以调整主题进行处理:

html
<script setup>
  import { useData } from 'vitepress'
  const { page, frontmatter } = useData()
</script>

<template>
  <h1>Custom Layout!</h1>

  <div v-if="page.isNotFound">
    Custom 404 page!
  </div>
  <div v-if="frontmatter.layout === 'home'">
    Custom home page!
  </div>
  <Content v-else />
</template>

使用自定义主题

要使用外部主题,请导入它并重新导出:

js
// .vitepress/theme/index.js
import Theme from 'awesome-vitepress-theme'

export default Theme

如果主题需要扩展:

js
// .vitepress/theme/index.js
import Theme from 'awesome-vitepress-theme'

export default {
  extends: Theme,
  enhanceApp(ctx) {
    // ...
  }
}

如果主题需要特殊的 VitePress 配置,也需要在配置中扩展:

ts
// .vitepress/config.ts
import baseConfig from 'awesome-vitepress-theme/config'

export default {
  // 扩展主题的基本配置(如需要)
  extends: baseConfig
}

最后,如果主题为其主题配置提供了类型:

ts
// .vitepress/config.ts
import baseConfig from 'awesome-vitepress-theme/config'
import { defineConfigWithTheme } from 'vitepress'
import type { ThemeConfig } from 'awesome-vitepress-theme'

export default defineConfigWithTheme<ThemeConfig>({
  extends: baseConfig,
  themeConfig: {
    // 类型为 `ThemeConfig`
  }
})

扩展默认主题

VitePress 默认的主题已经针对文档进行了优化,并且可以进行自定义

但是有一些情况仅靠配置是不够的。例如:

  1. 需要调整 CSS 样式
  2. 需要修改 Vue 应用实例,例如注册全局组件
  3. 需要通过 layout 插槽将自定义内容注入到主题中

这些高级自定义配置将需要使用自定义主题来“拓展”默认主题

自定义 CSS

可以通过覆盖根级别的 CSS 变量来自定义默认主题的 CSS:

js
// .vitepress/theme/index.js
import DefaultTheme from 'vitepress/theme'
import './custom.css'

export default DefaultTheme

custom.css 文件

css
/* .vitepress/theme/custom.css */
:root {
  --vp-c-brand-1: #646cff;
  --vp-c-brand-2: #747bff;
}

查看默认主题 CSS 变量来获取可以被覆盖的变量

布局插槽

默认主题的 <Layout/> 组件有一些插槽,能够被用来在页面的特定位置注入内容。下面这个例子展示了将一个组件注入到 outline 之前:

js
// .vitepress/theme/index.js
import DefaultTheme from 'vitepress/theme'
import MyLayout from './MyLayout.vue'

export default {
  extends: DefaultTheme,
  // 使用注入插槽的包装组件覆盖 Layout
  Layout: MyLayout
}
html
<!--.vitepress/theme/MyLayout.vue-->
<script setup>
import DefaultTheme from 'vitepress/theme'

const { Layout } = DefaultTheme
</script>

<template>
  <Layout>
    <template #aside-outline-before>
      My custom sidebar top content
    </template>
  </Layout>
</template>

也可以使用渲染函数。

js
// .vitepress/theme/index.js
import { h } from 'vue'
import DefaultTheme from 'vitepress/theme'
import MyComponent from './MyComponent.vue'

export default {
  extends: DefaultTheme,
  Layout() {
    return h(DefaultTheme.Layout, null, {
      'aside-outline-before': () => h(MyComponent)
    })
  }
}

默认主题布局的全部可用插槽如下:

layout: 'doc' (默认) 在 frontmatter 中被启用时:

  • doc-top
  • doc-bottom
  • doc-footer-before
  • doc-before
  • doc-after
  • sidebar-nav-before
  • sidebar-nav-after
  • aside-top
  • aside-bottom
  • aside-outline-before
  • aside-outline-after
  • aside-ads-before
  • aside-ads-after

layout: 'home' 在 frontmatter 中被启用时:

  • home-hero-before

  • home-hero-info-before

  • home-hero-info

  • home-hero-info-after

  • home-hero-actions-after

  • home-hero-image

  • home-hero-after

  • home-features-before

  • home-features-after

  • layout: 'page' 在 frontmatter 中被启用时:

    • page-top
    • page-bottom
  • 当未找到页面 (404) 时:

    • not-found
  • 总是启用:

    • layout-top
    • layout-bottom
    • nav-bar-title-before
    • nav-bar-title-after
    • nav-bar-content-before
    • nav-bar-content-after
    • nav-screen-content-before
    • nav-screen-content-after

重写内部组件

可以使用 Vite 的 aliases 来用自定义组件替换默认主题的组件:

ts
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vitepress'

export default defineConfig({
  vite: {
    resolve: {
      alias: [
        {
          find: /^.*\/VPNavBar\.vue$/,
          replacement: fileURLToPath(
            new URL('./components/CustomNavBar.vue', import.meta.url)
          )
        }
      ]
    }
  }
})

构建时数据加载

VitePress 提供数据加载的功能,它允许加载任意数据并从页面或组件中导入它。数据加载只在构建时执行:最终的数据将被序列化为 JavaScript 包中的 JSON。数据加载可以被用于获取远程数据,也可以基于本地文件生成元数据

用于数据加载的文件必须以 .data.js.data.ts 结尾。该文件应该提供一个默认导出的对象,该对象具有 load() 方法:

JavaScript
export default {
  load() {
    return {
      hello: 'world'
    }
  }
}

数据加载模块只在 Node.js 中执行,因此可以按需导入 Node API 和 npm 依赖。在 .md 页面和 .vue 组件中使用 data 具名导出从该文件中导入数据:

Vue SFC
<script setup>
import { data } from './example.data.js'
</script>

<pre>{{ data }}</pre>

VitePress 在后台调用 load() 方法,并通过名为 data 的具名导出隐式地暴露了结果。获取数据的方式也可以是异步的:

JavaScript
export default {
  async load() {
    // 获取远程数据
    return (await fetch('...')).json()
  }
}

本地文件生成数据

当需要基于本地文件生成数据时,需要在 data loader 中使用 watch 选项,以便这些文件改动时可以触发热更新。下面的例子展示使用 csv-parse 加载 CSV 文件并将其转换为 JSON。因为此文件仅在构建时执行,因此不会将 CSV 解析器发送到客户端

JavaScript
import fs from 'node:fs'
import { parse } from 'csv-parse/sync'

export default {
  watch: ['./data/*.csv'],
  load(watchedFiles) {
    // watchFiles 是一个所匹配文件的绝对路径的数组。
    // 生成一个博客文章元数据数组
    // 可用于在主题布局中呈现列表。
    return watchedFiles.map((file) => {
      return parse(fs.readFileSync(file, 'utf-8'), {
        columns: true,
        skip_empty_lines: true
      })
    })
  }
}

createContentLoader

当构建一个内容为主的站点时,经常需要创建一个“归档”或“索引”页面:可以列出内容中的所有可用条目的页面,例如博客文章或 API 页面。可以直接使用数据加载 API 实现这一点,但由于这会经常使用,VitePress 还提供一个 createContentLoader 辅助函数来简化这个过程:

JavaScript
import { createContentLoader } from 'vitepress'

export default createContentLoader('posts/*.md', /* options */)

该辅助函数接受一个相对于源目录的 glob 模式,并返回一个 { watch, load } 数据加载对象,该对象可以用作数据加载文件中的默认导出。它还基于文件修改时间戳实现了缓存以提高开发性能。

请注意,数据加载仅适用于 Markdown 文件——匹配的非 Markdown 文件将被跳过。加载的数据将是一个类型为 ContentData[] 的数组:

JavaScript
interface ContentData {
  // 页面的映射 URL,如 /posts/hello.html(不包括 base)
  // 手动迭代或使用自定义 `transform` 来标准化路径
  url: string
  // 页面的 frontmatter 数据
  frontmatter: Record<string, any>

  // 只有启用了相关选项,才会出现以下内容
  // 我们将在下面讨论它们
  src: string | undefined
  html: string | undefined
  excerpt: string | undefined
}

默认情况下只提供 urlfrontmatter。这是因为加载的数据将作为 JSON 内联在客户端 bundle 中,我们需要谨慎考虑其大小。下面的例子展示使用数据构建最小的博客索引页面:

Vue SFC
<script setup>
import { data as posts } from './posts.data.js'
</script>

<template>
  <h1>All Blog Posts</h1>
  <ul>
    <li v-for="post of posts">
      <a :href="post.url">{{ post.frontmatter.title }}</a>
      <span>by {{ post.frontmatter.author }}</span>
    </li>
  </ul>
</template>

选项

默认数据可能不适合所有需求——可以选择使用选项转换数据:

JavaScript
import { createContentLoader } from 'vitepress'

export default createContentLoader('posts/*.md', {
  includeSrc: true, // 包含原始 markdown 源?
  render: true,     // 包含渲染的整页 HTML?
  excerpt: true,    // 包含摘录?
  transform(rawData) {
    // 根据需要对原始数据进行 map、sort 或 filter
    // 最终的结果是将发送给客户端的内容
    return rawData.sort((a, b) => {
      return +new Date(b.frontmatter.date) - +new Date(a.frontmatter.date)
    }).map((page) => {
      page.src     // 原始 markdown 源
      page.html    // 渲染的整页 HTML
      page.excerpt // 渲染的摘录 HTML(第一个 `---` 上面的内容)
      return {/* ... */}
    })
  }
})

使用示例:

JavaScript
import { createContentLoader } from 'vitepress'

interface Post {
  title: string
  url: string
  date: {
    time: number
    string: string
  }
  excerpt: string | undefined
}

declare const data: Post[]
export { data }

export default createContentLoader('posts/*.md', {
  excerpt: true,
  transform(raw): Post[] {
    return raw
      .map(({ url, frontmatter, excerpt }) => ({
        title: frontmatter.title,
        url,
        excerpt,
        date: formatDate(frontmatter.date)
      }))
      .sort((a, b) => b.date.time - a.date.time)
  }
})

function formatDate(raw: string): Post['date'] {
  const date = new Date(raw)
  date.setUTCHours(12)
  return {
    time: +date,
    string: date.toLocaleDateString('en-US', {
      year: 'numeric',
      month: 'long',
      day: 'numeric'
    })
  }
}

为 data loader 导出类型

当使用 TypeScript 时,可以像这样为 loader 和 data 导出类型:

JavaScript
import { defineLoader } from 'vitepress'

export interface Data {
  // data 类型
}

declare const data: Data
export { data }

export default defineLoader({
  // 类型检查加载器选项
  watch: ['...'],
  async load(): Promise<Data> {
    // ...
  }
})

配置

要获取 data loader 中的配置信息,可以使用如下代码:

JavaScript
import type { SiteConfig } from 'vitepress'

const config: SiteConfig = (globalThis as any).VITEPRESS_CONFIG