/** * Clone Architect — Deploy Site to /opt/clone-architect-site * * 1. Génère public/index.html via generate-site.ts * 2. Copie public/index.html → /opt/clone-architect-site/index.html * 3. Copie chaque extractions/{domain}/showcase/index.html → /opt/clone-architect-site/brands/{domain}/index.html * 4. Crée /opt/clone-architect-site/404.html * 5. Affiche le résumé du déploiement * * Usage: npx tsx scripts/deploy-site.ts */ import { readFileSync, writeFileSync, mkdirSync, existsSync, copyFileSync } from 'fs'; import { join, dirname } from 'path'; import { execSync } from 'child_process'; const ROOT = process.cwd(); const DEPLOY_DIR = '/opt/clone-architect-site'; const CATALOG_PATH = join(ROOT, 'catalog', 'index.json'); const PUBLIC_INDEX = join(ROOT, 'public', 'index.html'); function writeWithSudo(targetPath: string, content: string): void { // Try direct write first (paul owns /opt/clone-architect-site) try { const dir = dirname(targetPath); mkdirSync(dir, { recursive: true }); writeFileSync(targetPath, content); } catch { // Fallback: sudo node write const escaped = content.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$'); execSync(`sudo /usr/bin/node -e " require('fs').mkdirSync(require('path').dirname('${targetPath}'), {recursive:true}); require('fs').writeFileSync('${targetPath}', \\\`${escaped}\\\`); "`, { stdio: 'ignore' }); } } function copyWithSudo(src: string, dest: string): void { try { const dir = dirname(dest); mkdirSync(dir, { recursive: true }); copyFileSync(src, dest); } catch { const dir = dirname(dest); execSync(`sudo /usr/bin/node -e " require('fs').mkdirSync('${dir}', {recursive:true}); require('fs').copyFileSync('${src}', '${dest}'); "`, { stdio: 'ignore' }); } } async function main() { console.log('🚀 Clone Architect — Deploy Site\n'); // Step 1 — Regenerate public/index.html console.log('📄 Génération public/index.html...'); execSync('npx tsx scripts/generate-site.ts', { cwd: ROOT, stdio: 'inherit' }); // Step 2 — Ensure deploy dir exists try { mkdirSync(DEPLOY_DIR, { recursive: true }); } catch { execSync(`sudo /usr/bin/node -e "require('fs').mkdirSync('${DEPLOY_DIR}', {recursive:true})"`, { stdio: 'ignore' }); } // Step 2b — Generate og-image.png (non-fatal: SEO/social-preview only, must NOT abort deploy). // A transient Playwright screenshot failure here previously killed the whole deploy before // showcases were copied. og-image regeneration is cosmetic — the existing one is reused on failure. console.log('\n🎨 Génération og-image.png...'); try { execSync('npx tsx scripts/generate-og-image.ts', { cwd: ROOT, stdio: 'inherit' }); } catch (e) { console.warn(` ⚠️ og-image generation failed (non-fatal, keeping existing): ${(e as Error).message.split('\n')[0]}`); } // Step 3 — Copy index.html + og-image.png console.log('\n📋 Copie index.html + og-image.png → /opt/clone-architect-site/...'); const indexContent = readFileSync(PUBLIC_INDEX, 'utf-8'); writeWithSudo(join(DEPLOY_DIR, 'index.html'), indexContent); const ogPng = join(ROOT, 'public', 'og-image.png'); if (existsSync(ogPng)) copyWithSudo(ogPng, join(DEPLOY_DIR, 'og-image.png')); console.log(' ✅ index.html + og-image.png déployés'); // Step 4 — Copy showcase for each brand console.log('\n🎨 Déploiement des showcases...'); const catalogRaw = readFileSync(CATALOG_PATH, 'utf-8'); const catalog = JSON.parse(catalogRaw); let deployed = 0; let skipped = 0; for (const brand of catalog.brands) { const domain = brand.domain; const showcaseSrc = join(ROOT, 'extractions', domain, 'showcase', 'index.html'); const showcaseDest = join(DEPLOY_DIR, 'brands', domain, 'index.html'); if (existsSync(showcaseSrc)) { copyWithSudo(showcaseSrc, showcaseDest); deployed++; } else { skipped++; } } console.log(` ✅ ${deployed} showcases déployés → /opt/clone-architect-site/brands/`); if (skipped > 0) console.log(` ⏭ ${skipped} brands sans showcase (extraction incomplète)`); // Step 4b — Generate sitemap.xml (SEO indexation) console.log('\n🗺️ Génération sitemap.xml...'); const baseUrl = 'https://clone-architect.ps-tools.dev'; const today = new Date().toISOString().slice(0, 10); const sitemapUrls = [ `${baseUrl}/${today}weekly1.0`, ]; for (const brand of catalog.brands) { const domain = brand.domain; const showcaseSrc = join(ROOT, 'extractions', domain, 'showcase', 'index.html'); if (existsSync(showcaseSrc)) { sitemapUrls.push(`${baseUrl}/brands/${domain}/${brand.extractedAt?.slice(0, 10) || today}monthly0.7`); } } const sitemapXml = ` ${sitemapUrls.join('\n')} `; writeWithSudo(join(DEPLOY_DIR, 'sitemap.xml'), sitemapXml); writeWithSudo(join(ROOT, 'public', 'sitemap.xml'), sitemapXml); console.log(` ✅ sitemap.xml généré (${sitemapUrls.length} URLs)`); // Step 4c — robots.txt const robotsTxt = `User-agent: * Allow: / Disallow: /api/ Sitemap: ${baseUrl}/sitemap.xml `; writeWithSudo(join(DEPLOY_DIR, 'robots.txt'), robotsTxt); writeWithSudo(join(ROOT, 'public', 'robots.txt'), robotsTxt); console.log(` ✅ robots.txt déployé`); // Step 5 — Create 404.html const notFoundHtml = ` Not found — Clone Architect

404

This brand hasn't been extracted yet.

Browse 82 extractions ↗
`; writeWithSudo(join(DEPLOY_DIR, '404.html'), notFoundHtml); writeWithSudo(join(ROOT, 'public', '404.html'), notFoundHtml); console.log('\n📄 404.html créé'); // Step 6 — Summary console.log('\n══════════════════════════════════════════'); console.log('✅ DEPLOY COMPLET'); console.log(` Site : https://clone-architect.ps-tools.dev`); console.log(` Brands : https://clone-architect.ps-tools.dev/brands/linear.app/`); console.log(` Total : ${deployed} showcases + index.html + 404.html`); console.log('══════════════════════════════════════════\n'); } main().catch(err => { console.error('❌ Deploy error:', err); process.exit(1); });