notespress 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # blog-vitepress
2
+
3
+ 一个用于托管个人笔记站的 VitePress 外壳仓库,同时也包含一个可发布的 `notespress` CLI。
4
+
5
+ Markdown 内容放在 `myBlog` 子模块中,当前仓库负责:
6
+
7
+ - 站点配置
8
+ - 自动生成导航
9
+ - 代码示例包装页生成
10
+ - GitHub Pages 部署
11
+
12
+ ## 快速开始
13
+
14
+ ```bash
15
+ git clone <repo-url>
16
+ cd blog-vitepress
17
+ git submodule update --init --recursive
18
+ pnpm install
19
+ pnpm dev
20
+ ```
21
+
22
+ ## 作为 npm 包使用
23
+
24
+ CLI 的目标用法是:在任意 Markdown 笔记目录下直接生成博客站点。
25
+
26
+ 本地安装:
27
+
28
+ ```bash
29
+ npm install -D notespress
30
+ npx notespress build
31
+ ```
32
+
33
+ 或者直接执行一次性命令:
34
+
35
+ ```bash
36
+ pnpm dlx notespress build
37
+ ```
38
+
39
+ 常用命令:
40
+
41
+ ```bash
42
+ notespress dev
43
+ notespress build
44
+ notespress preview
45
+ ```
46
+
47
+ 默认行为:
48
+
49
+ - 当前目录作为内容目录
50
+ - 构建产物输出到 `dist/`
51
+ - 临时工作区输出到 `.blog-cli/`
52
+ - `.js`、`.ts`、`.html` 会自动生成 `snippets/` 页面
53
+
54
+ ## 目录结构
55
+
56
+ - `myBlog/`:Markdown 内容仓库,使用 git submodule 管理
57
+ - `.vitepress/`:VitePress 配置
58
+ - `docs/`:项目说明页
59
+ - `scripts/`:构建前生成代码示例页面
60
+
61
+ ## 自动导航
62
+
63
+ 站点使用 `vitepress-auto-navigation` 根据 `myBlog` 目录自动生成 `nav` 和 `sidebar`。如果本地还没有拉取子模块,站点仍能启动,但不会显示笔记导航。
64
+
65
+ 除了 Markdown 页面,站点还会在构建前扫描 `myBlog` 里的 `.js`、`.ts` 和 `.html` 文件,并自动生成 `snippets/` 下的包装页,让这些代码文件也能通过导航访问。
66
+
67
+ ## 从 `myBlog` 直接生成网站
68
+
69
+ 根仓库现在提供了一个 `notespress`。如果你进入 `myBlog/`,可以直接运行:
70
+
71
+ ```bash
72
+ pnpm build
73
+ pnpm dev
74
+ pnpm preview
75
+ ```
76
+
77
+ 这些脚本会调用上层仓库里的 `notespress`,临时创建一个独立的 VitePress 工作区,并把当前 `myBlog/` 目录生成为静态站点。
78
+
79
+ - 输出目录默认是 `myBlog/dist`
80
+ - 临时工作区在 `myBlog/.blog-cli`
81
+ - 不会把 `.vitepress` 配置写回 `myBlog/`
82
+ - `.js`、`.ts`、`.html` 文件会自动生成 `snippets/` 页面
83
+
84
+ ## 部署
85
+
86
+ `.github/workflows/deploy.yml` 会在 `master` 分支收到 push 后自动:
87
+
88
+ 1. 拉取仓库和子模块
89
+ 2. 安装依赖
90
+ 3. 构建 VitePress 站点
91
+ 4. 发布到 GitHub Pages
@@ -0,0 +1,171 @@
1
+ .VPHome {
2
+ --home-accent: #0f766e;
3
+ --home-ink: #0f172a;
4
+ --home-surface: rgba(255, 255, 255, 0.84);
5
+ --home-border: rgba(15, 23, 42, 0.1);
6
+ }
7
+
8
+ .VPHome::before {
9
+ content: "";
10
+ position: fixed;
11
+ inset: 0;
12
+ background:
13
+ radial-gradient(circle at top left, rgba(15, 118, 110, 0.14), transparent 30%),
14
+ radial-gradient(circle at top right, rgba(249, 115, 22, 0.14), transparent 28%),
15
+ linear-gradient(180deg, rgba(248, 250, 252, 0.96), rgba(255, 255, 255, 0.98));
16
+ pointer-events: none;
17
+ z-index: -1;
18
+ }
19
+
20
+ .VPHome .VPHero {
21
+ padding-top: 48px;
22
+ }
23
+
24
+ .VPHome .VPHero .name,
25
+ .VPHome .VPHero .text {
26
+ letter-spacing: -0.04em;
27
+ }
28
+
29
+ .VPHome .VPHero .name {
30
+ color: var(--home-accent);
31
+ }
32
+
33
+ .VPHome .VPHero .text {
34
+ color: var(--home-ink);
35
+ max-width: 10ch;
36
+ font-size: clamp(2.8rem, 5vw, 4.8rem);
37
+ line-height: 0.96;
38
+ }
39
+
40
+ .VPHome .VPHero .tagline {
41
+ max-width: 42rem;
42
+ color: rgba(15, 23, 42, 0.72);
43
+ font-size: 1.05rem;
44
+ }
45
+
46
+ .VPHome .VPButton.brand {
47
+ background: linear-gradient(135deg, #0f766e, #115e59);
48
+ border-color: transparent;
49
+ }
50
+
51
+ .VPHome .VPButton.alt {
52
+ border-color: var(--home-border);
53
+ background: var(--home-surface);
54
+ }
55
+
56
+ .VPHome .VPFeature {
57
+ border: 1px solid var(--home-border);
58
+ border-radius: 24px;
59
+ background: var(--home-surface);
60
+ box-shadow: 0 20px 50px rgba(15, 23, 42, 0.06);
61
+ backdrop-filter: blur(10px);
62
+ }
63
+
64
+ .VPHome .VPFeature .title {
65
+ color: var(--home-ink);
66
+ }
67
+
68
+ .VPHome .VPFeature .details {
69
+ color: rgba(15, 23, 42, 0.68);
70
+ }
71
+
72
+ .snippet-page .vp-doc {
73
+ --snippet-surface: rgba(15, 23, 42, 0.04);
74
+ --snippet-border: rgba(15, 23, 42, 0.12);
75
+ --snippet-accent: #0f766e;
76
+ }
77
+
78
+ .snippet-page .snippet-breadcrumb {
79
+ display: flex;
80
+ flex-wrap: wrap;
81
+ align-items: center;
82
+ gap: 0.45rem;
83
+ margin: 0 0 1rem;
84
+ color: rgba(15, 23, 42, 0.52);
85
+ font-size: 0.86rem;
86
+ }
87
+
88
+ .snippet-page .snippet-breadcrumb__link {
89
+ color: inherit;
90
+ font-weight: 600;
91
+ text-decoration: none;
92
+ }
93
+
94
+ .snippet-page .snippet-breadcrumb__link:hover {
95
+ color: var(--snippet-accent);
96
+ }
97
+
98
+ .snippet-page .snippet-breadcrumb__divider {
99
+ opacity: 0.45;
100
+ }
101
+
102
+ .snippet-page .vp-doc h1 {
103
+ margin-bottom: 0.75rem;
104
+ }
105
+
106
+ .snippet-page .snippet-hero {
107
+ display: flex;
108
+ flex-wrap: wrap;
109
+ gap: 0.6rem;
110
+ margin: 0 0 1rem;
111
+ }
112
+
113
+ .snippet-page .snippet-hero__chip {
114
+ display: inline-flex;
115
+ align-items: center;
116
+ border: 1px solid var(--snippet-border);
117
+ border-radius: 999px;
118
+ background: linear-gradient(135deg, rgba(15, 118, 110, 0.14), rgba(15, 23, 42, 0.04));
119
+ color: var(--snippet-accent);
120
+ font-size: 0.78rem;
121
+ font-weight: 700;
122
+ letter-spacing: 0.06em;
123
+ padding: 0.35rem 0.7rem;
124
+ text-transform: uppercase;
125
+ }
126
+
127
+ .snippet-page .snippet-meta {
128
+ display: grid;
129
+ gap: 0.8rem;
130
+ margin: 0 0 1.35rem;
131
+ }
132
+
133
+ .snippet-page .snippet-meta__item {
134
+ border: 1px solid var(--snippet-border);
135
+ border-radius: 18px;
136
+ background: var(--snippet-surface);
137
+ padding: 0.9rem 1rem;
138
+ }
139
+
140
+ .snippet-page .snippet-meta__label {
141
+ display: block;
142
+ color: var(--snippet-accent);
143
+ font-size: 0.72rem;
144
+ font-weight: 700;
145
+ letter-spacing: 0.08em;
146
+ margin-bottom: 0.4rem;
147
+ text-transform: uppercase;
148
+ }
149
+
150
+ .snippet-page .snippet-meta code {
151
+ background: transparent;
152
+ border: 0;
153
+ padding: 0;
154
+ word-break: break-all;
155
+ }
156
+
157
+ .snippet-page .snippet-meta a {
158
+ color: var(--snippet-accent);
159
+ font-weight: 600;
160
+ text-decoration: none;
161
+ }
162
+
163
+ .snippet-page .snippet-meta a:hover {
164
+ text-decoration: underline;
165
+ }
166
+
167
+ .snippet-page .vp-doc div[class*="language-"] {
168
+ border: 1px solid var(--snippet-border);
169
+ border-radius: 22px;
170
+ box-shadow: 0 24px 60px rgba(15, 23, 42, 0.08);
171
+ }
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createServer, build, serve } from "vitepress"
4
+
5
+ import { generateSnippetPages } from "../lib/snippets.mjs"
6
+ import { prepareStandaloneWorkspace, resolveRepositoryRoot } from "../lib/standalone-site.mjs"
7
+
8
+ function printHelp() {
9
+ process.stdout.write(`notespress
10
+
11
+ Usage:
12
+ notespress build [content-dir] [--out-dir dir] [--base /path/]
13
+ notespress dev [content-dir] [--port 5173] [--host 0.0.0.0]
14
+ notespress preview [content-dir] [--port 4173]
15
+ notespress prepare-content [content-dir] [--output-dir dir] [--route-base /myBlog]
16
+
17
+ Aliases:
18
+ build-site -> build
19
+ `)
20
+ }
21
+
22
+ function parseArgs(argv) {
23
+ const [command = "help", ...rest] = argv
24
+ const options = {}
25
+ const positionals = []
26
+
27
+ for (let index = 0; index < rest.length; index += 1) {
28
+ const token = rest[index]
29
+
30
+ if (!token.startsWith("--")) {
31
+ positionals.push(token)
32
+ continue
33
+ }
34
+
35
+ const [rawKey, inlineValue] = token.slice(2).split("=", 2)
36
+
37
+ if (inlineValue != null) {
38
+ options[rawKey] = inlineValue
39
+ continue
40
+ }
41
+
42
+ const nextToken = rest[index + 1]
43
+
44
+ if (nextToken == null || nextToken.startsWith("--")) {
45
+ options[rawKey] = true
46
+ continue
47
+ }
48
+
49
+ options[rawKey] = nextToken
50
+ index += 1
51
+ }
52
+
53
+ return { command, options, positionals }
54
+ }
55
+
56
+ function resolveContentDir(positionals, options) {
57
+ return positionals[0] ?? options["content-dir"] ?? process.cwd()
58
+ }
59
+
60
+ function resolvePort(value, fallback) {
61
+ if (value == null) {
62
+ return fallback
63
+ }
64
+
65
+ const port = Number(value)
66
+
67
+ if (!Number.isInteger(port) || port <= 0) {
68
+ throw new Error(`Invalid port: ${value}`)
69
+ }
70
+
71
+ return port
72
+ }
73
+
74
+ async function runPrepareContent(positionals, options) {
75
+ const result = generateSnippetPages({
76
+ contentDir: resolveContentDir(positionals, options),
77
+ outputDir: options["output-dir"] ?? "snippets",
78
+ routeBase: options["route-base"] ?? "/myBlog",
79
+ snippetRouteBase: options["snippet-route-base"] ?? "/snippets",
80
+ sourceLinkPrefix: options["source-link-prefix"] ?? "myBlog",
81
+ })
82
+
83
+ process.stdout.write(`Generated ${result.count} snippet pages in ${result.outputDir}\n`)
84
+ }
85
+
86
+ async function runBuild(positionals, options) {
87
+ const workspace = prepareStandaloneWorkspace({
88
+ contentDir: resolveContentDir(positionals, options),
89
+ outDir: options["out-dir"],
90
+ base: options.base,
91
+ title: options.title,
92
+ description: options.description,
93
+ })
94
+
95
+ process.chdir(resolveRepositoryRoot())
96
+ await build(workspace.root)
97
+ }
98
+
99
+ async function runDev(positionals, options) {
100
+ const workspace = prepareStandaloneWorkspace({
101
+ contentDir: resolveContentDir(positionals, options),
102
+ outDir: options["out-dir"],
103
+ base: options.base,
104
+ title: options.title,
105
+ description: options.description,
106
+ })
107
+
108
+ process.chdir(resolveRepositoryRoot())
109
+ const server = await createServer(workspace.root, {
110
+ host: options.host,
111
+ port: resolvePort(options.port, 5173),
112
+ })
113
+
114
+ const closeServer = async () => {
115
+ await server.close()
116
+ process.exit(0)
117
+ }
118
+
119
+ process.on("SIGINT", closeServer)
120
+ process.on("SIGTERM", closeServer)
121
+
122
+ await server.listen()
123
+ server.printUrls()
124
+ }
125
+
126
+ async function runPreview(positionals, options) {
127
+ const workspace = prepareStandaloneWorkspace({
128
+ contentDir: resolveContentDir(positionals, options),
129
+ outDir: options["out-dir"],
130
+ base: options.base,
131
+ title: options.title,
132
+ description: options.description,
133
+ })
134
+
135
+ process.chdir(resolveRepositoryRoot())
136
+ await serve({
137
+ root: workspace.root,
138
+ port: resolvePort(options.port, 4173),
139
+ base: options.base,
140
+ })
141
+ }
142
+
143
+ async function main() {
144
+ const { command, options, positionals } = parseArgs(process.argv.slice(2))
145
+
146
+ switch (command) {
147
+ case "build":
148
+ case "build-site":
149
+ await runBuild(positionals, options)
150
+ return
151
+ case "dev":
152
+ await runDev(positionals, options)
153
+ return
154
+ case "preview":
155
+ await runPreview(positionals, options)
156
+ return
157
+ case "prepare-content":
158
+ await runPrepareContent(positionals, options)
159
+ return
160
+ case "help":
161
+ case "--help":
162
+ case "-h":
163
+ printHelp()
164
+ return
165
+ default:
166
+ process.stderr.write(`Unknown command: ${command}\n\n`)
167
+ printHelp()
168
+ process.exitCode = 1
169
+ }
170
+ }
171
+
172
+ main().catch((error) => {
173
+ process.stderr.write(`${error.stack ?? error.message}\n`)
174
+ process.exit(1)
175
+ })
@@ -0,0 +1,218 @@
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs"
2
+ import { dirname, extname, join, relative } from "node:path"
3
+
4
+ export const CODE_EXTENSIONS = new Set([".js", ".ts", ".html"])
5
+ export const IGNORED_DIRS = new Set([
6
+ ".blog-cli",
7
+ ".git",
8
+ ".github",
9
+ ".vitepress",
10
+ "node_modules",
11
+ "dist",
12
+ "build",
13
+ ])
14
+
15
+ const LANGUAGE_MAP = {
16
+ ".html": "html",
17
+ ".js": "js",
18
+ ".ts": "ts",
19
+ }
20
+
21
+ function toPosixPath(filePath) {
22
+ return filePath.replace(/\\/g, "/")
23
+ }
24
+
25
+ function escapeMarkdown(value) {
26
+ return value.replace(/\\/g, "\\\\").replace(/`/g, "\\`")
27
+ }
28
+
29
+ function escapeHtml(value) {
30
+ return value
31
+ .replace(/&/g, "&amp;")
32
+ .replace(/</g, "&lt;")
33
+ .replace(/>/g, "&gt;")
34
+ .replace(/"/g, "&quot;")
35
+ }
36
+
37
+ function normalizeRouteBase(routeBase = "") {
38
+ const normalized = routeBase.replace(/\\/g, "/").trim().replace(/\/+$/g, "")
39
+
40
+ if (normalized === "" || normalized === "/") {
41
+ return ""
42
+ }
43
+
44
+ return normalized.startsWith("/") ? normalized : `/${normalized}`
45
+ }
46
+
47
+ function createSectionRoute(routeBase, section) {
48
+ const normalizedBase = normalizeRouteBase(routeBase)
49
+ return `${normalizedBase}/${section}/`.replace(/\/+/g, "/")
50
+ }
51
+
52
+ function createSnippetRoute(snippetRouteBase, relativePath) {
53
+ const normalizedBase = normalizeRouteBase(snippetRouteBase) || "/snippets"
54
+ const routePath = relativePath.replace(/\.[^/.]+$/, "")
55
+ return `${normalizedBase}/${routePath}`.replace(/\/+/g, "/")
56
+ }
57
+
58
+ function createSnippetPage(relativePath, options) {
59
+ const {
60
+ contentDir,
61
+ outputDir,
62
+ routeBase,
63
+ snippetRouteBase,
64
+ sourceLinkPrefix,
65
+ } = options
66
+ const sourcePath = join(contentDir, relativePath)
67
+ const extension = extname(relativePath).toLowerCase()
68
+ const title = relativePath.split("/").at(-1)
69
+ const code = readFileSync(sourcePath, "utf8")
70
+ const language = LANGUAGE_MAP[extension] ?? "text"
71
+ const outputPath = join(outputDir, relativePath.replace(/\.[^/.]+$/, ".md"))
72
+ const outputDirname = dirname(outputPath)
73
+ const originalPath = toPosixPath(sourcePath)
74
+ const pathSegments = relativePath.split("/")
75
+ const lineCount = code === "" ? 0 : code.split(/\r?\n/).length
76
+ const section = pathSegments[0] ?? "snippet"
77
+ const sectionRoute = createSectionRoute(routeBase, section)
78
+ const snippetRoute = createSnippetRoute(snippetRouteBase, relativePath)
79
+ const includeSourcePath = [sourceLinkPrefix, relativePath]
80
+ .filter(Boolean)
81
+ .map(toPosixPath)
82
+ .join("/")
83
+ const breadcrumbItems = pathSegments.map((segment, index) => {
84
+ const href =
85
+ index === pathSegments.length - 1
86
+ ? snippetRoute
87
+ : createSnippetRoute(snippetRouteBase, pathSegments.slice(0, index + 1).join("/"))
88
+
89
+ return `<a class="snippet-breadcrumb__link" href="${escapeHtml(href)}">${escapeHtml(segment)}</a>`
90
+ })
91
+
92
+ mkdirSync(outputDirname, { recursive: true })
93
+ writeFileSync(
94
+ outputPath,
95
+ `---
96
+ title: ${JSON.stringify(title)}
97
+ outline: false
98
+ pageClass: snippet-page
99
+ ---
100
+
101
+ <div class="snippet-breadcrumb">
102
+ <a class="snippet-breadcrumb__link" href="${escapeHtml(sectionRoute)}">Back to ${escapeHtml(section)}</a>
103
+ <span class="snippet-breadcrumb__divider">/</span>
104
+ ${breadcrumbItems.join('\n <span class="snippet-breadcrumb__divider">/</span>\n ')}
105
+ </div>
106
+
107
+ <div class="snippet-hero">
108
+ <span class="snippet-hero__chip">${escapeHtml(language.toUpperCase())}</span>
109
+ <span class="snippet-hero__chip">${lineCount} lines</span>
110
+ <span class="snippet-hero__chip">${escapeHtml(section)}</span>
111
+ </div>
112
+
113
+ # ${escapeMarkdown(title)}
114
+
115
+ <div class="snippet-meta">
116
+ <div class="snippet-meta__item">
117
+ <span class="snippet-meta__label">Source</span>
118
+ <code>${escapeHtml(originalPath)}</code>
119
+ </div>
120
+ <div class="snippet-meta__item">
121
+ <span class="snippet-meta__label">Route</span>
122
+ <code>${escapeHtml(snippetRoute)}</code>
123
+ </div>
124
+ <div class="snippet-meta__item">
125
+ <span class="snippet-meta__label">Section</span>
126
+ <a href="${escapeHtml(sectionRoute)}">${escapeHtml(sectionRoute)}</a>
127
+ </div>
128
+ </div>
129
+
130
+ <<< @/${includeSourcePath}
131
+ `,
132
+ )
133
+ }
134
+
135
+ function walk(dir, options) {
136
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
137
+ if (entry.name.startsWith(".") || IGNORED_DIRS.has(entry.name)) {
138
+ continue
139
+ }
140
+
141
+ const fullPath = join(dir, entry.name)
142
+
143
+ if (entry.isDirectory()) {
144
+ walk(fullPath, options)
145
+ continue
146
+ }
147
+
148
+ if (!entry.isFile()) {
149
+ continue
150
+ }
151
+
152
+ const extension = extname(entry.name).toLowerCase()
153
+
154
+ if (!CODE_EXTENSIONS.has(extension)) {
155
+ continue
156
+ }
157
+
158
+ createSnippetPage(toPosixPath(relative(options.contentDir, fullPath)), options)
159
+ }
160
+ }
161
+
162
+ export function generateSnippetPages(options = {}) {
163
+ const contentDir = options.contentDir ?? "myBlog"
164
+ const outputDir = options.outputDir ?? "snippets"
165
+ const routeBase = options.routeBase ?? "/myBlog"
166
+ const snippetRouteBase = options.snippetRouteBase ?? "/snippets"
167
+ const sourceLinkPrefix = options.sourceLinkPrefix ?? ""
168
+
169
+ if (existsSync(outputDir)) {
170
+ rmSync(outputDir, { recursive: true, force: true })
171
+ }
172
+
173
+ if (!existsSync(contentDir)) {
174
+ return { count: 0 }
175
+ }
176
+
177
+ const generationOptions = {
178
+ contentDir,
179
+ outputDir,
180
+ routeBase,
181
+ snippetRouteBase,
182
+ sourceLinkPrefix,
183
+ }
184
+
185
+ walk(contentDir, generationOptions)
186
+
187
+ let count = 0
188
+
189
+ if (existsSync(outputDir)) {
190
+ const stack = [outputDir]
191
+
192
+ while (stack.length > 0) {
193
+ const currentDir = stack.pop()
194
+
195
+ for (const entry of readdirSync(currentDir, { withFileTypes: true })) {
196
+ const fullPath = join(currentDir, entry.name)
197
+
198
+ if (entry.isDirectory()) {
199
+ stack.push(fullPath)
200
+ continue
201
+ }
202
+
203
+ if (entry.isFile() && extname(entry.name) === ".md") {
204
+ count += 1
205
+ }
206
+ }
207
+ }
208
+ }
209
+
210
+ return {
211
+ count,
212
+ contentDir,
213
+ outputDir,
214
+ routeBase,
215
+ snippetRouteBase,
216
+ sourceLinkPrefix,
217
+ }
218
+ }
@@ -0,0 +1,90 @@
1
+ import { existsSync, readdirSync } from "node:fs"
2
+ import { basename, extname, join } from "node:path"
3
+ import { defineConfig } from "vitepress"
4
+ import genNav from "vitepress-auto-navigation"
5
+
6
+ const CODE_EXTENSIONS = [".js", ".ts", ".html"]
7
+
8
+ function hasMarkdownContent(dir) {
9
+ if (!existsSync(dir)) {
10
+ return false
11
+ }
12
+
13
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
14
+ if (entry.name.startsWith(".")) {
15
+ continue
16
+ }
17
+
18
+ if (entry.isFile() && entry.name.endsWith(".md")) {
19
+ return true
20
+ }
21
+
22
+ if (entry.isDirectory() && hasMarkdownContent(join(dir, entry.name))) {
23
+ return true
24
+ }
25
+ }
26
+
27
+ return false
28
+ }
29
+
30
+ function createSnippetSidebar(autoNavigation) {
31
+ return Object.fromEntries(
32
+ Object.entries(autoNavigation.sidebar).map(([path, items]) => [`/snippets${path}`, items]),
33
+ )
34
+ }
35
+
36
+ export function createStandaloneSiteConfig(options = {}) {
37
+ const contentDir = options.contentDir ?? process.cwd()
38
+ const siteTitle = options.title ?? basename(contentDir)
39
+ const description =
40
+ options.description ?? `Static site generated from ${basename(contentDir)} with notespress.`
41
+ const siteBase = options.base ?? "/"
42
+ const hasContent = hasMarkdownContent(contentDir)
43
+ const autoNavigation = hasContent
44
+ ? genNav({
45
+ sourceDir: contentDir,
46
+ routeBase: "",
47
+ extensions: [".md", ...CODE_EXTENSIONS],
48
+ resolveText: (file) => {
49
+ const extension = extname(file.relativePath).toLowerCase()
50
+
51
+ if (!CODE_EXTENSIONS.includes(extension)) {
52
+ return file.name
53
+ }
54
+
55
+ return basename(file.relativePath)
56
+ },
57
+ resolveLink: (file) => {
58
+ const extension = extname(file.relativePath).toLowerCase()
59
+
60
+ if (!CODE_EXTENSIONS.includes(extension)) {
61
+ return file.routePath
62
+ }
63
+
64
+ return `/snippets/${file.relativePath.replace(/\.[^/.]+$/, "")}`
65
+ },
66
+ })
67
+ : { nav: [], sidebar: {} }
68
+ const snippetSidebar = createSnippetSidebar(autoNavigation)
69
+
70
+ return defineConfig({
71
+ base: siteBase,
72
+ cleanUrls: true,
73
+ title: siteTitle,
74
+ description,
75
+ lastUpdated: true,
76
+ outDir: options.outDir,
77
+ cacheDir: options.cacheDir,
78
+ themeConfig: {
79
+ nav: autoNavigation.nav,
80
+ sidebar: {
81
+ ...(hasContent ? autoNavigation.sidebar : {}),
82
+ ...(hasContent ? snippetSidebar : {}),
83
+ },
84
+ search: {
85
+ provider: "local",
86
+ },
87
+ socialLinks: [{ icon: "github", link: "https://github.com/Notryag" }],
88
+ },
89
+ })
90
+ }
@@ -0,0 +1,98 @@
1
+ import { createHash } from "node:crypto"
2
+ import { mkdirSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs"
3
+ import { basename, dirname, join, resolve } from "node:path"
4
+ import { fileURLToPath, pathToFileURL } from "node:url"
5
+
6
+ import { generateSnippetPages, IGNORED_DIRS } from "./snippets.mjs"
7
+
8
+ const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..")
9
+
10
+ function workspaceIdFor(contentDir) {
11
+ const hash = createHash("sha1").update(contentDir).digest("hex").slice(0, 8)
12
+ return `${basename(contentDir)}-${hash}`
13
+ }
14
+
15
+ function ensureDir(dir) {
16
+ mkdirSync(dir, { recursive: true })
17
+ }
18
+
19
+ function writeStandaloneConfig(workspaceRoot, options) {
20
+ const configDir = join(workspaceRoot, ".vitepress")
21
+ const themeDir = join(configDir, "theme")
22
+ const configModuleUrl = pathToFileURL(join(REPO_ROOT, "lib", "standalone-config.mjs")).href
23
+ const customCss = readFileSync(join(REPO_ROOT, "assets", "theme", "custom.css"), "utf8")
24
+
25
+ ensureDir(themeDir)
26
+ writeFileSync(
27
+ join(configDir, "config.mjs"),
28
+ `import { createStandaloneSiteConfig } from ${JSON.stringify(configModuleUrl)}
29
+
30
+ export default createStandaloneSiteConfig(${JSON.stringify(options, null, 2)})
31
+ `,
32
+ )
33
+ writeFileSync(
34
+ join(themeDir, "index.js"),
35
+ `import DefaultTheme from "vitepress/theme"
36
+ import "./custom.css"
37
+
38
+ export default DefaultTheme
39
+ `,
40
+ )
41
+ writeFileSync(join(themeDir, "custom.css"), customCss)
42
+ }
43
+
44
+ function mirrorContentEntries(contentDir, workspaceRoot) {
45
+ for (const entry of readdirSync(contentDir, { withFileTypes: true })) {
46
+ if (entry.name.startsWith(".") || IGNORED_DIRS.has(entry.name)) {
47
+ continue
48
+ }
49
+
50
+ const targetPath = join(contentDir, entry.name)
51
+ const linkPath = join(workspaceRoot, entry.name)
52
+ const linkType = entry.isDirectory() ? "dir" : "file"
53
+
54
+ symlinkSync(targetPath, linkPath, linkType)
55
+ }
56
+ }
57
+
58
+ export function resolveRepositoryRoot() {
59
+ return REPO_ROOT
60
+ }
61
+
62
+ export function prepareStandaloneWorkspace(options = {}) {
63
+ const contentDir = resolve(options.contentDir ?? process.cwd())
64
+ const stateRoot = resolve(options.stateDir ?? join(contentDir, ".blog-cli"))
65
+ const workspaceRoot =
66
+ options.workspaceDir ??
67
+ join(stateRoot, "workspaces", workspaceIdFor(contentDir))
68
+ const outDir = resolve(options.outDir ?? join(contentDir, "dist"))
69
+ const cacheDir = resolve(options.cacheDir ?? join(stateRoot, "cache", workspaceIdFor(contentDir)))
70
+
71
+ rmSync(workspaceRoot, { recursive: true, force: true })
72
+ ensureDir(workspaceRoot)
73
+ ensureDir(dirname(cacheDir))
74
+
75
+ mirrorContentEntries(contentDir, workspaceRoot)
76
+ generateSnippetPages({
77
+ contentDir,
78
+ outputDir: join(workspaceRoot, "snippets"),
79
+ routeBase: "",
80
+ snippetRouteBase: "/snippets",
81
+ sourceLinkPrefix: "",
82
+ })
83
+ writeStandaloneConfig(workspaceRoot, {
84
+ contentDir,
85
+ title: options.title,
86
+ description: options.description,
87
+ base: options.base,
88
+ outDir,
89
+ cacheDir,
90
+ })
91
+
92
+ return {
93
+ root: workspaceRoot,
94
+ outDir,
95
+ cacheDir,
96
+ contentDir,
97
+ }
98
+ }
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "notespress",
3
+ "version": "0.1.0",
4
+ "description": "Generate a VitePress blog site directly from a Markdown notes directory.",
5
+ "license": "MIT",
6
+ "packageManager": "pnpm@10.30.3",
7
+ "type": "module",
8
+ "files": [
9
+ "assets",
10
+ "bin",
11
+ "lib",
12
+ "README.md"
13
+ ],
14
+ "keywords": [
15
+ "blog",
16
+ "cli",
17
+ "markdown",
18
+ "notes",
19
+ "vitepress"
20
+ ],
21
+ "engines": {
22
+ "node": ">=20"
23
+ },
24
+ "bin": {
25
+ "notespress": "./bin/blog-cli.mjs"
26
+ },
27
+ "scripts": {
28
+ "prepare:content": "node scripts/generate-snippets.mjs",
29
+ "dev": "pnpm prepare:content && vitepress dev",
30
+ "build": "pnpm prepare:content && vitepress build",
31
+ "preview": "vitepress preview",
32
+ "notespress:dev": "node ./bin/blog-cli.mjs dev myBlog",
33
+ "notespress:build": "node ./bin/blog-cli.mjs build myBlog",
34
+ "notespress:preview": "node ./bin/blog-cli.mjs preview myBlog",
35
+ "docs:build": "pnpm build",
36
+ "docs:preview": "pnpm preview"
37
+ },
38
+ "dependencies": {
39
+ "vitepress": "^1.6.4",
40
+ "vue": "^3.5.13",
41
+ "vitepress-auto-navigation": "^0.2.0"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^25.4.0",
45
+ "typescript": "^5.9.3"
46
+ }
47
+ }