← back to Instant Audit
src/fulfill.js
262 lines
// fulfill.js — the fulfillment pipeline for a paid Instant Website Audit.
//
// On a (TEST) paid order we: (1) run the SDCC-family generators on the buyer's
// URL, (2) assemble a multi-page HTML report bundle under reports/<orderId>/,
// (3) return a delivery link + a draft delivery email. Delivery is $0 (local
// compute) — the report generation is deterministic HTML assembly here so the
// pipeline is provable end-to-end without any paid API. The heavier Playwright
// / LLM-vision generators (site-audit, competitors, mockups, tools-pack,
// peer-survey, info-hub skills) are wired as OPTIONAL enrichment steps that a
// human/agent runs to deepen a report — the base bundle stands alone.
//
// TIER MATRIX (what each tier includes):
// lite $49 — audit + tools-pack
// standard $99 — audit + tools-pack + peer-survey + info-hub
// pro $199 — everything + competitor teardown + mockups
//
// NOTE: no live network calls are required for the base bundle. Where a real
// fetch of the buyer's homepage helps (title/meta/heuristics), we do a single
// best-effort GET with a short timeout and degrade gracefully offline.
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const REPORTS_DIR = path.join(__dirname, '..', 'reports');
export const TIER_SECTIONS = {
lite: ['audit', 'tools'],
standard: ['audit', 'tools', 'peers', 'hub'],
pro: ['audit', 'tools', 'peers', 'hub', 'competitors', 'mockups'],
};
const SECTION_META = {
audit: { title: 'Site Audit', skill: 'site-audit', file: 'report.html' },
tools: { title: 'Borrower/Visitor Tools', skill: 'tools-pack', file: 'tools.html' },
peers: { title: 'Peer Survey', skill: 'peer-survey', file: 'peers.html' },
hub: { title: 'Trusted-Sources Hub', skill: 'info-hub', file: 'hub.html' },
competitors: { title: 'Competitor Teardown', skill: 'competitors', file: 'competitors.html' },
mockups: { title: 'Homepage Concepts', skill: 'mockups', file: 'mockups.html' },
};
function esc(s) {
return String(s == null ? '' : s).replace(/[&<>"']/g, (c) =>
({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
}
function hostOf(url) {
try { return new URL(url).host; } catch { return String(url || 'the site'); }
}
// Single best-effort homepage fetch for real signal; degrades gracefully.
async function probe(url) {
const out = { ok: false, title: '', metaDesc: '', hasH1: false, statusText: '' };
try {
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), 6000);
const r = await fetch(url, { signal: ctrl.signal, redirect: 'follow' });
clearTimeout(t);
out.statusText = `${r.status} ${r.statusText}`;
const html = await r.text();
out.ok = r.ok;
out.title = (html.match(/<title[^>]*>([^<]*)<\/title>/i) || [])[1]?.trim() || '';
out.metaDesc = (html.match(/<meta[^>]+name=["']description["'][^>]+content=["']([^"']*)["']/i) || [])[1]?.trim() || '';
out.hasH1 = /<h1[\s>]/i.test(html);
} catch (e) { out.statusText = `unreachable (${e.name})`; }
return out;
}
const SHELL = (title, host, body) => `<!doctype html><html lang="en"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>${esc(title)}</title>
<style>
:root{--ink:#1a1a1a;--paper:#faf8f4;--line:#e4ddd2;--accent:#8a7355;--muted:#6b6257}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:Georgia,'Times New Roman',serif;background:var(--paper);color:var(--ink);line-height:1.6}
.wrap{max-width:860px;margin:0 auto;padding:48px 28px}
.rb{font-family:-apple-system,Segoe UI,sans-serif;letter-spacing:.22em;text-transform:uppercase;font-size:11px;color:var(--accent)}
h1{font-size:34px;letter-spacing:-.01em;margin:6px 0 4px}
h2{font-family:-apple-system,Segoe UI,sans-serif;font-size:19px;margin:36px 0 10px;padding-top:22px;border-top:1px solid var(--line)}
h3{font-family:-apple-system,Segoe UI,sans-serif;font-size:15px;margin:18px 0 6px}
p,li{font-size:16px;color:#2a2620;margin:8px 0}
ul{padding-left:22px}
.host{color:var(--muted);font-size:15px}
.grade{display:inline-block;font-family:-apple-system,sans-serif;font-weight:700;padding:2px 10px;border:1px solid var(--line);border-radius:3px;background:#fff}
table{border-collapse:collapse;width:100%;margin:12px 0;font-family:-apple-system,sans-serif;font-size:14px}
th,td{border:1px solid var(--line);padding:8px 10px;text-align:left}
th{background:#f0ebe1}
.nav{font-family:-apple-system,sans-serif;font-size:13px;margin:18px 0;padding:12px;background:#fff;border:1px solid var(--line);border-radius:4px}
.nav a{color:var(--accent);text-decoration:none;margin-right:14px}
.ft{margin-top:48px;padding-top:18px;border-top:1px solid var(--line);font-family:-apple-system,sans-serif;font-size:12px;color:var(--muted)}
</style></head><body><div class="wrap">
<div class="rb">Instant Website Audit</div>
<h1>${esc(title)}</h1><p class="host">${esc(host)}</p>
${body}
<div class="ft">Delivered by Instant Website Audit · generated locally · this report is confidential to the purchaser.</div>
</div></body></html>`;
function navBar(sections, current) {
const links = sections.map((s) =>
`<a href="${SECTION_META[s].file}"${s === current ? ' style="font-weight:700"' : ''}>${esc(SECTION_META[s].title)}</a>`).join('');
return `<div class="nav"><strong>Report:</strong> ${links}</div>`;
}
// ---- section generators (deterministic HTML from the probe + heuristics) ----
function genAudit(host, p, sections) {
const grade = p.ok ? (p.title && p.metaDesc && p.hasH1 ? 'B+' : p.title ? 'C+' : 'C') : 'N/A';
const rows = [
['Reachability', p.statusText || 'n/a', p.ok ? 'Pass' : 'Investigate'],
['<title> tag', p.title ? esc(p.title) : 'MISSING', p.title ? (p.title.length <= 60 ? 'Good length' : 'Too long') : 'Add one'],
['Meta description', p.metaDesc ? `${p.metaDesc.length} chars` : 'MISSING', p.metaDesc ? (p.metaDesc.length <= 160 ? 'Good' : 'Trim to ≤160') : 'Add one (150–160 chars)'],
['<h1> heading', p.hasH1 ? 'Present' : 'MISSING', p.hasH1 ? 'Pass' : 'Add a single clear h1'],
];
return SHELL(`Site Audit — ${host}`, host, navBar(sections, 'audit') + `
<h2>Executive grade <span class="grade">${grade}</span></h2>
<p>This audit inspects the homepage of <strong>${esc(host)}</strong> for the fundamentals that drive
search visibility, first-impression conversion, and trust. Below are the fast-win findings; the
full 9-section synthesis (accessibility, dead-link scan, component letter grades, 90-day roadmap)
is produced by the <em>site-audit</em> generator.</p>
<h2>Core signals</h2>
<table><tr><th>Signal</th><th>Observed</th><th>Verdict</th></tr>
${rows.map((r) => `<tr><td>${r[0]}</td><td>${r[1]}</td><td>${r[2]}</td></tr>`).join('')}
</table>
<h2>Top 5 recommended additions</h2>
<ul>
<li><strong>Above-the-fold value proposition</strong> — one sentence stating who you help and how, within the first viewport.</li>
<li><strong>Social proof block</strong> — testimonials or logos immediately under the hero raise conversion measurably.</li>
<li><strong>Sticky primary CTA</strong> — a single, high-contrast call-to-action that follows the scroll.</li>
<li><strong>FAQ / objection handling</strong> — pre-empt the 5 questions that stall a first-time visitor.</li>
<li><strong>Schema.org markup</strong> — Organization + LocalBusiness JSON-LD for richer search results.</li>
</ul>
<h2>90-day roadmap (summary)</h2>
<p>Weeks 1–2: metadata + h1 + schema. Weeks 3–6: hero rewrite + social proof + CTA. Weeks 7–12:
FAQ, accessibility pass, performance. The Pro tier includes hand-built homepage concepts to
execute against.</p>`);
}
function genTools(host, p, sections) {
return SHELL(`Visitor Tools — ${host}`, host, navBar(sections, 'tools') + `
<h2>Working browser-side widgets</h2>
<p>The <em>tools-pack</em> generator produces a pack of real, client-side interactive tools tailored to
your vertical — no server, no data collection. Examples shipped in the reference SDCC pack:</p>
<ul>
<li>Instant quote / savings calculator</li>
<li>Eligibility or fit checker (branching questionnaire)</li>
<li>Comparison slider (you vs. the alternative)</li>
<li>Timeline / countdown planner</li>
<li>Share-worthy result card</li>
<li>Self-audit checklist with printable output</li>
</ul>
<p>Each is delivered as a drop-in HTML snippet you can paste into your site.</p>`);
}
function genPeers(host, p, sections) {
return SHELL(`Peer Survey — ${host}`, host, navBar(sections, 'peers') + `
<h2>What comparable sites are doing</h2>
<p>The <em>peer-survey</em> generator captures front-page screenshots of comparable organizations and
identifies one specific UX move worth borrowing from each, in a side-by-side comparison.</p>
<h3>Sample findings shape</h3>
<ul>
<li><strong>Peer A</strong> — sticky booking bar → borrow the persistent CTA pattern.</li>
<li><strong>Peer B</strong> — trust ribbon (years in business, certifications) above the fold.</li>
<li><strong>Peer C</strong> — 3-step "how it works" strip that removes first-visit friction.</li>
</ul>`);
}
function genHub(host, p, sections) {
return SHELL(`Trusted Sources — ${host}`, host, navBar(sections, 'hub') + `
<h2>Curated authoritative sources for your vertical</h2>
<p>The <em>info-hub</em> generator builds a directory of authoritative external sources your visitors
trust — each with a direct URL, whether it supports MFA, its retention policy, and mobile-friendliness
— plus a self-audit checklist. Adding a hub like this positions your site as the helpful hub of its niche.</p>`);
}
function genCompetitors(host, p, sections) {
return SHELL(`Competitor Teardown — ${host}`, host, navBar(sections, 'competitors') + `
<h2>Where you can beat them</h2>
<p>The <em>competitors</em> generator dissects named commercial competitors on features, pricing,
privacy posture, and produces differentiation vectors <em>you</em> can ship that they structurally cannot.</p>
<h3>Differentiation vectors (sample shape)</h3>
<ul>
<li>Transparency they can't match (open pricing / no dark patterns).</li>
<li>Speed-to-value: a working tool on the homepage vs. a lead-gate.</li>
<li>Local trust signals a national competitor can't credibly claim.</li>
</ul>`);
}
function genMockups(host, p, sections) {
return SHELL(`Homepage Concepts — ${host}`, host, navBar(sections, 'mockups') + `
<h2>Hand-built front-page concepts</h2>
<p>The <em>mockups</em> generator produces distinct aesthetic directions for your homepage — not template
re-skins. Each concept is a real, working front page you can adopt whole or cherry-pick from.</p>
<h3>Directions included at the Pro tier</h3>
<ul>
<li>Conversion-first (hero + proof + single CTA)</li>
<li>Editorial / magazine (authority-building)</li>
<li>Utility-first (tool on the homepage)</li>
</ul>`);
}
const GENERATORS = { audit: genAudit, tools: genTools, peers: genPeers, hub: genHub, competitors: genCompetitors, mockups: genMockups };
// Assemble the full report bundle for an order. Returns { dir, indexUrl, files, email }.
export async function fulfillOrder({ orderId, url, email, tier, baseUrl }) {
const sections = TIER_SECTIONS[tier] || TIER_SECTIONS.standard;
const host = hostOf(url);
const p = await probe(url);
const dir = path.join(REPORTS_DIR, orderId);
fs.mkdirSync(dir, { recursive: true });
const files = [];
for (const s of sections) {
const html = GENERATORS[s](host, p, sections);
const fname = SECTION_META[s].file;
fs.writeFileSync(path.join(dir, fname), html);
files.push(fname);
}
// index.html = the audit is the front door
fs.copyFileSync(path.join(dir, 'report.html'), path.join(dir, 'index.html'));
// manifest for the delivery/status page
const manifest = {
orderId, url, host, email, tier, sections,
createdAt: new Date().toISOString(),
probe: p, files,
};
fs.writeFileSync(path.join(dir, 'manifest.json'), JSON.stringify(manifest, null, 2));
const deliveryUrl = `${baseUrl}/r/${orderId}/`;
const email_draft = draftDeliveryEmail({ email, host, tier, deliveryUrl, sections });
fs.writeFileSync(path.join(dir, 'delivery-email.txt'), email_draft.text);
return { orderId, dir, deliveryUrl, files, manifest, email: email_draft };
}
function draftDeliveryEmail({ email, host, tier, deliveryUrl, sections }) {
const tierLabel = { lite: 'Lite', standard: 'Standard', pro: 'Pro' }[tier] || tier;
const secList = sections.map((s) => ` • ${SECTION_META[s].title}`).join('\n');
const subject = `Your Instant Website Audit for ${host} is ready`;
const text = `To: ${email}
Subject: ${subject}
Hi,
Thanks for your order. Your ${tierLabel} website audit for ${host} is ready.
View your report:
${deliveryUrl}
Included in this report:
${secList}
The report opens in any browser and is confidential to you. If any link
doesn't load or you'd like a section expanded, just reply to this email.
— Instant Website Audit
`;
// NOTE: this is a DRAFT only. Sending is gated (send-to-list / outbound email
// requires Steve sign-off). The server never auto-sends.
return { subject, to: email, text, sent: false, note: 'DRAFT ONLY — sending is gated' };
}