@x-withu/page-withu 0.0.5 → 0.1.1
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/bin/page-withu.js +348 -110
- package/docs/deploy.md +57 -17
- package/docs/develop.md +1 -0
- package/docs/setup.md +31 -0
- package/package.json +2 -1
- package/template/.gitignore +10 -0
- package/template/src/components/blog-list.vue +1 -1
package/bin/page-withu.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
2
3
|
import { Command } from 'commander';
|
|
3
4
|
import inquirer from 'inquirer';
|
|
4
5
|
import fs from 'fs-extra';
|
|
@@ -12,131 +13,368 @@ const pkg = await fs.readJson(new URL('../package.json', import.meta.url));
|
|
|
12
13
|
const __filename = fileURLToPath(import.meta.url);
|
|
13
14
|
const __dirname = path.dirname(__filename);
|
|
14
15
|
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'];
|
|
15
19
|
|
|
16
20
|
const program = new Command();
|
|
17
21
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
.version(pkg.version);
|
|
22
|
+
function hashContent(content) {
|
|
23
|
+
return createHash('sha256').update(content).digest('hex');
|
|
24
|
+
}
|
|
22
25
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
{
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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))));
|
|
45
58
|
}
|
|
46
59
|
|
|
47
|
-
|
|
60
|
+
return files.sort((a, b) => a.localeCompare(b));
|
|
61
|
+
}
|
|
48
62
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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 });
|
|
52
91
|
}
|
|
92
|
+
return manifest;
|
|
93
|
+
}
|
|
53
94
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const author = options.author || (await inquirer.prompt([{
|
|
62
|
-
type: 'input',
|
|
63
|
-
name: 'author',
|
|
64
|
-
message: 'Author Name:',
|
|
65
|
-
default: 'Your Name',
|
|
66
|
-
}])).author;
|
|
67
|
-
|
|
68
|
-
const tabTitle = options.tabTitle || (await inquirer.prompt([{
|
|
69
|
-
type: 'input',
|
|
70
|
-
name: 'tabTitle',
|
|
71
|
-
message: 'Browser Tab Title:',
|
|
72
|
-
default: title,
|
|
73
|
-
}])).tabTitle;
|
|
74
|
-
|
|
75
|
-
const favicon = options.favicon || (await inquirer.prompt([{
|
|
76
|
-
type: 'input',
|
|
77
|
-
name: 'favicon',
|
|
78
|
-
message: 'Favicon path or URL:',
|
|
79
|
-
default: '/src/assets/bulb.svg',
|
|
80
|
-
}])).favicon;
|
|
81
|
-
|
|
82
|
-
const github = options.github !== undefined ? options.github : (await inquirer.prompt([{
|
|
83
|
-
type: 'input',
|
|
84
|
-
name: 'github',
|
|
85
|
-
message: 'GitHub URL (optional):',
|
|
86
|
-
}])).github;
|
|
87
|
-
|
|
88
|
-
const email = options.email !== undefined ? options.email : (await inquirer.prompt([{
|
|
89
|
-
type: 'input',
|
|
90
|
-
name: 'email',
|
|
91
|
-
message: 'Email (optional):',
|
|
92
|
-
}])).email;
|
|
93
|
-
|
|
94
|
-
const spinner = ora('Creating project...').start();
|
|
95
|
-
|
|
96
|
-
try {
|
|
97
|
-
// 1. Copy template
|
|
98
|
-
await fs.copy(templateDir, fullPath);
|
|
99
|
-
|
|
100
|
-
// 2. Generate config.js based on answers
|
|
101
|
-
const footerLinks = [];
|
|
102
|
-
if (github) footerLinks.push(`{ label: 'GitHub', url: '${github}' }`);
|
|
103
|
-
if (email) footerLinks.push(`{ label: 'Email', url: 'mailto:${email}' }`);
|
|
104
|
-
|
|
105
|
-
const configContent = `export default {
|
|
106
|
-
title: "${title}",
|
|
107
|
-
tabTitle: "${tabTitle}",
|
|
108
|
-
favicon: "${favicon}",
|
|
109
|
-
author: "${author}",
|
|
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)},
|
|
110
102
|
year: new Date().getFullYear(),
|
|
111
103
|
footerLinks: [
|
|
112
|
-
|
|
104
|
+
${links.join(',\n')}
|
|
113
105
|
],
|
|
114
106
|
pagination: {
|
|
115
107
|
pageSize: 5
|
|
116
108
|
}
|
|
117
109
|
}`;
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function detectProject(projectDir) {
|
|
113
|
+
const required = ['package.json', 'config.js', 'content', 'src', 'vite.config.js'];
|
|
114
|
+
const missing = [];
|
|
115
|
+
|
|
116
|
+
for (const item of required) {
|
|
117
|
+
if (!await fs.pathExists(path.join(projectDir, item))) missing.push(item);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return missing;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function loadManifest(projectDir) {
|
|
124
|
+
const manifestPath = path.join(projectDir, manifestName);
|
|
125
|
+
if (!await fs.pathExists(manifestPath)) return null;
|
|
126
|
+
return fs.readJson(manifestPath);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function shouldOverwriteConflict(file, options) {
|
|
130
|
+
if (options.force) return true;
|
|
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;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function planTemplateUpdates(projectDir, options) {
|
|
144
|
+
const manifest = await loadManifest(projectDir);
|
|
145
|
+
const files = await listManagedTemplateFiles();
|
|
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
|
+
}
|
|
164
|
+
|
|
165
|
+
const currentHash = await hashFile(targetFile);
|
|
166
|
+
if (currentHash === sourceHash) {
|
|
167
|
+
result.unchanged.push(file);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const previousHash = manifest?.files?.[file];
|
|
172
|
+
const safeToUpdate = previousHash && currentHash === previousHash;
|
|
173
|
+
const overwrite = safeToUpdate || await shouldOverwriteConflict(file, options);
|
|
174
|
+
|
|
175
|
+
if (overwrite) {
|
|
176
|
+
result.updated.push(file);
|
|
177
|
+
if (!options.dryRun) await fs.copy(sourceFile, targetFile);
|
|
178
|
+
} else {
|
|
179
|
+
result.skipped.push(file);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return result;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function mergeSection(projectPkg, templatePkg, key) {
|
|
187
|
+
const next = { ...(projectPkg[key] || {}) };
|
|
188
|
+
let changed = false;
|
|
189
|
+
|
|
190
|
+
for (const [name, value] of Object.entries(templatePkg[key] || {})) {
|
|
191
|
+
if (next[name] !== value) {
|
|
192
|
+
next[name] = value;
|
|
193
|
+
changed = true;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (changed) projectPkg[key] = next;
|
|
198
|
+
return changed;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function mergePackage(projectDir, dryRun) {
|
|
202
|
+
const projectPkgPath = path.join(projectDir, 'package.json');
|
|
203
|
+
const templatePkg = await fs.readJson(path.join(templateDir, 'package.json'));
|
|
204
|
+
const projectPkg = await fs.readJson(projectPkgPath);
|
|
205
|
+
const before = JSON.stringify(projectPkg);
|
|
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 });
|
|
139
214
|
}
|
|
140
|
-
});
|
|
141
215
|
|
|
142
|
-
|
|
216
|
+
return changed;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function printUpdateSummary(summary, dryRun) {
|
|
220
|
+
const label = dryRun ? 'Would update' : 'Updated';
|
|
221
|
+
|
|
222
|
+
if (summary.added.length) console.log(chalk.green(`${label} ${summary.added.length} missing file(s).`));
|
|
223
|
+
if (summary.updated.length) console.log(chalk.green(`${label} ${summary.updated.length} template file(s).`));
|
|
224
|
+
if (summary.packageChanged) console.log(chalk.green(`${label} package.json dependencies and scripts.`));
|
|
225
|
+
if (summary.skipped.length) {
|
|
226
|
+
console.log(chalk.yellow(`Skipped ${summary.skipped.length} file(s) with local changes:`));
|
|
227
|
+
for (const file of summary.skipped) console.log(chalk.yellow(` - ${file}`));
|
|
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
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
program
|
|
240
|
+
.name('page-withu')
|
|
241
|
+
.description('CLI to scaffold a lightweight, elegant personal homepage')
|
|
242
|
+
.version(pkg.version);
|
|
243
|
+
|
|
244
|
+
program
|
|
245
|
+
.command('create [project-name]')
|
|
246
|
+
.description('create a new PageWithU project')
|
|
247
|
+
.option('-t, --title <title>', 'site title')
|
|
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
|
+
});
|
|
346
|
+
|
|
347
|
+
program
|
|
348
|
+
.command('update')
|
|
349
|
+
.description('update an existing PageWithU project with the latest template')
|
|
350
|
+
.option('--dry-run', 'show what would change without writing files')
|
|
351
|
+
.option('--yes', 'skip prompts and apply safe updates')
|
|
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
|
+
}
|
|
364
|
+
|
|
365
|
+
spinner.stop();
|
|
366
|
+
const previousManifest = await loadManifest(projectDir);
|
|
367
|
+
const updates = await planTemplateUpdates(projectDir, options);
|
|
368
|
+
const packageChanged = await mergePackage(projectDir, options.dryRun);
|
|
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
|
+
});
|
|
379
|
+
|
|
380
|
+
program.parse();
|
package/docs/deploy.md
CHANGED
|
@@ -38,19 +38,60 @@ npm run preview
|
|
|
38
38
|
|
|
39
39
|
## 部署到 GitHub Pages
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
GitHub Pages 适合把你生成好的个人主页托管到 GitHub 仓库中。
|
|
42
|
+
|
|
43
|
+
### 方式一:使用 GitHub Actions 自动部署
|
|
44
|
+
|
|
45
|
+
在你自己的主页项目中创建部署工作流:
|
|
42
46
|
|
|
43
47
|
```text
|
|
44
48
|
.github/workflows/deploy-pages.yml
|
|
45
49
|
```
|
|
46
50
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
示例内容:
|
|
52
|
+
|
|
53
|
+
```yaml
|
|
54
|
+
name: Deploy to GitHub Pages
|
|
55
|
+
|
|
56
|
+
on:
|
|
57
|
+
push:
|
|
58
|
+
branches: [main]
|
|
59
|
+
workflow_dispatch:
|
|
60
|
+
|
|
61
|
+
permissions:
|
|
62
|
+
contents: read
|
|
63
|
+
pages: write
|
|
64
|
+
id-token: write
|
|
65
|
+
|
|
66
|
+
concurrency:
|
|
67
|
+
group: pages
|
|
68
|
+
cancel-in-progress: false
|
|
69
|
+
|
|
70
|
+
jobs:
|
|
71
|
+
build:
|
|
72
|
+
runs-on: ubuntu-latest
|
|
73
|
+
steps:
|
|
74
|
+
- uses: actions/checkout@v4
|
|
75
|
+
- uses: actions/setup-node@v4
|
|
76
|
+
with:
|
|
77
|
+
node-version: 20
|
|
78
|
+
cache: npm
|
|
79
|
+
- run: npm ci
|
|
80
|
+
- run: npm run build
|
|
81
|
+
- uses: actions/upload-pages-artifact@v3
|
|
82
|
+
with:
|
|
83
|
+
path: dist
|
|
84
|
+
|
|
85
|
+
deploy:
|
|
86
|
+
needs: build
|
|
87
|
+
runs-on: ubuntu-latest
|
|
88
|
+
environment:
|
|
89
|
+
name: github-pages
|
|
90
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
91
|
+
steps:
|
|
92
|
+
- id: deployment
|
|
93
|
+
uses: actions/deploy-pages@v4
|
|
94
|
+
```
|
|
54
95
|
|
|
55
96
|
首次使用前,需要在 GitHub 仓库中打开:
|
|
56
97
|
|
|
@@ -58,24 +99,23 @@ npm run preview
|
|
|
58
99
|
Settings → Pages → Build and deployment → Source → GitHub Actions
|
|
59
100
|
```
|
|
60
101
|
|
|
61
|
-
配置完成后,每次推送到 `main`
|
|
102
|
+
配置完成后,每次推送到 `main` 分支都会自动构建并发布你的站点。
|
|
62
103
|
|
|
63
|
-
|
|
64
|
-
https://explorer-dong.github.io/page-withu/
|
|
65
|
-
```
|
|
104
|
+
如果仓库名不是用于根域名的 `用户名.github.io`,通常需要给构建命令设置子路径。例如仓库名是 `my-homepage`:
|
|
66
105
|
|
|
67
|
-
|
|
106
|
+
```yaml
|
|
107
|
+
- run: BASE_PATH=/my-homepage/ npm run build
|
|
108
|
+
```
|
|
68
109
|
|
|
69
|
-
###
|
|
110
|
+
### 方式二:手动上传 dist
|
|
70
111
|
|
|
71
112
|
如果不想使用 GitHub Actions,也可以本地构建后手动上传 `dist/`。
|
|
72
113
|
|
|
73
114
|
```bash
|
|
74
|
-
|
|
75
|
-
BASE_PATH=/page-withu/ npm run build
|
|
115
|
+
npm run build
|
|
76
116
|
```
|
|
77
117
|
|
|
78
|
-
然后把 `
|
|
118
|
+
然后把 `dist/` 内容上传到用于 GitHub Pages 的发布分支或目录。
|
|
79
119
|
|
|
80
120
|
## 部署到 Cloudflare Pages
|
|
81
121
|
|
package/docs/develop.md
CHANGED
package/docs/setup.md
CHANGED
|
@@ -83,6 +83,37 @@ index_img: https://example.com/cover.jpg
|
|
|
83
83
|
- `tags`:文章标签。
|
|
84
84
|
- `index_img`:博客列表封面图,可选;不填会保留占位布局。
|
|
85
85
|
|
|
86
|
+
## 更新项目
|
|
87
|
+
|
|
88
|
+
如果你已经用 `page-withu create` 创建过项目,后续升级 CLI 后可以在项目目录中更新模板能力:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
npm install -g @x-withu/page-withu
|
|
92
|
+
cd my-homepage
|
|
93
|
+
page-withu update --dry-run
|
|
94
|
+
page-withu update
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`page-withu update` 会更新模板管理的文件,例如 `src/`、`index.html` 和 `vite.config.js`,并合并 `package.json` 中模板需要的脚本和依赖。
|
|
98
|
+
|
|
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
|
+
```
|
|
116
|
+
|
|
86
117
|
## Markdown
|
|
87
118
|
|
|
88
119
|
文章中可以使用常见 Markdown 语法,也支持以下增强能力:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@x-withu/page-withu",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "A lightweight, elegant personal homepage generator.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"template/content/",
|
|
17
17
|
"template/src/",
|
|
18
18
|
"template/index.html",
|
|
19
|
+
"template/.gitignore",
|
|
19
20
|
"template/package.json",
|
|
20
21
|
"template/vite.config.js",
|
|
21
22
|
"docs/",
|
|
@@ -84,7 +84,7 @@ const activeMonth = ref('')
|
|
|
84
84
|
const monthRefs = new Map()
|
|
85
85
|
|
|
86
86
|
const orderedPosts = computed(() => {
|
|
87
|
-
return [...props.posts].sort((a, b) => new Date(
|
|
87
|
+
return [...props.posts].sort((a, b) => new Date(b.date) - new Date(a.date))
|
|
88
88
|
})
|
|
89
89
|
|
|
90
90
|
const tags = computed(() => {
|