@x-withu/page-withu 0.1.2 → 1.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 CHANGED
@@ -41,18 +41,16 @@
41
41
  # 安装 page-withu
42
42
  npm install -g @x-withu/page-withu
43
43
 
44
- # 创建项目(会依次询问站点标题、作者名、标签页标题、favicon、GitHub 链接和邮箱)
45
- page-withu create my-homepage
44
+ # 创建项目,默认配置会写入 config.js,后续可自行编辑
45
+ page-withu new my-homepage
46
46
 
47
- # 也可以通过参数直接指定
48
- page-withu create my-homepage --title "My Site" --tab-title "My Blog" --favicon /favicon.svg
49
-
50
- # 安装依赖
51
47
  cd my-homepage
52
- npm install
53
48
 
54
- # 预览网站
55
- npm run dev
49
+ # 本地预览
50
+ page-withu serve
51
+
52
+ # 构建分发产物
53
+ page-withu build
56
54
  ```
57
55
 
58
56
  下一步?[配置网站](./docs/setup.md)、[部署网站](./docs/deploy.md)、[开发网站](./docs/develop.md)。
package/bin/page-withu.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { createHash } from 'node:crypto';
2
+ import { spawn } from 'node:child_process';
3
+ import { createRequire } from 'node:module';
3
4
  import { Command } from 'commander';
4
- import inquirer from 'inquirer';
5
5
  import fs from 'fs-extra';
6
6
  import path from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
@@ -12,120 +12,16 @@ const pkg = await fs.readJson(new URL('../package.json', import.meta.url));
12
12
 
13
13
  const __filename = fileURLToPath(import.meta.url);
14
14
  const __dirname = path.dirname(__filename);
15
- const templateDir = path.resolve(__dirname, '../template');
16
- const manifestName = '.page-withu.json';
17
- const managedRoots = ['src'];
18
- const managedFiles = ['index.html', 'vite.config.js'];
19
- const generatedGitignore = `node_modules/
20
- dist/
21
-
22
- .env
23
- .env.*
24
- !.env.example
25
- *.local
26
-
27
- .DS_Store
28
- .idea/
29
- `;
30
-
15
+ const packageRoot = path.resolve(__dirname, '..');
16
+ const templateDir = path.join(packageRoot, 'template');
17
+ const viteConfigPath = path.join(templateDir, 'vite.config.js');
18
+ const require = createRequire(import.meta.url);
19
+ const vitePackageDir = path.dirname(require.resolve('vite/package.json'));
20
+ const viteBin = path.join(vitePackageDir, 'bin/vite.js');
31
21
  const program = new Command();
32
22
 
33
- function hashContent(content) {
34
- return createHash('sha256').update(content).digest('hex');
35
- }
36
-
37
- async function hashFile(filePath) {
38
- return hashContent(await fs.readFile(filePath));
39
- }
40
-
41
- function toPosixPath(value) {
42
- return value.split(path.sep).join('/');
43
- }
44
-
45
- async function listFiles(dir) {
46
- const entries = await fs.readdir(dir, { withFileTypes: true });
47
- const files = [];
48
-
49
- for (const entry of entries) {
50
- const entryPath = path.join(dir, entry.name);
51
- if (entry.isDirectory()) {
52
- files.push(...await listFiles(entryPath));
53
- } else if (entry.isFile()) {
54
- files.push(entryPath);
55
- }
56
- }
57
-
58
- return files;
59
- }
60
-
61
- async function listManagedTemplateFiles() {
62
- const files = [];
63
-
64
- for (const file of managedFiles) {
65
- if (await fs.pathExists(path.join(templateDir, file))) files.push(file);
66
- }
67
-
68
- for (const root of managedRoots) {
69
- const rootPath = path.join(templateDir, root);
70
- if (!await fs.pathExists(rootPath)) continue;
71
- const rootFiles = await listFiles(rootPath);
72
- files.push(...rootFiles.map((file) => toPosixPath(path.relative(templateDir, file))));
73
- }
74
-
75
- return files.sort((a, b) => a.localeCompare(b));
76
- }
77
-
78
- async function createManifest(projectDir, previousManifest = null) {
79
- const files = {};
80
-
81
- for (const file of await listManagedTemplateFiles()) {
82
- const projectFile = path.join(projectDir, file);
83
- const templateFile = path.join(templateDir, file);
84
- if (!await fs.pathExists(projectFile)) continue;
85
-
86
- const projectHash = await hashFile(projectFile);
87
- const templateHash = await hashFile(templateFile);
88
- if (projectHash === templateHash) {
89
- files[file] = templateHash;
90
- } else if (previousManifest?.files?.[file]) {
91
- files[file] = previousManifest.files[file];
92
- }
93
- }
94
-
95
- return {
96
- templateVersion: pkg.version,
97
- updatedAt: new Date().toISOString(),
98
- files,
99
- };
100
- }
101
-
102
- async function writeManifest(projectDir, dryRun, previousManifest = null) {
103
- const manifest = await createManifest(projectDir, previousManifest);
104
- if (!dryRun) {
105
- await fs.writeJson(path.join(projectDir, manifestName), manifest, { spaces: 2 });
106
- }
107
- return manifest;
108
- }
109
-
110
- function renderConfig({ title, tabTitle, favicon, author, footerLinks }) {
111
- const links = footerLinks.map((link) => ` { label: ${JSON.stringify(link.label)}, url: ${JSON.stringify(link.url)} }`);
112
- return `export default {
113
- title: ${JSON.stringify(title)},
114
- tabTitle: ${JSON.stringify(tabTitle)},
115
- favicon: ${JSON.stringify(favicon)},
116
- author: ${JSON.stringify(author)},
117
- year: new Date().getFullYear(),
118
- footerLinks: [
119
- ${links.join(',\n')}
120
- ],
121
- pagination: {
122
- pageSize: 5
123
- }
124
- }`;
125
- }
126
-
127
23
  async function detectProject(projectDir) {
128
- const required = ['package.json', 'config.js', 'content', 'src', 'vite.config.js'];
24
+ const required = ['config.js', 'content/index.md', 'content/domains.md'];
129
25
  const missing = [];
130
26
 
131
27
  for (const item of required) {
@@ -135,277 +31,85 @@ async function detectProject(projectDir) {
135
31
  return missing;
136
32
  }
137
33
 
138
- async function loadManifest(projectDir) {
139
- const manifestPath = path.join(projectDir, manifestName);
140
- if (!await fs.pathExists(manifestPath)) return null;
141
- return fs.readJson(manifestPath);
142
- }
143
-
144
- async function ensureGeneratedGitignore(projectDir, dryRun) {
145
- const gitignorePath = path.join(projectDir, '.gitignore');
146
- const npmignorePath = path.join(projectDir, '.npmignore');
147
- const hasGitignore = await fs.pathExists(gitignorePath);
148
- const hasNpmignore = await fs.pathExists(npmignorePath);
149
-
150
- if (dryRun) return !hasGitignore || hasNpmignore;
151
- if (hasNpmignore) await fs.remove(npmignorePath);
152
- if (!hasGitignore) await fs.writeFile(gitignorePath, generatedGitignore);
153
-
154
- return !hasGitignore || hasNpmignore;
155
- }
156
-
157
- async function shouldOverwriteConflict(file, options) {
158
- if (options.force) return true;
159
- if (options.yes || options.dryRun) return false;
160
-
161
- const answer = await inquirer.prompt([{
162
- type: 'confirm',
163
- name: 'overwrite',
164
- message: `${file} has local changes. Overwrite it?`,
165
- default: false,
166
- }]);
167
-
168
- return answer.overwrite;
169
- }
170
-
171
- async function planTemplateUpdates(projectDir, options) {
172
- const manifest = await loadManifest(projectDir);
173
- const files = await listManagedTemplateFiles();
174
- const result = {
175
- added: [],
176
- updated: [],
177
- skipped: [],
178
- unchanged: [],
179
- };
180
-
181
- for (const file of files) {
182
- const sourceFile = path.join(templateDir, file);
183
- const targetFile = path.join(projectDir, file);
184
- const sourceHash = await hashFile(sourceFile);
185
- const exists = await fs.pathExists(targetFile);
186
-
187
- if (!exists) {
188
- result.added.push(file);
189
- if (!options.dryRun) await fs.copy(sourceFile, targetFile);
190
- continue;
191
- }
192
-
193
- const currentHash = await hashFile(targetFile);
194
- if (currentHash === sourceHash) {
195
- result.unchanged.push(file);
196
- continue;
197
- }
34
+ async function createProject(projectName = 'my-homepage') {
35
+ const targetDir = projectName.trim() || 'my-homepage';
36
+ const fullPath = path.resolve(process.cwd(), targetDir);
198
37
 
199
- const previousHash = manifest?.files?.[file];
200
- const safeToUpdate = previousHash && currentHash === previousHash;
201
- const overwrite = safeToUpdate || await shouldOverwriteConflict(file, options);
202
-
203
- if (overwrite) {
204
- result.updated.push(file);
205
- if (!options.dryRun) await fs.copy(sourceFile, targetFile);
206
- } else {
207
- result.skipped.push(file);
208
- }
38
+ if (await fs.pathExists(fullPath)) {
39
+ console.error(chalk.red(`\nError: Directory ${targetDir} already exists.`));
40
+ process.exit(1);
209
41
  }
210
42
 
211
- return result;
212
- }
213
-
214
- function mergeSection(projectPkg, templatePkg, key) {
215
- const next = { ...(projectPkg[key] || {}) };
216
- let changed = false;
217
-
218
- for (const [name, value] of Object.entries(templatePkg[key] || {})) {
219
- if (next[name] !== value) {
220
- next[name] = value;
221
- changed = true;
222
- }
43
+ const spinner = ora('Creating project...').start();
44
+
45
+ try {
46
+ await fs.ensureDir(fullPath);
47
+ await fs.copy(path.join(templateDir, 'config.js'), path.join(fullPath, 'config.js'));
48
+ await fs.copy(path.join(templateDir, 'content'), path.join(fullPath, 'content'));
49
+ await fs.ensureDir(path.join(fullPath, 'content/blog'));
50
+
51
+ spinner.succeed(chalk.green(`Successfully created PageWithU project in ${targetDir}!`));
52
+ console.log('\nNext steps:');
53
+ console.log(chalk.cyan(` cd ${targetDir}`));
54
+ console.log(chalk.cyan(' page-withu serve'));
55
+ console.log(chalk.cyan(' page-withu build\n'));
56
+ } catch (err) {
57
+ spinner.fail(chalk.red('Failed to create project.'));
58
+ console.error(err);
59
+ process.exit(1);
223
60
  }
224
-
225
- if (changed) projectPkg[key] = next;
226
- return changed;
227
61
  }
228
62
 
229
- async function mergePackage(projectDir, dryRun) {
230
- const projectPkgPath = path.join(projectDir, 'package.json');
231
- const templatePkg = await fs.readJson(path.join(templateDir, 'package.json'));
232
- const projectPkg = await fs.readJson(projectPkgPath);
233
- const before = JSON.stringify(projectPkg);
234
-
235
- mergeSection(projectPkg, templatePkg, 'scripts');
236
- mergeSection(projectPkg, templatePkg, 'dependencies');
237
- mergeSection(projectPkg, templatePkg, 'devDependencies');
238
-
239
- const changed = JSON.stringify(projectPkg) !== before;
240
- if (changed && !dryRun) {
241
- await fs.writeJson(projectPkgPath, projectPkg, { spaces: 2 });
63
+ async function runVite(command, args) {
64
+ const projectRoot = process.cwd();
65
+ const missing = await detectProject(projectRoot);
66
+ if (missing.length) {
67
+ console.error(chalk.red('Current directory does not look like a PageWithU project.'));
68
+ console.error(chalk.yellow(`Missing: ${missing.join(', ')}`));
69
+ process.exit(1);
242
70
  }
243
71
 
244
- return changed;
245
- }
246
-
247
- function printUpdateSummary(summary, dryRun) {
248
- const label = dryRun ? 'Would update' : 'Updated';
72
+ const viteArgs = command === 'serve'
73
+ ? ['--config', viteConfigPath, ...args]
74
+ : ['build', '--config', viteConfigPath, ...args];
75
+ const child = spawn(process.execPath, [viteBin, ...viteArgs], {
76
+ cwd: projectRoot,
77
+ stdio: 'inherit',
78
+ env: {
79
+ ...process.env,
80
+ PAGE_WITHU_PROJECT_ROOT: projectRoot,
81
+ },
82
+ });
249
83
 
250
- if (summary.added.length) console.log(chalk.green(`${label} ${summary.added.length} missing file(s).`));
251
- if (summary.updated.length) console.log(chalk.green(`${label} ${summary.updated.length} template file(s).`));
252
- if (summary.packageChanged) console.log(chalk.green(`${label} package.json dependencies and scripts.`));
253
- if (summary.gitignoreChanged) console.log(chalk.green(`${label} .gitignore.`));
254
- if (summary.skipped.length) {
255
- console.log(chalk.yellow(`Skipped ${summary.skipped.length} file(s) with local changes:`));
256
- for (const file of summary.skipped) console.log(chalk.yellow(` - ${file}`));
257
- }
258
- if (!summary.added.length && !summary.updated.length && !summary.packageChanged && !summary.gitignoreChanged && !summary.skipped.length) {
259
- console.log(chalk.green('Page With U project is already up to date.'));
260
- }
261
- if (!dryRun && (summary.added.length || summary.updated.length || summary.packageChanged || summary.gitignoreChanged)) {
262
- console.log(chalk.cyan('\nNext steps:'));
263
- console.log(chalk.cyan(' npm install'));
264
- console.log(chalk.cyan(' npm run build'));
265
- }
84
+ child.on('exit', (code, signal) => {
85
+ if (signal) {
86
+ process.kill(process.pid, signal);
87
+ return;
88
+ }
89
+ process.exit(code ?? 1);
90
+ });
266
91
  }
267
92
 
268
93
  program
269
94
  .name('page-withu')
270
- .description('CLI to scaffold a lightweight, elegant personal homepage')
95
+ .description('CLI to scaffold, preview, and build a lightweight personal homepage')
271
96
  .version(pkg.version);
272
97
 
273
98
  program
274
- .command('create [project-name]')
275
- .description('create a new PageWithU project')
276
- .option('-t, --title <title>', 'site title')
277
- .option('--tab-title <title>', 'browser tab title')
278
- .option('--favicon <path-or-url>', 'browser tab favicon path or URL')
279
- .option('-a, --author <author>', 'author name')
280
- .option('-g, --github <github>', 'github url')
281
- .option('-e, --email <email>', 'email address')
282
- .action(async (projectName, options) => {
283
- let targetDir = projectName;
284
-
285
- if (!targetDir) {
286
- const answers = await inquirer.prompt([
287
- {
288
- type: 'input',
289
- name: 'projectName',
290
- message: 'What is your project named?',
291
- default: 'my-homepage',
292
- },
293
- ]);
294
- targetDir = answers.projectName;
295
- }
296
-
297
- const fullPath = path.resolve(process.cwd(), targetDir);
298
-
299
- if (fs.existsSync(fullPath)) {
300
- console.error(chalk.red(`\nError: Directory ${targetDir} already exists.`));
301
- process.exit(1);
302
- }
303
-
304
- const title = options.title || (await inquirer.prompt([{
305
- type: 'input',
306
- name: 'title',
307
- message: 'Site Title:',
308
- default: 'PageWithU',
309
- }])).title;
310
-
311
- const author = options.author || (await inquirer.prompt([{
312
- type: 'input',
313
- name: 'author',
314
- message: 'Author Name:',
315
- default: 'Your Name',
316
- }])).author;
317
-
318
- const tabTitle = options.tabTitle || (await inquirer.prompt([{
319
- type: 'input',
320
- name: 'tabTitle',
321
- message: 'Browser Tab Title:',
322
- default: title,
323
- }])).tabTitle;
324
-
325
- const favicon = options.favicon || (await inquirer.prompt([{
326
- type: 'input',
327
- name: 'favicon',
328
- message: 'Favicon path or URL:',
329
- default: '/src/assets/bulb.svg',
330
- }])).favicon;
331
-
332
- const github = options.github !== undefined ? options.github : (await inquirer.prompt([{
333
- type: 'input',
334
- name: 'github',
335
- message: 'GitHub URL (optional):',
336
- }])).github;
337
-
338
- const email = options.email !== undefined ? options.email : (await inquirer.prompt([{
339
- type: 'input',
340
- name: 'email',
341
- message: 'Email (optional):',
342
- }])).email;
343
-
344
- const spinner = ora('Creating project...').start();
345
-
346
- try {
347
- await fs.copy(templateDir, fullPath);
348
-
349
- const footerLinks = [];
350
- if (github) footerLinks.push({ label: 'GitHub', url: github });
351
- if (email) footerLinks.push({ label: 'Email', url: `mailto:${email}` });
352
-
353
- await fs.writeFile(path.join(fullPath, 'config.js'), renderConfig({ title, tabTitle, favicon, author, footerLinks }));
354
-
355
- const pkgPath = path.join(fullPath, 'package.json');
356
- const projectPkg = await fs.readJson(pkgPath);
357
- projectPkg.name = targetDir.toLowerCase().replace(/[^a-z0-9-]/g, '-');
358
- await fs.writeJson(pkgPath, projectPkg, { spaces: 2 });
359
-
360
- await fs.ensureDir(path.join(fullPath, 'content/blog'));
361
- await ensureGeneratedGitignore(fullPath, false);
362
- await writeManifest(fullPath, false);
363
-
364
- spinner.succeed(chalk.green(`Successfully created PageWithU project in ${targetDir}!`));
365
- console.log('\nNext steps:');
366
- console.log(chalk.cyan(` cd ${targetDir}`));
367
- console.log(chalk.cyan(' npm install'));
368
- console.log(chalk.cyan(' npm run dev\n'));
369
-
370
- } catch (err) {
371
- spinner.fail(chalk.red('Failed to create project.'));
372
- console.error(err);
373
- process.exit(1);
374
- }
375
- });
99
+ .command('new [project-name]')
100
+ .description('create a new PageWithU project with the default content and config')
101
+ .action(createProject);
376
102
 
377
103
  program
378
- .command('update')
379
- .description('update an existing PageWithU project with the latest template')
380
- .option('--dry-run', 'show what would change without writing files')
381
- .option('--yes', 'skip prompts and apply safe updates')
382
- .option('--force', 'overwrite changed template-managed files')
383
- .action(async (options) => {
384
- const projectDir = process.cwd();
385
- const spinner = ora(options.dryRun ? 'Checking project update...' : 'Updating project...').start();
386
-
387
- try {
388
- const missing = await detectProject(projectDir);
389
- if (missing.length) {
390
- spinner.fail(chalk.red('Current directory does not look like a PageWithU project.'));
391
- console.error(chalk.yellow(`Missing: ${missing.join(', ')}`));
392
- process.exit(1);
393
- }
104
+ .command('serve [args...]')
105
+ .description('start local preview for the current PageWithU project')
106
+ .allowUnknownOption(true)
107
+ .action((args = []) => runVite('serve', args));
394
108
 
395
- spinner.stop();
396
- const previousManifest = await loadManifest(projectDir);
397
- const updates = await planTemplateUpdates(projectDir, options);
398
- const packageChanged = await mergePackage(projectDir, options.dryRun);
399
- const gitignoreChanged = await ensureGeneratedGitignore(projectDir, options.dryRun);
400
-
401
- if (!options.dryRun) await writeManifest(projectDir, false, previousManifest);
402
-
403
- printUpdateSummary({ ...updates, packageChanged, gitignoreChanged }, options.dryRun);
404
- } catch (err) {
405
- spinner.fail(chalk.red('Failed to update project.'));
406
- console.error(err);
407
- process.exit(1);
408
- }
409
- });
109
+ program
110
+ .command('build [args...]')
111
+ .description('build dist assets for the current PageWithU project')
112
+ .allowUnknownOption(true)
113
+ .action((args = []) => runVite('build', args));
410
114
 
411
115
  program.parse();
package/docs/deploy.md CHANGED
@@ -7,7 +7,7 @@
7
7
  构建生产版本:
8
8
 
9
9
  ```bash
10
- npm run build
10
+ page-withu build
11
11
  ```
12
12
 
13
13
  构建产物会输出到:
@@ -28,10 +28,10 @@ blog/<slug>.html
28
28
 
29
29
  这样可以直接部署到 GitHub Pages、Cloudflare Pages、OSS 等对象存储上。
30
30
 
31
- 预览生产版本:
31
+ 预览生产版本可以在构建后用任意静态服务器打开 `dist/`,例如:
32
32
 
33
33
  ```bash
34
- npm run preview
34
+ npx serve dist
35
35
  ```
36
36
 
37
37
  如果觉得没什么问题,就可以把这些文件上传到服务器进行部署了。
@@ -75,9 +75,8 @@ jobs:
75
75
  - uses: actions/setup-node@v4
76
76
  with:
77
77
  node-version: 20
78
- cache: npm
79
- - run: npm ci
80
- - run: npm run build
78
+ - run: npm install -g @x-withu/page-withu
79
+ - run: page-withu build
81
80
  - uses: actions/upload-pages-artifact@v3
82
81
  with:
83
82
  path: dist
@@ -104,7 +103,7 @@ Settings → Pages → Build and deployment → Source → GitHub Actions
104
103
  如果仓库名不是用于根域名的 `用户名.github.io`,通常需要给构建命令设置子路径。例如仓库名是 `my-homepage`:
105
104
 
106
105
  ```yaml
107
- - run: BASE_PATH=/my-homepage/ npm run build
106
+ - run: BASE_PATH=/my-homepage/ page-withu build
108
107
  ```
109
108
 
110
109
  ### 方式二:手动上传 dist
@@ -112,7 +111,7 @@ Settings → Pages → Build and deployment → Source → GitHub Actions
112
111
  如果不想使用 GitHub Actions,也可以本地构建后手动上传 `dist/`。
113
112
 
114
113
  ```bash
115
- npm run build
114
+ page-withu build
116
115
  ```
117
116
 
118
117
  然后把 `dist/` 内容上传到用于 GitHub Pages 的发布分支或目录。
@@ -132,7 +131,7 @@ Cloudflare Pages 适合托管静态站点,并且可以自动连接 GitHub 仓
132
131
 
133
132
  ```text
134
133
  Framework preset: None 或 Vue
135
- Build command: npm run build
134
+ Build command: npm install -g @x-withu/page-withu && page-withu build
136
135
  Build output directory: dist
137
136
  Root directory: /
138
137
  ```
@@ -177,7 +176,7 @@ Root directory: /
177
176
  本地构建:
178
177
 
179
178
  ```bash
180
- npm run build
179
+ page-withu build
181
180
  ```
182
181
 
183
182
  然后把 `dist/` 中的所有文件上传到 Bucket 根目录。
@@ -201,7 +200,7 @@ ossutil cp -r dist/ oss://your-bucket-name/ --update
201
200
  每次更新内容后重新执行:
202
201
 
203
202
  ```bash
204
- npm run build
203
+ page-withu build
205
204
  ossutil cp -r dist/ oss://your-bucket-name/ --update
206
205
  ```
207
206
 
package/docs/develop.md CHANGED
@@ -9,7 +9,7 @@ Page With U 的目标是保持轻量、清晰、容易定制。开发时建议
9
9
  - 优先修改已有文件,不要为了小功能引入复杂架构。
10
10
  - 模板默认配置要适合大多数用户开箱即用。
11
11
  - 文档中的用户流程要始终和 CLI 行为保持一致。
12
- - 修改 `template/` 后,需要用模板本身或新生成项目执行一次 `npm run build`。
12
+ - 修改 `template/` 后,需要用模板本身或新生成项目执行一次 `page-withu build`。
13
13
  - 不要引入大型 UI 框架,样式优先维护在 `template/src/styles/main.css`。
14
14
  - Markdown 能力优先在 `template/vite.config.js` 中集中处理,避免分散到多个组件。
15
15
 
@@ -21,12 +21,11 @@ page-withu/
21
21
  │ └── page-withu.js # CLI 入口
22
22
  ├── docs/ # 使用、部署、开发文档
23
23
  ├── template/
24
- │ ├── config.js # 模板默认配置
25
- │ ├── content/ # 模板默认内容
26
- │ ├── src/ # Vue 应用源码
27
- ├── vite.config.js # Markdown 渲染与静态路由生成
28
- │ └── package.json # 生成项目后的应用依赖
29
- ├── package.json # page-withu CLI 自身配置
24
+ │ ├── config.js # 新项目默认配置
25
+ │ ├── content/ # 新项目默认内容
26
+ │ ├── src/ # 包内 Vue 运行时源码
27
+ └── vite.config.js # 包内 Markdown 渲染与静态路由生成
28
+ ├── package.json # page-withu CLI 与运行时依赖配置
30
29
  └── README.md
31
30
  ```
32
31
 
@@ -46,18 +45,18 @@ bin/page-withu.js
46
45
 
47
46
  当前 CLI 主要负责:
48
47
 
49
- 1. 读取命令行参数或交互式输入。
50
- 2. 复制 `template/` 到目标目录。
51
- 3. 根据用户输入生成目标项目的 `config.js`。
52
- 4. 修改目标项目 `package.json` 的 `name` 字段。
53
- 5. 输出下一步命令提示。
48
+ 1. `new`:复制默认 `config.js` 和 `content/` 到目标目录。
49
+ 2. `serve`:用当前安装的包内运行时,在当前主页项目中启动本地预览。
50
+ 3. `build`:用当前安装的包内运行时,在当前主页项目中构建 `dist` 产物。
54
51
 
55
52
  本地调试 CLI:
56
53
 
57
54
  ```bash
58
55
  node bin/page-withu.js --help
59
- node bin/page-withu.js create my-homepage
60
- node bin/page-withu.js update --dry-run
56
+ node bin/page-withu.js new my-homepage
57
+ cd my-homepage
58
+ node /path/to/page-withu/bin/page-withu.js serve
59
+ node /path/to/page-withu/bin/page-withu.js build
61
60
  ```
62
61
 
63
62
  如果想全局挂载本地版本:
@@ -65,14 +64,12 @@ node bin/page-withu.js update --dry-run
65
64
  ```bash
66
65
  npm link
67
66
  page-withu --help
68
- page-withu create my-homepage
67
+ page-withu new my-homepage
69
68
  ```
70
69
 
71
- 新增 CLI 参数时,通常需要同步修改:
70
+ 调整 CLI 行为时,通常需要同步修改:
72
71
 
73
- - `bin/page-withu.js` 中的 commander option
74
- - 交互式 prompt 的默认值。
75
- - 生成 `config.js` 的逻辑。
72
+ - `bin/page-withu.js` 中的 commander command
76
73
  - 必要时修改 `template/config.js` 的默认字段。
77
74
  - 必要时修改模板中读取配置的 Vue 组件。
78
75
  - 必要时更新 `README.md` 和 `docs/setup.md`。
@@ -82,13 +79,12 @@ page-withu create my-homepage
82
79
  ```bash
83
80
  mkdir -p /tmp/page-withu-test
84
81
  cd /tmp/page-withu-test
85
- node /path/to/page-withu/bin/page-withu.js create demo-site --title "Demo" --author "Tester"
82
+ node /path/to/page-withu/bin/page-withu.js new demo-site
86
83
  cd demo-site
87
- npm install
88
- npm run build
84
+ node /path/to/page-withu/bin/page-withu.js build
89
85
  ```
90
86
 
91
- 如果 `npm run build` 成功,说明模板可以被正常生成和构建。
87
+ 如果构建成功,说明内容型项目可以通过包内运行时正常生成。
92
88
 
93
89
  ## 开发 Template
94
90
 
@@ -102,8 +98,7 @@ template/
102
98
 
103
99
  ```bash
104
100
  cd template
105
- npm install
106
- npm run dev
101
+ node ../bin/page-withu.js serve
107
102
  ```
108
103
 
109
104
  常见开发位置:
@@ -127,7 +122,7 @@ npm run dev
127
122
 
128
123
  ```bash
129
124
  cd template
130
- npm run build
125
+ node ../bin/page-withu.js build
131
126
  ```
132
127
 
133
128
  如果改动影响脚手架生成后的项目,还需要用 CLI 重新生成一个临时项目并构建。
@@ -202,11 +197,11 @@ npm publish --access public
202
197
 
203
198
  ```bash
204
199
  npm install -g @x-withu/page-withu
205
- page-withu create my-homepage
200
+ page-withu new my-homepage
206
201
  ```
207
202
 
208
203
  或在线构建无需本地安装:
209
204
 
210
205
  ```bash
211
- npx @x-withu/page-withu create my-homepage
206
+ npx @x-withu/page-withu new my-homepage
212
207
  ```
package/docs/setup.md CHANGED
@@ -83,36 +83,21 @@ index_img: https://example.com/cover.jpg
83
83
  - `tags`:文章标签。
84
84
  - `index_img`:博客列表封面图,可选;不填会保留占位布局。
85
85
 
86
- ## 更新项目
86
+ ## CLI 命令
87
87
 
88
- 如果你已经用 `page-withu create` 创建过项目,后续升级 CLI 后可以在项目目录中更新模板能力:
88
+ Page With U 只提供三个常用命令:
89
89
 
90
90
  ```bash
91
- npm install -g @x-withu/page-withu
92
- cd my-homepage
93
- page-withu update --dry-run
94
- page-withu update
91
+ page-withu new my-homepage
92
+ page-withu serve
93
+ page-withu build
95
94
  ```
96
95
 
97
- `page-withu update` 会更新模板管理的文件,例如 `src/`、`index.html` 和 `vite.config.js`,并合并 `package.json` 中模板需要的脚本和依赖。
96
+ - `page-withu new my-homepage`:新建主页项目,所有站点配置都保持模板默认值。
97
+ - `page-withu serve`:使用当前安装的 Page With U 运行时启动本地预览。
98
+ - `page-withu build`:使用当前安装的 Page With U 运行时构建 `dist` 产物。
98
99
 
99
- 为了避免覆盖你的内容,以下文件默认不会被更新:
100
-
101
- - `config.js`
102
- - `content/`
103
-
104
- 如果你修改过模板源码,更新时会跳过有冲突的文件并在终端列出来。确认要覆盖这些模板源码时,可以使用:
105
-
106
- ```bash
107
- page-withu update --force
108
- ```
109
-
110
- 更新完成后建议执行:
111
-
112
- ```bash
113
- npm install
114
- npm run build
115
- ```
100
+ 创建完成后,直接编辑 `config.js`、`content/index.md`、`content/domains.md` 和 `content/blog/` 即可定制网站。
116
101
 
117
102
  ## Markdown
118
103
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@x-withu/page-withu",
3
- "version": "0.1.2",
3
+ "version": "1.1.0",
4
4
  "description": "A lightweight, elegant personal homepage generator.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -16,7 +16,6 @@
16
16
  "template/content/",
17
17
  "template/src/",
18
18
  "template/index.html",
19
- "template/package.json",
20
19
  "template/vite.config.js",
21
20
  "docs/",
22
21
  "README.md"
@@ -34,10 +33,20 @@
34
33
  "author": "Explorer-Dong",
35
34
  "license": "MIT",
36
35
  "dependencies": {
36
+ "@traptitech/markdown-it-katex": "^3.6.0",
37
+ "@vitejs/plugin-vue": "^5.2.3",
37
38
  "chalk": "^5.3.0",
38
39
  "commander": "^12.1.0",
39
40
  "fs-extra": "^11.2.0",
40
- "inquirer": "^9.3.2",
41
- "ora": "^8.0.1"
41
+ "gray-matter": "^4.0.3",
42
+ "highlight.js": "^11.11.1",
43
+ "katex": "^0.16.45",
44
+ "markdown-it": "^14.1.0",
45
+ "markdown-it-emoji": "^3.0.0",
46
+ "markdown-it-footnote": "^4.0.0",
47
+ "mermaid": "^11.15.0",
48
+ "ora": "^8.0.1",
49
+ "vite": "^6.3.1",
50
+ "vue": "^3.5.13"
42
51
  }
43
52
  }
@@ -5,7 +5,7 @@ export default {
5
5
  author: "Your Name",
6
6
  year: new Date().getFullYear(),
7
7
  footerLinks: [
8
- { label: 'GitHub', url: 'https://github.com' },
8
+ { label: 'GitHub', url: 'https://github.com/Explorer-Dong/page-withu' },
9
9
  { label: 'Email', url: 'mailto:youremail@example.com' }
10
10
  ],
11
11
  pagination: {
@@ -54,16 +54,16 @@
54
54
 
55
55
  <script setup>
56
56
  import { ref, computed, onMounted, onUnmounted } from 'vue'
57
- import userConfig from '../config.js'
57
+ import userConfig from '@page-withu/user-config'
58
58
  import ThemeToggle from './components/theme_toggle.vue'
59
59
  import SiteCard from './components/site_card.vue'
60
60
  import BlogList from './components/blog-list.vue'
61
61
  import BlogDetail from './components/blog-detail.vue'
62
- import { frontmatter as domainsFrontmatter } from '../content/domains.md'
63
- import { sections as aboutSections } from '../content/index.md'
62
+ import { frontmatter as domainsFrontmatter } from '@page-withu/user-content/domains.md'
63
+ import { sections as aboutSections } from '@page-withu/user-content/index.md'
64
64
  import defaultFavicon from './assets/bulb.svg'
65
65
 
66
- const blogModules = import.meta.glob('../content/blog/*.md', { eager: true })
66
+ const blogModules = import.meta.glob('@page-withu/user-content/blog/*.md', { eager: true })
67
67
  const validPages = ['about', 'domains', 'blog']
68
68
  const basePath = import.meta.env.BASE_URL.replace(/\/$/, '')
69
69
  const navItems = [
@@ -24,10 +24,19 @@
24
24
  </nav>
25
25
  </div>
26
26
 
27
- <aside v-if="toc.length" class="blog-toc" aria-label="Table of contents">
28
- <p>Contents</p>
27
+ <button v-if="toc.length" class="blog-toc-toggle" type="button" :aria-expanded="tocOpen"
28
+ aria-controls="blog-toc" @click="tocOpen = true">
29
+ 目录
30
+ </button>
31
+ <div v-if="toc.length && tocOpen" class="blog-toc-backdrop" @click="tocOpen = false"></div>
32
+ <aside v-if="toc.length" id="blog-toc" :class="['blog-toc', { open: tocOpen }]" aria-label="Table of contents">
33
+ <div class="blog-toc-header">
34
+ <p>目录</p>
35
+ <button class="blog-toc-close" type="button" aria-label="Close table of contents"
36
+ @click="tocOpen = false">×</button>
37
+ </div>
29
38
  <a v-for="item in toc" :key="item.id" :href="`#${item.id}`"
30
- :class="[`toc-level-${item.level}`, { active: activeHeading === item.id }]">
39
+ :class="[`toc-level-${item.level}`, { active: activeHeading === item.id }]" @click="tocOpen = false">
31
40
  {{ item.title }}
32
41
  </a>
33
42
  </aside>
@@ -58,6 +67,7 @@ defineEmits(['back', 'open-post'])
58
67
  const contentRef = ref(null)
59
68
  const zoomImage = ref(null)
60
69
  const activeHeading = ref('')
70
+ const tocOpen = ref(false)
61
71
  const basePath = import.meta.env.BASE_URL.replace(/\/$/, '')
62
72
  const blogHref = `${basePath}/blog.html` || '/blog.html'
63
73
  let mermaidModule
@@ -118,6 +128,7 @@ onMounted(() => {
118
128
  onUnmounted(() => window.removeEventListener('scroll', updateActiveHeading))
119
129
 
120
130
  watch(() => props.post.slug, () => {
131
+ tocOpen.value = false
121
132
  activeHeading.value = props.toc[0]?.id || ''
122
133
  nextTick(() => {
123
134
  updateActiveHeading()
@@ -992,6 +992,14 @@ footer p {
992
992
  margin-bottom: 8px;
993
993
  }
994
994
 
995
+ .blog-toc-toggle {
996
+ display: none;
997
+ }
998
+
999
+ .blog-toc-backdrop {
1000
+ display: none;
1001
+ }
1002
+
995
1003
  .blog-toc {
996
1004
  position: sticky;
997
1005
  top: 24px;
@@ -1005,6 +1013,17 @@ footer p {
1005
1013
  font-size: 0.84rem;
1006
1014
  }
1007
1015
 
1016
+ .blog-toc-header {
1017
+ display: flex;
1018
+ align-items: center;
1019
+ justify-content: space-between;
1020
+ gap: 12px;
1021
+ }
1022
+
1023
+ .blog-toc-close {
1024
+ display: none;
1025
+ }
1026
+
1008
1027
  .blog-toc p {
1009
1028
  margin: 0 0 4px;
1010
1029
  color: var(--color-heading);
@@ -1197,7 +1216,7 @@ footer p {
1197
1216
  }
1198
1217
 
1199
1218
  /* ===== Responsive: Mobile ===== */
1200
- @media (max-width: 480px) {
1219
+ @media (max-width: 960px) {
1201
1220
  body {
1202
1221
  padding: 0 16px;
1203
1222
  }
@@ -1258,10 +1277,80 @@ footer p {
1258
1277
  padding-bottom: 16px;
1259
1278
  }
1260
1279
 
1280
+ .blog-article-layout {
1281
+ grid-template-columns: 1fr;
1282
+ }
1283
+
1284
+ .blog-toc-toggle {
1285
+ position: fixed;
1286
+ right: 18px;
1287
+ bottom: 18px;
1288
+ z-index: 88;
1289
+ display: inline-flex;
1290
+ align-items: center;
1291
+ gap: 6px;
1292
+ padding: 9px 14px;
1293
+ border: 1px solid var(--color-card-border);
1294
+ border-radius: 999px;
1295
+ color: var(--color-heading);
1296
+ background: var(--color-bg);
1297
+ box-shadow: var(--shadow-card);
1298
+ font: inherit;
1299
+ font-size: 0.84rem;
1300
+ cursor: pointer;
1301
+ }
1302
+
1303
+ .blog-toc-backdrop {
1304
+ position: fixed;
1305
+ inset: 0;
1306
+ z-index: 89;
1307
+ display: block;
1308
+ background: rgba(15, 23, 42, 0.36);
1309
+ }
1310
+
1261
1311
  .blog-toc {
1262
- position: static;
1263
- order: -1;
1264
- padding: 12px 0 12px 14px;
1312
+ position: fixed;
1313
+ top: 50%;
1314
+ left: 50%;
1315
+ z-index: 90;
1316
+ width: min(calc(100vw - 48px), 320px);
1317
+ max-height: min(70vh, 480px);
1318
+ padding: 16px;
1319
+ border: 1px solid var(--color-card-border);
1320
+ border-radius: 16px;
1321
+ background: var(--color-card-bg);
1322
+ box-shadow: var(--shadow-card);
1323
+ transform: translate(-50%, -50%);
1324
+ display: none;
1325
+ }
1326
+
1327
+ .blog-toc.open {
1328
+ display: flex;
1329
+ }
1330
+
1331
+ .blog-toc-header {
1332
+ position: sticky;
1333
+ top: -16px;
1334
+ margin: -16px -16px 4px;
1335
+ padding: 16px;
1336
+ background: var(--color-card-bg);
1337
+ border-bottom: 1px solid var(--color-footer-border);
1338
+ }
1339
+
1340
+ .blog-toc-close {
1341
+ display: inline-flex;
1342
+ align-items: center;
1343
+ justify-content: center;
1344
+ width: 28px;
1345
+ height: 28px;
1346
+ border: 1px solid var(--color-card-border);
1347
+ border-radius: 999px;
1348
+ color: var(--color-subtext);
1349
+ background: var(--color-bg);
1350
+ font: inherit;
1351
+ font-size: 1rem;
1352
+ line-height: 1;
1353
+ cursor: pointer;
1265
1354
  }
1266
1355
 
1267
1356
  .blog-nav {
@@ -1,3 +1,5 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { tmpdir } from 'node:os'
1
3
  import { defineConfig } from 'vite'
2
4
  import vue from '@vitejs/plugin-vue'
3
5
  import MarkdownIt from 'markdown-it'
@@ -7,9 +9,15 @@ import { full as emoji } from 'markdown-it-emoji'
7
9
  import matter from 'gray-matter'
8
10
  import hljs from 'highlight.js'
9
11
  import { mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
10
- import { dirname, join } from 'node:path'
11
- import userConfig from './config.js'
12
-
12
+ import { dirname, join, resolve } from 'node:path'
13
+ import { fileURLToPath, pathToFileURL } from 'node:url'
14
+
15
+ const runtimeRoot = dirname(fileURLToPath(import.meta.url))
16
+ const projectRoot = resolve(process.env.PAGE_WITHU_PROJECT_ROOT || process.cwd())
17
+ const userConfigFile = resolve(projectRoot, 'config.js')
18
+ const userContentDir = resolve(projectRoot, 'content')
19
+ const cacheKey = createHash('sha256').update(projectRoot).digest('hex').slice(0, 12)
20
+ const cacheDir = resolve(tmpdir(), `page-withu-${cacheKey}`)
13
21
  const basePath = process.env.BASE_PATH || '/'
14
22
 
15
23
  function slugify(text) {
@@ -147,14 +155,14 @@ function markdown() {
147
155
  }
148
156
  }
149
157
 
150
- function staticHtmlRoutes() {
158
+ function staticHtmlRoutes(userConfig) {
151
159
  return {
152
160
  name: 'static-html-routes',
153
161
  apply: 'build',
154
162
  closeBundle() {
155
- const dist = join(process.cwd(), 'dist')
163
+ const dist = join(projectRoot, 'dist')
156
164
  const index = readFileSync(join(dist, 'index.html'), 'utf8')
157
- const blogDir = join(process.cwd(), 'content', 'blog')
165
+ const blogDir = join(userContentDir, 'blog')
158
166
  let blogPosts = []
159
167
  try {
160
168
  blogPosts = readdirSync(blogDir)
@@ -164,7 +172,8 @@ function staticHtmlRoutes() {
164
172
  if (e.code !== 'ENOENT') throw e
165
173
  }
166
174
 
167
- const totalPages = Math.max(1, Math.ceil(blogPosts.length / userConfig.pagination.pageSize))
175
+ const pageSize = userConfig.pagination?.pageSize || 5
176
+ const totalPages = Math.max(1, Math.ceil(blogPosts.length / pageSize))
168
177
  const routes = ['domains.html', 'blog.html']
169
178
  for (let page = 2; page <= totalPages; page += 1) routes.push(`blog/page/${page}.html`)
170
179
  for (const slug of blogPosts) routes.push(`blog/${slug}.html`)
@@ -178,14 +187,29 @@ function staticHtmlRoutes() {
178
187
  }
179
188
  }
180
189
 
181
- export default defineConfig({
182
- base: basePath,
183
- plugins: [vue(), markdown(), staticHtmlRoutes()],
184
- server: {
185
- port: 5500,
186
- },
187
- build: {
188
- outDir: 'dist',
189
- emptyOutDir: true,
190
- },
191
- })
190
+ export default defineConfig(async () => {
191
+ const userConfig = (await import(`${pathToFileURL(userConfigFile).href}?t=${Date.now()}`)).default
192
+
193
+ return {
194
+ root: runtimeRoot,
195
+ base: basePath,
196
+ cacheDir,
197
+ resolve: {
198
+ alias: {
199
+ '@page-withu/user-config': userConfigFile,
200
+ '@page-withu/user-content': userContentDir,
201
+ },
202
+ },
203
+ plugins: [vue(), markdown(), staticHtmlRoutes(userConfig)],
204
+ server: {
205
+ port: 5500,
206
+ fs: {
207
+ allow: [runtimeRoot, projectRoot],
208
+ },
209
+ },
210
+ build: {
211
+ outDir: resolve(projectRoot, 'dist'),
212
+ emptyOutDir: true,
213
+ },
214
+ }
215
+ })
@@ -1,25 +0,0 @@
1
- {
2
- "name": "your-homepage-name",
3
- "private": true,
4
- "type": "module",
5
- "scripts": {
6
- "dev": "vite",
7
- "build": "vite build",
8
- "preview": "vite preview"
9
- },
10
- "dependencies": {
11
- "@traptitech/markdown-it-katex": "^3.6.0",
12
- "highlight.js": "^11.11.1",
13
- "katex": "^0.16.45",
14
- "markdown-it-emoji": "^3.0.0",
15
- "markdown-it-footnote": "^4.0.0",
16
- "mermaid": "^11.15.0",
17
- "vue": "^3.5.13"
18
- },
19
- "devDependencies": {
20
- "@vitejs/plugin-vue": "^5.2.3",
21
- "gray-matter": "^4.0.3",
22
- "markdown-it": "^14.1.0",
23
- "vite": "^6.3.1"
24
- }
25
- }