- 安装 nodejs
fedora:sudo dnf install nodejs
- 创建文件夹
mkdir username.github.io
cd username.github.io
和仓库名保持一致。名称格式推荐用username.github.io
,这样在访问页面时只需要用https://username.github.io
,不用再加仓库名
username
:github 账号用户名
- 初始化
npm add -D vitepress
npx vitepress init
- 本地预览
npm run docs:dev
自动生成导航栏和侧边栏
- 安装 node 库
npm install -D @types/node
- 添加
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
//  // 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
}
- 修改
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
---
部署
- 添加
.gitignore
忽略不需要上传的文件夹和文件
node_modules
cache
dist
- 添加
.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
- 创建 github 仓库
仓库名格式使用username.github.io
- 上传
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 的建议一行一行的执行,避免出现问题时不知道是哪一行有问题
- 设置 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 一旦生成就要自己存下来以便下次使用,刷新网页就会消失