@x-withu/page-withu 0.1.1 → 1.0.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 +7 -7
- package/bin/page-withu.js +61 -326
- package/docs/develop.md +15 -17
- package/docs/setup.md +9 -24
- package/package.json +1 -3
- package/template/src/components/blog-detail.vue +14 -3
- package/template/src/styles/main.css +93 -4
- package/template/.gitignore +0 -10
package/README.md
CHANGED
|
@@ -41,18 +41,18 @@
|
|
|
41
41
|
# 安装 page-withu
|
|
42
42
|
npm install -g @x-withu/page-withu
|
|
43
43
|
|
|
44
|
-
#
|
|
45
|
-
page-withu
|
|
46
|
-
|
|
47
|
-
# 也可以通过参数直接指定
|
|
48
|
-
page-withu create my-homepage --title "My Site" --tab-title "My Blog" --favicon /favicon.svg
|
|
44
|
+
# 创建项目,默认配置会写入 config.js,后续可自行编辑
|
|
45
|
+
page-withu new my-homepage
|
|
49
46
|
|
|
50
47
|
# 安装依赖
|
|
51
48
|
cd my-homepage
|
|
52
49
|
npm install
|
|
53
50
|
|
|
54
|
-
#
|
|
55
|
-
|
|
51
|
+
# 本地预览
|
|
52
|
+
page-withu serve
|
|
53
|
+
|
|
54
|
+
# 构建分发产物
|
|
55
|
+
page-withu build
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
下一步?[配置网站](./docs/setup.md)、[部署网站](./docs/deploy.md)、[开发网站](./docs/develop.md)。
|
package/bin/page-withu.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
3
|
import { Command } from 'commander';
|
|
4
|
-
import inquirer from 'inquirer';
|
|
5
4
|
import fs from 'fs-extra';
|
|
6
5
|
import path from 'node:path';
|
|
7
6
|
import { fileURLToPath } from 'node:url';
|
|
@@ -13,102 +12,8 @@ const pkg = await fs.readJson(new URL('../package.json', import.meta.url));
|
|
|
13
12
|
const __filename = fileURLToPath(import.meta.url);
|
|
14
13
|
const __dirname = path.dirname(__filename);
|
|
15
14
|
const templateDir = path.resolve(__dirname, '../template');
|
|
16
|
-
const manifestName = '.page-withu.json';
|
|
17
|
-
const managedRoots = ['src'];
|
|
18
|
-
const managedFiles = ['.gitignore', 'index.html', 'vite.config.js'];
|
|
19
|
-
|
|
20
15
|
const program = new Command();
|
|
21
16
|
|
|
22
|
-
function hashContent(content) {
|
|
23
|
-
return createHash('sha256').update(content).digest('hex');
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async function hashFile(filePath) {
|
|
27
|
-
return hashContent(await fs.readFile(filePath));
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function toPosixPath(value) {
|
|
31
|
-
return value.split(path.sep).join('/');
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
async function listFiles(dir) {
|
|
35
|
-
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
36
|
-
const files = [];
|
|
37
|
-
|
|
38
|
-
for (const entry of entries) {
|
|
39
|
-
const entryPath = path.join(dir, entry.name);
|
|
40
|
-
if (entry.isDirectory()) {
|
|
41
|
-
files.push(...await listFiles(entryPath));
|
|
42
|
-
} else if (entry.isFile()) {
|
|
43
|
-
files.push(entryPath);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return files;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async function listManagedTemplateFiles() {
|
|
51
|
-
const files = [...managedFiles];
|
|
52
|
-
|
|
53
|
-
for (const root of managedRoots) {
|
|
54
|
-
const rootPath = path.join(templateDir, root);
|
|
55
|
-
if (!await fs.pathExists(rootPath)) continue;
|
|
56
|
-
const rootFiles = await listFiles(rootPath);
|
|
57
|
-
files.push(...rootFiles.map((file) => toPosixPath(path.relative(templateDir, file))));
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return files.sort((a, b) => a.localeCompare(b));
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async function createManifest(projectDir, previousManifest = null) {
|
|
64
|
-
const files = {};
|
|
65
|
-
|
|
66
|
-
for (const file of await listManagedTemplateFiles()) {
|
|
67
|
-
const projectFile = path.join(projectDir, file);
|
|
68
|
-
const templateFile = path.join(templateDir, file);
|
|
69
|
-
if (!await fs.pathExists(projectFile)) continue;
|
|
70
|
-
|
|
71
|
-
const projectHash = await hashFile(projectFile);
|
|
72
|
-
const templateHash = await hashFile(templateFile);
|
|
73
|
-
if (projectHash === templateHash) {
|
|
74
|
-
files[file] = templateHash;
|
|
75
|
-
} else if (previousManifest?.files?.[file]) {
|
|
76
|
-
files[file] = previousManifest.files[file];
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return {
|
|
81
|
-
templateVersion: pkg.version,
|
|
82
|
-
updatedAt: new Date().toISOString(),
|
|
83
|
-
files,
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async function writeManifest(projectDir, dryRun, previousManifest = null) {
|
|
88
|
-
const manifest = await createManifest(projectDir, previousManifest);
|
|
89
|
-
if (!dryRun) {
|
|
90
|
-
await fs.writeJson(path.join(projectDir, manifestName), manifest, { spaces: 2 });
|
|
91
|
-
}
|
|
92
|
-
return manifest;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function renderConfig({ title, tabTitle, favicon, author, footerLinks }) {
|
|
96
|
-
const links = footerLinks.map((link) => ` { label: ${JSON.stringify(link.label)}, url: ${JSON.stringify(link.url)} }`);
|
|
97
|
-
return `export default {
|
|
98
|
-
title: ${JSON.stringify(title)},
|
|
99
|
-
tabTitle: ${JSON.stringify(tabTitle)},
|
|
100
|
-
favicon: ${JSON.stringify(favicon)},
|
|
101
|
-
author: ${JSON.stringify(author)},
|
|
102
|
-
year: new Date().getFullYear(),
|
|
103
|
-
footerLinks: [
|
|
104
|
-
${links.join(',\n')}
|
|
105
|
-
],
|
|
106
|
-
pagination: {
|
|
107
|
-
pageSize: 5
|
|
108
|
-
}
|
|
109
|
-
}`;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
17
|
async function detectProject(projectDir) {
|
|
113
18
|
const required = ['package.json', 'config.js', 'content', 'src', 'vite.config.js'];
|
|
114
19
|
const missing = [];
|
|
@@ -120,261 +25,91 @@ async function detectProject(projectDir) {
|
|
|
120
25
|
return missing;
|
|
121
26
|
}
|
|
122
27
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (!await fs.pathExists(manifestPath)) return null;
|
|
126
|
-
return fs.readJson(manifestPath);
|
|
28
|
+
function shouldCopyTemplateItem(source) {
|
|
29
|
+
return path.relative(templateDir, source) !== '.gitignore';
|
|
127
30
|
}
|
|
128
31
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (options.yes || options.dryRun) return false;
|
|
132
|
-
|
|
133
|
-
const answer = await inquirer.prompt([{
|
|
134
|
-
type: 'confirm',
|
|
135
|
-
name: 'overwrite',
|
|
136
|
-
message: `${file} has local changes. Overwrite it?`,
|
|
137
|
-
default: false,
|
|
138
|
-
}]);
|
|
139
|
-
|
|
140
|
-
return answer.overwrite;
|
|
32
|
+
function normalizePackageName(name) {
|
|
33
|
+
return name.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/^-+|-+$/g, '') || 'page-withu-site';
|
|
141
34
|
}
|
|
142
35
|
|
|
143
|
-
async function
|
|
144
|
-
const
|
|
145
|
-
const
|
|
146
|
-
const result = {
|
|
147
|
-
added: [],
|
|
148
|
-
updated: [],
|
|
149
|
-
skipped: [],
|
|
150
|
-
unchanged: [],
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
for (const file of files) {
|
|
154
|
-
const sourceFile = path.join(templateDir, file);
|
|
155
|
-
const targetFile = path.join(projectDir, file);
|
|
156
|
-
const sourceHash = await hashFile(sourceFile);
|
|
157
|
-
const exists = await fs.pathExists(targetFile);
|
|
158
|
-
|
|
159
|
-
if (!exists) {
|
|
160
|
-
result.added.push(file);
|
|
161
|
-
if (!options.dryRun) await fs.copy(sourceFile, targetFile);
|
|
162
|
-
continue;
|
|
163
|
-
}
|
|
36
|
+
async function createProject(projectName = 'my-homepage') {
|
|
37
|
+
const targetDir = projectName.trim() || 'my-homepage';
|
|
38
|
+
const fullPath = path.resolve(process.cwd(), targetDir);
|
|
164
39
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
40
|
+
if (await fs.pathExists(fullPath)) {
|
|
41
|
+
console.error(chalk.red(`\nError: Directory ${targetDir} already exists.`));
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
170
44
|
|
|
171
|
-
|
|
172
|
-
const safeToUpdate = previousHash && currentHash === previousHash;
|
|
173
|
-
const overwrite = safeToUpdate || await shouldOverwriteConflict(file, options);
|
|
45
|
+
const spinner = ora('Creating project...').start();
|
|
174
46
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if (!options.dryRun) await fs.copy(sourceFile, targetFile);
|
|
178
|
-
} else {
|
|
179
|
-
result.skipped.push(file);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
47
|
+
try {
|
|
48
|
+
await fs.copy(templateDir, fullPath, { filter: shouldCopyTemplateItem });
|
|
182
49
|
|
|
183
|
-
|
|
184
|
-
|
|
50
|
+
const pkgPath = path.join(fullPath, 'package.json');
|
|
51
|
+
const projectPkg = await fs.readJson(pkgPath);
|
|
52
|
+
projectPkg.name = normalizePackageName(path.basename(targetDir));
|
|
53
|
+
await fs.writeJson(pkgPath, projectPkg, { spaces: 2 });
|
|
185
54
|
|
|
186
|
-
|
|
187
|
-
const next = { ...(projectPkg[key] || {}) };
|
|
188
|
-
let changed = false;
|
|
55
|
+
await fs.ensureDir(path.join(fullPath, 'content/blog'));
|
|
189
56
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
57
|
+
spinner.succeed(chalk.green(`Successfully created PageWithU project in ${targetDir}!`));
|
|
58
|
+
console.log('\nNext steps:');
|
|
59
|
+
console.log(chalk.cyan(` cd ${targetDir}`));
|
|
60
|
+
console.log(chalk.cyan(' npm install'));
|
|
61
|
+
console.log(chalk.cyan(' page-withu serve'));
|
|
62
|
+
console.log(chalk.cyan(' page-withu build\n'));
|
|
63
|
+
} catch (err) {
|
|
64
|
+
spinner.fail(chalk.red('Failed to create project.'));
|
|
65
|
+
console.error(err);
|
|
66
|
+
process.exit(1);
|
|
195
67
|
}
|
|
196
|
-
|
|
197
|
-
if (changed) projectPkg[key] = next;
|
|
198
|
-
return changed;
|
|
199
68
|
}
|
|
200
69
|
|
|
201
|
-
async function
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
mergeSection(projectPkg, templatePkg, 'scripts');
|
|
208
|
-
mergeSection(projectPkg, templatePkg, 'dependencies');
|
|
209
|
-
mergeSection(projectPkg, templatePkg, 'devDependencies');
|
|
210
|
-
|
|
211
|
-
const changed = JSON.stringify(projectPkg) !== before;
|
|
212
|
-
if (changed && !dryRun) {
|
|
213
|
-
await fs.writeJson(projectPkgPath, projectPkg, { spaces: 2 });
|
|
70
|
+
async function runProjectScript(script, args) {
|
|
71
|
+
const missing = await detectProject(process.cwd());
|
|
72
|
+
if (missing.length) {
|
|
73
|
+
console.error(chalk.red('Current directory does not look like a PageWithU project.'));
|
|
74
|
+
console.error(chalk.yellow(`Missing: ${missing.join(', ')}`));
|
|
75
|
+
process.exit(1);
|
|
214
76
|
}
|
|
215
77
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
78
|
+
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
79
|
+
const child = spawn(npmCommand, ['run', script, '--', ...args], {
|
|
80
|
+
cwd: process.cwd(),
|
|
81
|
+
stdio: 'inherit',
|
|
82
|
+
});
|
|
221
83
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
229
|
-
if (!summary.added.length && !summary.updated.length && !summary.packageChanged && !summary.skipped.length) {
|
|
230
|
-
console.log(chalk.green('Page With U project is already up to date.'));
|
|
231
|
-
}
|
|
232
|
-
if (!dryRun && (summary.added.length || summary.updated.length || summary.packageChanged)) {
|
|
233
|
-
console.log(chalk.cyan('\nNext steps:'));
|
|
234
|
-
console.log(chalk.cyan(' npm install'));
|
|
235
|
-
console.log(chalk.cyan(' npm run build'));
|
|
236
|
-
}
|
|
84
|
+
child.on('exit', (code, signal) => {
|
|
85
|
+
if (signal) {
|
|
86
|
+
process.kill(process.pid, signal);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
process.exit(code ?? 1);
|
|
90
|
+
});
|
|
237
91
|
}
|
|
238
92
|
|
|
239
93
|
program
|
|
240
94
|
.name('page-withu')
|
|
241
|
-
.description('CLI to scaffold a lightweight
|
|
95
|
+
.description('CLI to scaffold, preview, and build a lightweight personal homepage')
|
|
242
96
|
.version(pkg.version);
|
|
243
97
|
|
|
244
98
|
program
|
|
245
|
-
.command('
|
|
246
|
-
.description('create a new PageWithU project')
|
|
247
|
-
.
|
|
248
|
-
.option('--tab-title <title>', 'browser tab title')
|
|
249
|
-
.option('--favicon <path-or-url>', 'browser tab favicon path or URL')
|
|
250
|
-
.option('-a, --author <author>', 'author name')
|
|
251
|
-
.option('-g, --github <github>', 'github url')
|
|
252
|
-
.option('-e, --email <email>', 'email address')
|
|
253
|
-
.action(async (projectName, options) => {
|
|
254
|
-
let targetDir = projectName;
|
|
255
|
-
|
|
256
|
-
if (!targetDir) {
|
|
257
|
-
const answers = await inquirer.prompt([
|
|
258
|
-
{
|
|
259
|
-
type: 'input',
|
|
260
|
-
name: 'projectName',
|
|
261
|
-
message: 'What is your project named?',
|
|
262
|
-
default: 'my-homepage',
|
|
263
|
-
},
|
|
264
|
-
]);
|
|
265
|
-
targetDir = answers.projectName;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
const fullPath = path.resolve(process.cwd(), targetDir);
|
|
269
|
-
|
|
270
|
-
if (fs.existsSync(fullPath)) {
|
|
271
|
-
console.error(chalk.red(`\nError: Directory ${targetDir} already exists.`));
|
|
272
|
-
process.exit(1);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const title = options.title || (await inquirer.prompt([{
|
|
276
|
-
type: 'input',
|
|
277
|
-
name: 'title',
|
|
278
|
-
message: 'Site Title:',
|
|
279
|
-
default: 'PageWithU',
|
|
280
|
-
}])).title;
|
|
281
|
-
|
|
282
|
-
const author = options.author || (await inquirer.prompt([{
|
|
283
|
-
type: 'input',
|
|
284
|
-
name: 'author',
|
|
285
|
-
message: 'Author Name:',
|
|
286
|
-
default: 'Your Name',
|
|
287
|
-
}])).author;
|
|
288
|
-
|
|
289
|
-
const tabTitle = options.tabTitle || (await inquirer.prompt([{
|
|
290
|
-
type: 'input',
|
|
291
|
-
name: 'tabTitle',
|
|
292
|
-
message: 'Browser Tab Title:',
|
|
293
|
-
default: title,
|
|
294
|
-
}])).tabTitle;
|
|
295
|
-
|
|
296
|
-
const favicon = options.favicon || (await inquirer.prompt([{
|
|
297
|
-
type: 'input',
|
|
298
|
-
name: 'favicon',
|
|
299
|
-
message: 'Favicon path or URL:',
|
|
300
|
-
default: '/src/assets/bulb.svg',
|
|
301
|
-
}])).favicon;
|
|
302
|
-
|
|
303
|
-
const github = options.github !== undefined ? options.github : (await inquirer.prompt([{
|
|
304
|
-
type: 'input',
|
|
305
|
-
name: 'github',
|
|
306
|
-
message: 'GitHub URL (optional):',
|
|
307
|
-
}])).github;
|
|
308
|
-
|
|
309
|
-
const email = options.email !== undefined ? options.email : (await inquirer.prompt([{
|
|
310
|
-
type: 'input',
|
|
311
|
-
name: 'email',
|
|
312
|
-
message: 'Email (optional):',
|
|
313
|
-
}])).email;
|
|
314
|
-
|
|
315
|
-
const spinner = ora('Creating project...').start();
|
|
316
|
-
|
|
317
|
-
try {
|
|
318
|
-
await fs.copy(templateDir, fullPath);
|
|
319
|
-
|
|
320
|
-
const footerLinks = [];
|
|
321
|
-
if (github) footerLinks.push({ label: 'GitHub', url: github });
|
|
322
|
-
if (email) footerLinks.push({ label: 'Email', url: `mailto:${email}` });
|
|
323
|
-
|
|
324
|
-
await fs.writeFile(path.join(fullPath, 'config.js'), renderConfig({ title, tabTitle, favicon, author, footerLinks }));
|
|
325
|
-
|
|
326
|
-
const pkgPath = path.join(fullPath, 'package.json');
|
|
327
|
-
const projectPkg = await fs.readJson(pkgPath);
|
|
328
|
-
projectPkg.name = targetDir.toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
329
|
-
await fs.writeJson(pkgPath, projectPkg, { spaces: 2 });
|
|
330
|
-
|
|
331
|
-
await fs.ensureDir(path.join(fullPath, 'content/blog'));
|
|
332
|
-
await writeManifest(fullPath, false);
|
|
333
|
-
|
|
334
|
-
spinner.succeed(chalk.green(`Successfully created PageWithU project in ${targetDir}!`));
|
|
335
|
-
console.log('\nNext steps:');
|
|
336
|
-
console.log(chalk.cyan(` cd ${targetDir}`));
|
|
337
|
-
console.log(chalk.cyan(' npm install'));
|
|
338
|
-
console.log(chalk.cyan(' npm run dev\n'));
|
|
339
|
-
|
|
340
|
-
} catch (err) {
|
|
341
|
-
spinner.fail(chalk.red('Failed to create project.'));
|
|
342
|
-
console.error(err);
|
|
343
|
-
process.exit(1);
|
|
344
|
-
}
|
|
345
|
-
});
|
|
99
|
+
.command('new [project-name]')
|
|
100
|
+
.description('create a new PageWithU project with the default template config')
|
|
101
|
+
.action(createProject);
|
|
346
102
|
|
|
347
103
|
program
|
|
348
|
-
.command('
|
|
349
|
-
.description('
|
|
350
|
-
.
|
|
351
|
-
.
|
|
352
|
-
.option('--force', 'overwrite changed template-managed files')
|
|
353
|
-
.action(async (options) => {
|
|
354
|
-
const projectDir = process.cwd();
|
|
355
|
-
const spinner = ora(options.dryRun ? 'Checking project update...' : 'Updating project...').start();
|
|
356
|
-
|
|
357
|
-
try {
|
|
358
|
-
const missing = await detectProject(projectDir);
|
|
359
|
-
if (missing.length) {
|
|
360
|
-
spinner.fail(chalk.red('Current directory does not look like a PageWithU project.'));
|
|
361
|
-
console.error(chalk.yellow(`Missing: ${missing.join(', ')}`));
|
|
362
|
-
process.exit(1);
|
|
363
|
-
}
|
|
104
|
+
.command('serve [args...]')
|
|
105
|
+
.description('start local preview for the current PageWithU project')
|
|
106
|
+
.allowUnknownOption(true)
|
|
107
|
+
.action((args = []) => runProjectScript('dev', args));
|
|
364
108
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
if (!options.dryRun) await writeManifest(projectDir, false, previousManifest);
|
|
371
|
-
|
|
372
|
-
printUpdateSummary({ ...updates, packageChanged }, options.dryRun);
|
|
373
|
-
} catch (err) {
|
|
374
|
-
spinner.fail(chalk.red('Failed to update project.'));
|
|
375
|
-
console.error(err);
|
|
376
|
-
process.exit(1);
|
|
377
|
-
}
|
|
378
|
-
});
|
|
109
|
+
program
|
|
110
|
+
.command('build [args...]')
|
|
111
|
+
.description('build dist assets for the current PageWithU project')
|
|
112
|
+
.allowUnknownOption(true)
|
|
113
|
+
.action((args = []) => runProjectScript('build', args));
|
|
379
114
|
|
|
380
115
|
program.parse();
|
package/docs/develop.md
CHANGED
|
@@ -46,18 +46,18 @@ bin/page-withu.js
|
|
|
46
46
|
|
|
47
47
|
当前 CLI 主要负责:
|
|
48
48
|
|
|
49
|
-
1.
|
|
50
|
-
2.
|
|
51
|
-
3.
|
|
52
|
-
4. 修改目标项目 `package.json` 的 `name` 字段。
|
|
53
|
-
5. 输出下一步命令提示。
|
|
49
|
+
1. `new`:复制 `template/` 到目标目录,保留默认配置。
|
|
50
|
+
2. `serve`:在当前主页项目中启动本地预览。
|
|
51
|
+
3. `build`:在当前主页项目中构建 `dist` 产物。
|
|
54
52
|
|
|
55
53
|
本地调试 CLI:
|
|
56
54
|
|
|
57
55
|
```bash
|
|
58
56
|
node bin/page-withu.js --help
|
|
59
|
-
node bin/page-withu.js
|
|
60
|
-
|
|
57
|
+
node bin/page-withu.js new my-homepage
|
|
58
|
+
cd my-homepage
|
|
59
|
+
node /path/to/page-withu/bin/page-withu.js serve
|
|
60
|
+
node /path/to/page-withu/bin/page-withu.js build
|
|
61
61
|
```
|
|
62
62
|
|
|
63
63
|
如果想全局挂载本地版本:
|
|
@@ -65,14 +65,12 @@ node bin/page-withu.js update --dry-run
|
|
|
65
65
|
```bash
|
|
66
66
|
npm link
|
|
67
67
|
page-withu --help
|
|
68
|
-
page-withu
|
|
68
|
+
page-withu new my-homepage
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
调整 CLI 行为时,通常需要同步修改:
|
|
72
72
|
|
|
73
|
-
- `bin/page-withu.js` 中的 commander
|
|
74
|
-
- 交互式 prompt 的默认值。
|
|
75
|
-
- 生成 `config.js` 的逻辑。
|
|
73
|
+
- `bin/page-withu.js` 中的 commander command。
|
|
76
74
|
- 必要时修改 `template/config.js` 的默认字段。
|
|
77
75
|
- 必要时修改模板中读取配置的 Vue 组件。
|
|
78
76
|
- 必要时更新 `README.md` 和 `docs/setup.md`。
|
|
@@ -82,13 +80,13 @@ page-withu create my-homepage
|
|
|
82
80
|
```bash
|
|
83
81
|
mkdir -p /tmp/page-withu-test
|
|
84
82
|
cd /tmp/page-withu-test
|
|
85
|
-
node /path/to/page-withu/bin/page-withu.js
|
|
83
|
+
node /path/to/page-withu/bin/page-withu.js new demo-site
|
|
86
84
|
cd demo-site
|
|
87
85
|
npm install
|
|
88
|
-
|
|
86
|
+
node /path/to/page-withu/bin/page-withu.js build
|
|
89
87
|
```
|
|
90
88
|
|
|
91
|
-
|
|
89
|
+
如果构建成功,说明模板可以被正常生成和构建。
|
|
92
90
|
|
|
93
91
|
## 开发 Template
|
|
94
92
|
|
|
@@ -202,11 +200,11 @@ npm publish --access public
|
|
|
202
200
|
|
|
203
201
|
```bash
|
|
204
202
|
npm install -g @x-withu/page-withu
|
|
205
|
-
page-withu
|
|
203
|
+
page-withu new my-homepage
|
|
206
204
|
```
|
|
207
205
|
|
|
208
206
|
或在线构建无需本地安装:
|
|
209
207
|
|
|
210
208
|
```bash
|
|
211
|
-
npx @x-withu/page-withu
|
|
209
|
+
npx @x-withu/page-withu new my-homepage
|
|
212
210
|
```
|
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
|
-
|
|
88
|
+
Page With U 只提供三个常用命令:
|
|
89
89
|
|
|
90
90
|
```bash
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
page-withu
|
|
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
|
|
96
|
+
- `page-withu new my-homepage`:新建主页项目,所有站点配置都保持模板默认值。
|
|
97
|
+
- `page-withu serve`:在当前项目目录启动本地预览,对应模板项目的 `npm run dev`。
|
|
98
|
+
- `page-withu build`:在当前项目目录构建 `dist` 产物,对应模板项目的 `npm run build`。
|
|
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.
|
|
3
|
+
"version": "1.0.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/.gitignore",
|
|
20
19
|
"template/package.json",
|
|
21
20
|
"template/vite.config.js",
|
|
22
21
|
"docs/",
|
|
@@ -38,7 +37,6 @@
|
|
|
38
37
|
"chalk": "^5.3.0",
|
|
39
38
|
"commander": "^12.1.0",
|
|
40
39
|
"fs-extra": "^11.2.0",
|
|
41
|
-
"inquirer": "^9.3.2",
|
|
42
40
|
"ora": "^8.0.1"
|
|
43
41
|
}
|
|
44
42
|
}
|
|
@@ -24,10 +24,19 @@
|
|
|
24
24
|
</nav>
|
|
25
25
|
</div>
|
|
26
26
|
|
|
27
|
-
<
|
|
28
|
-
|
|
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:
|
|
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:
|
|
1263
|
-
|
|
1264
|
-
|
|
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 {
|