- Create .gitignore, .env.example, README.md, LICENSE - Add astro.config.mjs and template.config.json - Scaffold .vscode extensions, launch, and settings - Add CLAUDE.md with development commands and guidelines - Include public assets, fonts, and media files - Add src components, layouts, pages, and utils - Add cursor rules and worktrees configuration - Add package.json, pnpm-lock.yaml, and tsconfig.json
261 lines
9.2 KiB
JavaScript
261 lines
9.2 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Template Initialization Script
|
|
*
|
|
* This script personalizes the Astro portfolio template with your information.
|
|
*
|
|
* Usage:
|
|
* node init-template.js # Interactive mode (prompts for info)
|
|
* node init-template.js --config # Config mode (reads template.config.json)
|
|
* node init-template.js --help # Show help
|
|
*/
|
|
|
|
import fs from 'fs/promises';
|
|
import path from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
import readline from 'readline';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
|
|
// ANSI color codes for better terminal output
|
|
const colors = {
|
|
reset: '\x1b[0m',
|
|
bright: '\x1b[1m',
|
|
dim: '\x1b[2m',
|
|
green: '\x1b[32m',
|
|
yellow: '\x1b[33m',
|
|
blue: '\x1b[34m',
|
|
cyan: '\x1b[36m',
|
|
red: '\x1b[31m'
|
|
};
|
|
|
|
function log(message, color = 'reset') {
|
|
console.log(`${colors[color]}${message}${colors.reset}`);
|
|
}
|
|
|
|
function prompt(question) {
|
|
const rl = readline.createInterface({
|
|
input: process.stdin,
|
|
output: process.stdout
|
|
});
|
|
|
|
return new Promise((resolve) => {
|
|
rl.question(`${colors.cyan}${question}${colors.reset} `, (answer) => {
|
|
rl.close();
|
|
resolve(answer.trim());
|
|
});
|
|
});
|
|
}
|
|
|
|
async function loadConfig() {
|
|
try {
|
|
const configPath = path.join(__dirname, 'template.config.json');
|
|
const configData = await fs.readFile(configPath, 'utf-8');
|
|
return JSON.parse(configData);
|
|
} catch (error) {
|
|
log('Warning: Could not load template.config.json', 'yellow');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function interactiveMode() {
|
|
log('\n=== Astro Portfolio Template Setup ===\n', 'bright');
|
|
log('This wizard will help you personalize your template.\n', 'dim');
|
|
|
|
const config = {
|
|
site: {},
|
|
branding: {},
|
|
personal: {},
|
|
social: {},
|
|
seo: {},
|
|
cloudflare: {}
|
|
};
|
|
|
|
// Site information
|
|
log('\n--- Site Information ---\n', 'blue');
|
|
config.personal.fullName = await prompt('Your full name:') || 'Your Name';
|
|
config.personal.firstName = config.personal.fullName.split(' ')[0];
|
|
config.personal.lastName = config.personal.fullName.split(' ').slice(1).join(' ') || 'Name';
|
|
config.personal.jobTitle = await prompt('Your profession/job title:') || 'Your Profession';
|
|
config.site.title = `${config.personal.fullName} — ${config.personal.jobTitle}`;
|
|
config.site.description = await prompt('Brief description of what you do:') || 'Professional portfolio and blog';
|
|
config.site.url = await prompt('Your site URL (e.g., https://example.com):') || 'https://yoursite.com';
|
|
config.site.author = config.personal.fullName;
|
|
|
|
// Branding
|
|
log('\n--- Branding ---\n', 'blue');
|
|
const defaultInitials = config.personal.firstName[0] + (config.personal.lastName[0] || 'N');
|
|
config.branding.initials = await prompt(`Your initials (default: ${defaultInitials}):`) || defaultInitials;
|
|
config.branding.year = await prompt('Portfolio year (default: 2025):') || '2025';
|
|
config.branding.backgroundText = await prompt(`Large background text (default: ${config.personal.lastName.toUpperCase()}):`) || config.personal.lastName.toUpperCase();
|
|
config.branding.companyName = await prompt('Company name (optional):') || 'Your Company';
|
|
|
|
// Contact information
|
|
log('\n--- Contact Information ---\n', 'blue');
|
|
config.personal.email = await prompt('Email address:') || 'your@email.com';
|
|
config.personal.location = await prompt('Location (e.g., San Francisco, CA):') || 'Your City, State';
|
|
config.personal.locationCountry = await prompt('Country:') || 'Your Country';
|
|
config.personal.bio = await prompt('Professional bio (1-2 sentences):') || 'Professional bio here.';
|
|
|
|
// Social links
|
|
log('\n--- Social Links ---\n', 'blue');
|
|
config.social.linkedin = await prompt('LinkedIn URL (full URL):') || 'https://linkedin.com/in/yourprofile';
|
|
config.social.github = await prompt('GitHub URL:') || 'https://github.com/yourusername';
|
|
config.social.twitter = await prompt('Twitter URL:') || 'https://twitter.com/yourhandle';
|
|
const twitterHandle = config.social.twitter.split('/').pop();
|
|
config.social.twitterHandle = twitterHandle.startsWith('@') ? twitterHandle : `@${twitterHandle}`;
|
|
config.social.website = config.site.url;
|
|
|
|
// Cloudflare
|
|
log('\n--- Deployment ---\n', 'blue');
|
|
const defaultProjectName = config.site.url.replace(/https?:\/\//, '').replace(/[^a-z0-9-]/gi, '-').toLowerCase();
|
|
config.cloudflare.projectName = await prompt(`Cloudflare project name (default: ${defaultProjectName}):`) || defaultProjectName;
|
|
|
|
// SEO
|
|
config.seo.keywords = ['Portfolio', 'Blog', config.personal.jobTitle];
|
|
config.seo.serviceTypes = ['Service 1', 'Service 2', 'Service 3'];
|
|
config.seo.companyUrl = config.branding.companyName !== 'Your Company'
|
|
? await prompt('Company website URL (optional):')
|
|
: 'https://example.com';
|
|
|
|
return config;
|
|
}
|
|
|
|
async function applyConfig(config) {
|
|
log('\n🔧 Applying configuration...\n', 'bright');
|
|
|
|
const replacements = {
|
|
// Site-wide
|
|
'Your Name — Your Profession': config.site.title,
|
|
'Your Name': config.personal.fullName,
|
|
'Your Profession': config.personal.jobTitle,
|
|
'Your professional description here. Describe what you do, who you work with, and what makes you unique.': config.site.description,
|
|
'your professional description': config.site.description,
|
|
'https://yoursite.com': config.site.url,
|
|
'your@email.com': config.personal.email,
|
|
|
|
// Branding
|
|
'YN / 2025': `${config.branding.initials} / ${config.branding.year}`,
|
|
'NAME': config.branding.backgroundText,
|
|
|
|
// Personal
|
|
'Your': config.personal.firstName,
|
|
'Your City, State': config.personal.location,
|
|
'Your Country': config.personal.locationCountry,
|
|
'Your Company': config.branding.companyName,
|
|
|
|
// Social
|
|
'https://linkedin.com/in/yourprofile': config.social.linkedin,
|
|
'https://github.com/yourusername': config.social.github,
|
|
'https://twitter.com/yourhandle': config.social.twitter,
|
|
'@yourhandle': config.social.twitterHandle,
|
|
|
|
// Cloudflare
|
|
'astro-portfolio-template': config.cloudflare.projectName
|
|
};
|
|
|
|
const filesToUpdate = [
|
|
'src/consts.ts',
|
|
'src/components/BaseHead.astro',
|
|
'src/components/Navigation.astro',
|
|
'src/components/Footer.astro',
|
|
'src/content/sections/hero.mdx',
|
|
'src/content/sections/experience.mdx',
|
|
'src/content/pages/contact.mdx',
|
|
'astro.config.mjs',
|
|
'wrangler.jsonc',
|
|
'package.json'
|
|
];
|
|
|
|
for (const file of filesToUpdate) {
|
|
try {
|
|
const filePath = path.join(__dirname, file);
|
|
let content = await fs.readFile(filePath, 'utf-8');
|
|
|
|
for (const [search, replace] of Object.entries(replacements)) {
|
|
content = content.replaceAll(search, replace);
|
|
}
|
|
|
|
await fs.writeFile(filePath, content, 'utf-8');
|
|
log(`✓ Updated ${file}`, 'green');
|
|
} catch (error) {
|
|
log(`✗ Failed to update ${file}: ${error.message}`, 'red');
|
|
}
|
|
}
|
|
|
|
// Save config for reference
|
|
try {
|
|
const configPath = path.join(__dirname, 'template.config.json');
|
|
await fs.writeFile(
|
|
configPath,
|
|
JSON.stringify(config, null, 2),
|
|
'utf-8'
|
|
);
|
|
log('✓ Saved configuration to template.config.json', 'green');
|
|
} catch (error) {
|
|
log(`✗ Failed to save config: ${error.message}`, 'red');
|
|
}
|
|
|
|
log('\n✨ Template personalization complete!\n', 'bright');
|
|
log('Next steps:', 'cyan');
|
|
log(' 1. Review the changes made to your files', 'dim');
|
|
log(' 2. Replace placeholder images in src/assets/ and public/media/', 'dim');
|
|
log(' 3. Update content in src/content/sections/ with your info', 'dim');
|
|
log(' 4. Run `pnpm dev` to preview your site', 'dim');
|
|
log(' 5. Run `pnpm deploy` when ready to publish\n', 'dim');
|
|
}
|
|
|
|
async function showHelp() {
|
|
log('\n=== Astro Portfolio Template Setup ===\n', 'bright');
|
|
log('Usage:', 'cyan');
|
|
log(' node init-template.js Interactive mode (prompts for info)', 'dim');
|
|
log(' node init-template.js --config Config mode (reads template.config.json)', 'dim');
|
|
log(' node init-template.js --help Show this help message\n', 'dim');
|
|
log('Interactive mode will ask you questions and personalize the template.', 'dim');
|
|
log('Config mode reads from template.config.json file.\n', 'dim');
|
|
}
|
|
|
|
async function main() {
|
|
const args = process.argv.slice(2);
|
|
|
|
if (args.includes('--help') || args.includes('-h')) {
|
|
await showHelp();
|
|
return;
|
|
}
|
|
|
|
if (args.includes('--config')) {
|
|
log('\n📄 Running in config mode...\n', 'bright');
|
|
const config = await loadConfig();
|
|
|
|
if (!config) {
|
|
log('Error: template.config.json not found or invalid.', 'red');
|
|
log('Create one or run without --config flag for interactive mode.\n', 'yellow');
|
|
process.exit(1);
|
|
}
|
|
|
|
await applyConfig(config);
|
|
} else {
|
|
// Interactive mode
|
|
const config = await interactiveMode();
|
|
|
|
log('\n📋 Configuration summary:', 'yellow');
|
|
console.log(JSON.stringify(config, null, 2));
|
|
|
|
const confirm = await prompt('\nApply this configuration? (yes/no):');
|
|
|
|
if (confirm.toLowerCase() === 'yes' || confirm.toLowerCase() === 'y') {
|
|
await applyConfig(config);
|
|
} else {
|
|
log('\nSetup cancelled. No changes were made.\n', 'yellow');
|
|
}
|
|
}
|
|
}
|
|
|
|
main().catch((error) => {
|
|
log(`\n❌ Error: ${error.message}\n`, 'red');
|
|
console.error(error);
|
|
process.exit(1);
|
|
});
|