Skip to content

时间线

采用了 @HanochMa/vitepress-markdown-timeline 的项目

Demo:https://hanochma.github.io/daily/2023-04

sh
npm install vitepress-markdown-timeline

config.mts 中注册 markdown 解析插件

js
import timeline from "vitepress-markdown-timeline"; 

export default {
  markdown: { 
    // 行号显示
    lineNumbers: true, 

    // 时间线
    config: (md) => { 
      md.use(timeline); 
    },
  }, 
}

.vitepress/theme/index.ts 中引入时间线样式

说明

如果你没有这个文件,就自己新建

js
// .vitepress/theme/index.ts
import DefaultTheme from 'vitepress/theme'

// 只需添加以下一行代码,引入时间线样式
import "vitepress-markdown-timeline/dist/theme/index.css";

export default {
  extends: DefaultTheme,
}

最后我们在markdown文件中,按格式使用即可

输入:

markdown
::: timeline 2023-04-24
- 一个非常棒的开源项目 H5-Dooring 目前 star 3.1k
  - 开源地址 https://github.com/MrXujiang/h5-Dooring
  - 基本介绍 http://h5.dooring.cn/doc/zh/guide/
- 《深入浅出webpack》 http://webpack.wuhaolin.cn/
:::

::: timeline 2023-04-23
:::

谷歌分析

利用插件 google-analytics ,来查看网站访问量

这里我们用 @ZhongxuYang/vitepress-plugin-google-analytics 的插件

markdown
npm install vitepress-plugin-google-analytics

.vitepress/theme/index.ts 中引入

js
// .vitepress/theme/index.ts
import DefaultTheme from "vitepress/theme"
import googleAnalytics from 'vitepress-plugin-google-analytics'

export default {
  extends: DefaultTheme,
  enhanceApp({app}) {
    googleAnalytics({  
      id: 'G-******', //跟踪ID,在analytics.google.com注册即可
    }),
  },
}

看板娘

第一次接触的人会比较懵,其实就是在右下角有个二次元的人物,类似电子宠物

这里使用 @xinlei3166/vitepress-theme-websiteLive2D 插件

sh
npm install vitepress-theme-website

.vitepress/theme/index.ts 粘贴下面代码并保存

js
// .vitepress/theme/index.ts
import DefaultTheme from 'vitepress/theme'

import { useLive2d } from 'vitepress-theme-website'

export default {
  extends: DefaultTheme,

  setup() {

    // 看板娘
    useLive2d({
      enable: true,
      model: {
        url: 'https://raw.githubusercontent.com/iCharlesZ/vscode-live2d-models/master/model-library/hibiki/hibiki.model.json'
      },
      display: {
        position: 'right',
        width: '135px',
        height: '300px',
        xOffset: '35px',
        yOffset: '5px'
      },
      mobile: {
        show: true
      },
      react: {
        opacity: 0.8
      }
    })

  }
}

想要更换模型在 @iCharlesZ 这里找,替换 model 中的 url 链接即可

js
useLive2d({
  model: {
  url: 'https://raw.githubusercontent.com/iCharlesZ/vscode-live2d-models/master/model-library/bilibili-22/index.json'
  }
})

浏览量

基本上使用的是 不蒜子,免费的且足够好用

sh
npm install busuanzi.pure.js
js
// .vitepress/theme/index.ts
import DefaultTheme from 'vitepress/theme'

import { inBrowser } from 'vitepress'
import busuanzi from 'busuanzi.pure.js'

export default {
  extends: DefaultTheme,

  enhanceApp({ app , router }) { 
    if (inBrowser) {
      router.onAfterRouteChanged = () => {
        busuanzi.fetch()
      }
    }
  },
  
}

使用就很简单了,复制到页面中使用即可

说明

本地开发出现数字即算成功,等你部署后会显示正确的数值

markdown
本站总访问量 <span id="busuanzi_value_site_pv" /> 次
本站访客数 <span id="busuanzi_value_site_uv" /> 人次

样式还可以根据自己需求选择封装

⭐自动侧边栏(插件vitepress-sidebar)

发现一款自动侧边栏,简单好用 @jooy2/vitepress-sidebar

安装文档:https://vitepress-sidebar.jooy2.com/guide/getting-started

sh
npm i -D vitepress-sidebar

configs.mts 中引入配置,可以根据 作者api文档 按需修改

js
// .vitepress/configs.mts
import { generateSidebar } from 'vitepress-sidebar';  

const vitepressSidebarOptions = {
  /* Options... */
};

export default defineConfig({
  themeConfig: {
    sidebar: generateSidebar({ 
      /*
       * For detailed instructions, see the links below:
       * https://vitepress-sidebar.jooy2.com/guide/api
       */
      documentRootPath: '/docs', //文档根目录
      // scanStartPath: null,
      // resolvePath: null,
      // useTitleFromFileHeading: true,
      // useTitleFromFrontmatter: true,
      // frontmatterTitleFieldName: 'title',
      // useFolderTitleFromIndexFile: false, //是否使用层级首页文件名做分级标题
      // useFolderLinkFromIndexFile: false, //是否链接至层级首页文件
      // hyphenToSpace: true,
      // underscoreToSpace: true,
      // capitalizeFirst: false,
      // capitalizeEachWords: false,
      collapsed: false, //折叠组关闭
      collapseDepth: 2, //折叠组2级菜单
      // sortMenusByName: false,
      // sortMenusByFrontmatterOrder: false,
      // sortMenusByFrontmatterDate: false,
      // sortMenusOrderByDescending: false,
      // sortMenusOrderNumericallyFromTitle: false,
      // sortMenusOrderNumericallyFromLink: false,
      // frontmatterOrderDefaultValue: 0,
      // manualSortFileNameByPriority: ['first.md', 'second', 'third.md'], //手动排序,文件夹不用带后缀
      removePrefixAfterOrdering: false, //删除前缀,必须与prefixSeparator一起使用
      prefixSeparator: '.', //删除前缀的符号
      // excludeFiles: ['first.md', 'secret.md'],
      // excludeFilesByFrontmatterFieldName: 'exclude',
      // excludeFolders: ['secret-folder'],
      // includeDotFiles: false,
      // includeRootIndexFile: false,
      // includeFolderIndexFile: false, //是否包含层级主页
      // includeEmptyFolder: false,
      // rootGroupText: 'Contents',
      // rootGroupLink: 'https://github.com/jooy2',
      // rootGroupCollapsed: false,
      // convertSameNameSubFileToGroupIndexPage: false,
      // folderLinkNotIncludesFileName: false,
      // keepMarkdownSyntaxFromTitle: false,
      // debugPrint: false,
    }),
  },
})

为了避免安装插件影响原项目,可以看下面的示例

stackblitz演示:https://stackblitz.com/edit/vite-y1rga7

等待生成后可查看,左侧是目录,右侧是页面

注意:插件在读取目录之后,你再修改文件名,需要重启才能生效

当位于一个页面时,隐藏另一个页面的菜单,可以参考多侧边栏操作方法 | VitePress Sidebar

自动侧边栏(插件vite-plugin-vitepress-auto-nav)

项目地址:https://github.com/Xaviw/vite-plugin-vitepress-auto-nav

教程介绍:https://juejin.cn/post/7283060133646975012

安装

sh
# 使用 ts 时推荐安装 vite,否则会有类型错误
npm i vite-plugin-vitepress-auto-nav vite -D

添加插件

js
// .vitepress/config.ts
import AutoNav from "vite-plugin-vitepress-auto-nav";

export default defineConfig({
  vite: {
    plugins: [
      AutoNav({
        // 自定义配置
        pattern: ["**/!(README|TODO).md"], // 也可以在这里排除不展示的文件,例如不匹配 README 和 TODO 文件
        settings: {
           a: { hide: true }, // 不显示名称为 a 的文件夹或 md 文件
           b: { title: 'bb' }, // 名称为 b 的文件夹或文件在菜单中显示为 bb
           c/b: { sort : 3 }, // 通过路径精确匹配 c 文件夹下的 b 进行配置,排序时位于下标3的位置或最后
           c/b2: { useArticleTitle: false }, // 关闭使用文章一级标题作为文章名称
           d: { collapsed: true }, // 文件夹折叠配置
         },
         compareFn: (a, b) => {
           // 按最新提交时间(没有提交记录时为本地文件修改时间)升序排列
            return (b.options.lastCommitTime || b.options.modifyTime) - (a.options.lastCommitTime || a.options.modifyTime)
          },
          useArticleTitle: true // 全局开启使用文章一级标题作为文章名称
      }),
    ],
  },
});

自动导航和侧边栏(工具)

参考自Notes 的VitePress 自动生成导航和侧边栏

优点:配置根目录后能自动生成导航和侧边栏

弊端:无法自定义导航栏名称

js
/* .vitepress/navSidebarUtil.ts */
import { resolve, join, sep } from 'path'
import { readdirSync, statSync } from 'fs'
import { DefaultTheme } from 'vitepress'

interface SidebarGenerateConfig {
  /**
   * 需要遍历的目录. 默认:articles
   */
  dirName?: string
  /**
   * 忽略的文件名. 默认: index.md
   */
  ignoreFileName?: string
  /**
   * 忽略的文件夹名称. 默认: ['demo','asserts']
   */
  ignoreDirNames?: string[]
}

interface SideBarItem {
  text: string
  collapsible?: boolean
  collapsed?: boolean
  items?: SideBarItem[]
  link?: string
}

interface NavGenerateConfig {
  /**
   * 需要遍历的目录. 默认:articles
   */
  dirName?: string
  /**
   * 最大遍历层级. 默认:1
   */
  maxLevel?: number
}

/**
 * 判断是否为markdown文件
 * @param fileName 文件名
 * @returns 有返回值则表示是markdown文件,否则不是
 */
function isMarkdownFile(fileName: string) {
  return !!fileName.match(/.+\.md$/)
}

// 获取docs目录的完整名称(从根目录一直到docs目录)
const docsDirFullPath = join(__dirname, '../')
// 获取docs目录的完整长度
const docsDirFullPathLen = docsDirFullPath.length

/**
 * 获取dirOrFileFullName中第一个/docs/后的所有内容
 *  如:
 * /a-root/docs/test 则 获取到 /test
 * /a-root-docs/docs/test 则 获取到 /test
 * /a-root-docs/docs/docs/test 则 获取到 /docs/test
 * @param dirOrFileFullName 文件或者目录名
 * @returns
 */
function getDocsDirNameAfterStr(dirOrFileFullName: string) {
  // 使用docsDirFullPathLen采用字符串截取的方式,避免多层目录都叫docs的问题
  return `${sep}${dirOrFileFullName.substring(docsDirFullPathLen)}`
}

export function getSidebarData(sidebarGenerateConfig: SidebarGenerateConfig = {}) {
  const {
    dirName = 'articles',
    ignoreFileName = 'index.md',
    ignoreDirNames = ['demo', 'asserts'],
  } = sidebarGenerateConfig

  // 获取目录的绝对路径
  const dirFullPath = resolve(__dirname, `../${dirName}`)
  const allDirAndFileNameArr = readdirSync(dirFullPath)
  const obj = {}

  allDirAndFileNameArr.map(dirName => {
    let subDirFullName = join(dirFullPath, dirName)

    const property = getDocsDirNameAfterStr(subDirFullName).replace(/\\/g, '/') + '/'
    const arr = getSideBarItemTreeData(subDirFullName, 1, 3, ignoreFileName, ignoreDirNames)

    obj[property] = arr
  })

  return obj
}

function getSideBarItemTreeData(
  dirFullPath: string,
  level: number,
  maxLevel: number,
  ignoreFileName: string,
  ignoreDirNames: string[]
): SideBarItem[] {
  // 获取所有文件名和目录名
  const allDirAndFileNameArr = readdirSync(dirFullPath)
  const result: SideBarItem[] = []
  allDirAndFileNameArr.map((fileOrDirName: string, idx: number) => {
    const fileOrDirFullPath = join(dirFullPath, fileOrDirName)
    const stats = statSync(fileOrDirFullPath)
    if (stats.isDirectory()) {
      if (!ignoreDirNames.includes(fileOrDirName)) {
        const text = fileOrDirName.match(/^[0-9]{2}-.+/) ? fileOrDirName.substring(3) : fileOrDirName
        // 当前为文件夹
        const dirData: SideBarItem = {
          text,
          collapsed: false,
        }
        if (level !== maxLevel) {
          dirData.items = getSideBarItemTreeData(fileOrDirFullPath, level + 1, maxLevel, ignoreFileName, ignoreDirNames)
        }
        if (dirData.items) {
          dirData.collapsible = true
        }
        result.push(dirData)
      }
    } else if (isMarkdownFile(fileOrDirName) && ignoreFileName !== fileOrDirName) {
      // 当前为文件
      const matchResult = fileOrDirName.match(/(.+)\.md/)
      let text = matchResult ? matchResult[1] : fileOrDirName
      text = text.match(/^[0-9]{2}-.+/) ? text.substring(3) : text

      const fileData: SideBarItem = {
        text,
        link: getDocsDirNameAfterStr(fileOrDirFullPath).replace('.md', '').replace(/\\/g, '/'),
      }

      result.push(fileData)
    }
  })

  return result
}

export function getNavData(navGenerateConfig: NavGenerateConfig = {}) {
  const { dirName = 'articles', maxLevel = 2 } = navGenerateConfig
  const dirFullPath = resolve(__dirname, `../${dirName}`)
  const result = getNavDataArr(dirFullPath, 1, maxLevel)

  return result
}

/**
 * 获取顶部导航数据
 *
 * @param   {string}     dirFullPath  当前需要遍历的目录绝对路径
 * @param   {number}     level        当前层级
 * @param   {number[]}   maxLevel     允许遍历的最大层级
 * @return  {NavItem[]}               导航数据数组
 */
function getNavDataArr(dirFullPath: string, level: number, maxLevel: number): DefaultTheme.NavItem[] {
  // 获取所有文件名和目录名
  const allDirAndFileNameArr = readdirSync(dirFullPath)
  const result: DefaultTheme.NavItem[] = []

  allDirAndFileNameArr.map((fileOrDirName: string, idx: number) => {
    const fileOrDirFullPath = join(dirFullPath, fileOrDirName)
    const stats = statSync(fileOrDirFullPath)
    const link = getDocsDirNameAfterStr(fileOrDirFullPath).replace('.md', '').replace(/\\/g, '/')

    const text = fileOrDirName.match(/^[0-9]{2}-.+/) ? fileOrDirName.substring(3) : fileOrDirName

    if (stats.isDirectory()) {
      // 当前为文件夹
      const dirData: any = {
        text,
        link: `${link}/`,
      }

      if (level !== maxLevel) {
        const arr = getNavDataArr(fileOrDirFullPath, level + 1, maxLevel).filter(v => v.text !== 'index.md')
        if (arr.length > 0) {
          // @ts-ignore
          dirData.items = arr
          delete dirData.link
        }
      }

      dirData.activeMatch = link + '/'
      result.push(dirData)
    } else if (isMarkdownFile(fileOrDirName)) {
      // 当前为文件
      const fileData: DefaultTheme.NavItem = {
        text,
        link,
      }
      fileData.activeMatch = link + '/'
      result.push(fileData)
    }
  })

  return result
}

引入到配置中

js
/* .vitepress/config.ts */
import { defineConfig } from 'vitepress'
import { getSidebarData, getNavData } from './navSidebarUtil'

export default defineConfig({
  // ...
  themeConfig: {
    nav: getNavData(),
    sidebar: getSidebarData(),
  },
})

其他侧边栏工具

https://blog.csdn.net/weixin_46463785/article/details/128592038

https://docs.zhengxinonly.com (VitePress 系列教程:自动生成侧边栏 #7)

https://juejin.cn/post/7227358177489961018

代码组图标

使用的是 @yuyinws/vitepress-plugin-group-icons

参照教程安装:https://vpgi.vercel.app/

sh
npm install vitepress-plugin-group-icons

然后在 config.mts 中配置

groupIconMdPlugin 报错?

请备份配置及文件后,重新安装VitePress

js
// .vitepress/config.mts
import { defineConfig } from 'vitepress'
import { groupIconMdPlugin, groupIconVitePlugin } from 'vitepress-plugin-group-icons'

export default defineConfig({

  markdown: {
    config(md) {   
      md.use(groupIconMdPlugin) // 代码组图标
    },
  },

  vite: {   
    plugins: [
      groupIconVitePlugin() // 代码组图标
    ],
  },

})

最后还需要再 index.ts 中引入样式

js
// .vitepress/theme/index.ts
import DefaultTheme from 'vitepress/theme'

import 'virtual:group-icons.css' // 代码组样式

export default {
  extends: DefaultTheme,
}

使用时,请确保代码后有对应的文字触发

markdown
::: code-group
```sh [pnpm]
pnpm -v
```

```sh [yarn]
yarn -v
```

```sh [bun]
bun -v
```
:::

已经内置的常用图标有

js
export const builtInIcons: Record<string, string> = {
  // package manager
  pnpm: 'logos:pnpm',
  npm: 'logos:npm-icon',
  yarn: 'logos:yarn',
  bun: 'logos:bun',
  // framework
  vue: 'logos:vue',
  svelte: 'logos:svelte-icon',
  angular: 'logos:angular-icon',
  react: 'logos:react',
  next: 'logos:nextjs-icon',
  nuxt: 'logos:nuxt-icon',
  solid: 'logos:solidjs-icon',
  // bundler
  rollup: 'logos:rollupjs',
  webpack: 'logos:webpack',
  vite: 'logos:vitejs',
  esbuild: 'logos:esbuild',
}

那么如何自定义呢,我们先在 iconify 中找到中意的图标

说明

  • 本地图标格式:只能使用相对路径
  • 远程图标格式:必须是 logos:***

图标名复制后,可以在 config.mts 中配置

js
// .vitepress/config.mts
import { defineConfig } from 'vitepress'
import { groupIconMdPlugin, groupIconVitePlugin, localIconLoader } from 'vitepress-plugin-group-icons'

export default defineConfig({

  markdown: {
    config(md) {
      md.use(groupIconMdPlugin) //代码组图标
    },
  },

  vite: {
    plugins: [
      groupIconVitePlugin({  
        customIcon: {
          ts: localIconLoader(import.meta.url, '../public/svg/typescript.svg'), //本地ts图标导入
          js: 'logos:javascript', //js图标
          md: 'logos:markdown', //markdown图标
          css: 'logos:css-3', //css图标
        },
      })
    ],
  },

})

禁用F12

使用的是 @cellinlab/vitepress-protect-plugin

npm install vitepress-protect-plugin

然后在 config.mts 中配置,不用的功能不配置即可

js
import { defineConfig } from "vitepress"
import vitepressProtectPlugin from "vitepress-protect-plugin"

export default defineConfig({
  // other VitePress configs...
  vite: {
    plugins: [
      vitepressProtectPlugin({ 
        disableF12: true, // 禁用F12开发者模式
        disableCopy: true, // 禁用文本复制
        disableSelect: true, // 禁用文本选择
      }),
    ],
  },
})

切换路由进度条

当你切换页面,顶部会显示进度条,使用的是 @Skyleen77/nprogress-v2

说明

本方式由 Aurorxa 提供推送 #36

先安装 nprogress-v2

sh
npm install nprogress-v2

然后再 index.ts 中配置,即可生效

js
// .vitepress/theme/index.ts

import { NProgress } from 'nprogress-v2/dist/index.js' // 进度条组件
import 'nprogress-v2/dist/index.css' // 进度条样式

if (inBrowser) {
      NProgress.configure({ showSpinner: false }) 
      router.onBeforeRouteChange = () => {
        NProgress.start() // 开始进度条
      }
      router.onAfterRouteChanged = () => {
         busuanzi.fetch()
         NProgress.done() // 停止进度条
      }
}

评论

各种评论系统对比

评论系统说明
Valine不用登录账号即可评论,但容易产生垃圾评论,其次没有评论提醒通知
Waline是Valine的升级版,登录后方可评论,有通知,但是需要自己部署服务端
Twikoo不用登录账号即可评论,但容易产生垃圾评论,有通知,但是需要自己部署服务端
Artalk可设置是否启用登录账号后评论,有通知,但是需要自己部署服务端
utterancGitHub登录后方可评论,评论数据在 GitHub issues 中,评论后有邮件通知,无需部署服务端,但UI样式一般,且移动端不显示头像
gitalkGitHub登录后方可评论,评论数据在 GitHub issues 中,评论后有邮件通知,无需部署服务端,UI样式一般,评论不支持点赞
GiscusGitHub登录后方可评论,评论数据在 GitHub Discussions 中,评论后有邮件通知,无需部署服务端,UI爱了

之前在 vuepress 中用过 Valine,效果并不是特别好

从上面对比来看,Giscus 最佳,因此决定使用 Giscus

关于 @xinlei3166 的 waline 插件

在使用看板娘发时候就已经装好了,直接引用就行了

js
// .vitepress/theme/index.ts
import DefaultTheme from 'vitepress/theme'

import { useWaline } from 'vitepress-theme-website'

export default {
  extends: DefaultTheme,

  setup() {

    useWaline({ 
      serverURL: 'https://you_url.com'
    }),

  }
}

记得 serverURL 换成自己的即可,但是作者的插件有个bug,暗黑模式下看不清字

所以要用 waline 话就 参考官方的教程

安装giscus

Giscus 是一个基于 GitHub Discussion 的评论系统,启用简便

进 Giscus App官网:https://github.com/apps/giscus

点击 Install 安装

选择 Only select repositories,再指定一个你想开启讨论的仓库

注意

仓库必须是公开的,私有的不行

想单独放评论,新建一个也可

安装完成后可以在个人头像-设置-应用 Applications 中看到

开启讨论

因为giscus会把评论数据都放到讨论 discussions

我们进入要开启讨论的仓库,点设置 - 勾选讨论 Settings - discussions

生成数据

进入官网:https://giscus.app/zh-CN

输入自己的仓库链接,满足条件会提示可用

下拉到 Discussion 分类推荐选 General ,懒加载评论也可以勾选下

关于讨论的类型分类如下,来自于 Github的讨论文档

类别中文说明
Announcements公告每次评论都会推送所有人
General常规开放式讨论
Ideas想法开放式讨论
Polls投票可投票与讨论
Q&A问答问答形式
Show and tell展示和说明开放式讨论

下方就自动生成了你的关键数据

js
<script src="https://giscus.app/client.js"
        data-repo="github repository"
        data-repo-id="R_******"
        data-category="General"
        data-category-id="DIC_******"
        data-mapping="pathname"
        data-strict="0"
        data-reactions-enabled="1"
        data-emit-metadata="0"
        data-input-position="bottom"
        data-theme="preferred_color_scheme"
        data-lang="zh-CN"
        data-loading="lazy"
        crossorigin="anonymous"
        async>
</script>

其中 data-repodata-repo-iddata-categorydata-category-id 这4个是我们的关键数据

安装使用

有能力的可以用官方给的js数据封装

我这里用 @T-miracle/vitepress-plugin-comment-with-giscus 的插件

sh
npm install vitepress-plugin-comment-with-giscus

.vitepress/theme/index.ts 中填入下面代码

并将我们之前获取的4个关键数据填入,其他保持默认保存

js
// .vitepress/theme/index.ts
import DefaultTheme from 'vitepress/theme';
import giscusTalk from 'vitepress-plugin-comment-with-giscus'; 
import { useData, useRoute } from 'vitepress';

export default {
  extends: DefaultTheme,

  setup() {
    // Get frontmatter and route
    const { frontmatter } = useData();
    const route = useRoute();
        
    // giscus配置
    giscusTalk({
      repo: 'your github repository', //仓库
      repoId: 'your repository id', //仓库ID
      category: 'Announcements', // 讨论分类
      categoryId: 'your category id', //讨论分类ID
      mapping: 'pathname',
      inputPosition: 'bottom',
      lang: 'zh-CN',
      }, 
      {
        frontmatter, route
      },
      //默认值为true,表示已启用,此参数可以忽略;
      //如果为false,则表示未启用
      //您可以使用“comment:true”序言在页面上单独启用它
      true
    );

}

安装完看下底部的效果吧

如果某一页不想启用,可以在当前页使用 Frontmatter 关闭

markdown
---
comment: false
---

Released under the MIT License.