// ============================================ // SITEGAP CLOUDFLARE WORKER v3 // Handles: scan, generate, build-demo // ============================================ const CORS = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type', }; function json(data, status = 200) { return new Response(JSON.stringify(data), { status, headers: { 'Content-Type': 'application/json', ...CORS }, }); } function err(msg, status = 400) { return json({ error: msg }, status); } export default { async fetch(request, env) { if (request.method === 'OPTIONS') return new Response(null, { headers: CORS }); const url = new URL(request.url); const path = url.pathname; try { if (path === '/api/scan' && request.method === 'POST') return await handleScan(request, env); if (path === '/api/generate' && request.method === 'POST') return await handleGenerate(request, env); if (path === '/api/build-demo' && request.method === 'POST') return await handleBuildDemo(request, env); if (path === '/api/health') return json({ status: 'ok', version: '3.0.0' }); return err('Not found', 404); } catch (e) { return err('Internal server error: ' + e.message, 500); } } }; // ============================================ // SCAN // ============================================ async function handleScan(request, env) { const body = await request.json(); const { type, city, max = 20, filterNoWebsite = true } = body; if (!type || !city) return err('type and city are required'); if (!env.SERP_API_KEY) return err('SerpAPI key not configured'); const query = `${type} in ${city}`; const limit = Math.min(parseInt(max), 50); const leads = []; let start = 0; let totalScanned = 0; let totalWithWebsite = 0; const maxPages = 5; let page = 0; while (leads.length < limit && page < maxPages) { const url = `https://serpapi.com/search.json?engine=google_maps&q=${encodeURIComponent(query)}&type=search&start=${start}&api_key=${env.SERP_API_KEY}`; const res = await fetch(url); const data = await res.json(); if (data.error) return err('SerpAPI error: ' + data.error); const places = data.local_results || data.results || []; if (!places.length) break; for (const place of places) { totalScanned++; const hasWebsite = place.website || (place.extensions && place.extensions.website) || (place.links && place.links.website); if (filterNoWebsite && hasWebsite) { totalWithWebsite++; continue; } if (place.permanently_closed) continue; leads.push({ id: place.place_id || Math.random().toString(36).slice(2), name: place.title || place.name || 'Unknown', type: place.type || type, phone: place.phone || null, rating: place.rating || null, reviews: place.reviews || 0, address: place.address || city, website: hasWebsite ? place.website : false, score: calcScore(place), city, }); if (leads.length >= limit) break; } start += 20; page++; } return json({ leads, total: leads.length, query, debug: { totalScanned, totalWithWebsite, noWebsite: leads.length, pages: page } }); } function calcScore(place) { let score = 50; if (place.rating >= 4.5) score += 20; else if (place.rating >= 4.0) score += 15; else if (place.rating >= 3.5) score += 8; if (place.reviews >= 50) score += 20; else if (place.reviews >= 20) score += 12; else if (place.reviews >= 10) score += 6; if (place.phone) score += 10; return Math.min(score, 100); } // ============================================ // GENERATE — Email or call script // ============================================ async function handleGenerate(request, env) { const body = await request.json(); const { task, leads, agentName = 'Shaan', agencyName = 'Wrench Digital' } = body; if (!task || !leads || !leads.length) return err('task and leads are required'); if (!env.ANTHROPIC_KEY) return err('Anthropic API key not configured'); if (!['email', 'script'].includes(task)) return err('task must be email or script'); const results = []; for (const lead of leads) { const content = task === 'email' ? `Write a short friendly cold email from ${agentName} at ${agencyName} to the owner of ${lead.name}, a ${lead.type} business in ${lead.city} with no website. 4-6 sentences, mention their name, offer a FREE custom demo first, end with a yes/no question, sign off as ${agentName}, ${agencyName}. No subject line.` : `Write a cold call script for ${agentName} at ${agencyName} calling ${lead.name}, a ${lead.type} in ${lead.city} with no website. Sections: OPENER: THE HOOK: THE OFFER: OBJECTION: CLOSE: Keep each 2-3 sentences, conversational, offer FREE demo.`; const res = await fetch('https://api.anthropic.com/v1/messages', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': env.ANTHROPIC_KEY, 'anthropic-version': '2023-06-01' }, body: JSON.stringify({ model: 'claude-haiku-4-5-20251001', max_tokens: 600, messages: [{ role: 'user', content }] }), }); const data = await res.json(); if (data.error) { results.push({ biz: lead.name, error: data.error.message }); continue; } results.push({ biz: lead.name, type: lead.type, city: lead.city, body: data.content?.[0]?.text?.trim() || '' }); } return json({ results, task }); } // ============================================ // BUILD DEMO // ============================================ // Color schemes per business type const TYPE_THEMES = { landscaping: { accent: '#2d7a3a', accent_dark: '#1a4d22' }, landscape: { accent: '#2d7a3a', accent_dark: '#1a4d22' }, lawn: { accent: '#2d7a3a', accent_dark: '#1a4d22' }, plumber: { accent: '#1565c0', accent_dark: '#0d47a1' }, plumbing: { accent: '#1565c0', accent_dark: '#0d47a1' }, hvac: { accent: '#0277bd', accent_dark: '#01579b' }, electrician: { accent: '#e65100', accent_dark: '#bf360c' }, electrical: { accent: '#e65100', accent_dark: '#bf360c' }, contractor: { accent: '#5d4037', accent_dark: '#3e2723' }, renovation: { accent: '#5d4037', accent_dark: '#3e2723' }, roofing: { accent: '#455a64', accent_dark: '#263238' }, painting: { accent: '#6a1b9a', accent_dark: '#4a148c' }, cleaning: { accent: '#00838f', accent_dark: '#006064' }, restaurant: { accent: '#c62828', accent_dark: '#b71c1c' }, mechanic: { accent: '#212121', accent_dark: '#000000' }, auto: { accent: '#212121', accent_dark: '#000000' }, salon: { accent: '#ad1457', accent_dark: '#880e4f' }, nail: { accent: '#ad1457', accent_dark: '#880e4f' }, dentist: { accent: '#0288d1', accent_dark: '#01579b' }, dental: { accent: '#0288d1', accent_dark: '#01579b' }, physio: { accent: '#00897b', accent_dark: '#00695c' }, physiotherapy: { accent: '#00897b', accent_dark: '#00695c' }, chiro: { accent: '#00897b', accent_dark: '#00695c' }, medical: { accent: '#1565c0', accent_dark: '#0d47a1' }, clinic: { accent: '#1565c0', accent_dark: '#0d47a1' }, optometrist: { accent: '#283593', accent_dark: '#1a237e' }, lawyer: { accent: '#37474f', accent_dark: '#263238' }, legal: { accent: '#37474f', accent_dark: '#263238' }, accounting: { accent: '#1b5e20', accent_dark: '#143d17' }, accountant: { accent: '#1b5e20', accent_dark: '#143d17' }, mortgage: { accent: '#1a237e', accent_dark: '#0d1259' }, real_estate: { accent: '#1a237e', accent_dark: '#0d1259' }, gym: { accent: '#b71c1c', accent_dark: '#7f0000' }, fitness: { accent: '#b71c1c', accent_dark: '#7f0000' }, barber: { accent: '#212121', accent_dark: '#000000' }, flooring: { accent: '#4e342e', accent_dark: '#3e2723' }, pest: { accent: '#558b2f', accent_dark: '#33691e' }, moving: { accent: '#e65100', accent_dark: '#bf360c' }, default: { accent: '#1a237e', accent_dark: '#0d1259' }, }; // Unsplash image URLs per business type // Unsplash image URLs per business type const TYPE_IMAGES = { landscaping: { hero: 'https://images.unsplash.com/photo-1558904541-efa843a96f01?w=1600&q=80', about: 'https://images.unsplash.com/photo-1416879595882-3373a0480b5b?w=800&q=80' }, landscape: { hero: 'https://images.unsplash.com/photo-1558904541-efa843a96f01?w=1600&q=80', about: 'https://images.unsplash.com/photo-1416879595882-3373a0480b5b?w=800&q=80' }, lawn: { hero: 'https://images.unsplash.com/photo-1558904541-efa843a96f01?w=1600&q=80', about: 'https://images.unsplash.com/photo-1416879595882-3373a0480b5b?w=800&q=80' }, plumber: { hero: 'https://images.unsplash.com/photo-1607472586893-edb57bdc0e39?w=1600&q=80', about: 'https://images.unsplash.com/photo-1504328345606-18bbc8c9d7d1?w=800&q=80' }, plumbing: { hero: 'https://images.unsplash.com/photo-1607472586893-edb57bdc0e39?w=1600&q=80', about: 'https://images.unsplash.com/photo-1504328345606-18bbc8c9d7d1?w=800&q=80' }, hvac: { hero: 'https://images.unsplash.com/photo-1621609764180-2ca554a9d6f2?w=1600&q=80', about: 'https://images.unsplash.com/photo-1558618047-3c8c76ca7d13?w=800&q=80' }, electrician: { hero: 'https://images.unsplash.com/photo-1621905252507-b35492cc74b4?w=1600&q=80', about: 'https://images.unsplash.com/photo-1621905251918-48416bd8575a?w=800&q=80' }, electrical: { hero: 'https://images.unsplash.com/photo-1621905252507-b35492cc74b4?w=1600&q=80', about: 'https://images.unsplash.com/photo-1621905251918-48416bd8575a?w=800&q=80' }, contractor: { hero: 'https://images.unsplash.com/photo-1503387762-592deb58ef4e?w=1600&q=80', about: 'https://images.unsplash.com/photo-1504307651254-35680f356dfd?w=800&q=80' }, renovation: { hero: 'https://images.unsplash.com/photo-1503387762-592deb58ef4e?w=1600&q=80', about: 'https://images.unsplash.com/photo-1504307651254-35680f356dfd?w=800&q=80' }, roofing: { hero: 'https://images.unsplash.com/photo-1558618047-3c8c76ca7d13?w=1600&q=80', about: 'https://images.unsplash.com/photo-1632207691143-643e2a9a9361?w=800&q=80' }, painting: { hero: 'https://images.unsplash.com/photo-1562259949-e8e7689d7828?w=1600&q=80', about: 'https://images.unsplash.com/photo-1589939705384-5185137a7f0f?w=800&q=80' }, cleaning: { hero: 'https://images.unsplash.com/photo-1581578731548-c64695cc6952?w=1600&q=80', about: 'https://images.unsplash.com/photo-1563453392212-326f5e854473?w=800&q=80' }, mechanic: { hero: 'https://images.unsplash.com/photo-1486262715619-67b85e0b08d3?w=1600&q=80', about: 'https://images.unsplash.com/photo-1530046339160-ce3e530c7d2f?w=800&q=80' }, auto: { hero: 'https://images.unsplash.com/photo-1486262715619-67b85e0b08d3?w=1600&q=80', about: 'https://images.unsplash.com/photo-1530046339160-ce3e530c7d2f?w=800&q=80' }, restaurant: { hero: 'https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=1600&q=80', about: 'https://images.unsplash.com/photo-1414235077428-338989a2e8c0?w=800&q=80' }, salon: { hero: 'https://images.unsplash.com/photo-1560066984-138daaa4e4e0?w=1600&q=80', about: 'https://images.unsplash.com/photo-1522337360788-8b13dee7a37e?w=800&q=80' }, nail: { hero: 'https://images.unsplash.com/photo-1604654894610-df63bc536371?w=1600&q=80', about: 'https://images.unsplash.com/photo-1522337360788-8b13dee7a37e?w=800&q=80' }, dentist: { hero: 'https://images.unsplash.com/photo-1629909613654-28e377c37b09?w=1600&q=80', about: 'https://images.unsplash.com/photo-1606811841689-23dfddce3e95?w=800&q=80' }, dental: { hero: 'https://images.unsplash.com/photo-1629909613654-28e377c37b09?w=1600&q=80', about: 'https://images.unsplash.com/photo-1606811841689-23dfddce3e95?w=800&q=80' }, physio: { hero: 'https://images.unsplash.com/photo-1576091160550-2173dba999ef?w=1600&q=80', about: 'https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?w=800&q=80' }, physiotherapy:{ hero: 'https://images.unsplash.com/photo-1576091160550-2173dba999ef?w=1600&q=80', about: 'https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?w=800&q=80' }, chiro: { hero: 'https://images.unsplash.com/photo-1576091160550-2173dba999ef?w=1600&q=80', about: 'https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?w=800&q=80' }, medical: { hero: 'https://images.unsplash.com/photo-1519494026892-80bbd2d6fd0d?w=1600&q=80', about: 'https://images.unsplash.com/photo-1551190822-a9333d879b1f?w=800&q=80' }, clinic: { hero: 'https://images.unsplash.com/photo-1519494026892-80bbd2d6fd0d?w=1600&q=80', about: 'https://images.unsplash.com/photo-1551190822-a9333d879b1f?w=800&q=80' }, optometrist: { hero: 'https://images.unsplash.com/photo-1601244005535-a48d21ef6a2a?w=1600&q=80', about: 'https://images.unsplash.com/photo-1508296695146-257a814070b4?w=800&q=80' }, lawyer: { hero: 'https://images.unsplash.com/photo-1589829545856-d10d557cf95f?w=1600&q=80', about: 'https://images.unsplash.com/photo-1521791055366-0d553872952f?w=800&q=80' }, legal: { hero: 'https://images.unsplash.com/photo-1589829545856-d10d557cf95f?w=1600&q=80', about: 'https://images.unsplash.com/photo-1521791055366-0d553872952f?w=800&q=80' }, accounting: { hero: 'https://images.unsplash.com/photo-1554224155-6726b3ff858f?w=1600&q=80', about: 'https://images.unsplash.com/photo-1450101499163-c8848c66ca85?w=800&q=80' }, accountant: { hero: 'https://images.unsplash.com/photo-1554224155-6726b3ff858f?w=1600&q=80', about: 'https://images.unsplash.com/photo-1450101499163-c8848c66ca85?w=800&q=80' }, mortgage: { hero: 'https://images.unsplash.com/photo-1560518883-ce09059eeffa?w=1600&q=80', about: 'https://images.unsplash.com/photo-1448630360428-65456885c650?w=800&q=80' }, real_estate: { hero: 'https://images.unsplash.com/photo-1560518883-ce09059eeffa?w=1600&q=80', about: 'https://images.unsplash.com/photo-1448630360428-65456885c650?w=800&q=80' }, gym: { hero: 'https://images.unsplash.com/photo-1534438327276-14e5300c3a48?w=1600&q=80', about: 'https://images.unsplash.com/photo-1571019614242-c5c5dee9f50b?w=800&q=80' }, fitness: { hero: 'https://images.unsplash.com/photo-1534438327276-14e5300c3a48?w=1600&q=80', about: 'https://images.unsplash.com/photo-1571019614242-c5c5dee9f50b?w=800&q=80' }, barber: { hero: 'https://images.unsplash.com/photo-1503951914875-452162b0f3f1?w=1600&q=80', about: 'https://images.unsplash.com/photo-1599351431202-1e0f0137899a?w=800&q=80' }, flooring: { hero: 'https://images.unsplash.com/photo-1504307651254-35680f356dfd?w=1600&q=80', about: 'https://images.unsplash.com/photo-1581094794329-c8112a89af12?w=800&q=80' }, pest: { hero: 'https://images.unsplash.com/photo-1464983953574-0892a716854b?w=1600&q=80', about: 'https://images.unsplash.com/photo-1581094794329-c8112a89af12?w=800&q=80' }, moving: { hero: 'https://images.unsplash.com/photo-1464983953574-0892a716854b?w=1600&q=80', about: 'https://images.unsplash.com/photo-1600585154340-be6161a56a0c?w=800&q=80' }, default: { hero: '', about: 'https://images.unsplash.com/photo-1497366811353-6870744d04b2?w=800&q=80' }, }; function normalizeType(type) { const t = (type || '').toLowerCase(); // Google Maps type normalization if (t.includes('landscape') || t.includes('lawn') || t.includes('garden') || t.includes('lawncare')) return 'landscaping'; if (t.includes('plumb')) return 'plumbing'; if (t.includes('electric')) return 'electrician'; if (t.includes('hvac') || t.includes('heat') || t.includes('air condition') || t.includes('furnace')) return 'hvac'; if (t.includes('roof')) return 'roofing'; if (t.includes('paint')) return 'painting'; if (t.includes('clean')) return 'cleaning'; if (t.includes('mechanic') || t.includes('auto repair') || t.includes('car repair')) return 'mechanic'; if (t.includes('restaurant') || t.includes('food') || t.includes('cafe') || t.includes('pizza')) return 'restaurant'; if (t.includes('salon') || t.includes('hair') || t.includes('beauty')) return 'salon'; if (t.includes('nail')) return 'nail'; if (t.includes('dent')) return 'dentist'; if (t.includes('physio') || t.includes('chiro') || t.includes('massage')) return 'physio'; if (t.includes('doctor') || t.includes('clinic') || t.includes('medical')) return 'medical'; if (t.includes('optom') || t.includes('eye')) return 'optometrist'; if (t.includes('law') || t.includes('legal') || t.includes('attorney')) return 'lawyer'; if (t.includes('account') || t.includes('tax') || t.includes('bookkeep')) return 'accounting'; if (t.includes('mortgage') || t.includes('real estate') || t.includes('realtor')) return 'mortgage'; if (t.includes('gym') || t.includes('fitness') || t.includes('yoga')) return 'gym'; if (t.includes('barber')) return 'barber'; if (t.includes('contractor') || t.includes('construction') || t.includes('renovati') || t.includes('building')) return 'contractor'; if (t.includes('floor')) return 'flooring'; if (t.includes('pest')) return 'pest'; if (t.includes('mov')) return 'moving'; return t; } function getTheme(type) { const normalized = normalizeType(type); return TYPE_THEMES[normalized] || TYPE_THEMES.default; } function getImages(type) { const normalized = normalizeType(type); return TYPE_IMAGES[normalized] || TYPE_IMAGES.default; } async function handleBuildDemo(request, env) { const body = await request.json(); const { name, type, phone, city } = body; if (!name || !type || !city) return err('name, type, and city are required'); if (!env.ANTHROPIC_KEY) return err('Anthropic API key not configured'); if (!env.GITHUB_TOKEN) return err('GitHub token not configured'); if (!env.GITHUB_REPO) return err('GitHub repo not configured'); const theme = getTheme(type); const images = getImages(type); const prompt = `You are filling in a website template for a local business. Return ONLY valid JSON for: "${name}", a ${type} business in ${city}, phone: ${phone || 'N/A'}. Return ONLY this JSON (no markdown, no explanation): { "TAGLINE": "short punchy tagline for a ${type}", "LOGO_FIRST": "first word of business name", "LOGO_SECOND": "rest of business name (can be empty if one word)", "HERO_LINE_1": "bold hero headline line 1", "HERO_LINE_2": "italic accent line (the wow phrase)", "HERO_SUB": "2 sentence compelling hero subheading about what they do and why they're the best in ${city}", "STAT_1_N": "number like 500+", "STAT_1_L": "label like Projects Completed", "STAT_1_RAW": "500", "STAT_2_N": "number like 12+", "STAT_2_L": "label like Years Experience", "STAT_2_RAW": "12", "STAT_3_N": "number like 98%", "STAT_3_L": "label like Client Satisfaction", "STAT_3_RAW": "98", "STAT_4_N": "number like 24/7", "STAT_4_L": "label like Available", "STAT_4_RAW": "247", "SERVICES_TITLE": "Our Services", "SERVICES_SUB": "compelling subtitle about the range of ${type} services offered in ${city}", "S1_TITLE": "service 1 name", "S1_DESC": "service 1 description, 2 sentences", "S2_TITLE": "service 2 name", "S2_DESC": "service 2 description", "S3_TITLE": "service 3 name", "S3_DESC": "service 3 description", "S4_TITLE": "service 4 name", "S4_DESC": "service 4 description", "S5_TITLE": "service 5 name", "S5_DESC": "service 5 description", "S6_TITLE": "service 6 name", "S6_DESC": "service 6 description", "ABOUT_TITLE": "about section title with em tag for italic word", "ABOUT_P1": "about paragraph 1, 2-3 sentences about the business", "ABOUT_P2": "about paragraph 2, 2-3 sentences about their approach/values", "CRED_1": "credential like Licensed & Insured", "CRED_2": "credential like 10+ Years Experience", "CRED_3": "credential like Free Estimates", "CRED_4": "credential like Satisfaction Guaranteed", "WHY_TITLE": "why choose us title", "WHY_SUB": "why choose us subtitle", "W1_ICON": "single relevant emoji", "W1_TITLE": "reason 1 title (2-3 words)", "W1_DESC": "reason 1 description 1-2 sentences", "W2_ICON": "single relevant emoji", "W2_TITLE": "reason 2 title", "W2_DESC": "reason 2 description", "W3_ICON": "single relevant emoji", "W3_TITLE": "reason 3 title", "W3_DESC": "reason 3 description", "W4_ICON": "single relevant emoji", "W4_TITLE": "reason 4 title", "W4_DESC": "reason 4 description", "REVIEWS_TITLE": "reviews section title", "R1_TEXT": "realistic detailed 5-star review 2-3 sentences", "R1_NAME": "First Name L.", "R1_INITIAL": "F", "R1_LOCATION": "Neighbourhood, ${city}", "R2_TEXT": "realistic detailed 5-star review", "R2_NAME": "First Name L.", "R2_INITIAL": "F", "R2_LOCATION": "Neighbourhood, ${city}", "R3_TEXT": "realistic detailed 5-star review", "R3_NAME": "First Name L.", "R3_INITIAL": "F", "R3_LOCATION": "Neighbourhood, ${city}", "CONTACT_TITLE": "Get in touch title", "FOOTER_DESC": "footer tagline 1-2 sentences", "HOURS": "Mon-Fri 8am-6pm", "PHONE": "${phone || '(416) 555-0100'}", "PHONE_RAW": "${(phone || '4165550100').replace(/\D/g, '')}", "CITY": "${city}", "BUSINESS_NAME": "${name}", "BUSINESS_TYPE": "${type}" }`; const aiRes = await fetch('https://api.anthropic.com/v1/messages', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': env.ANTHROPIC_KEY, 'anthropic-version': '2023-06-01' }, body: JSON.stringify({ model: 'claude-haiku-4-5-20251001', max_tokens: 2000, messages: [{ role: 'user', content: prompt }] }), }); const aiData = await aiRes.json(); if (aiData.error) return err('Claude error: ' + aiData.error.message); let vars; try { const text = aiData.content?.[0]?.text?.trim() || ''; const clean = text.replace(/```json|```/g, '').trim(); vars = JSON.parse(clean); } catch (e) { return err('Failed to parse Claude response: ' + e.message); } vars.ACCENT_COLOR = theme.accent; vars.ACCENT_DARK = theme.accent_dark; vars.HERO_IMAGE = images.hero; vars.ABOUT_IMAGE = images.about; let html = getTemplate(); for (const [key, val] of Object.entries(vars)) { html = html.replaceAll(`{{${key}}}`, val || ''); } const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''); // Deploy to GitHub → Vercel auto-deploys const deployed = await deployToGitHub(slug, html, env); if (!deployed.ok) return err('Failed to deploy to GitHub: ' + deployed.error); const url = `https://demos.wrenchdigital.ca/${slug}`; return json({ url, slug, name }); } async function deployToGitHub(slug, html, env) { try { const path = `demos/${slug}/index.html`; const [owner, repo] = env.GITHUB_REPO.split('/'); const encoder = new TextEncoder(); const bytes = encoder.encode(html); let binary = ''; for (let i = 0; i < bytes.length; i++) { binary += String.fromCharCode(bytes[i]); } const content = btoa(binary); const ghHeaders = { 'Authorization': `token ${env.GITHUB_TOKEN}`, 'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'SiteGap-Demo-Builder/1.0', 'Content-Type': 'application/json', }; let sha; const checkRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/contents/${path}`, { headers: ghHeaders, }); if (checkRes.ok) { const existing = await checkRes.json(); sha = existing.sha; } const body = { message: `Add demo: ${slug}`, content, ...(sha ? { sha } : {}), }; const res = await fetch(`https://api.github.com/repos/${owner}/${repo}/contents/${path}`, { method: 'PUT', headers: ghHeaders, body: JSON.stringify(body), }); if (!res.ok) { const errText = await res.text(); console.error('GitHub API error:', errText); return { ok: false, error: errText.slice(0, 200) }; } return { ok: true }; } catch (e) { console.error('GitHub deploy error:', e); return { ok: false, error: e.message }; } } function getTemplate() { return `
{{HERO_SUB}}
{{SERVICES_SUB}}
{{ABOUT_P1}}
{{ABOUT_P2}}
{{WHY_SUB}}
{{W1_DESC}}
{{W2_DESC}}
{{W3_DESC}}
{{W4_DESC}}
{{R1_TEXT}}
{{R2_TEXT}}
{{R3_TEXT}}
Contact us today for a free, no-obligation quote.
Fill out the form and we'll get back to you within a few hours.