修改后的 Docker 版 Docsify,通过 Github 或 Gitea 的 Git Webhook 自动更新文档内容并重新生成侧边栏,新增了右侧标题栏插件,使用左边文件名 + 右边标题的布局
修改后的 Docker 版 Docsify
- Docsify
- 1 构建与运行
- 1.1 构建 Docker 镜像
- 1.2 运行 Docker 容器
- 2 具体修改内容
- 2.1 index.html 示例
- 3 同步远程 Git 仓库实现远程修改
- 3.1 同步 Git 仓库
- 3.2 根据目录自动生成 \_sidebar.md 文件
- 3.3 刷新 Docsify 页面
- 4 根据远程 Git 仓库提交自动更新本地 Docsify 项目(以 Gitea 为例)
- Obsidian
- Gitea
Docsify
Docsify 是一个神奇的文档网站生成器。它能够快速地将 Markdown 文件转换为美观的文档网站,无需构建过程。它轻量、易用,支持自定义主题和插件扩展,非常适合用于项目文档、API 文档或知识库的构建。
1 构建与运行
本项目提供了一个基于 Docker 的 Docsify 环境,基于 stilleshan/docsify 镜像修改而成,新增了右侧标题栏插件,使用左边文件名+右边标题的布局,支持通过 Git Webhook 自动更新文档内容并重新生成侧边栏。
- 教程:bilibili:修改后的Docker版Docsify,同步Github或Gitea自动更新文档内容并重新生成侧边栏,新增了右侧标题栏插件,使用左边文件名+右边标题的布局
- Docker 镜像同步项目:https://github.com/sqing33/docker-image-sync
CSDN:分享一下个人在使用 Docker 过程中无法下载镜像的一些解决方法
- Docsify的github构建项目:https://github.com/sqing33/docker-docsify
- Docsify的DockerHub:https://hub.docker.com/r/sqing33/docsify
1.1 构建 Docker 镜像
因为本地网络问题无法获取到源镜像stilleshan/docsify的元数据,所以将 dockerhub 同步到 github 的 ghcr 仓库以继续进行(ghcr.io/sqing33/docsify-stilleshan)
docker buildx build \ --platform linux/amd64,linux/arm64 \ -t sqing33/docsify:latest \ -t ghcr.io/sqing33/docsify:latest \ --push .
1.2 运行 Docker 容器
你需要根据你的需求配置以下环境变量:
-
DOCSIFY_PORT: Docsify 服务监听的端口 (默认为 6158)。
-
ENABLE_GIT: 是否启用 Git 功能 (默认为 true)。
-
WEBHOOK_PORT: Webhook 监听器监听的端口 (默认为 6159)。
-
WEBHOOK_SECRET: 用于验证 Webhook 签名的密钥。
-
GIT_REMOTE_URL: 你的 Git 仓库的 URL (例如: https://gitea.example.com/user/repo.git)。
-
GIT_REMOTE: Git 远程仓库的名称 (默认为 origin)。
-
GIT_BRANCH: 你希望拉取和监听的分支 (默认为 main)。
挂载容器内的 /docs 目录,以便于持久化存储文档内容。
示例 docker run 命令:
docker run -d \ --name docsify \ -p 6158:6158 \ -p 6159:6159 \ -e DOCSIFY_PORT=6158 \ -e ENABLE_GIT="true" \ -e WEBHOOK_PORT=6159 \ -e WEBHOOK_SECRET=YOUR_WEBHOOK_SECRET \ -e GIT_REMOTE_URL=YOUR_GIT_REPO_URL \ -e GIT_REMOTE=origin \ -e GIT_BRANCH=main \ -e DOCS_DIR=/docs \ -v /vol1/1000/Docker/docsify/docs:/docs \ --restart unless-stopped \ sqing33/docsify:latest # 无法拉取可使用 ghcr.io/sqing33/docsify:latest
示例 docker-compose.yml 文件:
services: docsify: image: sqing33/docsify:latest # 无法拉取可使用 ghcr.io/sqing33/docsify:latest container_name: docsify ports: # Docsify 服务器端口映射 - "6158:6158" # Webhook 监听器端口映射,listener.js 在容器内部监听 6159 端口 - "6159:6159" environment: # Docsify 服务端口 - DOCSIFY_PORT=6158 # 是否启用 Git 更新 - ENABLE_GIT="true" # WEBHOOK_PORT 监听端口 - WEBHOOK_PORT=6159 # Webhook 验证密钥 - WEBHOOK_SECRET=YOUR_WEBHOOK_SECRET # Git 远程仓库的 URL,用于 entrypoint 中的 git remote add origin - GIT_REMOTE_URL=YOUR_GIT_REPO_URL # Git 远程源的名称,用于 listener.js 中的 git pull - GIT_REMOTE=origin # 通常是 origin,如果你的仓库不是则修改 # Git 分支名称,用于 listener.js 中的 git pull 和事件判断 - GIT_BRANCH=main - DOCS_DIR=/docs # 默认 /docs ,这应与卷挂载点匹配 volumes: # 将宿主机目录挂载到容器内的 /docs - '/vol1/1000/Docker/docsify/docs:/docs' # 重启策略 restart: unless-stopped
2 具体修改内容
2.1 index.html 示例
Docsify .sidebar { padding: 20px 0 0; } .content { padding-top: 0; } window.$docsify = { name: "Docsify", logo: "/_media/icon.svg", repo: "", loadSidebar: true, // 是否加载侧边栏 auto2top: true, // 是否自动滚动到页面顶部 toc: { tocMaxLevel: 6, target: "h1, h2, h3, h4, h5, h6", ignoreHeaders: [ "", "", ], }, count: { countable: true, fontsize: "0.9em", color: "rgb(90,90,90)", language: "chinese", }, copyCode: { buttonText: "复制", errorText: "复制错误", successText: "复制成功", }, }; function wrapLogoAndTitle() { return function (hook, vm) { hook.doneEach(function () { const appName = document.querySelector(".sidebar .app-name"); if (appName) { const logoImg = appName.querySelector("img"); const titleLink = appName.querySelector("a"); const titleText = vm.config.name; if (logoImg && titleLink) { // 创建包含logo和标题的新链接 const newLink = document.createElement("a"); newLink.href = titleLink.href; newLink.className = "app-name-link"; // 复制原始链接的事件监听器 newLink.onclick = titleLink.onclick; // 添加logo到新链接,并设置尺寸 const logoClone = logoImg.cloneNode(true); logoClone.style.width = "35px"; logoClone.style.height = "35px"; logoClone.style.objectFit = "contain"; // 保持图片比例 logoClone.style.verticalAlign = "middle"; // 添加标题到新链接 const titleSpan = document.createElement("span"); titleSpan.textContent = titleText; titleSpan.style.display = "inline-block"; titleSpan.style.verticalAlign = "middle"; titleSpan.style.marginLeft = "10px"; titleSpan.style.fontWeight = "bold"; titleSpan.style.fontSize = "1.2em"; titleSpan.style.lineHeight = "35px"; // 关键:使行高等于logo高度 newLink.appendChild(logoClone); newLink.appendChild(titleSpan); // 替换原始链接 appName.innerHTML = ""; appName.appendChild(newLink); document.head.appendChild(style); } } }); }; } // 注册插件时可以传入自定义配置 window.$docsify.plugins = [].concat( wrapLogoAndTitle(), window.$docsify.plugins || [] );
插件包含:右侧标题栏、字数统计、emoji 解析、图片缩放、代码复制、自定义左上角 logo + 标题
3 同步远程 Git 仓库实现远程修改
3.1 同步 Git 仓库
- 在映射的数据目录/vol1/1000/Docker/docsify/docs:/docs’新建 git 仓库:
git init git remote add origin http://192.168.1.100:3111/sqing/wiki.js.git git config --global --add safe.directory /docs
- 添加.gitignore文件设置忽略项
index.html generate-sidebar.js _sidebar.md _media
- 同步远程 Git 仓库到本地
git pull origin main
3.2 根据目录自动生成 _sidebar.md 文件
// dockerfile/generate-sidebar.js const fs = require('fs'); const path = require('path'); // 脚本假设在 /docs 目录下运行 const docsDir = '.'; // 你的文档根目录,通常是当前目录 const sidebarFile = '_sidebar.md'; let sidebarContent = ''; // 排除列表 const excludeList = [ '_sidebar.md', // 排除侧边栏文件本身 '_media', // 排除媒体文件 'index.html', // 排除主页 HTML 文件 '.git', // 排除 .git 目录 '.gitignore', // 排除 .gitignore 目录 'readme.md', // 排除 readme.md 文件 'git_listen', // 排除 git_listen 文件夹 ]; function buildSidebar(currentDir, level) { const items = fs.readdirSync(currentDir); // 对items进行排序,让侧边栏顺序更可控,例如按字母排序 // 确保排序时不区分大小写,并处理中文等非英文字符(如果需要更复杂的排序) items.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' })); items.forEach(item => { // 在处理前检查是否在排除列表中 // 使用 path.basename 确保只比较文件名/目录名本身 if (excludeList.includes(item) || item.startsWith('.')) { console.log(`Excluding: ${item}`); // 可选:打印排除项 return; // 跳过当前循环迭代 } const itemPath = path.join(currentDir, item); const stat = fs.statSync(itemPath); // 使用两个空格作为基本缩进,因为 docsify 对缩进要求可能不同 const indent = ' '.repeat(level); if (stat.isDirectory()) { // 在目录名后添加斜杠以便清晰地表示目录 sidebarContent += `${indent}* **${item}**\n`; // 可以加粗目录名 // 递归处理子目录,层级加1 buildSidebar(itemPath, level + 1); } else if (stat.isFile() && item.endsWith('.md')) { const fileNameWithoutExt = path.parse(item).name; // 排除 README.md 文件本身,如果需要链接它,应该在父级目录处特殊处理 if (fileNameWithoutExt.toLowerCase() === 'readme') { console.log(`Excluding README.md file entry: ${item}`); return; // 跳过 README.md 的文件条目 } const linkPath = path.relative(docsDir, itemPath).replace(/\\/g, '/'); // 确保路径使用正斜杠 sidebarContent += `${indent}* [${fileNameWithoutExt}](${linkPath})\n`; // 使用原始文件名作为链接文本 } }); } // 从根目录(/docs,因为脚本会在那里运行)开始构建 buildSidebar(docsDir, 0); // 在顶部添加一个指向根目录(README.md 或 index.html)的链接 // 这通常是 Docsify 侧边栏的第一项 sidebarContent = `* [首页](/)\n` + sidebarContent; // 写入 _sidebar.md 文件 fs.writeFileSync(sidebarFile, sidebarContent); console.log(`${sidebarFile} generated successfully.`);
3.3 刷新 Docsify 页面
4 根据远程 Git 仓库提交自动更新本地 Docsify 项目(以 Gitea 为例)
- 在挂载的项目路径下新建一个git_listen目录,在其中新建一个listener.js文件,内容如下:
// dockerfile/listener.js (只包含监听和命令执行逻辑) const express = require('express'); const bodyParser = require('body-parser'); const crypto = require('crypto'); const { exec } = require('child_process'); const app = express(); // --- 配置 --- // 从环境变量读取配置,如果未设置则使用默认值 const PORT = process.env.WEBHOOK_PORT || 6159; // !! IMPORTANT !!: 从环境变量读取密钥 const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET; // 当前容器内的文档目录,从环境变量读取或使用默认值 const DOCS_DIR_IN_CONTAINER = process.env.DOCS_DIR || '/docs'; // 从环境变量读取 Git 配置 const GIT_REMOTE = process.env.GIT_REMOTE || 'origin'; // 用于 git pull 命令 const GIT_BRANCH = process.env.GIT_BRANCH || 'main'; // 用于 git pull 和事件类型判断 // 检查是否设置了必需的环境变量 if (!WEBHOOK_SECRET) { console.error("错误: 必须设置 WEBHOOK_SECRET 环境变量。"); // 在实际生产环境中,可能需要更优雅地处理或延迟启动 // 为了脚本能立即运行,这里只警告,但 webhook 将无法验证 // process.exit(1); // 如果密钥未设置就退出,但这样容器会停止 } // GIT_REMOTE 和 GIT_BRANCH 如果未设置有默认值,GIT_REMOTE_URL 用于 entrypoint 中的 git remote add // --- 中间件 --- // 使用原始 body 以便进行签名验证 app.use(bodyParser.raw({ type: 'application/json' })); // --- Webhook 处理路由 --- app.post('/webhook', (req, res) => { console.log('接收到 Webhook 请求。'); // 1. 验证签名 (Gitea 使用 x-gitea-signature) const signature = req.headers['x-gitea-signature']; if (!signature) { console.warn('接收到没有 x-gitea-signature 签名的 Webhook 请求。'); return res.status(401).send('Signature required'); } const payload = req.body; const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET); hmac.update(payload); const digest = hmac.digest('hex'); if (digest !== signature) { console.warn('无效的 Webhook 签名。'); return res.status(401).send('Invalid signature'); } console.log('Webhook 签名验证成功。'); let payloadJson; try { payloadJson = JSON.parse(payload.toString()); } catch (e) { console.error('解析 Webhook Payload JSON 失败:', e); return res.status(400).send('Invalid JSON payload'); } // 2. 判断事件类型 (Gitea 使用 x-gitea-event) const eventType = req.headers['x-gitea-event']; console.log(`Webhook 事件类型: ${eventType}`); // 只处理 push 事件 if (eventType === 'push') { // 判断是否是指定的分支 // ref 的格式是 'refs/heads/分支名' 或 'refs/tags/标签名' const pushBranch = payloadJson.ref.split('/').pop(); // 从 'refs/heads/main' 中提取 'main' if (pushBranch !== GIT_BRANCH) { console.log(`Push 事件发生在分支 ${pushBranch},但只监听分支 ${GIT_BRANCH}。忽略。`); return res.status(200).send(`Ignored event for branch '${pushBranch}'`); } console.log(`接收到分支 ${GIT_BRANCH} 的 Push 事件。`); // 3. 执行容器内部的命令:进入 docs 目录,拉取最新更改,然后运行 generate-sidebar.js console.log(`正在容器内部执行更新命令: cd ${DOCS_DIR_IN_CONTAINER} && git pull ${GIT_REMOTE} ${GIT_BRANCH} && node generate-sidebar.js`); // 构建在容器内部执行的命令序列 // 注意:如果 generate-sidebar.js 不在 DOCS_DIR_IN_CONTAINER 根目录,需要调整路径 const containerCommands = `cd ${DOCS_DIR_IN_CONTAINER} && git pull ${GIT_REMOTE} ${GIT_BRANCH} && node generate-sidebar.js`; exec(containerCommands, (error, stdout, stderr) => { if (error) { console.error(`执行命令失败: ${error.message}`); // 即使失败,也回复 500 以外的状态码,避免 Gitea 重试过多,但通常 500 是可以的 return res.status(500).send('Failed to update docs'); } if (stderr) { console.error(`命令的标准错误输出: ${stderr}`); } console.log(`命令的标准输出: ${stdout}`); const now = new Date(); const timestamp = now.toLocaleString(); console.log(`文档更新并侧边栏生成成功。完成时间: ${timestamp}`); res.status(200).send('Docs updated'); }); } else { // 忽略其他类型的事件 console.log(`事件类型 '${eventType}' 已忽略。`); res.status(200).send(`事件类型 '${eventType}' 已忽略`); } }); // --- 启动服务 --- app.listen(PORT, () => { console.log(`Webhook 监听服务正在容器内部运行,端口号: ${PORT}`); console.log(`监听分支: ${GIT_BRANCH} 的 push 事件`); console.log(`Git 远程源名称: ${GIT_REMOTE}`); // 注意这里是远程源名称,不是 URL console.log(`文档目录: ${DOCS_DIR_IN_CONTAINER}`); // 注意:WEBHOOK_SECRET 不应该打印到日志,因为它是一个密钥 });
-
设置listener.js的端口PORT、为 Gitea Webhook 设置的 Secret TokenWEBHOOK_SECRET,GIT_REMOTE和GIT_BRANCH根据创建的 git 仓库填写
-
进入 Gitea 仓库的设置 -> 点击Web 钩子(Webhooks) -> 添加 Web 钩子 -> 选择 Gitea
-
目标 URL设置为http://192.168.1.100:6159/webhook,HTTP 方法为POST,密钥文本填WEBHOOK_SECRET,触发条件:选择所有事件,点击添加 Web 钩子
-
进入 Docsify 容器的终端 -> 进入目录/docs/Gitea listen -> 安装依赖npm install express body-parser crypto -> 运行监听服务node listener.js
Obsidian
Obsidian 是一款强大的笔记应用,它使用 Markdown 格式存储笔记,并支持双向链接,使其成为构建个人知识库的理想工具。 它可以通过插件扩展功能,并支持多种导出格式。
把 Obsidian 粘贴的图片上传到 Docker 部署的 PicList 上的具体过程与一些目前使用的插件推荐
Gitea
Gitea 是一个自托管的 Git 服务,类似于 GitHub 或 GitLab,但更轻量级,易于安装和管理。 它允许用户在自己的服务器上托管 Git 仓库,进行代码版本控制、协作和代码审查。
services: server : image: ghcr.nju.edu.cn/sqing33/gitea # 源镜像:gitea/gitea container_name: gitea restart: always volumes : - /vol1/1000/Docker/gitea/data:/data - /etc/timezone:/etc/timezone:ro - /etc/localtime:/etc/localtime:ro ports : - "3111:3000" - "2222:2222"
- 同步远程 Git 仓库到本地
- 添加.gitignore文件设置忽略项
- 在映射的数据目录/vol1/1000/Docker/docsify/docs:/docs’新建 git 仓库:
-