vitepress

发布于:2025-02-10 ⋅ 阅读:(45) ⋅ 点赞:(0)
  1. 安装 nodejs

fedora:sudo dnf install nodejs

  1. 创建文件夹
mkdir username.github.io
cd username.github.io

和仓库名保持一致。名称格式推荐用username.github.io,这样在访问页面时只需要用https://username.github.io,不用再加仓库名

username:github 账号用户名

  1. 初始化
npm add -D vitepress
npx vitepress init
  1. 本地预览
npm run docs:dev

自动生成导航栏和侧边栏

  1. 安装 node 库
npm install -D @types/node
  1. 添加 docs/.vitepress/nav_sidebar.mts
// 文件夹或文件命名为 0-abc、1-bcd,序号用于排序。如果没有序号就使用原名
//
// username.github.io
// | docs
//   | articles
//     | 0-abc // nav
//       | xyz // subnav,sidebar title
//         | asserts
//           | abcd.png
//         | index.md // subnav head page
//         | a.md // sidebar subtitle
//           ![](asserts/abcd.png) // import picture

import { resolve, join, sep } from 'path'
import { readdirSync, statSync } from 'fs'
import { DefaultTheme } from 'vitepress'

const EXP_MATCH = /^\d+-.+/ // 匹配序号的正则
const BEGIN_DIR = 'articles'
const IGNORE_MD_FILE = 'index.md'
const IGNORE_DIR = ['demo', 'asserts']

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 = BEGIN_DIR,
    ignoreFileName = IGNORE_MD_FILE,
    ignoreDirNames = IGNORE_DIR,
  } = 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(EXP_MATCH) ? fileOrDirName.substring(fileOrDirName.indexOf('-') + 1) : 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
        }

        // console.log('sidebar parent:')
        // console.log(dirData)
        // console.log('')
        result.push(dirData)
      }
    } else if (isMarkdownFile(fileOrDirName) && ignoreFileName !== fileOrDirName) {
      // 当前为文件
      const matchResult = fileOrDirName.match(/(.+)\.md/)
      let text = matchResult ? matchResult[1] : fileOrDirName
      text = text.match(EXP_MATCH) ? text.substring(fileOrDirName.indexOf('-') + 1) : text

      const fileData: SideBarItem = {
        text,
        link: getDocsDirNameAfterStr(fileOrDirFullPath).replace('.md', '').replace(/\\/g, '/'),
      }
      
      // console.log('sidebar children:')
      // console.log(fileData)
      // console.log('')
      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(EXP_MATCH) ? fileOrDirName.substring(fileOrDirName.indexOf('-') + 1) : 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') // text 会报错,不用管
        if (arr.length > 0) {
          // @ts-ignore
          dirData.items = arr
          delete dirData.link
        }
      }

      dirData.activeMatch = link + '/'

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

  return result
}
  1. 修改 docs/.vitepress/config.mts
import { getSidebarData, getNavData } from './nav_sidebar.mts'

...

nav: getNavData(), // 顶部的导航栏
sidebar: getSidebarData(), // 侧边栏

搜索框

参考docs/.vitepress/config.mts里的search
config.mts:

import { defineConfig } from "vitepress";
import { getSidebarData, getNavData } from "./nav_sidebar.mts";

export default defineConfig({
  head: [["link", { rel: "icon", href: "/logo.svg" }]], // 浏览器标签栏网页图标
  title: "Home", // 顶部的 Home 名,浏览器标签栏名

  themeConfig: {
    logo: "/logo.svg", // 顶部的 Home 图标
    nav: getNavData(), // 顶部的导航栏
    sidebar: getSidebarData(), // 侧边栏
    socialLinks: [
      {
        icon: "github",
        link: "https://github.com/xiaguangbo/xiaguangbo.github.io",
      },
    ], // 顶部右侧的社交链接

    // 页脚翻页按钮
    docFooter: {
      prev: "上一章",
      next: "下一章",
    },

    // 标题栏
    outline: {
      label: "标题栏",
    },

    // 移动端
    returnToTopLabel: "回到顶部",
    sidebarMenuLabel: "文章栏",
    darkModeSwitchLabel: "主题颜色",
    lightModeSwitchTitle: "切换到浅色模式",
    darkModeSwitchTitle: "切换到深色模式",

    // 搜索框
    search: {
      provider: "local",
      options: {
        translations: {
          button: {
            buttonText: "搜索",
          },
          modal: {
            displayDetails: "展开",
            resetButtonTitle: "清除",
            noResultsText: "未找到",
            footer: {
              navigateText: "移动",
              selectText: "选择",
              closeText: "关闭",
            },
          },
        },
      },
    },
  },
});

首页

参考docs/index.md

---
layout: home

hero:
  name: hhh
  tagline: hhhhhhh
  image:
    src: /logo.svg
  actions:
    - theme: alt
      text: abc
    - theme: alt
      text: abc
    - theme: alt
      text: abc

features:
  - title: 123
---

部署

  1. 添加 .gitignore

忽略不需要上传的文件夹和文件

node_modules
cache
dist
  1. 添加 .github/workflows/deploy.yml

github CI 脚本。自动云编译,无需本地编译

# Sample workflow for building and deploying a VitePress site to GitHub Pages
#
name: Deploy VitePress site to Pages

on:
# Runs on pushes targeting the `main` branch. Change this to `master` if you're
# using the `master` branch as the default branch.
push:
  branches: [main]

# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:

# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write

# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: pages
cancel-in-progress: false

jobs:
# Build job
build:
  runs-on: ubuntu-latest
  steps:
    - name: Checkout
      uses: actions/checkout@v4
      with:
        fetch-depth: 0 # Not needed if lastUpdated is not enabled
    # - uses: pnpm/action-setup@v3 # Uncomment this if you're using pnpm
    # - uses: oven-sh/setup-bun@v1 # Uncomment this if you're using Bun
    - name: Setup Node
      uses: actions/setup-node@v4
      with:
        node-version: 20
        cache: npm # or pnpm / yarn
    - name: Setup Pages
      uses: actions/configure-pages@v4
    - name: Install dependencies
      run: npm ci # or pnpm install / yarn install / bun install
    - name: Build with VitePress
      run: npm run docs:build # or pnpm docs:build / yarn docs:build / bun run docs:build
    - name: Upload artifact
      uses: actions/upload-pages-artifact@v3
      with:
        path: docs/.vitepress/dist

# Deployment job
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
  1. 创建 github 仓库

仓库名格式使用username.github.io

  1. 上传
echo "# username.github.io" >> README.md
git init
git add README.md
git commit -m "first commit"
git branch -M main
git remote add origin https://github.com/username/username.github.io.git
git push -u origin main

不熟悉 git 的建议一行一行的执行,避免出现问题时不知道是哪一行有问题

  1. 设置 Pages

Settings/Pages/Build and deployment/Source里选择 GitHub Actions。使用 CI 输出的文件

问题

  • 预览时没任何显示一片空白,应关闭代理

  • 没有package-lock.json时 CI 会失败,运行npm add -D vitepress会生成

  • 使用他人模板时不能正常工作就自己搭建

  • git 没设置提交用的用户名和邮箱会在commit时报错并提示

  • 第一次push时需要输入 github 账户和 token 密码。不同的 token 具有不同的权限

    获取 token,在 github 网页上点击自己的头像,进入Settings/Developer Settings/Personal access tokens/Tokens (classic),在这里添加 token 或更新 token 的权限。这里需要勾选workflow的权限

    token 一旦生成就要自己存下来以便下次使用,刷新网页就会消失


网站公告

今日签到

点亮在社区的每一天
去签到