Skip to content
山不让尘,川不辞盈
搜索 K
Main Navigation 主页闲聊关于
实用网站
工具网站
Excel工具

Appearance

Sidebar Navigation

pages

闲聊

关于我

tools

ExcelTools

recommendation

工具网页

实用网页

2025

五一长沙游玩攻略

2024

Coze Agent 调研

Git 多仓库提交用户信息动态设置

vitepress博客搭建

Vue3 + vite 实用依赖与配置

解决nvm use无效问题

2023

Flutter windows 环境配置

Electron windows robotjs 安装教程

Electron 控制屏幕亮度

现阶段AI建站工具简析

2022

Echarts-dataZoom在windows无法触屏拖动问题

Vue全局错误监测

Electron前端开启局域网接口

快速搭建Electron桌面应用

Node.js 笔记

el-table懒加载局部更新

uniapp 小程序自定义顶部标题

JS 代码编写优化

十个常用工具库

前端常用的响应式布局方法

Vue 面试知识点

JavaScript 手写方法

JS音频的总时长 NaN || Infinity 问题

B站前端路线资源整合

Vue 组件间通信的6种方式

TypeScript 基础知识点笔记

2021

JavaScript 面试知识点

LeeCode 笔记

axios简单封装

Vue3 笔记

Vue2 笔记

JavaScript 笔记

Git 常用命令

JavaScript DOM 常用知识点

vscode 插件推荐

ES6模块化

请求与跨域

CSS 图片处理技巧

文章目录

vitepress博客搭建 ​

阅读时间:

29

分钟
文章字数:

6.5k

字
发布日期:

2024-11-12

最近更新:

2025-04-27

vitepress
vuepress

前言 ​

服务器过期快一年了,博客也快一年没更新了,最近重新搭建了一下博客,记录一下搭建过程。

以前的博客是使用vuepress搭建的,这次换成了vitepress,vitepress是vuepress的下一代,使用vite构建,性能更好,体验更好

缺点:vitepress的插件生态还没有vuepress那么丰富,很多功能需要自己实现

优点:vitepress可配置项、api都比较多,大部分功能都能实现

相关链接

vuepress博客

vuepress仓库地址

vitepress博客

vitepress仓库地址

vitepress-blog-template

旧版本图片预览:

vuepress
vuepress
vuepress

博客介绍 ​

  • 自定义首页
  • 网站加载页
  • 全文搜索
  • 全文图片放大
  • 网站访问量统计
  • GitHub评论系统
  • 自动配置侧边栏
  • 自动打包部署GitHub Pages
  • 自动统计文章字数/阅读时间/最近更新时间
  • 未完待续......

1. 安装 ​

vitepress官方文档

Node.js 18 及以上版本,推荐使用pnpm安装

sh
# 初始化项目并安装 VitePress 依赖
mkdir blog
cd blog
npm init -y
npm install vitepress --save-dev

# 使用 VitePress CLI 初始化目录结构
npx vitepress init
sh
# 初始化项目并安装 VitePress 依赖
mkdir blog
cd blog
yarn init -y
yarn add -D vitepress

# 使用 VitePress CLI 初始化目录结构
yarn vitepress init
sh
# 创建项目并安装 VitePress 依赖
mkdir blog
cd blog
pnpm init
pnpm add -D vitepress

# 使用 VitePress CLI 初始化目录结构
pnpm vitepress init
sh
# 创建项目并安装 VitePress 依赖
mkdir blog
cd blog
bun init
bun add vitepress --dev

# 使用 VitePress CLI 初始化目录结构
bun vitepress init
sh
┌ Welcome to VitePress!
│
◇ Where should VitePress initialize the config?
│ ./docs
│
◇ Site title:
│ My Awesome Project
│
◇ Site description:
│ A VitePress Site
│
◇ Theme:
│ ● Default Theme (Out of the box, good-looking docs)
│ ○ Default Theme + Customization
│ ○ Custom Theme
│
◇ Use TypeScript for config and theme files?
│  Yes
│
◆ Add VitePress npm scripts to package.json?
│  Yes
└

2. 运行 ​

sh
npm run docs:dev
sh
yarn docs:dev
sh
pnpm docs:dev
sh
bun run docs:dev

3. 结构 ​

官方文档:vitepress目录结构

需要手动新建文件夹,我的目录结构如下:

.
├─ .github                # 配置GitHub Actions
├─ docs
│  ├─ .vitepress
│  │  ├─ components       # 自定义组件
│  │  ├─ plugins          # 自定义插件
│  │  ├─ theme            # 主题配置
│  │  ├─ utils            # 工具函数
│  │  └─ config.mts       # 配置文件
│  ├─ 2024
│  │  └─ xx.md            # 文章
│  ├─ img                 # 文章图片
│  ├─ pages               # 自定义页面
│  ├─ public              # 静态资源
│  └─ index.md            # 首页
└─ package.json

4. 导航栏 ​

配置文件:/docs/.vitepress/config.mts

4.1 标题 ​

官方文档:vitepress站点标题和图标

ts
export default defineConfig({
  title:'山不让尘,川不辞盈',
  // ...
})

4.2 搜索 ​

官方文档:vitepress搜索

有多种方式可以实现,我采用的是 vitepress-plugin-pagefind 插件

该插件支持i18n,具体配置请查看文档

sh
npm i vitepress-plugin-pagefind pagefind
sh
yarn add vitepress-plugin-pagefind pagefind
sh
pnpm add vitepress-plugin-pagefind pagefind
sh
bun add vitepress-plugin-pagefind pagefind
ts
import { pagefindPlugin } from 'vitepress-plugin-pagefind'

export default defineConfig({
  title:'山不让尘,川不辞盈',
  vite:{
    plugins:[
     pagefindPlugin({
        btnPlaceholder: '搜索',
        placeholder: '搜索文档',
        emptyText: '空空如也',
        heading: '共: {{searchResult}} 条结果',
        customSearchQuery(input) {
          return input
            .replace(/[\u4E00-\u9FA5]/g, ' $& ')
            .replace(/\s+/g, ' ')
            .trim()
        },
      }),
    ]
  }
  // ...
})

4.3 导航链接 ​

官方文档:vitepress导航链接

配置中的link是md文件的地址,比如:/pages/about 对应 docs/pages/about.md

ts
export default defineConfig({
  title:'山不让尘,川不辞盈',
  themeConfig:{
   nav: [
      { text: '主页', link: '/' },
      { text: '闲聊', link: '/pages/comment' },
      { text: '关于', link: '/pages/about' },
      {
        text: '推荐',
        items: [
          {
            items: [
              { text: '实用网页', link: '/pages/webPage' },
              { text: '工具插件', link: '/pages/tools' },
            ],
          },
        ],
      },
    ],
  }
  // ...
})

4.4 社交链接 ​

官方文档:vitepress社交链接

ts
export default defineConfig({
  themeConfig: {
    socialLinks: [
      { icon: 'github', link: 'https://github.com/vuejs/vitepress' },
      { icon: 'twitter', link: '...' },
      // 可以通过将 SVG 作为字符串传递来添加自定义图标:
      {
        icon: {
          svg: '<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Dribbble</title><path d="M12...6.38z"/></svg>',
        },
        link: '...',
        // 也可以为无障碍添加一个自定义标签 (可选但推荐):
        ariaLabel: 'cool link',
      },
    ],
  },
})

4.5 效果 ​

配置完后效果图:

效果图

5. 布局框架 ​

5.1 Naive UI ​

Naive UI 文档

(1)安装 ​

图标库:xicons material

时间库:dayjs

sh
npm install -D @css-render/vue3-ssr naive-ui @vicons/material dayjs
sh
yarn add -D @css-render/vue3-ssr naive-ui @vicons/material dayjs
sh
pnpm add -D @css-render/vue3-ssr naive-ui @vicons/material dayjs
sh
bun add -d @css-render/vue3-ssr naive-ui @vicons/material dayjs

(2)配置 ​

新建 .vitepress/theme/index.ts 文件

ts
import { defineComponent, h, inject } from 'vue'
import DefaultTheme from 'vitepress/theme'
import { NConfigProvider } from 'naive-ui'
import { setup } from '@css-render/vue3-ssr'
import { useRoute } from 'vitepress'

const { Layout } = DefaultTheme

const CssRenderStyle = defineComponent({
  setup() {
    const collect = inject<() => string>('css-render-collect')
    return {
      style: collect ? collect() : '',
    }
  },
  render() {
    return h('css-render-style', {
      innerHTML: this.style,
    })
  },
})

const VitepressPath = defineComponent({
  setup() {
    const route = useRoute()
    return () => {
      return h('vitepress-path', null, [route.path])
    }
  },
})

const NaiveUIProvider = defineComponent({
  render() {
    return h(
      NConfigProvider,
      { abstract: true, inlineThemeDisabled: true },
      {
        default: () => [
          h(Layout, null, { default: this.$slots.default?.() }),
          import.meta.env.SSR ? [h(CssRenderStyle), h(VitepressPath)] : null,
        ],
      }
    )
  },
})

export default {
  extends: DefaultTheme,
  Layout: NaiveUIProvider,
  enhanceApp: ({ app }) => {
    if (import.meta.env.SSR) {
      const { collect } = setup(app)
      app.provide('css-render-collect', collect)
    }
  },
}

.vitepress/config.mts 文件

ts
import { defineConfig } from 'vitepress'

const fileAndStyles: Record<string, string> = {}

export default defineConfig({
  // ...
  vite: {
    ssr: {
      noExternal: ['naive-ui', 'date-fns', 'vueuc'],
    },
  },
  postRender(context) {
    const styleRegex = /<css-render-style>((.|\s)+)<\/css-render-style>/
    const vitepressPathRegex = /<vitepress-path>(.+)<\/vitepress-path>/
    const style = styleRegex.exec(context.content)?.[1]
    const vitepressPath = vitepressPathRegex.exec(context.content)?.[1]
    if (vitepressPath && style) {
      fileAndStyles[vitepressPath] = style
    }
    context.content = context.content.replace(styleRegex, '')
    context.content = context.content.replace(vitepressPathRegex, '')
  },
  transformHtml(code, id) {
    const html = id.split('/').pop()
    if (!html) return
    const style = fileAndStyles[`/${html}`]
    if (style) {
      return code.replace(/<\/head>/, `${style}</head>`)
    }
  },
  // ...
})

(3)解决ts报错 ​

安装vite

sh
npm install -D vite vue
sh
yarn add -D vite vue
sh
pnpm add -D vite vue
sh
bun add -D vite vue

根目录下新建 type.d.ts 文件

ts
/// <reference types="vite/client" />

interface ImportMetaEnv {}

interface ImportMeta {
  readonly env: ImportMetaEnv
}

(5)测试 ​

在 docs/index.md 文件中测试

js
<script setup>
import { NButton } from 'naive-ui'
</script>

<NButton>Hello World</NButton>

按钮正常出现则配置完成

效果图

5.2 Sass ​

此项为选配,按需安装

(1)安装 ​

sh
npm install -D sass
sh
yarn add -D sass
sh
pnpm add -D sass
sh
bun add -D sass

(2)忽视告警 ​

安装sass会出现此告警,目前没有发现什么问题

WARNING

Deprecation Warning: The legacy JS API is deprecated and will be removed in Dart Sass 2.0.0.

More info: https://sass-lang.com/d/legacy-js-api

.vitepress/config.mts

ts
import { defineConfig } from 'vite'

export default defineConfig({
  // ...
  vite:{
    css: {
     preprocessorOptions: {
       scss: {
          api: 'modern-compiler', // or 'modern'
        },
      },
    },
  }
  // ...
})

6. 首页 ​

默认首页是docs/index.md

6.1 自定义组件 ​

由于想自己写首页样式,所以仅保留 layout: home

新建 docs/.vitepress/components/ArticleList.vue 文件

vue
<script setup lang="ts"></script>

<template>
  <div class="artical-list"></div>
</template>

<style scoped lang="scss">
.artical-list {
  width: 100%;
  height: 100%;
  color: var(--black-color-1);
}
</style>

修改 docs/index.md 文件

md
---
layout: home
---

<script setup>
import ArticleList from './.vitepress/components/ArticleList.vue'
</script>

<ArticleList />

此时首页会白屏,因为自定义组件没有内容

效果图

6.2 设置文章信息 ​

首先得了解 vitepress frontmatter . 文章可以在顶部自定义信息,如标题、描述、作者、标签、时间等,自定义信息后,可使用各种api访问

  • 在 docs文件夹下新建 2023、2024 、pages 文件夹,移入示例文章

    此项目的是以文件夹的名称按年份排序,个人习惯,可根据个人需求调整

    文件夹

  • frontmatter 可自定义key,我的配置如下(每个文章都需要配置):

    md
    ---
    title: vitepress博客搭建
    date: 2024-11-12
    info: 个人博客技术栈更新,快速搭建一个vitepress自定义博客
    tags:
      - vitepress
      - vuepress
    ---
  • 示例md

    next/pre 指的是文章底部的下一篇/上一篇

    sidebar 指的是左侧文章列表

    md
    ---
    title: 关于我
    date: 2024-11-12
    prev: false
    next: false
    ---
    
    # 关于我
    
    这里是关于我页面
    md
    ---
    title: 实用网页
    date: 2024-11-12
    prev: false
    next: false
    sidebar: false
    ---
    
    # 实用网页
    
    这里是实用网页页面

6.3 首页获取文章列表 ​

官方文档:vitepress createcontentloader

VitePress 提供了一个 createContentLoader 辅助函数,可通过它获取到匹配的文章列表信息

  • ESM模式 package.json

    json
    "type": "module"
  • 新建 .vitepress/utils/posts.data.ts 文件

    ts
    import { createContentLoader } from 'vitepress'
    
    export default createContentLoader('../docs/*/*.md' /* options */)
  • 在主页组件中引入并打印

    .vitepress/components/ArticleList.vue 此处用了类型忽略

    ts
    // @ts-ignore
    import { data as posts } from '../utils/posts.data'
    console.log(posts)

6.4 时间线展示文章 ​

此处可以自行设计,我使用的是时间线展示。

  • 首先,需要定义一些暗色和亮色的公共样式

    官方文档:vitepress 自定义css

    新建 .vitepress/theme/styles/global.css & .vitepress/theme/styles/rewrite.css 文件

    此处是区分重写样式和个人自定义的样式,可根据个人需求调整

    css
    @import './rewrite.css';
    
    :root {
      --border-color-1: rgb(242, 243, 245);
      --black-color-1: rgb(60, 60, 67);
      --grey-color-1: rgb(134, 144, 156);
      --grey-color-2: rgb(229, 230, 235);
      --blue-color-1: rgb(22, 93, 255);
      --border-radius: 8px;
      img {
        display: block;
        margin: auto;
        cursor: pointer;
      }
      .vp-doc {
        h2:first-of-type {
          margin: 10px 0 16px;
          padding-top: 15px;
        }
      }
      .VPHome {
        margin-bottom: 23px;
      }
    }
    
    .dark {
      --border-color-1: rgba(255, 255, 255, 0.08);
      --black-color-1: rgba(255, 255, 255, 0.9);
      --grey-color-1: rgba(255, 255, 255, 0.5);
      --grey-color-2: rgb(72, 72, 73);
      --blue-color-1: rgb(60, 126, 255);
    }
    css
    .VPNavBar.home.top {
      border-bottom: 1px solid #f2f3f5;
    }
    
    .dark {
      .VPNavBar.home.top {
        border-bottom: 1px solid #000;
      }
    }
    
    :root {
      .vp-doc .custom-block {
        padding: 8px 16px;
      }
    
      .vp-doc .custom-block :first-child:first-child {
        margin: 8px 0;
      }
    
      .VPMenuGroup > .title {
        font-size: 0.7em;
      }
    
      /* 链接 */
    
      .vp-doc a {
        background: linear-gradient(var(--vp-c-brand-soft), var(--vp-c-brand-soft)) no-repeat center
          bottom / 100% 2px;
        text-decoration: none;
        transition: 0.2s;
      }
    
      .vp-doc a:hover {
        border-radius: 0.2em;
        background: linear-gradient(var(--vp-c-brand-soft), var(--vp-c-brand-soft)) no-repeat center
          bottom / 100% 100%;
      }
    
      .vp-doc strong {
        background: linear-gradient(var(--vp-c-brand-soft), var(--vp-c-brand-soft)) no-repeat center
          bottom / 100% 40%;
      }
    
      .vp-doc s {
        opacity: 0.6;
      }
    
      /* 文章目录hover */
      .VPDocOutlineItem.root > li > a,
      .VPDocOutlineItem.nested > li > a {
        padding-left: 5px;
        padding-right: 5px;
        border-radius: 5px;
      }
      .VPDocOutlineItem.root > li > a:first-child:hover,
      .VPDocOutlineItem.root > li > a:first-child.active,
      .VPDocOutlineItem.nested li > a:hover,
      .VPDocOutlineItem.nested li > a.active {
        background-color: var(--grayA3);
      }
    }
  • 导入样式

    官方文档:vitepress 自定义主题

    新建 .vitepress/theme/index.ts 文件

    ts
    import './styles/global.css'
    // ...
  • 主页设计

    不多解释,放置一个头像 docs/public/assets/avatar.jpg 即可。

    注意:此处深色区域,过滤了 /pages/ 路径下的文章,因为该路径下的文件是作为独立页面展示的,参考博客中的关于我/闲聊。

    vue
    <script setup lang="ts">
    import { NTimeline, NTimelineItem, NIcon, NBackTop, NTag } from 'naive-ui'
    import { useRouter } from 'vitepress'
    import dayjs from 'dayjs'
    import { EmailOutlined, DiscountOutlined } from '@vicons/material'
    // @ts-ignore
    import { data as posts } from '../utils/posts.data'
    const router = useRouter()
    const list = posts
      .filter((item) => !item.url.includes('/pages/'))
      .map((item) => ({
        ...item,
        unixDate: dayjs(item.frontmatter.date).unix(),
      }))
      .sort((a, b) => b.unixDate - a.unixDate)
      .map((item) => {
        const { unixDate, ...rest } = item
        return rest
      })
    const jump = (path: string) => {
      router.go(path)
    }
    </script>
    
    <template>
      <div class="artical-list">
        <section class="left-wrapper">
          <img class="avatar" src="/assets/avatar.jpg" alt="avatar" />
          <p class="name">holden</p>
          <p class="text">快不快乐有天总过去</p>
          <div class="email">
            <NIcon :size="23">
              <EmailOutlined />
            </NIcon>
            holden.lee@aliyun.com
          </div>
        </section>
        <section class="right-wrapper">
          <n-timeline size="large">
            <n-timeline-item v-for="item in list">
              <template #icon>
                <div class="icon">
                  <p>{{ dayjs(item.frontmatter.date).format('YYYY-MM-DD') }}</p>
                  <div class="dot"></div>
                </div>
              </template>
              <template #default>
                <div class="card" @click="jump(item.url)">
                  <div class="title">{{ item.frontmatter.title }}</div>
                  <div class="tags">
                    <n-tag :bordered="false" type="info" v-for="tagItem in item.frontmatter.tags">
                      {{ tagItem }}
                      <template #icon>
                        <n-icon :size="16" :component="DiscountOutlined" />
                      </template>
                    </n-tag>
                  </div>
                  <div class="info">{{ item.frontmatter.info ?? '无简介' }}</div>
                  <div class="date">{{ dayjs(item.frontmatter.date).format('YYYY-MM-DD') }}</div>
                </div>
              </template>
            </n-timeline-item>
          </n-timeline>
        </section>
        <n-back-top :right="10" />
      </div>
    </template>
    
    <style scoped lang="scss">
    .artical-list {
      width: 100%;
      height: 100%;
      color: var(--black-color-1);
      display: flex;
    
      .left-wrapper {
        position: sticky;
        top: 92px;
        margin-top: 3vh;
        border: 1px solid var(--border-color-1);
        width: 250px;
        height: 300px;
        display: flex;
        flex-direction: column;
        align-items: center;
        border-radius: var(--border-radius);
    
        p {
          margin: 0;
        }
    
        .avatar {
          width: 100px;
          border-radius: 100%;
          user-select: none;
          cursor: auto;
          margin: 40px 0 0 0;
        }
    
        .name {
          font-size: 20px;
          margin: 10px 0;
        }
    
        .text {
          font-size: 14px;
          color: var(--grey-color-1);
          user-select: none;
        }
    
        .email {
          width: 100%;
          height: 25px;
          display: flex;
          align-items: center;
          justify-content: center;
          cursor: pointer;
          margin-top: 10px;
        }
      }
    
      .right-wrapper {
        margin-left: 150px;
        margin-top: 3vh;
        width: calc(100% - 250px - 150px);
        min-width: 300px;
    
        :deep(.n-timeline-item-timeline__line) {
          background-color: var(--grey-color-2);
        }
    
        .card {
          cursor: pointer;
        }
    
        .icon {
          width: 6px;
          height: 6px;
          position: relative;
    
          p {
            position: absolute;
            margin: 0;
            width: 130px;
            left: -140px;
            top: -2px;
            font-size: 12px;
            line-height: 12px;
            height: 12px;
            text-align: right;
          }
    
          .dot {
            width: 100%;
            height: 100%;
            border-radius: 100%;
            background-color: var(--blue-color-1);
          }
        }
    
        .card {
          width: 100%;
          min-height: 120px;
          color: var(--black-color-1);
          border: 1px solid var(--border-color-1);
          border-radius: var(--border-radius);
          padding: 15px;
          display: flex;
          flex-direction: column;
          justify-content: space-around;
    
          .title {
            font-size: 20px;
            font-weight: 700;
            cursor: pointer;
          }
    
          .tags {
            width: 100%;
            display: flex;
            flex-wrap: wrap;
    
            .n-tag {
              margin-right: 10px;
            }
          }
    
          .info,
          .date {
            font-size: 14px;
            color: var(--grey-color-1);
            margin-top: 5px;
            overflow: hidden;
            white-space: nowrap;
            text-overflow: ellipsis;
          }
    
          .date {
            display: none;
          }
        }
      }
    }
    
    @media (max-width: 730px) {
      .artical-list {
        flex-direction: column;
    
        .left-wrapper {
          min-width: 300px;
          width: 100%;
          position: static;
        }
    
        .right-wrapper {
          margin-left: 0;
          width: 100%;
    
          .icon {
            p {
              display: none;
            }
          }
        }
    
        .card {
          .date {
            display: block !important;
          }
    
          .info {
            display: none !important;
          }
        }
      }
    }
    </style>
  • 效果图

    图

7. 文章侧边栏 ​

官方文档:vitepress 侧边栏

正常情况下,需要手动配置侧边栏

ts
export default {
  themeConfig: {
    sidebar: [
      {
        text: 'Guide',
        items: [
          { text: 'Introduction', link: '/introduction' },
          { text: 'Getting Started', link: '/getting-started' },
          ...
        ]
      }
    ]
  }
}

7.1 使用插件 ​

插件可以自动生成侧边栏并且根据文章名称日期排序

vitepress-sidebar

sh
npm i -D vitepress-sidebar
sh
yarn add -D vitepress-sidebar
sh
pnpm add -D vitepress-sidebar
sh
bun add -D vitepress-sidebar

.vitepress/config.mts 文件,具体配置请看官方文档

ts
import { generateSidebar } from 'vitepress-sidebar'
// ...

const autoSidebar = () => {
  let result: any = generateSidebar({
    documentRootPath: '/docs',
    collapseDepth: 2,
    useTitleFromFrontmatter: true,
    sortMenusByFrontmatterDate: true,
    sortMenusOrderByDescending: true,
  })
  return result.map((year) => ({
    ...year,
    items: year.items.reverse(),
  }))
}

export default defineConfig({
  // ...
  themeConfig: {
    sidebar: autoSidebar(),
  },
  // ...
})

配置完成后的效果

图

7.2 优化 ​

侧边栏中,会显示docs文件夹下所有的md,包括了我们需要单独显示的pages目录

如果直接在autoSideBar函数中过滤pages目录下的文件,则无法跳转,因此得从页面下手,css隐藏。(如果不需要隐藏该目录的话,以下步骤忽视)

  • 新建 .vitepress/theme/MyLayout.vue 文件

    vue
    <script setup lang="ts">
    import DefaultTheme from 'vitepress/theme'
    import { useRoute } from 'vitepress'
    import { watch, nextTick, onMounted } from 'vue'
    const { Layout } = DefaultTheme
    const route = useRoute()
    
    onMounted(() => {
      hideSpecificSidebarItem()
    })
    
    watch(
      () => route.path,
      (_, oldPath) => {
        if (oldPath === '/') {
          nextTick(() => {
            hideSpecificSidebarItem()
          })
        }
      }
    )
    
    // 隐藏pages
    function hideSpecificSidebarItem() {
      const sidebarItems = document.querySelectorAll(
        '#VPSidebarNav > .group'
      ) as NodeListOf<HTMLElement>
      sidebarItems.forEach((item, index) => {
        const textContent = item.querySelector('.text')?.textContent?.trim()
        if (textContent === 'pages') {
          item.style.display = 'none'
          sidebarItems[index + 1].style.borderTop = 'none'
        }
      })
    }
    </script>
    
    <template>
      <Layout></Layout>
    </template>
    
    <style scoped lang="scss"></style>
  • 修改 .vitepress/theme/index.ts 文件

    ts
    import MyLayout from './MyLayout.vue'
    // ...
    
    const NaiveUIProvider = defineComponent({
      render() {
        return h(
          NConfigProvider,
          { abstract: true, inlineThemeDisabled: true },
          {
            default: () => [
              h(MyLayout, null, { default: this.$slots.default?.() }),
              import.meta.env.SSR ? [h(CssRenderStyle), h(VitepressPath)] : null,
            ],
          }
        )
      },
    })
    
    // ...
  • 效果

    图

8. 文章信息统计 ​

图

  • 新建 .vitepress/utils/getReadingTime.ts 文件

    ts
    export function getWords(content: string): RegExpMatchArray | null {
      // 仅匹配英文单词,忽略标点和纯数字
      return content.match(/\b[a-zA-Z]+(?:['-]?[a-zA-Z]+)?\b/gu)
    }
    
    export function getChinese(content: string): RegExpMatchArray | null {
      // 匹配中文字符
      return content.match(/[\u4E00-\u9FD5]/gu)
    }
    
    export function getEnWordCount(content: string): number {
      // 英文单词数量
      return getWords(content)?.length || 0
    }
    
    export function getCnWordCount(content: string): number {
      // 中文字符数量
      return getChinese(content)?.length || 0
    }
    
    export function getWordNumber(content: string): number {
      // 总字数统计
      const enWordCount = getEnWordCount(content)
      const cnWordCount = getCnWordCount(content)
      return enWordCount + cnWordCount
    }
    
    export function getReadingTime(content: string, cnWordPerMinute = 350, enWordPerMinute = 160) {
      const trimmedContent = content.trim()
      const enWord = getEnWordCount(trimmedContent)
      const cnWord = getCnWordCount(trimmedContent)
    
      const totalWords = enWord + cnWord
      const words = totalWords >= 1000 ? `${Math.round(totalWords / 100) / 10}k` : totalWords
    
      const readingTime = cnWord / cnWordPerMinute + enWord / enWordPerMinute
      const readTime = Math.ceil(readingTime)
    
      return {
        readTime,
        words,
      }
    }
  • 新建 .vitepress/plugins/headerPlugin.ts 文件

    ts
    import { Plugin } from 'vite'
    import { getReadingTime } from '../utils/getReadingTime'
    import fs from 'fs'
    
    export function HeaderPlugin(): Plugin {
      return {
        name: 'header-plugin',
        enforce: 'pre',
        async transform(code, id) {
          if (!id.match(/\.md\b/)) return null
    
          const cleanContent = cleanMarkdownContent(code)
    
          // 获取文件的最近更新时间
          const lastUpdated = getLastUpdatedTime(id)
    
          // 获取阅读时间和字数
          const { readTime, words } = getReadingTime(cleanContent)
    
          // 插入组件到文章中
          code = insertReadingTimeAndWords(
            `<ArticleHeader readTime="${readTime}" words="${words}" lastUpdated="${lastUpdated}" />`,
            code
          )
          return code
        },
      }
    }
    
    // 获取文件的最近更新时间
    function getLastUpdatedTime(filePath: string): string {
      const stats = fs.statSync(filePath)
      const lastModifiedTime = stats.mtime
      return lastModifiedTime.toLocaleString()
    }
    
    // 插入目标字符串到第一个一级标题后
    function insertReadingTimeAndWords(target: string, source: string) {
      const headerRegex = /(^#\s.+$)/m
      return source.replace(headerRegex, `$1\n\n${target}`)
    }
    
    // 去掉 Frontmatter
    function cleanMarkdownContent(content: string): string {
      return content.replace(/^---[\s\S]+?---\n+/g, '').trim()
    }
  • .vitepress/config.mts 文件

    ts
    import { HeaderPlugin } from './plugins/headerPlugin'
    // ...
    export default defineConfig({
      vite: [
        // ...
        HeaderPlugin(),
      ],
    })
  • 新建 .vitepress/components/ArticleHeader.vue 组件

    vue
    <script setup lang="ts">
    import {
      AccessTimeFilled,
      ArticleOutlined,
      BorderColorOutlined,
      UpdateOutlined,
      DiscountOutlined,
    } from '@vicons/material'
    import { NIcon, NTag } from 'naive-ui'
    import { useData } from 'vitepress'
    import dayjs from 'dayjs'
    const { frontmatter } = useData()
    defineProps<{
      readTime: string
      words: string
      lastUpdated: string
    }>()
    </script>
    
    <template>
      <div class="header">
        <section class="info">
          <div class="read">
            <NIcon :size="20">
              <AccessTimeFilled />
            </NIcon>
            阅读时间:
            <p>{{ readTime }}</p>
            分钟
          </div>
          <div class="words">
            <NIcon :size="20">
              <ArticleOutlined />
            </NIcon>
            文章字数:
            <p>{{ words }}</p>
            字
          </div>
          <div class="write">
            <NIcon :size="18">
              <BorderColorOutlined />
            </NIcon>
            发布日期:
            <p>{{ dayjs(frontmatter.date).format('YYYY-MM-DD') }}</p>
          </div>
          <div class="update">
            <NIcon :size="20">
              <UpdateOutlined />
            </NIcon>
            最近更新:
            <p>{{ dayjs(lastUpdated).format('YYYY-MM-DD') }}</p>
          </div>
        </section>
        <section class="tags">
          <n-tag :bordered="false" type="info" v-for="item in frontmatter.tags">
            {{ item }}
            <template #icon>
              <n-icon :size="16" :component="DiscountOutlined" />
            </template>
          </n-tag>
        </section>
      </div>
    </template>
    
    <style scoped lang="scss">
    .header {
      width: 100%;
    
      .info {
        width: 100%;
        display: flex;
        margin-top: 5px;
        margin-bottom: 5px;
        flex-wrap: wrap;
        font-size: 14px;
        color: var(--grey-color-1);
    
        .read,
        .words,
        .write,
        .update {
          display: flex;
          align-items: center;
          justify-content: center;
          margin-right: 8px;
    
          p {
            margin: 0 5px;
          }
    
          i {
            margin-right: 2px;
          }
        }
      }
    
      .tags {
        width: 100%;
        display: flex;
        flex-wrap: wrap;
    
        .n-tag {
          margin-right: 10px;
          margin-bottom: 10px;
        }
      }
    }
    </style>
  • 配置全局组件

    .vitepress/theme/index.ts

    ts
    import ArticleHeader from '../components/ArticleHeader.vue'
    // ...
    export default {
      extends: DefaultTheme,
      Layout: NaiveUIProvider,
      enhanceApp: ({ app }) => {
        import ArticleHeader from '../components/ArticleHeader.vue'
        if (import.meta.env.SSR) {
          const { collect } = setup(app)
          app.provide('css-render-collect', collect)
        }
      },
    }
  • 效果

    图

9. 评论插件 ​

我使用的是:@giscus/vue,无跟踪,无广告,永久免费,github邮箱通知,支持暗色切换。

9.1 安装配置 ​

  • 新建一个 公开 仓库,打开仓库 Settings,勾选 Discussions,开启评论区

    私有仓库的话访客无法查看讨论

    图

  • GitHub 安装 giscus

    点击此处安装

    图

  • giscus 配置

    安装完毕后,点击 Configure 配置 giscus,选中刚刚创建的仓库,点击保存

    图

  • 项目中安装

    sh
    npm i -D @giscus/vue
    sh
    yarn add -D @giscus/vue
    sh
    pnpm add -D @giscus/vue
    sh
    bun add -D @giscus/vue

9.2 获取设置 ​

  • 去官方文档获取设置

    点击这里去获取

  • 填写自己的仓库信息

    图

  • 滚到到下边,获取设置

    图

9.3 使用 ​

利用默认布局组件 Layout 的 doc-after 插槽将 giscus 组件放入页面中

官方文档:vitepress 布局插槽

.vitepress/theme/MyLayout.vue 文件

vue
<script setup lang="ts">
import Giscus from '@giscus/vue'
import { useRoute,useData } from "vitepress";
const { page } = useData()
// ...
</script>
<template>
  <Layout>
     <template #doc-after>
        <div style="margin-top: 24px">
          <Giscus
            :key="page.filePath"
            repo="lee-holden/vitepress-blog-template"
            repo-id="R_kgDONRAkeA"
            category="Announcements"
            category-id="IC_kwDONRAkeM4CkXRA"
            mapping="title"
            strict="0"
            reactions-enabled="1"
            emit-metadata="0"
            input-position="top"
            lang="zh-CN"
            crossorigin="anonymous"
          />
        </div>
    </template>
  </Layout>
</template>

<style scoped lang="scss"></style>
  • 效果

    图

9.4 优化 ​

尝试切换亮/暗样式会发现评论组件不会跟随切换,这需要与 giscus 通信实现。

giscus 可以通过 message 与 giscus iframe 通信,所以我们在切换样式时通知 giscus 同步切换即可,恰好vitepress提供了 isDark 数据,我们可以监听它进行切换

官方文档:vitepress useData

官方文档:giscus-to-parent-message-events

.vitepress/theme/MyLayout.vue 文件

vue
<script setup lang="ts">
const { page, isDark } = useData()
import { useRoute, useData, inBrowser } from 'vitepress'

watch(isDark, (dark) => {
  if (!inBrowser) return

  const iframe = document.querySelector('giscus-widget')?.shadowRoot?.querySelector('iframe')

  iframe?.contentWindow?.postMessage(
    { giscus: { setConfig: { theme: dark ? 'dark' : 'light' } } },
    'https://giscus.app'
  )
})

// ...
</script>

<template>
  <Layout>
    <template #doc-after>
      <div style="margin-top: 24px">
        <Giscus
          :key="page.filePath"
          repo="lee-holden/vitepress-blog-template"
          repo-id="R_kgDONRAkeA"
          category="Announcements"
          category-id="IC_kwDONRAkeM4CkXRA"
          mapping="title"
          strict="0"
          reactions-enabled="1"
          emit-metadata="0"
          input-position="top"
          :theme="isDark ? 'dark' : 'light'"
          lang="zh-CN"
          crossorigin="anonymous"
        />
      </div>
    </template>
  </Layout>
</template>

<style scoped lang="scss"></style>
  • 效果

    图

10. 项目配置 ​

10.1 prettier ​

  • 安装vscode拓展:Prettier - Code formatter

  • 安装prettier库

    sh
    npm i -D prettier
    sh
    yarn add -D prettier
    sh
    pnpm add -D prettier
    sh
    bun add -D prettier
  • 项目根目录,新建 .prettierrc 文件

    json
    {
      "printWidth": 100,
      "tabWidth": 2,
      "useTabs": false,
      "semi": false,
      "singleQuote": true,
      "quoteProps": "as-needed",
      "jsxSingleQuote": false,
      "trailingComma": "es5",
      "bracketSpacing": true,
      "jsxBracketSameLine": false,
      "arrowParens": "always",
      "proseWrap": "preserve",
      "htmlWhitespaceSensitivity": "css",
      "endOfLine": "lf"
    }
  • 项目根目录,新建 .vscode/settings.json

    json
    {
      "editor.defaultFormatter": "esbenp.prettier-vscode",
      "editor.formatOnSave": true,
      "[javascript]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
      },
      "[typescript]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
      },
      "[json]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
      },
      "[html]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
      },
      "[css]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
      },
      "[scss]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
      },
      "[vue]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
      },
      "prettier.configPath": "./.prettierrc"
    }
  • 项目根目录,新建 .prettierignore 文件

    json
    cache
    node_modules
    dist
    temp
    public
    !docs
  • 格式化全部文件

    sh
    npx prettier --write .
    sh
    yarn prettier --write .
    sh
    pnpm prettier --write .
    sh
    bun prettier --write .

10.2 git ​

  • 项目根目录,新建 .gitignore 文件

    json
    node_modules
    .temp
    docs/.vitepress/cache
    dist
    cache
    .eslintcache
    components.d.ts
    .env.local
    .env.\*.local
    npm-debug.log*
    yarn-debug.log*
    yarn-error.log*
    pnpm-debug.log*
    meta.json
  • 配置git仓库并且推送

    sh
    git init
    git add .
    git commit -m 'first commit'
    git remote add origin git@github.com:lee-holden/vitepress-blog-template.git
    git push -u origin master

11. 自动部署GitHub Pages ​

官方文档:vitepress 部署

  • 设置根目录

    官方文档:vitepress 根目录设置

    如果你使用的是 Github 页面并部署到 user.github.io/repo/,请将 base 设置为 /repo/。

    .vitepress/config.mts 文件

    ts
    // ...
    export default defineConfig({
      base: '/vitepress-blog-template/', // 替换成你的仓库名称
      // ...
    })

    .vitepress/components/ArticleList.vue

    vue
    <script setup lang="ts">
    // ...
    const jump = (path: string) => {
      router.go('vitepress-blog-template' + path)
    }
    </script>
    
    // ...
  • 开启GitHub Pages 功能

    图

  • 项目根目录,新建 .github/workflows/deploy.yml

    yml
    # 构建 VitePress 站点并将其部署到 GitHub Pages 的示例工作流程
    #
    name: Deploy VitePress site to Pages
    
    on:
      # 在针对 `main` 分支的推送上运行。如果你
      # 使用 `master` 分支作为默认分支,请将其更改为 `master`
      push:
        branches: [main]
    
      # 允许你从 Actions 选项卡手动运行此工作流程
      workflow_dispatch:
    
    # 设置 GITHUB_TOKEN 的权限,以允许部署到 GitHub Pages
    permissions:
      contents: read
      pages: write
      id-token: write
    
    # 只允许同时进行一次部署,跳过正在运行和最新队列之间的运行队列
    # 但是,不要取消正在进行的运行,因为我们希望允许这些生产部署完成
    concurrency:
      group: pages
      cancel-in-progress: false
    
    jobs:
      # 构建工作
      build:
        runs-on: ubuntu-latest
        steps:
          - name: Checkout
            uses: actions/checkout@v4
            with:
              fetch-depth: 0 # 如果未启用 lastUpdated,则不需要
          # - uses: pnpm/action-setup@v3 # 如果使用 pnpm,请取消此区域注释
          #   with:
          #     version: 9
          # - uses: oven-sh/setup-bun@v1 # 如果使用 Bun,请取消注释
          - name: Setup Node
            uses: actions/setup-node@v4
            with:
              node-version: 20
              cache: npm # 或 pnpm / yarn
          - name: Setup Pages
            uses: actions/configure-pages@v4
          - name: Install dependencies
            run: npm ci # 或 pnpm install / yarn install / bun install
          - name: Build with VitePress
            run: npm run docs:build # 或 pnpm docs:build / yarn docs:build / bun run docs:build
          - name: Upload artifact
            uses: actions/upload-pages-artifact@v3
            with:
              path: docs/.vitepress/dist
    
      # 部署工作
      deploy:
        environment:
          name: github-pages
          url: ${{ steps.deployment.outputs.page_url }}
        needs: build
        runs-on: ubuntu-latest
        name: Deploy
        steps:
          - name: Deploy to GitHub Pages
            id: deployment
            uses: actions/deploy-pages@v4
    yml
    name: Deploy VitePress site to Pages
    
    on:
      push:
        branches:
          - master
          - main
      workflow_dispatch:
    
    permissions:
      contents: read
      pages: write
      id-token: write
    
    concurrency:
      group: pages
      cancel-in-progress: false
    
    jobs:
      build:
        runs-on: ubuntu-latest
        steps:
          - name: Checkout
            uses: actions/checkout@v4
            with:
              fetch-depth: 1 # 如果启用了 vitepress lastUpdated,则改成 0
          - uses: pnpm/action-setup@v3
            with:
              version: 9
          - name: Setup Node
            uses: actions/setup-node@v4
            with:
              node-version: 20
              cache: pnpm
          - name: Setup Pages
            uses: actions/configure-pages@v4
          - name: Install dependencies
            run: pnpm install
          - name: Build with VitePress
            run: pnpm docs:build
          - name: Upload artifact
            uses: actions/upload-pages-artifact@v3
            with:
              path: docs/.vitepress/dist
    
      # 部署工作
      deploy:
        environment:
          name: github-pages
          url: ${{ steps.deployment.outputs.page_url }}
        needs: build
        runs-on: ubuntu-latest
        name: Deploy
        steps:
          - name: Deploy to GitHub Pages
            id: deployment
            uses: actions/deploy-pages@v4
  • 推送代码到仓库,查看Action

    此处可以看到,Action已经成功运行,点进去可以看到build和部署进程,如果出现报错,可以查看报错信息

    图

    图

    图

  • 部署成功

    vitepress-blog-template

12. 访问统计 ​

用的是 busuanzi

  • 安装

    sh
    pnpm add -D busuanzi.pure.js
    sh
    npm i -D busuanzi.pure.js
    sh
    yarn add -D busuanzi.pure.js
    sh
    bun add -D busuanzi.pure.js
  • .vitepress/theme/index.ts 文件

    ts
    import { inBrowser } from 'vitepress'
    import busuanzi from 'busuanzi.pure.js'
    // ...
    
    export default {
      extends: DefaultTheme,
      Layout: NaiveUIProvider,
      enhanceApp: ({ app, router }) => {
        app.component('ArticleHeader', ArticleHeader)
        if (import.meta.env.SSR) {
          const { collect } = setup(app)
          app.provide('css-render-collect', collect)
        }
        if (inBrowser) {
          router.onAfterRouteChanged = () => {
            busuanzi.fetch()
          }
        }
      },
    }
  • .vitepress/theme/MyLayout.vue 文件

    在网站底部插槽放入,官方文档:vitepress 布局插槽

    vue
    <template>
      <Layout>
        <template #layout-bottom>
          <div class="bottom">
            <div>
              本站总访问量
              <span id="busuanzi_value_site_pv" class="font-bold">--</span> 次 本站访客数
              <span id="busuanzi_value_site_uv" class="font-bold">--</span> 人次
            </div>
            <p>前端狗都不如 © 2021-2024 holden</p>
          </div>
        </template>
      </Layout>
      <!-- ... -->
    </template>
    
    <style lang="scss" scoped>
    .bottom {
      margin-left: 5%;
      width: 90%;
      height: 100px;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      border-top: 1px solid var(--border-color-1);
      text-align: center;
    
      p {
        margin-top: 5px;
      }
    }
    </style>
  • 效果

    可以自行调整底部样式

    图

13. 网站加载 ​

部署Github Pages后发现,白屏时间比较长,此时可以考虑使用加载页

  • 新建 .vitepress/components/Loading.vue 文件

    vue
    <script setup lang="ts"></script>
    
    <template>
      <div class="loading">
        <div class="loader">
          <div v-for="_ in 5"></div>
        </div>
      </div>
    </template>
    
    <style scoped lang="scss">
    $color: #3451b2;
    
    .loading {
      width: 100vw;
      height: 100vh;
      display: flex;
      justify-content: center;
      align-items: center;
    }
    
    .loader {
      position: relative;
    }
    .loader > div:nth-child(2) {
      -webkit-animation: pacman-balls 1s -0.99s infinite linear;
      animation: pacman-balls 1s -0.99s infinite linear;
    }
    .loader > div:nth-child(3) {
      -webkit-animation: pacman-balls 1s -0.66s infinite linear;
      animation: pacman-balls 1s -0.66s infinite linear;
    }
    .loader > div:nth-child(4) {
      -webkit-animation: pacman-balls 1s -0.33s infinite linear;
      animation: pacman-balls 1s -0.33s infinite linear;
    }
    .loader > div:nth-child(5) {
      -webkit-animation: pacman-balls 1s 0s infinite linear;
      animation: pacman-balls 1s 0s infinite linear;
    }
    .loader > div:first-of-type {
      width: 0px;
      height: 0px;
      border-right: 25px solid transparent;
      border-top: 25px solid $color;
      border-left: 25px solid $color;
      border-bottom: 25px solid $color;
      border-radius: 25px;
      -webkit-animation: rotate_pacman_half_up 0.5s 0s infinite;
      animation: rotate_pacman_half_up 0.5s 0s infinite;
      position: relative;
      left: -30px;
    }
    .loader > div:nth-child(2) {
      width: 0px;
      height: 0px;
      border-right: 25px solid transparent;
      border-top: 25px solid $color;
      border-left: 25px solid $color;
      border-bottom: 25px solid $color;
      border-radius: 25px;
      -webkit-animation: rotate_pacman_half_down 0.5s 0s infinite;
      animation: rotate_pacman_half_down 0.5s 0s infinite;
      margin-top: -50px;
      position: relative;
      left: -30px;
    }
    .loader > div:nth-child(3),
    .loader > div:nth-child(4),
    .loader > div:nth-child(5),
    .loader > div:nth-child(6) {
      background-color: $color;
      width: 15px;
      height: 15px;
      border-radius: 100%;
      margin: 2px;
      width: 10px;
      height: 10px;
      position: absolute;
      -webkit-transform: translate(0, -6.25px);
      transform: translate(0, -6.25px);
      top: 25px;
      left: 70px;
    }
    @-webkit-keyframes cube-transition {
      25% {
        -webkit-transform: translateX(50px) scale(0.5) rotate(-90deg);
        transform: translateX(50px) scale(0.5) rotate(-90deg);
      }
      50% {
        -webkit-transform: translate(50px, 50px) rotate(-180deg);
        transform: translate(50px, 50px) rotate(-180deg);
      }
      75% {
        -webkit-transform: translateY(50px) scale(0.5) rotate(-270deg);
        transform: translateY(50px) scale(0.5) rotate(-270deg);
      }
      100% {
        -webkit-transform: rotate(-360deg);
        transform: rotate(-360deg);
      }
    }
    @keyframes cube-transition {
      25% {
        -webkit-transform: translateX(50px) scale(0.5) rotate(-90deg);
        transform: translateX(50px) scale(0.5) rotate(-90deg);
      }
      50% {
        -webkit-transform: translate(50px, 50px) rotate(-180deg);
        transform: translate(50px, 50px) rotate(-180deg);
      }
      75% {
        -webkit-transform: translateY(50px) scale(0.5) rotate(-270deg);
        transform: translateY(50px) scale(0.5) rotate(-270deg);
      }
      100% {
        -webkit-transform: rotate(-360deg);
        transform: rotate(-360deg);
      }
    }
    @-webkit-keyframes pacman-balls {
      75% {
        opacity: 0.7;
      }
      100% {
        -webkit-transform: translate(-100px, -6.25px);
        transform: translate(-100px, -6.25px);
      }
    }
    @keyframes pacman-balls {
      75% {
        opacity: 0.7;
      }
      100% {
        -webkit-transform: translate(-100px, -6.25px);
        transform: translate(-100px, -6.25px);
      }
    }
    @-webkit-keyframes rotate_pacman_half_down {
      0% {
        -webkit-transform: rotate(90deg);
        transform: rotate(90deg);
      }
      50% {
        -webkit-transform: rotate(0deg);
        transform: rotate(0deg);
      }
      100% {
        -webkit-transform: rotate(90deg);
        transform: rotate(90deg);
      }
    }
    @keyframes rotate_pacman_half_down {
      0% {
        -webkit-transform: rotate(90deg);
        transform: rotate(90deg);
      }
      50% {
        -webkit-transform: rotate(0deg);
        transform: rotate(0deg);
      }
      100% {
        -webkit-transform: rotate(90deg);
        transform: rotate(90deg);
      }
    }
    @-webkit-keyframes rotate_pacman_half_up {
      0% {
        -webkit-transform: rotate(270deg);
        transform: rotate(270deg);
      }
      50% {
        -webkit-transform: rotate(360deg);
        transform: rotate(360deg);
      }
      100% {
        -webkit-transform: rotate(270deg);
        transform: rotate(270deg);
      }
    }
    @keyframes rotate_pacman_half_up {
      0% {
        -webkit-transform: rotate(270deg);
        transform: rotate(270deg);
      }
      50% {
        -webkit-transform: rotate(360deg);
        transform: rotate(360deg);
      }
      100% {
        -webkit-transform: rotate(270deg);
        transform: rotate(270deg);
      }
    }
    </style>
  • .vitepress/theme/MyLayout.vue 文件

    vue
    <script setup lang="ts">
    // ...
    import { watch, nextTick, onMounted, ref } from 'vue'
    import Loading from '../components/Loading.vue'
    const loading = ref(true)
    
    onMounted(() => {
      loading.value = false
    })
    
    // ...
    </script>
    
    <template>
      <Loading v-show="loading" />
      <Layout v-show="!loading">
        <!-- ... -->
      </Layout>
    </template>
  • 效果

    loading

14. 图片放大 ​

vitepress文章中,图片点击没有任何效果,可以使用 vitepress-plugin-image-viewer 这个插件

  • 安装

    sh
    # Tip: If you use pnpm to install, you need to install viewerjs additionally.
    pnpm add vitepress-plugin-image-viewer viewerjs
    sh
    npm i vitepress-plugin-image-viewer
    sh
    yarn add vitepress-plugin-image-viewer
  • .vitepress/theme/index.ts 文件

    ts
    // ...
    import 'viewerjs/dist/viewer.min.css'
    import imageViewer from 'vitepress-plugin-image-viewer'
    import vImageViewer from 'vitepress-plugin-image-viewer/lib/vImageViewer.vue'
    
    // ...
    export default {
      extends: DefaultTheme,
      Layout: NaiveUIProvider,
      enhanceApp: ({ app, router }) => {
        app.component('ArticleHeader', ArticleHeader)
        app.component('vImageViewer', vImageViewer)
        if (import.meta.env.SSR) {
          const { collect } = setup(app)
          app.provide('css-render-collect', collect)
        }
        if (inBrowser) {
          router.onAfterRouteChanged = () => {
            busuanzi.fetch()
          }
        }
      },
      setup() {
        const route = useRoute()
        imageViewer(route)
      },
    }
  • 效果 pic

总结 ​

从0创建vitepress博客,一步步来,收获满满。

有什么问题欢迎到评论区咨询,一起交流学习。

Pager
上一篇Git 多仓库提交用户信息动态设置
下一篇Vue3 + vite 实用依赖与配置
本站总访问量 -- 次 本站访客数 -- 人次

前端狗都不如 © 2021-2025 holden