devfolio-page 0.1.4 → 0.2.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/README.md +25 -4
- package/dist/cli/commands/dev.js +162 -0
- package/dist/cli/index.js +10 -1
- package/dist/cli/postinstall.js +1 -0
- package/dist/generator/builder.js +279 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -18,11 +18,11 @@ devfolio-page init
|
|
|
18
18
|
cd my-portfolio
|
|
19
19
|
nano portfolio.yaml
|
|
20
20
|
|
|
21
|
-
#
|
|
22
|
-
devfolio-page
|
|
21
|
+
# Start development server with auto-rebuild
|
|
22
|
+
devfolio-page dev
|
|
23
23
|
|
|
24
|
-
#
|
|
25
|
-
|
|
24
|
+
# Or generate the static site once
|
|
25
|
+
devfolio-page render
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
## Folder Structure
|
|
@@ -48,6 +48,27 @@ Create a new portfolio folder with config and images directory.
|
|
|
48
48
|
devfolio-page init
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
+
### `devfolio-page dev`
|
|
52
|
+
|
|
53
|
+
Start development server with file watching and auto-rebuild.
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
devfolio-page dev # Start server on port 3000
|
|
57
|
+
devfolio-page dev --port 8080 # Use custom port
|
|
58
|
+
devfolio-page dev --theme modern # Use a specific theme
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Options:**
|
|
62
|
+
|
|
63
|
+
- `-p, --port <port>` - Port for dev server (default: 3000)
|
|
64
|
+
- `-t, --theme <theme>` - Theme to use (srcl, modern, dark-academia)
|
|
65
|
+
- `-o, --output <dir>` - Output directory (default: ./site)
|
|
66
|
+
|
|
67
|
+
The dev server will:
|
|
68
|
+
- Watch `portfolio.yaml` and `images/` for changes
|
|
69
|
+
- Automatically rebuild your site when files change
|
|
70
|
+
- Serve your site at `http://localhost:3000`
|
|
71
|
+
|
|
51
72
|
### `devfolio-page render`
|
|
52
73
|
|
|
53
74
|
Generate a static website. Run from inside your portfolio folder.
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import chokidar from 'chokidar';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
import http from 'http';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { buildStaticSiteInMemory } from '../../generator/builder.js';
|
|
7
|
+
import { validatePortfolio } from '../helpers/validate.js';
|
|
8
|
+
let isBuilding = false;
|
|
9
|
+
let buildQueued = false;
|
|
10
|
+
let siteCache = new Map();
|
|
11
|
+
async function rebuild(file, options) {
|
|
12
|
+
// If already building, queue another build
|
|
13
|
+
if (isBuilding) {
|
|
14
|
+
buildQueued = true;
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
isBuilding = true;
|
|
18
|
+
try {
|
|
19
|
+
console.log();
|
|
20
|
+
console.log(chalk.dim(`[${new Date().toLocaleTimeString()}]`) + ' File changed: ' + chalk.cyan(file));
|
|
21
|
+
console.log(chalk.dim('Rebuilding in memory...'));
|
|
22
|
+
console.log();
|
|
23
|
+
// Validate and build in memory
|
|
24
|
+
const portfolio = validatePortfolio('portfolio.yaml');
|
|
25
|
+
const result = await buildStaticSiteInMemory(portfolio, {
|
|
26
|
+
theme: options.theme,
|
|
27
|
+
});
|
|
28
|
+
// Update cache
|
|
29
|
+
siteCache = result.files;
|
|
30
|
+
console.log();
|
|
31
|
+
console.log(chalk.green('✓') + ` Rebuilt ${result.fileList.length} files in memory`);
|
|
32
|
+
console.log();
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
console.error(chalk.red('✗') + ' Build failed:');
|
|
36
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
37
|
+
console.log();
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
isBuilding = false;
|
|
41
|
+
// If a build was queued while we were building, run it now
|
|
42
|
+
if (buildQueued) {
|
|
43
|
+
buildQueued = false;
|
|
44
|
+
setTimeout(() => rebuild(file, options), 100);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function startServer(port) {
|
|
49
|
+
const server = http.createServer(async (req, res) => {
|
|
50
|
+
try {
|
|
51
|
+
// Default to index.html
|
|
52
|
+
let filePath = req.url === '/' ? 'index.html' : (req.url || 'index.html');
|
|
53
|
+
// Remove leading slash and query string
|
|
54
|
+
filePath = filePath.replace(/^\//, '').split('?')[0];
|
|
55
|
+
// Check if file exists in cache
|
|
56
|
+
if (!siteCache.has(filePath)) {
|
|
57
|
+
res.writeHead(404);
|
|
58
|
+
res.end('Not found');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
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);
|
|
65
|
+
const contentTypes = {
|
|
66
|
+
'.html': 'text/html',
|
|
67
|
+
'.css': 'text/css',
|
|
68
|
+
'.js': 'application/javascript',
|
|
69
|
+
'.json': 'application/json',
|
|
70
|
+
'.png': 'image/png',
|
|
71
|
+
'.jpg': 'image/jpeg',
|
|
72
|
+
'.jpeg': 'image/jpeg',
|
|
73
|
+
'.gif': 'image/gif',
|
|
74
|
+
'.svg': 'image/svg+xml',
|
|
75
|
+
'.ico': 'image/x-icon',
|
|
76
|
+
'.woff': 'font/woff',
|
|
77
|
+
'.woff2': 'font/woff2',
|
|
78
|
+
'.ttf': 'font/ttf',
|
|
79
|
+
'.otf': 'font/otf',
|
|
80
|
+
};
|
|
81
|
+
res.writeHead(200, { 'Content-Type': contentTypes[ext] || 'application/octet-stream' });
|
|
82
|
+
res.end(content);
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
console.error('Server error:', error);
|
|
86
|
+
res.writeHead(500);
|
|
87
|
+
res.end('Internal server error');
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
server.listen(port);
|
|
91
|
+
return server;
|
|
92
|
+
}
|
|
93
|
+
export async function devCommand(file = 'portfolio.yaml', options = {}) {
|
|
94
|
+
const port = options.port || 3000;
|
|
95
|
+
// Check if portfolio.yaml exists
|
|
96
|
+
if (!existsSync(file)) {
|
|
97
|
+
console.error(chalk.red('✗') + ` File not found: ${file}`);
|
|
98
|
+
console.log();
|
|
99
|
+
console.log('Make sure you are in a portfolio directory with a portfolio.yaml file.');
|
|
100
|
+
console.log('Run ' + chalk.cyan('devfolio-page init') + ' to create a new portfolio.');
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
console.log();
|
|
104
|
+
console.log(chalk.cyan('┌─────────────────────────────────────────────────────────┐'));
|
|
105
|
+
console.log(chalk.cyan('│') + ' ' + chalk.bold('devfolio.page') + ' - Development Mode ' + chalk.cyan('│'));
|
|
106
|
+
console.log(chalk.cyan('└─────────────────────────────────────────────────────────┘'));
|
|
107
|
+
console.log();
|
|
108
|
+
// Initial build
|
|
109
|
+
console.log(chalk.bold('Building initial site in memory...'));
|
|
110
|
+
console.log();
|
|
111
|
+
try {
|
|
112
|
+
const portfolio = validatePortfolio(file);
|
|
113
|
+
const result = await buildStaticSiteInMemory(portfolio, {
|
|
114
|
+
theme: options.theme,
|
|
115
|
+
});
|
|
116
|
+
siteCache = result.files;
|
|
117
|
+
console.log(chalk.green(' ✓') + ` Built ${result.fileList.length} files`);
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
console.error(chalk.red('✗') + ' Initial build failed:');
|
|
121
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
// Start server
|
|
125
|
+
const server = startServer(port);
|
|
126
|
+
console.log();
|
|
127
|
+
console.log(chalk.green('✓') + ' Dev server started!');
|
|
128
|
+
console.log();
|
|
129
|
+
console.log(' ' + chalk.bold('Local:') + ' ' + chalk.cyan(`http://localhost:${port}`));
|
|
130
|
+
console.log(' ' + chalk.bold('Mode:') + ' ' + chalk.dim('In-memory (no site/ folder)'));
|
|
131
|
+
console.log();
|
|
132
|
+
console.log(chalk.dim('Watching for changes...'));
|
|
133
|
+
console.log(chalk.dim('Press Ctrl+C to stop'));
|
|
134
|
+
console.log();
|
|
135
|
+
// Watch for file changes
|
|
136
|
+
const watcher = chokidar.watch([file, 'images/**/*'], {
|
|
137
|
+
persistent: true,
|
|
138
|
+
ignoreInitial: true,
|
|
139
|
+
awaitWriteFinish: {
|
|
140
|
+
stabilityThreshold: 100,
|
|
141
|
+
pollInterval: 100,
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
watcher.on('change', (changedFile) => {
|
|
145
|
+
rebuild(changedFile, options);
|
|
146
|
+
});
|
|
147
|
+
watcher.on('add', (changedFile) => {
|
|
148
|
+
rebuild(changedFile, options);
|
|
149
|
+
});
|
|
150
|
+
watcher.on('unlink', (changedFile) => {
|
|
151
|
+
rebuild(changedFile, options);
|
|
152
|
+
});
|
|
153
|
+
// Handle process exit
|
|
154
|
+
process.on('SIGINT', () => {
|
|
155
|
+
console.log();
|
|
156
|
+
console.log();
|
|
157
|
+
console.log(chalk.dim('Shutting down...'));
|
|
158
|
+
watcher.close();
|
|
159
|
+
server.close();
|
|
160
|
+
process.exit(0);
|
|
161
|
+
});
|
|
162
|
+
}
|
package/dist/cli/index.js
CHANGED
|
@@ -5,11 +5,12 @@ import { initCommand } from './commands/init.js';
|
|
|
5
5
|
import { validateCommand } from './commands/validate.js';
|
|
6
6
|
import { renderCommand } from './commands/render.js';
|
|
7
7
|
import { themesCommand } from './commands/themes.js';
|
|
8
|
+
import { devCommand } from './commands/dev.js';
|
|
8
9
|
const program = new Command();
|
|
9
10
|
program
|
|
10
11
|
.name('devfolio-page')
|
|
11
12
|
.description('Your portfolio as code. Version control it like software.')
|
|
12
|
-
.version('0.1
|
|
13
|
+
.version('0.2.1');
|
|
13
14
|
program
|
|
14
15
|
.command('init')
|
|
15
16
|
.description('Create a new portfolio.yaml with interactive prompts')
|
|
@@ -28,6 +29,13 @@ program
|
|
|
28
29
|
.command('themes')
|
|
29
30
|
.description('List available themes with examples')
|
|
30
31
|
.action(themesCommand);
|
|
32
|
+
program
|
|
33
|
+
.command('dev [file]')
|
|
34
|
+
.description('Start development server with file watching')
|
|
35
|
+
.option('-p, --port <port>', 'Port for dev server', '3000')
|
|
36
|
+
.option('-t, --theme <theme>', 'Theme to use (defaults to YAML theme or srcl)')
|
|
37
|
+
.option('-o, --output <dir>', 'Output directory', './site')
|
|
38
|
+
.action((file, options) => devCommand(file || 'portfolio.yaml', options));
|
|
31
39
|
// Show welcome message if no arguments provided
|
|
32
40
|
if (process.argv.length === 2) {
|
|
33
41
|
console.log();
|
|
@@ -38,6 +46,7 @@ if (process.argv.length === 2) {
|
|
|
38
46
|
console.log(chalk.bold('Commands:'));
|
|
39
47
|
console.log();
|
|
40
48
|
console.log(' ' + chalk.cyan('devfolio-page init') + ' Create a new portfolio folder');
|
|
49
|
+
console.log(' ' + chalk.cyan('devfolio-page dev') + ' Start dev server with auto-rebuild');
|
|
41
50
|
console.log(' ' + chalk.cyan('devfolio-page render') + ' Generate site (uses portfolio.yaml)');
|
|
42
51
|
console.log(' ' + chalk.cyan('devfolio-page themes') + ' List available themes');
|
|
43
52
|
console.log(' ' + chalk.cyan('devfolio-page validate') + ' <file> Validate a portfolio YAML file');
|
package/dist/cli/postinstall.js
CHANGED
|
@@ -11,6 +11,7 @@ log();
|
|
|
11
11
|
log(chalk.bold('Available commands:'));
|
|
12
12
|
log();
|
|
13
13
|
log(' ' + chalk.cyan('devfolio-page init') + ' Create a new portfolio folder');
|
|
14
|
+
log(' ' + chalk.cyan('devfolio-page dev') + ' Start dev server with auto-rebuild');
|
|
14
15
|
log(' ' + chalk.cyan('devfolio-page render') + ' Generate static site from YAML');
|
|
15
16
|
log(' ' + chalk.cyan('devfolio-page validate') + ' <file> Validate a portfolio YAML file');
|
|
16
17
|
log(' ' + chalk.cyan('devfolio-page --help') + ' Show all options');
|
|
@@ -33,6 +33,34 @@ export async function buildStaticSite(portfolio, options) {
|
|
|
33
33
|
return { outputDir, files };
|
|
34
34
|
}
|
|
35
35
|
// =============================================================================
|
|
36
|
+
// In-Memory Build Function (for dev mode)
|
|
37
|
+
// =============================================================================
|
|
38
|
+
export async function buildStaticSiteInMemory(portfolio, options) {
|
|
39
|
+
const theme = options.theme || portfolio.theme || 'srcl';
|
|
40
|
+
const themePath = path.join(import.meta.dirname, 'themes', theme);
|
|
41
|
+
const files = new Map();
|
|
42
|
+
const fileList = [];
|
|
43
|
+
// 1. Copy theme assets (CSS, JS)
|
|
44
|
+
const assetFiles = await copyThemeAssetsInMemory(themePath, portfolio.settings, files);
|
|
45
|
+
fileList.push(...assetFiles);
|
|
46
|
+
// 2. Copy fonts
|
|
47
|
+
const fontFiles = await copyFontsInMemory(files);
|
|
48
|
+
fileList.push(...fontFiles);
|
|
49
|
+
// 3. Generate pages based on portfolio structure
|
|
50
|
+
const hasRichProjects = (portfolio.projects?.length ?? 0) > 0;
|
|
51
|
+
if (hasRichProjects) {
|
|
52
|
+
// Multi-page site with project case studies
|
|
53
|
+
const pageFiles = await generateMultiPageSiteInMemory(portfolio, themePath, theme, files);
|
|
54
|
+
fileList.push(...pageFiles);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
// Single-page site (backwards compatible)
|
|
58
|
+
const pageFiles = await generateSinglePageSiteInMemory(portfolio, themePath, theme, files);
|
|
59
|
+
fileList.push(...pageFiles);
|
|
60
|
+
}
|
|
61
|
+
return { files, fileList };
|
|
62
|
+
}
|
|
63
|
+
// =============================================================================
|
|
36
64
|
// Directory Structure
|
|
37
65
|
// =============================================================================
|
|
38
66
|
async function createDirectoryStructure(outputDir) {
|
|
@@ -554,3 +582,254 @@ async function fileExists(filePath) {
|
|
|
554
582
|
return false;
|
|
555
583
|
}
|
|
556
584
|
}
|
|
585
|
+
// =============================================================================
|
|
586
|
+
// In-Memory Build Helpers
|
|
587
|
+
// =============================================================================
|
|
588
|
+
async function copyThemeAssetsInMemory(themePath, settings, files) {
|
|
589
|
+
const fileList = [];
|
|
590
|
+
// Copy CSS
|
|
591
|
+
const cssPath = path.join(themePath, 'styles.css');
|
|
592
|
+
if (await fileExists(cssPath)) {
|
|
593
|
+
const content = await fs.readFile(cssPath);
|
|
594
|
+
files.set('assets/styles.css', content);
|
|
595
|
+
fileList.push('assets/styles.css');
|
|
596
|
+
}
|
|
597
|
+
// Copy JS
|
|
598
|
+
const jsPath = path.join(themePath, 'script.js');
|
|
599
|
+
if (await fileExists(jsPath)) {
|
|
600
|
+
const content = await fs.readFile(jsPath);
|
|
601
|
+
files.set('assets/script.js', content);
|
|
602
|
+
fileList.push('assets/script.js');
|
|
603
|
+
}
|
|
604
|
+
return fileList;
|
|
605
|
+
}
|
|
606
|
+
async function copyFontsInMemory(files) {
|
|
607
|
+
const fileList = [];
|
|
608
|
+
const srcFontDir = '/Users/louanne/www-sacred/public/fonts';
|
|
609
|
+
try {
|
|
610
|
+
const fontFiles = await fs.readdir(srcFontDir);
|
|
611
|
+
for (const file of fontFiles) {
|
|
612
|
+
if (file.endsWith('.woff') ||
|
|
613
|
+
file.endsWith('.woff2') ||
|
|
614
|
+
file.endsWith('.ttf') ||
|
|
615
|
+
file.endsWith('.otf') ||
|
|
616
|
+
file.endsWith('.css')) {
|
|
617
|
+
const content = await fs.readFile(path.join(srcFontDir, file));
|
|
618
|
+
files.set(`assets/fonts/${file}`, content);
|
|
619
|
+
fileList.push(`assets/fonts/${file}`);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
catch {
|
|
624
|
+
// Fonts directory doesn't exist or isn't accessible - that's fine
|
|
625
|
+
}
|
|
626
|
+
return fileList;
|
|
627
|
+
}
|
|
628
|
+
async function generateSinglePageSiteInMemory(portfolio, themePath, theme, files) {
|
|
629
|
+
const fileList = [];
|
|
630
|
+
// Load template and partials
|
|
631
|
+
const template = await fs.readFile(path.join(themePath, 'template.html'), 'utf-8');
|
|
632
|
+
const partials = await loadPartials(themePath);
|
|
633
|
+
// Prepare template data
|
|
634
|
+
const templateData = prepareTemplateData(portfolio);
|
|
635
|
+
// Render each partial
|
|
636
|
+
const renderedPartials = renderPartials(partials, templateData);
|
|
637
|
+
// Render main template with settings
|
|
638
|
+
const settings = portfolio.settings || {};
|
|
639
|
+
const defaultColorScheme = theme === 'dark-academia' ? 'light' : 'dark';
|
|
640
|
+
const html = Mustache.render(template, {
|
|
641
|
+
...templateData,
|
|
642
|
+
...renderedPartials,
|
|
643
|
+
colorScheme: settings.color_scheme || defaultColorScheme,
|
|
644
|
+
showGrid: settings.show_grid || false,
|
|
645
|
+
enableHotkeys: settings.enable_hotkeys !== false,
|
|
646
|
+
animate: settings.animate || 'subtle',
|
|
647
|
+
hasExperience: (portfolio.sections.experience?.length ?? 0) > 0,
|
|
648
|
+
hasProjects: (portfolio.sections.projects?.length ?? 0) > 0,
|
|
649
|
+
hasSkills: portfolio.sections.skills && Object.keys(portfolio.sections.skills).length > 0,
|
|
650
|
+
hasWriting: (portfolio.sections.writing?.length ?? 0) > 0,
|
|
651
|
+
hasEducation: (portfolio.sections.education?.length ?? 0) > 0,
|
|
652
|
+
});
|
|
653
|
+
files.set('index.html', Buffer.from(html, 'utf-8'));
|
|
654
|
+
fileList.push('index.html');
|
|
655
|
+
return fileList;
|
|
656
|
+
}
|
|
657
|
+
async function generateMultiPageSiteInMemory(portfolio, themePath, theme, files) {
|
|
658
|
+
const fileList = [];
|
|
659
|
+
// Check if theme has multi-page templates
|
|
660
|
+
const hasMultiPageTemplates = await fileExists(path.join(themePath, 'templates/homepage.html'));
|
|
661
|
+
if (!hasMultiPageTemplates) {
|
|
662
|
+
return generateSinglePageSiteInMemory(portfolio, themePath, theme, files);
|
|
663
|
+
}
|
|
664
|
+
// Generate homepage
|
|
665
|
+
const homepageFiles = await generateHomepageInMemory(portfolio, themePath, theme, files);
|
|
666
|
+
fileList.push(...homepageFiles);
|
|
667
|
+
// Generate project pages
|
|
668
|
+
const projectFiles = await generateProjectPagesInMemory(portfolio, themePath, theme, files);
|
|
669
|
+
fileList.push(...projectFiles);
|
|
670
|
+
// Generate projects index
|
|
671
|
+
const projectIndexFiles = await generateProjectsIndexInMemory(portfolio, themePath, theme, files);
|
|
672
|
+
fileList.push(...projectIndexFiles);
|
|
673
|
+
// Generate experiments index
|
|
674
|
+
const experimentsIndexFiles = await generateExperimentsIndexInMemory(portfolio, themePath, theme, files);
|
|
675
|
+
fileList.push(...experimentsIndexFiles);
|
|
676
|
+
// Generate writing index
|
|
677
|
+
const writingIndexFiles = await generateWritingIndexInMemory(portfolio, themePath, theme, files);
|
|
678
|
+
fileList.push(...writingIndexFiles);
|
|
679
|
+
// Copy user images
|
|
680
|
+
await copyUserImagesInMemory(portfolio, files);
|
|
681
|
+
return fileList;
|
|
682
|
+
}
|
|
683
|
+
async function generateHomepageInMemory(portfolio, themePath, theme, files) {
|
|
684
|
+
const templatePath = path.join(themePath, 'templates/homepage.html');
|
|
685
|
+
if (!(await fileExists(templatePath))) {
|
|
686
|
+
return [];
|
|
687
|
+
}
|
|
688
|
+
const template = await fs.readFile(templatePath, 'utf-8');
|
|
689
|
+
const settings = portfolio.settings || {};
|
|
690
|
+
const defaultColorScheme = theme === 'dark-academia' ? 'light' : 'dark';
|
|
691
|
+
const data = {
|
|
692
|
+
name: portfolio.meta.name,
|
|
693
|
+
title: portfolio.meta.title,
|
|
694
|
+
location: portfolio.meta.location,
|
|
695
|
+
timezone: portfolio.meta.timezone,
|
|
696
|
+
email: portfolio.contact.email,
|
|
697
|
+
website: portfolio.contact.website,
|
|
698
|
+
github: portfolio.contact.github,
|
|
699
|
+
linkedin: portfolio.contact.linkedin,
|
|
700
|
+
twitter: portfolio.contact.twitter,
|
|
701
|
+
bio: portfolio.bio,
|
|
702
|
+
bio_html: parseBio(portfolio.bio),
|
|
703
|
+
about_short: portfolio.about?.short || '',
|
|
704
|
+
featured_projects: portfolio.projects?.filter((p) => p.featured) || [],
|
|
705
|
+
featured_writing: portfolio.sections.writing?.filter((w) => 'featured' in w && w.featured) || [],
|
|
706
|
+
show_experiments: portfolio.layout?.show_experiments,
|
|
707
|
+
experiments: portfolio.experiments?.slice(0, 4) || [],
|
|
708
|
+
colorScheme: settings.color_scheme || defaultColorScheme,
|
|
709
|
+
showGrid: settings.show_grid || false,
|
|
710
|
+
enableHotkeys: settings.enable_hotkeys !== false,
|
|
711
|
+
animate: settings.animate || 'subtle',
|
|
712
|
+
hasProjects: (portfolio.projects?.length ?? 0) > 0,
|
|
713
|
+
hasExperiments: (portfolio.experiments?.length ?? 0) > 0,
|
|
714
|
+
hasWriting: (portfolio.sections.writing?.length ?? 0) > 0,
|
|
715
|
+
};
|
|
716
|
+
const html = Mustache.render(template, data);
|
|
717
|
+
files.set('index.html', Buffer.from(html, 'utf-8'));
|
|
718
|
+
return ['index.html'];
|
|
719
|
+
}
|
|
720
|
+
async function generateProjectPagesInMemory(portfolio, themePath, theme, files) {
|
|
721
|
+
if (!portfolio.projects || portfolio.projects.length === 0) {
|
|
722
|
+
return [];
|
|
723
|
+
}
|
|
724
|
+
const templatePath = path.join(themePath, 'templates/project.html');
|
|
725
|
+
if (!(await fileExists(templatePath))) {
|
|
726
|
+
return [];
|
|
727
|
+
}
|
|
728
|
+
const template = await fs.readFile(templatePath, 'utf-8');
|
|
729
|
+
const partials = await loadProjectPartials(themePath);
|
|
730
|
+
const settings = portfolio.settings || {};
|
|
731
|
+
const defaultColorScheme = theme === 'dark-academia' ? 'light' : 'dark';
|
|
732
|
+
const fileList = [];
|
|
733
|
+
for (const project of portfolio.projects) {
|
|
734
|
+
const sectionsHtml = project.sections
|
|
735
|
+
.map((section) => renderContentSection(section, partials))
|
|
736
|
+
.join('\n');
|
|
737
|
+
const data = {
|
|
738
|
+
...project,
|
|
739
|
+
sections_html: sectionsHtml,
|
|
740
|
+
site_name: portfolio.meta.name,
|
|
741
|
+
nav_links: generateNavLinks(portfolio, true, 'project'),
|
|
742
|
+
colorScheme: settings.color_scheme || defaultColorScheme,
|
|
743
|
+
showGrid: settings.show_grid || false,
|
|
744
|
+
enableHotkeys: settings.enable_hotkeys !== false,
|
|
745
|
+
};
|
|
746
|
+
const html = Mustache.render(template, data);
|
|
747
|
+
const filename = `projects/${project.id}.html`;
|
|
748
|
+
files.set(filename, Buffer.from(html, 'utf-8'));
|
|
749
|
+
fileList.push(filename);
|
|
750
|
+
}
|
|
751
|
+
return fileList;
|
|
752
|
+
}
|
|
753
|
+
async function generateProjectsIndexInMemory(portfolio, themePath, theme, files) {
|
|
754
|
+
const templatePath = path.join(themePath, 'templates/projects-index.html');
|
|
755
|
+
if (!(await fileExists(templatePath))) {
|
|
756
|
+
return [];
|
|
757
|
+
}
|
|
758
|
+
const template = await fs.readFile(templatePath, 'utf-8');
|
|
759
|
+
const settings = portfolio.settings || {};
|
|
760
|
+
const defaultColorScheme = theme === 'dark-academia' ? 'light' : 'dark';
|
|
761
|
+
const data = {
|
|
762
|
+
site_name: portfolio.meta.name,
|
|
763
|
+
projects: portfolio.projects || [],
|
|
764
|
+
colorScheme: settings.color_scheme || defaultColorScheme,
|
|
765
|
+
nav_links: generateNavLinks(portfolio, true, 'projects'),
|
|
766
|
+
};
|
|
767
|
+
const html = Mustache.render(template, data);
|
|
768
|
+
files.set('projects/index.html', Buffer.from(html, 'utf-8'));
|
|
769
|
+
return ['projects/index.html'];
|
|
770
|
+
}
|
|
771
|
+
async function generateExperimentsIndexInMemory(portfolio, themePath, theme, files) {
|
|
772
|
+
if (!portfolio.experiments || portfolio.experiments.length === 0) {
|
|
773
|
+
return [];
|
|
774
|
+
}
|
|
775
|
+
const templatePath = path.join(themePath, 'templates/experiments-index.html');
|
|
776
|
+
if (!(await fileExists(templatePath))) {
|
|
777
|
+
return [];
|
|
778
|
+
}
|
|
779
|
+
const template = await fs.readFile(templatePath, 'utf-8');
|
|
780
|
+
const settings = portfolio.settings || {};
|
|
781
|
+
const defaultColorScheme = theme === 'dark-academia' ? 'light' : 'dark';
|
|
782
|
+
const data = {
|
|
783
|
+
site_name: portfolio.meta.name,
|
|
784
|
+
experiments: portfolio.experiments || [],
|
|
785
|
+
colorScheme: settings.color_scheme || defaultColorScheme,
|
|
786
|
+
nav_links: generateNavLinks(portfolio, true, 'experiments'),
|
|
787
|
+
};
|
|
788
|
+
const html = Mustache.render(template, data);
|
|
789
|
+
files.set('experiments/index.html', Buffer.from(html, 'utf-8'));
|
|
790
|
+
return ['experiments/index.html'];
|
|
791
|
+
}
|
|
792
|
+
async function generateWritingIndexInMemory(portfolio, themePath, theme, files) {
|
|
793
|
+
if (!portfolio.sections.writing || portfolio.sections.writing.length === 0) {
|
|
794
|
+
return [];
|
|
795
|
+
}
|
|
796
|
+
const templatePath = path.join(themePath, 'templates/writing-index.html');
|
|
797
|
+
if (!(await fileExists(templatePath))) {
|
|
798
|
+
return [];
|
|
799
|
+
}
|
|
800
|
+
const template = await fs.readFile(templatePath, 'utf-8');
|
|
801
|
+
const settings = portfolio.settings || {};
|
|
802
|
+
const defaultColorScheme = theme === 'dark-academia' ? 'light' : 'dark';
|
|
803
|
+
const data = {
|
|
804
|
+
site_name: portfolio.meta.name,
|
|
805
|
+
writing: portfolio.sections.writing || [],
|
|
806
|
+
colorScheme: settings.color_scheme || defaultColorScheme,
|
|
807
|
+
nav_links: generateNavLinks(portfolio, true, 'writing'),
|
|
808
|
+
};
|
|
809
|
+
const html = Mustache.render(template, data);
|
|
810
|
+
files.set('writing/index.html', Buffer.from(html, 'utf-8'));
|
|
811
|
+
return ['writing/index.html'];
|
|
812
|
+
}
|
|
813
|
+
async function copyUserImagesInMemory(portfolio, files) {
|
|
814
|
+
const imagePaths = extractAllImagePaths(portfolio);
|
|
815
|
+
for (const imgPath of imagePaths) {
|
|
816
|
+
if (!imgPath) {
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
// Skip absolute URLs
|
|
820
|
+
if (imgPath.startsWith('http://') || imgPath.startsWith('https://')) {
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
const srcPath = imgPath.startsWith('/')
|
|
824
|
+
? path.join(process.cwd(), imgPath)
|
|
825
|
+
: path.join(process.cwd(), imgPath);
|
|
826
|
+
try {
|
|
827
|
+
const content = await fs.readFile(srcPath);
|
|
828
|
+
files.set(imgPath, content);
|
|
829
|
+
}
|
|
830
|
+
catch {
|
|
831
|
+
// Image doesn't exist - that's okay, it might be a placeholder
|
|
832
|
+
console.warn(`Warning: Could not copy image ${imgPath}`);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devfolio-page",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Your portfolio as code. Version control it like software.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"chalk": "^5.6.2",
|
|
44
|
+
"chokidar": "^5.0.0",
|
|
44
45
|
"commander": "^14.0.2",
|
|
45
46
|
"js-yaml": "^4.1.1",
|
|
46
47
|
"marked": "^17.0.1",
|