@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 +7 -9
- package/bin/page-withu.js +70 -366
- package/docs/deploy.md +10 -11
- package/docs/develop.md +23 -28
- package/docs/setup.md +9 -24
- package/package.json +13 -4
- package/template/config.js +1 -1
- package/template/src/app.vue +4 -4
- package/template/src/components/blog-detail.vue +14 -3
- package/template/src/styles/main.css +93 -4
- package/template/vite.config.js +42 -18
- package/template/package.json +0 -25
package/README.md
CHANGED
|
@@ -41,18 +41,16 @@
|
|
|
41
41
|
# 安装 page-withu
|
|
42
42
|
npm install -g @x-withu/page-withu
|
|
43
43
|
|
|
44
|
-
#
|
|
45
|
-
page-withu
|
|
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
|
-
|
|
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 {
|
|
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
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
|
|
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 = ['
|
|
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
|
|
139
|
-
const
|
|
140
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
|
230
|
-
const
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
|
95
|
+
.description('CLI to scaffold, preview, and build a lightweight personal homepage')
|
|
271
96
|
.version(pkg.version);
|
|
272
97
|
|
|
273
98
|
program
|
|
274
|
-
.command('
|
|
275
|
-
.description('create a new PageWithU project')
|
|
276
|
-
.
|
|
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('
|
|
379
|
-
.description('
|
|
380
|
-
.
|
|
381
|
-
.
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
79
|
-
- run:
|
|
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/
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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/` 后,需要用模板本身或新生成项目执行一次 `
|
|
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
|
-
│
|
|
28
|
-
|
|
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.
|
|
51
|
-
3.
|
|
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
|
|
60
|
-
|
|
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
|
|
67
|
+
page-withu new my-homepage
|
|
69
68
|
```
|
|
70
69
|
|
|
71
|
-
|
|
70
|
+
调整 CLI 行为时,通常需要同步修改:
|
|
72
71
|
|
|
73
|
-
- `bin/page-withu.js` 中的 commander
|
|
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
|
|
82
|
+
node /path/to/page-withu/bin/page-withu.js new demo-site
|
|
86
83
|
cd demo-site
|
|
87
|
-
|
|
88
|
-
npm run build
|
|
84
|
+
node /path/to/page-withu/bin/page-withu.js build
|
|
89
85
|
```
|
|
90
86
|
|
|
91
|
-
|
|
87
|
+
如果构建成功,说明内容型项目可以通过包内运行时正常生成。
|
|
92
88
|
|
|
93
89
|
## 开发 Template
|
|
94
90
|
|
|
@@ -102,8 +98,7 @@ template/
|
|
|
102
98
|
|
|
103
99
|
```bash
|
|
104
100
|
cd template
|
|
105
|
-
|
|
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
|
-
|
|
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
|
|
200
|
+
page-withu new my-homepage
|
|
206
201
|
```
|
|
207
202
|
|
|
208
203
|
或在线构建无需本地安装:
|
|
209
204
|
|
|
210
205
|
```bash
|
|
211
|
-
npx @x-withu/page-withu
|
|
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
|
-
|
|
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`:使用当前安装的 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": "
|
|
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
|
-
"
|
|
41
|
-
"
|
|
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
|
}
|
package/template/config.js
CHANGED
|
@@ -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: {
|
package/template/src/app.vue
CHANGED
|
@@ -54,16 +54,16 @@
|
|
|
54
54
|
|
|
55
55
|
<script setup>
|
|
56
56
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
|
57
|
-
import userConfig from '
|
|
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 '
|
|
63
|
-
import { sections as aboutSections } from '
|
|
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('
|
|
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
|
-
<
|
|
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 {
|
package/template/vite.config.js
CHANGED
|
@@ -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
|
|
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(
|
|
163
|
+
const dist = join(projectRoot, 'dist')
|
|
156
164
|
const index = readFileSync(join(dist, 'index.html'), 'utf8')
|
|
157
|
-
const blogDir = join(
|
|
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
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
+
})
|
package/template/package.json
DELETED
|
@@ -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
|
-
}
|