devfolio-page 0.2.1 → 0.2.3

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) {
@@ -96,8 +96,8 @@ const contentSectionSchema = z.discriminatedUnion('type', [
96
96
  const simpleProjectSchema = z.object({
97
97
  name: z.string().min(1, 'Project name is required'),
98
98
  url: urlString.optional(),
99
- description: z.string().min(1, 'Project description is required'),
100
- tags: z.array(z.string()).min(1, 'At least one tag is required'),
99
+ description: z.string().optional(),
100
+ tags: z.array(z.string()).optional(),
101
101
  featured: z.boolean().optional(),
102
102
  });
103
103
  // Rich project schema (for case studies)
@@ -109,10 +109,10 @@ const richProjectSchema = z.object({
109
109
  thumbnail: urlOrPath.optional(),
110
110
  hero: urlOrPath.optional(),
111
111
  meta: z.object({
112
- year: z.union([z.string(), z.number()]),
113
- role: z.string().min(1, 'Role is required'),
112
+ year: z.union([z.string(), z.number()]).optional(),
113
+ role: z.string().optional(),
114
114
  timeline: z.string().optional(),
115
- tech: z.array(z.string()).min(1, 'At least one technology is required'),
115
+ tech: z.array(z.string()).optional(),
116
116
  links: z
117
117
  .object({
118
118
  github: urlString.optional(),
@@ -121,30 +121,30 @@ const richProjectSchema = z.object({
121
121
  case_study: urlString.optional(),
122
122
  })
123
123
  .optional(),
124
- }),
125
- sections: z.array(contentSectionSchema).min(1, 'At least one section is required'),
124
+ }).optional(),
125
+ sections: z.array(contentSectionSchema).optional(),
126
126
  });
127
127
  // =============================================================================
128
128
  // Experience & Education Schemas
129
129
  // =============================================================================
130
130
  const experienceSchema = z.object({
131
131
  company: z.string().min(1, 'Company name is required'),
132
- role: z.string().min(1, 'Role is required'),
132
+ role: z.string().optional(),
133
133
  date: z.object({
134
- start: dateFormat,
135
- end: dateOrPresent,
136
- }),
134
+ start: dateFormat.optional(),
135
+ end: dateOrPresent.optional(),
136
+ }).optional(),
137
137
  location: z.string().optional(),
138
138
  description: z.string().optional(),
139
- highlights: z.array(z.string()).min(1, 'At least one highlight is required'),
139
+ highlights: z.array(z.string()).optional(),
140
140
  });
141
141
  const educationSchema = z.object({
142
142
  institution: z.string().min(1, 'Institution name is required'),
143
- degree: z.string().min(1, 'Degree is required'),
143
+ degree: z.string().optional(),
144
144
  date: z.object({
145
- start: dateFormat,
146
- end: dateFormat,
147
- }),
145
+ start: dateFormat.optional(),
146
+ end: dateFormat.optional(),
147
+ }).optional(),
148
148
  location: z.string().optional(),
149
149
  description: z.string().optional(),
150
150
  highlights: z.array(z.string()).optional(),
@@ -172,9 +172,9 @@ const richSkillsSchema = z.object({
172
172
  // =============================================================================
173
173
  const writingSchema = z.object({
174
174
  title: z.string().min(1, 'Article title is required'),
175
- url: urlString,
176
- date: dateFormat,
177
- description: z.string().optional(), // Backwards compatible
175
+ url: urlString.optional(),
176
+ date: dateFormat.optional(),
177
+ description: z.string().optional(),
178
178
  excerpt: z.string().optional(),
179
179
  cover: urlOrPath.optional(),
180
180
  publication: z.string().optional(),
@@ -186,23 +186,23 @@ const writingSchema = z.object({
186
186
  // =============================================================================
187
187
  const experimentSchema = z.object({
188
188
  title: z.string().min(1, 'Experiment title is required'),
189
- description: z.string().min(1, 'Experiment description is required'),
189
+ description: z.string().optional(),
190
190
  image: urlOrPath.optional(),
191
191
  github: urlString.optional(),
192
192
  demo: urlString.optional(),
193
- tags: z.array(z.string()).min(1, 'At least one tag is required'),
193
+ tags: z.array(z.string()).optional(),
194
194
  });
195
195
  const testimonialSchema = z.object({
196
196
  quote: z.string().min(1, 'Quote is required'),
197
- author: z.string().min(1, 'Author name is required'),
197
+ author: z.string().optional(),
198
198
  company: z.string().optional(),
199
199
  role: z.string().optional(),
200
200
  image: urlOrPath.optional(),
201
201
  });
202
202
  const timelineItemSchema = z.object({
203
- year: z.union([z.string(), z.number()]),
203
+ year: z.union([z.string(), z.number()]).optional(),
204
204
  title: z.string().min(1, 'Timeline item title is required'),
205
- description: z.string().min(1, 'Timeline item description is required'),
205
+ description: z.string().optional(),
206
206
  image: urlOrPath.optional(),
207
207
  });
208
208
  // =============================================================================
@@ -227,28 +227,28 @@ export const portfolioSchema = z.object({
227
227
  // Core metadata
228
228
  meta: z.object({
229
229
  name: z.string().min(1, 'Name is required'),
230
- title: z.string().min(1, 'Professional title is required'),
230
+ title: z.string().optional(),
231
231
  tagline: z.string().optional(),
232
- location: z.string().min(1, 'Location is required'),
232
+ location: z.string().optional(),
233
233
  timezone: z.string().optional(),
234
234
  avatar: urlOrPath.optional(),
235
235
  hero_image: urlOrPath.optional(),
236
236
  }),
237
237
  // Contact information
238
238
  contact: z.object({
239
- email: z.string().email('Must be a valid email address'),
239
+ email: z.string().email('Must be a valid email address').optional(),
240
240
  website: urlString.optional(),
241
241
  github: z.string().optional(),
242
242
  linkedin: z.string().optional(),
243
243
  twitter: z.string().optional(),
244
- }),
245
- // Bio (required for backwards compatibility)
246
- bio: z.string().min(10, 'Bio should be at least 10 characters'),
244
+ }).optional(),
245
+ // Bio
246
+ bio: z.string().optional(),
247
247
  // Extended about section (optional)
248
248
  about: z
249
249
  .object({
250
- short: z.string().min(1, 'Short bio is required'),
251
- long: z.string().min(1, 'Long bio is required'),
250
+ short: z.string().optional(),
251
+ long: z.string().optional(),
252
252
  })
253
253
  .optional(),
254
254
  // Sections (backwards compatible structure)
@@ -258,7 +258,7 @@ export const portfolioSchema = z.object({
258
258
  skills: simpleSkillsSchema.optional(),
259
259
  writing: z.array(writingSchema).optional(),
260
260
  education: z.array(educationSchema).optional(),
261
- }),
261
+ }).optional(),
262
262
  // Rich projects (new structure for case studies)
263
263
  projects: z.array(richProjectSchema).optional(),
264
264
  // New content types
@@ -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.3",
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