Add VitePress site with local login and deployment docs

Enable static site build on port 12100 with Express session auth, auto sidebar, and DEPLOY.md for Ubuntu/NPS setup.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-05 16:12:35 +08:00
parent 2a1dc95253
commit 68d816b3f3
102 changed files with 4419 additions and 7 deletions
+47
View File
@@ -0,0 +1,47 @@
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { defineConfig } from 'vitepress'
import { missingAssetsPlugin } from './missing-assets-plugin.mjs'
import { generateSidebar } from './sidebar.mts'
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
export default defineConfig({
title: 'DAO DE JING',
description: '传统文化典籍资料库',
lang: 'zh-CN',
srcDir: '.',
srcExclude: [
'**/node_modules/**',
'**/.vitepress/**',
'**/server/**',
'**/金瓶梅/**',
'**/黄帝内经/**',
'.env.production',
'**/健康学习到150岁 - 人体系统调优不完全指南/**',
'**/梅花/**',
],
cleanUrls: true,
ignoreDeadLinks: true,
themeConfig: {
nav: [
{ text: '首页', link: '/' },
{ text: '五行', link: '/金、木、水、火、土%20-%20五行/' },
{ text: '道德经', link: '/道德经/01' },
{ text: '周易', link: '/周易/' },
{ text: '中医', link: '/中医宝典/' },
],
sidebar: generateSidebar(),
outline: { level: [2, 4] },
search: {
provider: 'local',
},
},
vite: {
plugins: [missingAssetsPlugin()],
server: {
host: '0.0.0.0',
},
publicDir: path.join(root, '.vitepress', 'public'),
},
})
+49
View File
@@ -0,0 +1,49 @@
import fs from 'node:fs'
import path from 'node:path'
const assetPattern = /\.(png|jpe?g|gif|webp|svg|bmp)(\?.*)?$/i
function toPublicUrl(source) {
if (source.startsWith('../images/')) {
return `/images/${source.slice('../images/'.length)}`
}
if (source.startsWith('../assets/')) {
return `/assets/${source.slice('../assets/'.length)}`
}
if (source.startsWith('images/')) {
return `/images/${source.slice('images/'.length)}`
}
if (source.startsWith('assets/')) {
return `/assets/${source.slice('assets/'.length)}`
}
return `/${source.replace(/^\.\//, '')}`
}
export function missingAssetsPlugin() {
return {
name: 'vitepress-missing-assets',
enforce: 'pre',
resolveId(source, importer) {
if (!importer?.endsWith('.md') || !assetPattern.test(source)) {
return null
}
if (/^https?:\/\//.test(source)) {
return null
}
const resolved = path.resolve(path.dirname(importer), source)
if (fs.existsSync(resolved)) {
return null
}
return `\0missing-asset:${toPublicUrl(source)}`
},
load(id) {
if (!id.startsWith('\0missing-asset:')) {
return null
}
const url = id.slice('\0missing-asset:'.length)
return `export default ${JSON.stringify(url)}`
},
}
}
+112
View File
@@ -0,0 +1,112 @@
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
const EXCLUDE_DIRS = new Set([
'node_modules',
'.vitepress',
'server',
'assets',
'images',
'.git',
'金瓶梅',
'黄帝内经',
'健康学习到150岁 - 人体系统调优不完全指南',
'梅花',
])
type SidebarItem = {
text: string
link?: string
collapsed?: boolean
items?: SidebarItem[]
}
function naturalCompare(a: string, b: string) {
return a.localeCompare(b, 'zh-CN', { numeric: true, sensitivity: 'base' })
}
function titleFromFilename(name: string) {
const base = name.replace(/\.md$/i, '')
if (/^readme$/i.test(base)) return '导读'
return base
}
function toLink(relativePath: string) {
const normalized = relativePath.replace(/\\/g, '/').replace(/\.md$/i, '')
if (normalized.endsWith('/README')) {
return `/${normalized.slice(0, -'/README'.length)}/`
}
return `/${normalized}`
}
function collectItems(dir: string, relativeDir: string): SidebarItem[] {
const entries = fs.readdirSync(dir, { withFileTypes: true })
const files = entries.filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
const subdirs = entries.filter((entry) => entry.isDirectory())
const items: SidebarItem[] = files
.sort((a, b) => naturalCompare(a.name, b.name))
.map((file) => ({
text: titleFromFilename(file.name),
link: toLink(path.join(relativeDir, file.name)),
}))
for (const subdir of subdirs.sort((a, b) => naturalCompare(a.name, b.name))) {
const subRelative = path.join(relativeDir, subdir.name)
const subItems = collectItems(path.join(dir, subdir.name), subRelative)
if (subItems.length === 0) continue
items.push({
text: subdir.name,
collapsed: true,
items: subItems,
})
}
return items
}
export function generateSidebar(): Record<string, SidebarItem[]> {
const groups: SidebarItem[] = []
const rootFiles = fs
.readdirSync(root, { withFileTypes: true })
.filter(
(entry) =>
entry.isFile() &&
entry.name.endsWith('.md') &&
entry.name !== 'README.md' &&
entry.name !== 'index.md',
)
.sort((a, b) => naturalCompare(a.name, b.name))
if (rootFiles.length > 0) {
groups.push({
text: '典籍',
collapsed: false,
items: rootFiles.map((file) => ({
text: titleFromFilename(file.name),
link: toLink(file.name),
})),
})
}
const dirs = fs
.readdirSync(root, { withFileTypes: true })
.filter((entry) => entry.isDirectory() && !EXCLUDE_DIRS.has(entry.name))
.sort((a, b) => naturalCompare(a.name, b.name))
for (const dir of dirs) {
const items = collectItems(path.join(root, dir.name), dir.name)
if (items.length === 0) continue
groups.push({
text: dir.name,
collapsed: false,
items,
})
}
return { '/': groups }
}