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:
@@ -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'),
|
||||
},
|
||||
})
|
||||
@@ -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)}`
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
Reference in New Issue
Block a user