devfolio-page 0.2.1 → 0.2.2

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.
@@ -29,11 +29,37 @@ async function rebuild(file, options) {
29
29
  siteCache = result.files;
30
30
  console.log();
31
31
  console.log(chalk.green('✓') + ` Rebuilt ${result.fileList.length} files in memory`);
32
+ // Show available pages (HTML only)
33
+ const htmlFiles = Array.from(siteCache.keys()).filter(key => key.endsWith('.html'));
34
+ if (htmlFiles.length > 0) {
35
+ console.log(chalk.dim(' Available pages:'));
36
+ htmlFiles.forEach((key) => {
37
+ // Convert file path to route
38
+ let route = '/' + key.replace(/\.html$/, '').replace(/\/index$/, '');
39
+ if (route === '/')
40
+ route = '/ (home)';
41
+ console.log(chalk.dim(` ${route}`));
42
+ });
43
+ }
32
44
  console.log();
33
45
  }
34
46
  catch (error) {
35
47
  console.error(chalk.red('✗') + ' Build failed:');
36
- console.error(error instanceof Error ? error.message : String(error));
48
+ // Show detailed validation errors if available
49
+ if (error instanceof Error && 'errors' in error) {
50
+ const validationError = error;
51
+ console.error(chalk.red(error.message));
52
+ console.log();
53
+ validationError.errors?.forEach((err) => {
54
+ console.error(chalk.yellow(` ${err.path}`) + `: ${err.message}`);
55
+ if (err.hint) {
56
+ console.error(chalk.dim(` Example: ${err.hint}`));
57
+ }
58
+ });
59
+ }
60
+ else {
61
+ console.error(error instanceof Error ? error.message : String(error));
62
+ }
37
63
  console.log();
38
64
  }
39
65
  finally {
@@ -52,16 +78,62 @@ function startServer(port) {
52
78
  let filePath = req.url === '/' ? 'index.html' : (req.url || 'index.html');
53
79
  // Remove leading slash and query string
54
80
  filePath = filePath.replace(/^\//, '').split('?')[0];
55
- // Check if file exists in cache
56
- if (!siteCache.has(filePath)) {
57
- res.writeHead(404);
81
+ // Debug: log the request
82
+ console.log(chalk.dim(`\n Request: ${req.url} → normalized: ${filePath}`));
83
+ // Try different path variations to find the file
84
+ let content;
85
+ let matchedPath;
86
+ // 1. Try exact path
87
+ const tryExact = filePath;
88
+ console.log(chalk.dim(` [1] Exact: ${tryExact} ${siteCache.has(tryExact) ? chalk.green('✓') : chalk.red('✗')}`));
89
+ if (siteCache.has(tryExact)) {
90
+ content = siteCache.get(tryExact);
91
+ matchedPath = tryExact;
92
+ }
93
+ // 2. Try adding .html extension
94
+ if (!content) {
95
+ const tryHtml = filePath + '.html';
96
+ console.log(chalk.dim(` [2] +.html: ${tryHtml} ${siteCache.has(tryHtml) ? chalk.green('✓') : chalk.red('✗')}`));
97
+ if (siteCache.has(tryHtml)) {
98
+ content = siteCache.get(tryHtml);
99
+ matchedPath = tryHtml;
100
+ }
101
+ }
102
+ // 3. Try as directory with index.html
103
+ if (!content) {
104
+ const tryDir = filePath + '/index.html';
105
+ console.log(chalk.dim(` [3] +/index.html: ${tryDir} ${siteCache.has(tryDir) ? chalk.green('✓') : chalk.red('✗')}`));
106
+ if (siteCache.has(tryDir)) {
107
+ content = siteCache.get(tryDir);
108
+ matchedPath = tryDir;
109
+ }
110
+ }
111
+ // 4. Try removing trailing slash and adding index.html
112
+ if (!content && filePath.endsWith('/')) {
113
+ const trySlash = filePath + 'index.html';
114
+ console.log(chalk.dim(` [4] trailing/: ${trySlash} ${siteCache.has(trySlash) ? chalk.green('✓') : chalk.red('✗')}`));
115
+ if (siteCache.has(trySlash)) {
116
+ content = siteCache.get(trySlash);
117
+ matchedPath = trySlash;
118
+ }
119
+ }
120
+ // If still not found, return 404
121
+ if (!content) {
122
+ console.log(chalk.dim(` 404: ${req.url}`));
123
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
58
124
  res.end('Not found');
59
125
  return;
60
126
  }
61
- // Get file from cache
62
- const content = siteCache.get(filePath);
63
- // Set content type based on file extension
64
- const ext = path.extname(filePath);
127
+ // Log successful requests (excluding assets)
128
+ if (matchedPath && matchedPath.endsWith('.html')) {
129
+ console.log(chalk.dim(` 200: ${req.url}`) + chalk.green(` ${matchedPath}`));
130
+ }
131
+ // Determine content type from original request or file path
132
+ let ext = path.extname(req.url || '');
133
+ // If no extension in URL, check if we're serving HTML
134
+ if (!ext && content) {
135
+ ext = '.html';
136
+ }
65
137
  const contentTypes = {
66
138
  '.html': 'text/html',
67
139
  '.css': 'text/css',
@@ -82,8 +154,8 @@ function startServer(port) {
82
154
  res.end(content);
83
155
  }
84
156
  catch (error) {
85
- console.error('Server error:', error);
86
- res.writeHead(500);
157
+ console.error(chalk.red('✗') + ' Server error:', error);
158
+ res.writeHead(500, { 'Content-Type': 'text/plain' });
87
159
  res.end('Internal server error');
88
160
  }
89
161
  });
@@ -115,10 +187,37 @@ export async function devCommand(file = 'portfolio.yaml', options = {}) {
115
187
  });
116
188
  siteCache = result.files;
117
189
  console.log(chalk.green(' ✓') + ` Built ${result.fileList.length} files`);
190
+ console.log();
191
+ // Show available pages (HTML only)
192
+ const htmlFiles = Array.from(siteCache.keys()).filter(key => key.endsWith('.html'));
193
+ if (htmlFiles.length > 0) {
194
+ console.log(chalk.dim(' Available pages:'));
195
+ htmlFiles.forEach((key) => {
196
+ // Convert file path to route
197
+ let route = '/' + key.replace(/\.html$/, '').replace(/\/index$/, '');
198
+ if (route === '/')
199
+ route = '/ (home)';
200
+ console.log(chalk.dim(` ${route}`));
201
+ });
202
+ }
118
203
  }
119
204
  catch (error) {
120
205
  console.error(chalk.red('✗') + ' Initial build failed:');
121
- console.error(error instanceof Error ? error.message : String(error));
206
+ // Show detailed validation errors if available
207
+ if (error instanceof Error && 'errors' in error) {
208
+ const validationError = error;
209
+ console.error(chalk.red(error.message));
210
+ console.log();
211
+ validationError.errors?.forEach((err) => {
212
+ console.error(chalk.yellow(` ${err.path}`) + `: ${err.message}`);
213
+ if (err.hint) {
214
+ console.error(chalk.dim(` Example: ${err.hint}`));
215
+ }
216
+ });
217
+ }
218
+ else {
219
+ console.error(error instanceof Error ? error.message : String(error));
220
+ }
122
221
  process.exit(1);
123
222
  }
124
223
  // Start server
@@ -35,6 +35,47 @@ function question(rl, prompt) {
35
35
  });
36
36
  });
37
37
  }
38
+ function isValidEmail(email) {
39
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
40
+ return emailRegex.test(email);
41
+ }
42
+ function isValidUrl(url) {
43
+ try {
44
+ const parsed = new URL(url);
45
+ return parsed.protocol === 'http:' || parsed.protocol === 'https:';
46
+ }
47
+ catch {
48
+ return false;
49
+ }
50
+ }
51
+ function isValidUsername(username) {
52
+ // Basic validation: alphanumeric, hyphens, underscores
53
+ const usernameRegex = /^[a-zA-Z0-9_-]+$/;
54
+ return usernameRegex.test(username);
55
+ }
56
+ async function questionWithValidation(rl, prompt, validator, optional = false) {
57
+ while (true) {
58
+ const answer = await question(rl, prompt);
59
+ // Allow empty if optional
60
+ if (!answer && optional) {
61
+ return '';
62
+ }
63
+ // Require value if not optional
64
+ if (!answer && !optional) {
65
+ console.log(chalk.red(' ✗ This field is required'));
66
+ continue;
67
+ }
68
+ // Validate if validator provided
69
+ if (answer && validator) {
70
+ const result = validator(answer);
71
+ if (!result.valid) {
72
+ console.log(chalk.red(` ✗ ${result.error}`));
73
+ continue;
74
+ }
75
+ }
76
+ return answer;
77
+ }
78
+ }
38
79
  function printHeader() {
39
80
  console.log();
40
81
  console.log(chalk.cyan('┌────────────────────────────────────────┐'));
@@ -195,26 +236,15 @@ export async function initCommand() {
195
236
  rl.close();
196
237
  process.exit(1);
197
238
  }
198
- const name = await question(rl, chalk.cyan('? ') + 'What\'s your name? ');
199
- if (!name) {
200
- console.log(chalk.red('') + ' Name is required');
201
- process.exit(1);
202
- }
203
- const title = await question(rl, chalk.cyan('? ') + 'What\'s your title/role? ');
204
- if (!title) {
205
- console.log(chalk.red('✗') + ' Title is required');
206
- process.exit(1);
207
- }
208
- const location = await question(rl, chalk.cyan('? ') + 'Where are you located? ');
209
- if (!location) {
210
- console.log(chalk.red('✗') + ' Location is required');
211
- process.exit(1);
212
- }
213
- const email = await question(rl, chalk.cyan('? ') + 'What\'s your email? ');
214
- if (!email) {
215
- console.log(chalk.red('✗') + ' Email is required');
216
- process.exit(1);
217
- }
239
+ const name = await questionWithValidation(rl, chalk.cyan('? ') + 'What\'s your name? ');
240
+ const title = await questionWithValidation(rl, chalk.cyan('? ') + 'What\'s your title/role? ');
241
+ const location = await questionWithValidation(rl, chalk.cyan('? ') + 'Where are you located? ');
242
+ const email = await questionWithValidation(rl, chalk.cyan('? ') + 'What\'s your email? ', (value) => {
243
+ if (!isValidEmail(value)) {
244
+ return { valid: false, error: 'Invalid email format (example: you@example.com)' };
245
+ }
246
+ return { valid: true };
247
+ });
218
248
  // Theme selection
219
249
  console.log();
220
250
  console.log(chalk.cyan('? ') + 'Which theme would you like?');
@@ -236,10 +266,30 @@ export async function initCommand() {
236
266
  console.log(chalk.yellow(' Invalid choice, using SRCL'));
237
267
  }
238
268
  console.log(chalk.dim('\n Optional fields (press Enter to skip)'));
239
- const github = await question(rl, chalk.cyan('? ') + 'GitHub username? ' + chalk.dim('(optional) '));
240
- const linkedin = await question(rl, chalk.cyan('? ') + 'LinkedIn username? ' + chalk.dim('(optional) '));
241
- const twitter = await question(rl, chalk.cyan('? ') + 'Twitter username? ' + chalk.dim('(optional) '));
242
- const website = await question(rl, chalk.cyan('? ') + 'Website URL? ' + chalk.dim('(optional) '));
269
+ const github = await questionWithValidation(rl, chalk.cyan('? ') + 'GitHub username? ' + chalk.dim('(optional) '), (value) => {
270
+ if (!isValidUsername(value)) {
271
+ return { valid: false, error: 'Invalid username format (letters, numbers, hyphens, underscores only)' };
272
+ }
273
+ return { valid: true };
274
+ }, true);
275
+ const linkedin = await questionWithValidation(rl, chalk.cyan('? ') + 'LinkedIn username? ' + chalk.dim('(optional) '), (value) => {
276
+ if (!isValidUsername(value)) {
277
+ return { valid: false, error: 'Invalid username format (letters, numbers, hyphens, underscores only)' };
278
+ }
279
+ return { valid: true };
280
+ }, true);
281
+ const twitter = await questionWithValidation(rl, chalk.cyan('? ') + 'Twitter username? ' + chalk.dim('(optional) '), (value) => {
282
+ if (!isValidUsername(value)) {
283
+ return { valid: false, error: 'Invalid username format (letters, numbers, hyphens, underscores only)' };
284
+ }
285
+ return { valid: true };
286
+ }, true);
287
+ const website = await questionWithValidation(rl, chalk.cyan('? ') + 'Website URL? ' + chalk.dim('(optional) '), (value) => {
288
+ if (!isValidUrl(value)) {
289
+ return { valid: false, error: 'Invalid URL format (must start with http:// or https://)' };
290
+ }
291
+ return { valid: true };
292
+ }, true);
243
293
  rl.close();
244
294
  const input = {
245
295
  name,
@@ -267,12 +317,12 @@ export async function initCommand() {
267
317
  console.log(chalk.dim(' └── ') + chalk.cyan('site/') + chalk.dim(' (generated after render)'));
268
318
  console.log();
269
319
  console.log(chalk.bold('Next steps:'));
270
- console.log(chalk.dim(' 1.') + ' Edit ' + chalk.cyan(`${folderName}/portfolio.yaml`) + ' and add your projects');
320
+ console.log(chalk.dim(' 1.') + ' Edit ' + chalk.cyan(`${folderName}/portfolio.yaml`) + ' and add your content');
271
321
  console.log(chalk.dim(' 2.') + ' Add images to ' + chalk.cyan(`${folderName}/images/`));
272
- console.log(chalk.dim(' 3.') + ' Run: ' + chalk.cyan(`cd ${folderName} && devfolio-page render`));
273
- console.log(chalk.dim(' 4.') + ' Open: ' + chalk.cyan(`${folderName}/site/index.html`) + ' to view');
322
+ console.log(chalk.dim(' 3.') + ' Run: ' + chalk.cyan(`cd ${folderName} && devfolio-page dev`));
323
+ console.log(chalk.dim(' 4.') + ' Open: ' + chalk.cyan(`http://localhost:3000`) + ' in your browser');
274
324
  console.log();
275
- console.log('Tip: Check ' + chalk.cyan('https://devfolio.page/portfolios/') + ' for examples');
325
+ console.log('Tip: Check ' + chalk.cyan('https://devfolio.page/docs') + ' for full documentation');
276
326
  console.log();
277
327
  }
278
328
  catch (err) {
@@ -645,7 +645,8 @@ async function generateSinglePageSiteInMemory(portfolio, themePath, theme, files
645
645
  enableHotkeys: settings.enable_hotkeys !== false,
646
646
  animate: settings.animate || 'subtle',
647
647
  hasExperience: (portfolio.sections.experience?.length ?? 0) > 0,
648
- hasProjects: (portfolio.sections.projects?.length ?? 0) > 0,
648
+ hasProjects: ((portfolio.projects?.length ?? 0) > 0) || ((portfolio.sections.projects?.length ?? 0) > 0),
649
+ hasExperiments: (portfolio.experiments?.length ?? 0) > 0,
649
650
  hasSkills: portfolio.sections.skills && Object.keys(portfolio.sections.skills).length > 0,
650
651
  hasWriting: (portfolio.sections.writing?.length ?? 0) > 0,
651
652
  hasEducation: (portfolio.sections.education?.length ?? 0) > 0,
@@ -1,5 +1,5 @@
1
1
  /* ============================================
2
- Dark Academia Theme for dev.page
2
+ Dark Academia Theme for devfolio.page
3
3
  Scholarly, vintage, warm aesthetic
4
4
  ============================================ */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /* ============================================
2
- Modern Theme for dev.page
2
+ Modern Theme for devfolio.page
3
3
  Clean, contemporary, minimalist design
4
4
  ============================================ */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /* ============================================
2
- SRCL Theme for dev.page
2
+ SRCL Theme for devfolio.page
3
3
  Adapted from sacred.computer components
4
4
  ============================================ */
5
5
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devfolio-page",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Your portfolio as code. Version control it like software.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,5 +1,5 @@
1
1
  /* ============================================
2
- Dark Academia Theme for dev.page
2
+ Dark Academia Theme for devfolio.page
3
3
  Scholarly, vintage, warm aesthetic
4
4
  ============================================ */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /* ============================================
2
- Modern Theme for dev.page
2
+ Modern Theme for devfolio.page
3
3
  Clean, contemporary, minimalist design
4
4
  ============================================ */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /* ============================================
2
- SRCL Theme for dev.page
2
+ SRCL Theme for devfolio.page
3
3
  Adapted from sacred.computer components
4
4
  ============================================ */
5
5