git-paid 2.1.3 → 2.1.4

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/lib/engine.js CHANGED
@@ -1,8 +1,10 @@
1
- // Core Autonomous Engine for Postbuild (postbuild.dev)
2
- // Handles live URL auditing, Launch Kit synthesis, 25-route Programmatic SEO generation,
3
- // 85-directory dispatch preparation, and Live Buyer Intent Radar.
1
+ // Core Autonomous Engine for Postbuild (www.postbuild.dev)
2
+ // Handles SSRF-safe live URL auditing, Launch Kit synthesis, 25-route Programmatic SEO generation,
3
+ // 85-directory launch kit preparation + IndexNow/WebSub dispatch, and Buyer Intent Radar.
4
4
 
5
5
  import { DIRECTORIES, buildPrefilledDirectoryUrl } from "./directories.js";
6
+ import { getBotInboxForDomain } from "./inbox.js";
7
+ import { assertSafeUrl, safeFetch } from "./safe-fetch.js";
6
8
 
7
9
  export { DIRECTORIES };
8
10
 
@@ -34,22 +36,26 @@ function inferCategories(text) {
34
36
  return cats.slice(0, 3);
35
37
  }
36
38
 
37
- export async function scanTargetUrl(rawInput) {
38
- let targetUrl = (rawInput || "https://postbuild.dev").trim();
39
+ export async function scanTargetUrl(rawInput, options = {}) {
40
+ let targetUrl = (rawInput || "https://www.postbuild.dev").trim();
39
41
  if (!/^https?:\/\//i.test(targetUrl)) {
40
42
  targetUrl = "https://" + targetUrl;
41
43
  }
42
44
 
43
- let parsedUrl;
44
- try {
45
- parsedUrl = new URL(targetUrl);
46
- } catch {
47
- parsedUrl = new URL("https://postbuild.dev");
45
+ // P1-4: Validate target URL against SSRF rules (blocks loopback, RFC1918 private ranges, link-local, metadata)
46
+ const parsedUrl = await assertSafeUrl(targetUrl, { lookupFn: options.lookupFn });
47
+
48
+ const domain = parsedUrl.hostname.replace(/^www\./, "").toLowerCase();
49
+ if (["github.com", "gitlab.com", "bitbucket.org"].includes(domain)) {
50
+ const repoErr = new Error(
51
+ "Please enter your deployed live website URL rather than a code repository host (e.g. https://your-app.com)."
52
+ );
53
+ repoErr.statusCode = 422;
54
+ throw repoErr;
48
55
  }
49
56
 
50
- const domain = parsedUrl.hostname.replace(/^www\./, "");
51
57
  const slug = domain.split(".")[0].toLowerCase().replace(/[^a-z0-9-]/g, "") || "app";
52
- const botEmail = `${slug}-bot@postbuild.dev`;
58
+ const botEmail = getBotInboxForDomain(domain);
53
59
 
54
60
  let html = "";
55
61
  let reachable = false;
@@ -59,15 +65,13 @@ export async function scanTargetUrl(rawInput) {
59
65
  let hasSitemap = false;
60
66
 
61
67
  try {
62
- const controller = new AbortController();
63
- const timer = setTimeout(() => controller.abort(), 5500);
64
- const res = await fetch(parsedUrl.toString(), {
65
- signal: controller.signal,
66
- headers: { "User-Agent": "PostbuildBot/1.0 (+https://postbuild.dev)" }
68
+ const res = await safeFetch(parsedUrl.toString(), {
69
+ timeoutMs: 5000,
70
+ headers: { "User-Agent": "PostbuildBot/2.1 (+https://www.postbuild.dev)" },
71
+ lookupFn: options.lookupFn
67
72
  });
68
- clearTimeout(timer);
69
73
  if (res.ok) {
70
- html = await res.text();
74
+ html = res.text();
71
75
  reachable = true;
72
76
  }
73
77
  } catch {
@@ -75,9 +79,12 @@ export async function scanTargetUrl(rawInput) {
75
79
  }
76
80
 
77
81
  try {
78
- const rRes = await fetch(`${parsedUrl.origin}/robots.txt`, { signal: AbortSignal.timeout(3000) });
82
+ const rRes = await safeFetch(`${parsedUrl.origin}/robots.txt`, {
83
+ timeoutMs: 3500,
84
+ lookupFn: options.lookupFn
85
+ });
79
86
  if (rRes.ok) {
80
- robotsText = await rRes.text();
87
+ robotsText = rRes.text();
81
88
  hasRobots = robotsText.length > 5 && !/<html/i.test(robotsText);
82
89
  }
83
90
  } catch {
@@ -85,18 +92,22 @@ export async function scanTargetUrl(rawInput) {
85
92
  }
86
93
 
87
94
  try {
88
- const sRes = await fetch(`${parsedUrl.origin}/sitemap.xml`, { signal: AbortSignal.timeout(3000) });
95
+ const sRes = await safeFetch(`${parsedUrl.origin}/sitemap.xml`, {
96
+ timeoutMs: 3500,
97
+ lookupFn: options.lookupFn
98
+ });
89
99
  if (sRes.ok) {
90
- sitemapText = await sRes.text();
100
+ sitemapText = sRes.text();
91
101
  hasSitemap = /<urlset|<sitemapindex/i.test(sitemapText);
92
102
  }
93
103
  } catch {
94
104
  hasSitemap = false;
95
105
  }
96
106
 
107
+ const extractedTitle = extractTag(html, /<title[^>]*>([^<]+)<\/title>/i);
97
108
  const rawTitle =
98
- extractTag(html, /<title[^>]*>([^<]+)<\/title>/i) ||
99
- `${slug.charAt(0).toUpperCase() + slug.slice(1)} — Modern Workflow & Automation Platform`;
109
+ extractedTitle ||
110
+ `${slug.charAt(0).toUpperCase() + slug.slice(1)} — Web Application`;
100
111
 
101
112
  const description =
102
113
  extractTag(html, /<meta[^>]+name=["']description["'][^>]+content=["']([^"']+)["']/i) ||
@@ -117,7 +128,8 @@ export async function scanTargetUrl(rawInput) {
117
128
 
118
129
  const hasJsonLd = /application\/ld\+json/i.test(html);
119
130
  const hasCheckout = /polar\.sh|lemonsqueezy\.com|paddle\.com|buy\.stripe\.com|checkout\.stripe\.com|\/api\/checkout|Launch Pass/i.test(html);
120
- const hasAnalytics = /posthog|plausible|gtag|googletagmanager|umami|vercel-insights|sola/i.test(html);
131
+ // Fixed telemetry check: never match CSS tokens like "sola"
132
+ const hasAnalytics = /posthog|plausible|gtag|googletagmanager|umami|vercel-insights|_vercel\/insights|data-domain=|analytics\.js/i.test(html);
121
133
  const hasDuplicateSnapshotMeta = (html.match(/manages 5 data types/gi) || []).length > 3;
122
134
 
123
135
  const hasAiCrawlerRules = hasRobots && /OAI-SearchBot|PerplexityBot|ClaudeBot|GPTBot/i.test(robotsText);
@@ -127,9 +139,14 @@ export async function scanTargetUrl(rawInput) {
127
139
  const checks = [
128
140
  {
129
141
  id: "title",
130
- label: "Title Tag & Keyword Hook",
131
- passed: Boolean(rawTitle && rawTitle.length > 10),
132
- detail: rawTitle || "Missing <title>"
142
+ label: "Title Tag & Server-Rendered H1",
143
+ passed: Boolean(extractedTitle && extractedTitle.length > 10 && h1List.length > 0),
144
+ detail:
145
+ extractedTitle && h1List.length > 0
146
+ ? `${extractedTitle} (H1: ${h1List[0].slice(0, 48)})`
147
+ : !extractedTitle
148
+ ? "Missing <title> tag in HTML"
149
+ : "Missing server-rendered <h1> in initial HTML response"
133
150
  },
134
151
  {
135
152
  id: "desc",
@@ -169,11 +186,11 @@ export async function scanTargetUrl(rawInput) {
169
186
  },
170
187
  {
171
188
  id: "sitemap",
172
- label: "Programmatic SEO Sitemap (25+ Routes)",
189
+ label: "Programmatic SEO Sitemap (10+ Routes)",
173
190
  passed: hasPseoSitemap,
174
191
  detail: hasSitemap
175
192
  ? `sitemap.xml active (${sitemapUrlCount} indexed routes — ${hasPseoSitemap ? "pSEO active" : "25 pSEO routes ready to add"})`
176
- : "No sitemap.xml found — 25 high-intent pSEO routes ready to deploy"
193
+ : "No sitemap.xml found — 25 structured pSEO routes ready to deploy"
177
194
  },
178
195
  {
179
196
  id: "paywall",
@@ -187,19 +204,19 @@ export async function scanTargetUrl(rawInput) {
187
204
  id: "analytics",
188
205
  label: "Conversion Telemetry",
189
206
  passed: hasAnalytics,
190
- detail: hasAnalytics ? "Conversion telemetry active" : "No conversion pixel/analytics detected"
207
+ detail: hasAnalytics ? "Conversion telemetry script detected" : "No conversion pixel/analytics script detected"
191
208
  }
192
209
  ];
193
210
 
194
- const passedCount = checks.filter(c => c.passed).length;
195
- const readinessScore = Math.max(18, Math.round((passedCount / checks.length) * 100));
211
+ const passedCount = checks.filter((c) => c.passed).length;
212
+ const readinessScore = reachable ? Math.round((passedCount / checks.length) * 100) : 0;
196
213
 
197
214
  const titleParts = rawTitle.split(/\s*[|—–-]\s*/);
198
215
  const cleanName = (titleParts[0] || slug).trim();
199
216
  const extractedTagline =
200
217
  (titleParts[1] && titleParts[1].length >= 12 ? titleParts[1].trim() : null) ||
201
218
  (h1List[0] && h1List[0].length <= 75 ? h1List[0] : null) ||
202
- (description ? description.slice(0, 60).trim() : `Automated workflow and growth engine for ${cleanName}`);
219
+ (description ? description.slice(0, 60).trim() : `${cleanName} (${domain})`);
203
220
 
204
221
  const launchKit = generateLaunchKit({
205
222
  origin: parsedUrl.origin,
@@ -213,10 +230,8 @@ export async function scanTargetUrl(rawInput) {
213
230
  botEmail
214
231
  });
215
232
 
216
- // Generate 25 Programmatic SEO page blueprints tailored to this domain/slug
217
233
  const pseoPages = generatePseoPages(parsedUrl.origin, slug, cleanName);
218
234
 
219
- // Generate ready-to-drop SEO & Paywall code artifacts (with all 25 pSEO routes in sitemap.xml)
220
235
  const artifacts = generateArtifacts(
221
236
  parsedUrl.origin,
222
237
  slug,
@@ -225,10 +240,9 @@ export async function scanTargetUrl(rawInput) {
225
240
  pseoPages
226
241
  );
227
242
 
228
- // Attach prefilled submission URLs for all 85 directories
229
- const enrichedDirectories = DIRECTORIES.map(dir => ({
243
+ const enrichedDirectories = DIRECTORIES.map((dir) => ({
230
244
  ...dir,
231
- status: "Queued",
245
+ status: "Ready to submit",
232
246
  prefilledUrl: buildPrefilledDirectoryUrl(dir, launchKit)
233
247
  }));
234
248
 
@@ -258,7 +272,7 @@ function generateLaunchKit({ origin, domain, slug, cleanName, tagline, descripti
258
272
 
259
273
  const featureBullets =
260
274
  h2List.length >= 2
261
- ? h2List.slice(0, 4).map(h => `- **${h}**`).join("\n")
275
+ ? h2List.slice(0, 4).map((h) => `- **${h}**`).join("\n")
262
276
  : [
263
277
  `- **Instant Setup**: Point ${cleanName} at your live workflow and start in under 60 seconds.`,
264
278
  `- **Automated Execution**: Replaces repetitive manual checklists with deterministic automation.`,
@@ -293,31 +307,31 @@ Try it live: ${utmUrl}`;
293
307
 
294
308
  function generatePseoPages(origin, slug, cleanName) {
295
309
  const templates = [
296
- { path: `/use-cases/${slug}-for-solo-founders`, keyword: `best ${slug} workflow for solo founders`, volume: "1,400/mo", kd: 12, intent: "Transactional" },
297
- { path: `/use-cases/${slug}-automation-guide`, keyword: `how to automate tasks with ${cleanName}`, volume: "2,100/mo", kd: 15, intent: "High Intent" },
298
- { path: `/compare/${slug}-vs-manual-workflow`, keyword: `${cleanName} vs manual spreadsheets`, volume: "880/mo", kd: 8, intent: "Comparison" },
299
- { path: `/templates/${slug}-starter-kit`, keyword: `free ${slug} template download`, volume: "3,200/mo", kd: 18, intent: "Lead Magnet" },
300
- { path: `/alternatives/best-${slug}-alternatives-2026`, keyword: `top ${slug} tools for small teams`, volume: "1,950/mo", kd: 14, intent: "Comparison" },
301
- { path: `/calculators/${slug}-roi-calculator`, keyword: `${slug} time & cost calculator`, volume: "1,150/mo", kd: 9, intent: "Interactive" },
302
- { path: `/guides/${slug}-api-integration`, keyword: `connect ${cleanName} to webhook & API`, volume: "740/mo", kd: 7, intent: "Developer" },
303
- { path: `/use-cases/${slug}-for-agencies`, keyword: `${slug} client reporting for agencies`, volume: "1,600/mo", kd: 16, intent: "B2B Impulse" },
304
- { path: `/checklists/${slug}-launch-checklist`, keyword: `complete ${slug} setup checklist`, volume: "2,400/mo", kd: 11, intent: "Lead Magnet" },
305
- { path: `/compare/${slug}-pricing-breakdown`, keyword: `is ${cleanName} worth it pricing guide`, volume: "620/mo", kd: 5, intent: "Bottom Funnel" },
306
- { path: `/use-cases/${slug}-for-developers`, keyword: `${slug} CLI and developer workflow`, volume: "1,320/mo", kd: 10, intent: "Transactional" },
307
- { path: `/use-cases/${slug}-for-indie-hackers`, keyword: `${cleanName} for indie SaaS launches`, volume: "1,780/mo", kd: 11, intent: "Transactional" },
308
- { path: `/compare/${slug}-vs-legacy-tools`, keyword: `why switch to ${cleanName} in 2026`, volume: "540/mo", kd: 6, intent: "Comparison" },
309
- { path: `/guides/${slug}-programmatic-seo-setup`, keyword: `automated seo setup with ${cleanName}`, volume: "1,490/mo", kd: 13, intent: "High Intent" },
310
- { path: `/templates/${slug}-nextjs-boilerplate`, keyword: `${slug} nextjs integration template`, volume: "960/mo", kd: 9, intent: "Developer" },
311
- { path: `/use-cases/${slug}-for-saas-teams`, keyword: `scale b2b saas with ${cleanName}`, volume: "1,120/mo", kd: 14, intent: "B2B Impulse" },
312
- { path: `/guides/${slug}-stripe-checkout-guide`, keyword: `add self-serve checkout with ${cleanName}`, volume: "830/mo", kd: 8, intent: "Transactional" },
313
- { path: `/checklists/${slug}-seo-audit-checklist`, keyword: `technical seo audit checklist ${slug}`, volume: "2,050/mo", kd: 15, intent: "Lead Magnet" },
314
- { path: `/calculators/${slug}-conversion-estimator`, keyword: `saas launch conversion rate calculator`, volume: "1,280/mo", kd: 10, intent: "Interactive" },
315
- { path: `/alternatives/${slug}-open-source-comparison`, keyword: `${cleanName} vs open source scripts`, volume: "690/mo", kd: 7, intent: "Comparison" },
316
- { path: `/use-cases/${slug}-for-product-hunt-launch`, keyword: `prepare product hunt launch with ${cleanName}`, volume: "1,540/mo", kd: 12, intent: "High Intent" },
317
- { path: `/guides/${slug}-ai-search-indexing`, keyword: `get indexed on chatgpt and perplexity with ${slug}`, volume: "2,650/mo", kd: 11, intent: "High Intent" },
318
- { path: `/templates/${slug}-directory-submission-kit`, keyword: `85 startup directory submission list 2026`, volume: "3,400/mo", kd: 16, intent: "Lead Magnet" },
319
- { path: `/use-cases/${slug}-for-ai-wrappers`, keyword: `monetize ai apps with ${cleanName}`, volume: "1,890/mo", kd: 13, intent: "Transactional" },
320
- { path: `/compare/${slug}-roi-case-study`, keyword: `${cleanName} traffic and revenue case study`, volume: "480/mo", kd: 4, intent: "Bottom Funnel" }
310
+ { path: `/use-cases/${slug}-for-solo-founders`, keyword: `${slug} workflow for solo founders`, category: "Use Case", schema: "FAQPage + SoftwareApplication", intent: "Transactional" },
311
+ { path: `/use-cases/${slug}-automation-guide`, keyword: `how to automate tasks with ${cleanName}`, category: "Use Case", schema: "HowTo + FAQPage", intent: "High Intent" },
312
+ { path: `/compare/${slug}-vs-manual-workflow`, keyword: `${cleanName} vs manual workflow`, category: "Comparison", schema: "Article + FAQPage", intent: "Comparison" },
313
+ { path: `/templates/${slug}-starter-kit`, keyword: `${slug} starter template & configuration`, category: "Template", schema: "SoftwareSourceCode", intent: "Lead Magnet" },
314
+ { path: `/alternatives/best-${slug}-alternatives-2026`, keyword: `${slug} workflow comparison guide`, category: "Comparison", schema: "ItemList + FAQPage", intent: "Comparison" },
315
+ { path: `/calculators/${slug}-roi-calculator`, keyword: `${slug} time & cost estimator`, category: "Calculator", schema: "WebApplication", intent: "Interactive" },
316
+ { path: `/guides/${slug}-api-integration`, keyword: `connect ${cleanName} to webhook & API`, category: "Guide", schema: "TechArticle", intent: "Developer" },
317
+ { path: `/use-cases/${slug}-for-agencies`, keyword: `${slug} multi-project workflow`, category: "Use Case", schema: "FAQPage + SoftwareApplication", intent: "B2B" },
318
+ { path: `/checklists/${slug}-launch-checklist`, keyword: `complete ${slug} launch checklist`, category: "Checklist", schema: "HowTo + ItemList", intent: "Lead Magnet" },
319
+ { path: `/compare/${slug}-pricing-breakdown`, keyword: `${cleanName} pricing & plan breakdown`, category: "Comparison", schema: "Product + Offer", intent: "Bottom Funnel" },
320
+ { path: `/use-cases/${slug}-for-developers`, keyword: `${slug} CLI and developer workflow`, category: "Use Case", schema: "TechArticle", intent: "Transactional" },
321
+ { path: `/use-cases/${slug}-for-indie-hackers`, keyword: `${cleanName} for indie SaaS launches`, category: "Use Case", schema: "FAQPage + SoftwareApplication", intent: "Transactional" },
322
+ { path: `/compare/${slug}-vs-legacy-tools`, keyword: `migrating to ${cleanName}`, category: "Comparison", schema: "Article + FAQPage", intent: "Comparison" },
323
+ { path: `/guides/${slug}-programmatic-seo-setup`, keyword: `structured seo setup with ${cleanName}`, category: "Guide", schema: "TechArticle", intent: "High Intent" },
324
+ { path: `/templates/${slug}-nextjs-boilerplate`, keyword: `${slug} nextjs integration guide`, category: "Template", schema: "TechArticle", intent: "Developer" },
325
+ { path: `/use-cases/${slug}-for-saas-teams`, keyword: `${cleanName} for engineering teams`, category: "Use Case", schema: "FAQPage + SoftwareApplication", intent: "B2B" },
326
+ { path: `/guides/${slug}-stripe-checkout-guide`, keyword: `add self-serve checkout with ${cleanName}`, category: "Guide", schema: "HowTo + TechArticle", intent: "Transactional" },
327
+ { path: `/checklists/${slug}-seo-audit-checklist`, keyword: `technical seo audit checklist ${slug}`, category: "Checklist", schema: "ItemList + FAQPage", intent: "Lead Magnet" },
328
+ { path: `/calculators/${slug}-conversion-estimator`, keyword: `saas launch conversion estimator`, category: "Calculator", schema: "WebApplication", intent: "Interactive" },
329
+ { path: `/alternatives/${slug}-open-source-comparison`, keyword: `${cleanName} open source architecture`, category: "Comparison", schema: "TechArticle", intent: "Comparison" },
330
+ { path: `/use-cases/${slug}-for-product-hunt-launch`, keyword: `prepare product hunt launch with ${cleanName}`, category: "Use Case", schema: "HowTo + FAQPage", intent: "High Intent" },
331
+ { path: `/guides/${slug}-ai-search-indexing`, keyword: `configure OAI-SearchBot & PerplexityBot for ${slug}`, category: "Guide", schema: "TechArticle", intent: "High Intent" },
332
+ { path: `/templates/${slug}-directory-submission-kit`, keyword: `85 startup directory launch kit`, category: "Template", schema: "ItemList", intent: "Lead Magnet" },
333
+ { path: `/use-cases/${slug}-for-ai-wrappers`, keyword: `monetize ai apps with ${cleanName}`, category: "Use Case", schema: "FAQPage + SoftwareApplication", intent: "Transactional" },
334
+ { path: `/compare/${slug}-roi-case-study`, keyword: `${cleanName} technical launch breakdown`, category: "Walkthrough", schema: "Article", intent: "Bottom Funnel" }
321
335
  ];
322
336
 
323
337
  return templates.map((t, idx) => ({
@@ -325,10 +339,10 @@ function generatePseoPages(origin, slug, cleanName) {
325
339
  url: `${origin}${t.path}`,
326
340
  path: t.path,
327
341
  keyword: t.keyword,
328
- volume: t.volume,
329
- kd: `KD ${t.kd}`,
342
+ volume: t.category,
343
+ kd: t.schema,
330
344
  intent: t.intent,
331
- status: "Generated"
345
+ status: "Blueprint Ready"
332
346
  }));
333
347
  }
334
348
 
@@ -337,7 +351,7 @@ function generateArtifacts(origin, slug, cleanName, description, pseoPages) {
337
351
 
338
352
  const routeEntries = pseoPages
339
353
  .map(
340
- p => ` <url>
354
+ (p) => ` <url>
341
355
  <loc>${origin}${p.path}</loc>
342
356
  <lastmod>${today}</lastmod>
343
357
  <changefreq>weekly</changefreq>
@@ -387,116 +401,218 @@ Sitemap: ${origin}/sitemap.xml`;
387
401
  }
388
402
  </script>`;
389
403
 
390
- const paywallCode = `// Postbuild Drop-In Stripe Checkout Gate (${cleanName})
391
- export async function triggerPostbuildCheckout(plan = "launch_pass", domain = "${slug}.com") {
404
+ const paywallCode = `// 1. Client Component: Drop-In Stripe Checkout Gate for ${cleanName}
405
+ export async function startCheckout({ priceId = "price_YOUR_STRIPE_PRICE_ID", plan = "pro", email = "" } = {}) {
392
406
  const res = await fetch("/api/checkout", {
393
407
  method: "POST",
394
408
  headers: { "Content-Type": "application/json" },
395
- body: JSON.stringify({ plan, domain })
409
+ body: JSON.stringify({ priceId, plan, email })
396
410
  });
397
411
  const data = await res.json();
398
412
  if (data.checkoutUrl) window.location.href = data.checkoutUrl;
399
413
  return data;
400
414
  }
401
415
 
402
- export function PostbuildCheckoutGate({ domain = "${slug}.com" }) {
416
+ export function CheckoutGate({ priceId = "price_YOUR_STRIPE_PRICE_ID", label = "Get ${cleanName} Pro — $29" }) {
403
417
  return (
404
418
  <button
405
419
  type="button"
406
- onClick={() => triggerPostbuildCheckout("launch_pass", domain)}
420
+ onClick={() => startCheckout({ priceId })}
407
421
  style={{
408
422
  display: "inline-flex",
409
423
  alignItems: "center",
410
424
  gap: "8px",
411
425
  padding: "10px 18px",
412
426
  borderRadius: "8px",
413
- background: "#09090b",
427
+ background: "#059669",
414
428
  color: "#ffffff",
415
429
  fontWeight: 600,
416
430
  fontSize: "13px",
417
- border: "1px solid #09090b",
431
+ border: "1px solid #047857",
418
432
  cursor: "pointer"
419
433
  }}
420
434
  >
421
- <span>Get Launch Pass — $29</span>
435
+ <span>{label}</span>
422
436
  </button>
423
437
  );
424
- }`;
438
+ }
439
+
440
+ // 2. Server Route (/api/checkout.js): Self-Serve Stripe Checkout Session
441
+ // import Stripe from "stripe";
442
+ // const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
443
+ // export default async function handler(req, res) {
444
+ // const { priceId, email } = req.body || {};
445
+ // const session = await stripe.checkout.sessions.create({
446
+ // mode: "payment",
447
+ // line_items: [{ price: priceId, quantity: 1 }],
448
+ // customer_email: email || undefined,
449
+ // allow_promotion_codes: true,
450
+ // success_url: "${origin}/?checkout=success&session_id={CHECKOUT_SESSION_ID}",
451
+ // cancel_url: "${origin}/?checkout=cancelled"
452
+ // });
453
+ // res.status(200).json({ ok: true, checkoutUrl: session.url });
454
+ // }`;
425
455
 
426
456
  return { sitemapXml, robotsTxt, jsonLd, paywallCode };
427
457
  }
428
458
 
429
- export async function dispatchDirectories({ targetUrl = "https://postbuild.dev", directoryIds = [] }) {
430
- const scan = await scanTargetUrl(targetUrl);
459
+ export const INDEXNOW_KEY = "pb7f9a2e4c8b1d6f3a5e0c9b2d4f6a8e";
460
+
461
+ export async function dispatchDirectories({ targetUrl = "https://www.postbuild.dev", directoryIds = [], lookupFn }) {
462
+ const scan = await scanTargetUrl(targetUrl, { lookupFn });
431
463
  const selected =
432
464
  Array.isArray(directoryIds) && directoryIds.length > 0
433
- ? scan.directories.filter(d => directoryIds.includes(d.id))
465
+ ? scan.directories.filter((d) => directoryIds.includes(d.id))
434
466
  : scan.directories;
435
467
 
468
+ const origin = scan.targetUrl.replace(/\/$/, "");
469
+ const parsedHost = new URL(origin).hostname;
470
+ const allSeoUrls = [`${origin}/`, ...scan.pseoPages.map((p) => `${origin}${p.path}`)];
471
+
472
+ // 1. Live IndexNow API Ping (Bing, Yandex, Seznam, Naver, ChatGPT Search / OAI-SearchBot index)
473
+ let indexNowResult = {
474
+ dispatched: false,
475
+ status: 0,
476
+ urlsSubmitted: allSeoUrls.length,
477
+ endpoint: "https://api.indexnow.org/indexnow"
478
+ };
479
+ try {
480
+ const idxResp = await fetch("https://api.indexnow.org/indexnow", {
481
+ method: "POST",
482
+ headers: { "Content-Type": "application/json; charset=utf-8" },
483
+ body: JSON.stringify({
484
+ host: parsedHost,
485
+ key: INDEXNOW_KEY,
486
+ keyLocation: `${origin}/${INDEXNOW_KEY}.txt`,
487
+ urlList: allSeoUrls
488
+ }),
489
+ signal: AbortSignal.timeout(4500)
490
+ });
491
+ indexNowResult = {
492
+ dispatched: idxResp.ok || idxResp.status === 202,
493
+ status: idxResp.status,
494
+ urlsSubmitted: allSeoUrls.length,
495
+ endpoint: "https://api.indexnow.org/indexnow"
496
+ };
497
+ } catch (err) {
498
+ indexNowResult.error = err.message;
499
+ }
500
+
501
+ // 2. Live Google WebSub / PubSubHubbub Sitemap Feed Notification
502
+ let webSubResult = {
503
+ dispatched: false,
504
+ status: 0,
505
+ sitemapUrl: `${origin}/sitemap.xml`,
506
+ endpoint: "https://pubsubhubbub.appspot.com/"
507
+ };
508
+ try {
509
+ const bodyParams = new URLSearchParams({
510
+ "hub.mode": "publish",
511
+ "hub.url": `${origin}/sitemap.xml`
512
+ });
513
+ const wsResp = await fetch("https://pubsubhubbub.appspot.com/", {
514
+ method: "POST",
515
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
516
+ body: bodyParams.toString(),
517
+ signal: AbortSignal.timeout(4500)
518
+ });
519
+ webSubResult = {
520
+ dispatched: wsResp.status === 204 || wsResp.ok,
521
+ status: wsResp.status,
522
+ sitemapUrl: `${origin}/sitemap.xml`,
523
+ endpoint: "https://pubsubhubbub.appspot.com/"
524
+ };
525
+ } catch (err) {
526
+ webSubResult.error = err.message;
527
+ }
528
+
436
529
  const timestamp = new Date().toISOString();
437
- const results = selected.map(dir => ({
530
+ const results = selected.map((dir) => ({
438
531
  id: dir.id,
439
532
  name: dir.name,
440
533
  category: dir.category,
441
534
  dr: dir.dr,
442
535
  authType: dir.authType,
536
+ autoSubmit: Boolean(dir.autoSubmit),
443
537
  submissionUrl: dir.url,
444
538
  prefilledUrl: dir.prefilledUrl,
445
539
  botEmail: scan.botEmail,
446
- status: "Dispatched",
540
+ status: "Prepared",
447
541
  dispatchedAt: timestamp
448
542
  }));
449
543
 
544
+ const summary = {
545
+ totalPrepared: results.length,
546
+ directFormCount: results.filter((r) => r.autoSubmit).length,
547
+ prefilledPortalCount: results.filter((r) => !r.autoSubmit).length,
548
+ seoUrlsSubmitted: allSeoUrls.length,
549
+ indexNowStatus: indexNowResult.status,
550
+ webSubStatus: webSubResult.status
551
+ };
552
+
450
553
  return {
451
554
  ok: true,
452
555
  domain: scan.domain,
453
556
  botEmail: scan.botEmail,
454
557
  launchKit: scan.launchKit,
558
+ seoDispatch: {
559
+ indexNow: indexNowResult,
560
+ webSub: webSubResult,
561
+ urlsSubmitted: allSeoUrls.length
562
+ },
563
+ summary,
455
564
  totalDispatched: results.length,
456
565
  results
457
566
  };
458
567
  }
459
568
 
460
- export async function searchBuyerIntentRadar(query) {
461
- const searchTerms = (query || "marketing automation seo indie hacker").trim();
462
- const url = `https://hn.algolia.com/api/v1/search_by_date?query=${encodeURIComponent(searchTerms)}&tags=story&hitsPerPage=8`;
463
-
464
- try {
569
+ export async function searchBuyerIntentRadar(query, targetDomain = "") {
570
+ const searchTerms = (query || "Show HN").trim();
571
+ const thirtyDaysAgo = Math.floor(Date.now() / 1000) - 30 * 86400;
572
+ const cleanTarget = String(targetDomain || "")
573
+ .toLowerCase()
574
+ .trim()
575
+ .replace(/^https?:\/\//, "")
576
+ .replace(/^www\./, "")
577
+ .split("/")[0];
578
+ const hasRealDomain = Boolean(cleanTarget && cleanTarget.includes(".") && cleanTarget !== "postbuild.dev");
579
+
580
+ async function queryAlgolia(q, minTimestamp) {
581
+ const filter = minTimestamp ? `&numericFilters=created_at_i>${minTimestamp}` : "";
582
+ const url = `https://hn.algolia.com/api/v1/search_by_date?query=${encodeURIComponent(q)}&tags=story&hitsPerPage=8${filter}`;
465
583
  const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
466
584
  if (!res.ok) throw new Error("HN API status " + res.status);
467
585
  const data = await res.json();
468
- const hits = (data.hits || []).map((h, idx) => ({
586
+ return (data.hits || []).filter((h) => h && h.title);
587
+ }
588
+
589
+ try {
590
+ let rawHits = await queryAlgolia(searchTerms, thirtyDaysAgo);
591
+ if (rawHits.length < 3 && searchTerms.includes(" ")) {
592
+ const firstKeyword = searchTerms.split(/\s+/)[0];
593
+ rawHits = await queryAlgolia(firstKeyword, thirtyDaysAgo);
594
+ }
595
+ if (rawHits.length === 0) {
596
+ rawHits = await queryAlgolia("Show HN", thirtyDaysAgo);
597
+ }
598
+
599
+ return rawHits.map((h, idx) => ({
469
600
  id: h.objectID || `hn-${idx}`,
470
601
  source: "Hacker News",
471
- title: h.title || "Discussion thread",
472
- author: h.author || "founder",
602
+ title: h.title,
603
+ author: h.author || "author",
473
604
  createdAt: h.created_at ? h.created_at.split("T")[0] : "Recent",
474
605
  url: h.url || `https://news.ycombinator.com/item?id=${h.objectID}`,
475
606
  discussionUrl: `https://news.ycombinator.com/item?id=${h.objectID}`,
476
607
  points: h.points || 1,
477
608
  comments: h.num_comments || 0,
478
609
  locked: idx >= 3,
479
- draftReply: `Hey @${h.author || "there"} — ran into this exact bottleneck after finishing a build. I ended up running \`git paid\` (https://postbuild.dev) to auto-generate 25 programmatic SEO routes, provision a @postbuild.dev verification inbox, and dispatch to 85 directories.`
610
+ draftReply: hasRealDomain
611
+ ? `Hi @${h.author || "there"}, I'm the maker of ${cleanTarget} (disclosure). If it helps with this, here's how we approach it: https://${cleanTarget}. Happy to answer questions.`
612
+ : ""
480
613
  }));
481
-
482
- if (hits.length > 0) return hits;
483
614
  } catch {
484
- // Fallback if offline
615
+ return [];
485
616
  }
486
-
487
- return [
488
- {
489
- id: "fallback-1",
490
- source: "Hacker News",
491
- title: "Ask HN: How are solo devs handling SEO and directory launches after building?",
492
- author: "dev_minimalist",
493
- createdAt: "Today",
494
- url: "https://news.ycombinator.com/ask",
495
- discussionUrl: "https://news.ycombinator.com/ask",
496
- points: 42,
497
- comments: 29,
498
- locked: false,
499
- draftReply: "I built Postbuild (https://postbuild.dev) for this exact reason — you run `git paid` after `git push` and it provisions a @postbuild.dev bot email, generates 25 programmatic SEO pages, and dispatches to 85 directories."
500
- }
501
- ];
502
617
  }
618
+
package/lib/fixer.js CHANGED
@@ -73,6 +73,59 @@ ${report.artifacts?.jsonLd || ""}
73
73
  `;
74
74
  }
75
75
 
76
+ export function buildStartHereMd(report) {
77
+ const domain = report.domain || "your-project.com";
78
+ const brand = report.launchKit?.brandName || report.launchKit?.productName || domain;
79
+ const botEmail = report.botEmail || `${domain.replace(/\./g, "-")}-bot@postbuild.dev`;
80
+ return `# START HERE — 3-Step Launch Guide for ${brand} (${domain})
81
+
82
+ Built with **Lovable, Bolt, v0, Replit, Cursor, or Claude**? You do **not** need to know how to code or configure SEO manually. Follow these 3 quick steps:
83
+
84
+ ---
85
+
86
+ ## Step 1: Drop the SEO & AI Crawler Files Into Your App (60 Seconds)
87
+
88
+ Upload or drag this folder into your AI builder (Lovable, Bolt, Replit, Cursor, Claude Code) and paste this exact prompt:
89
+
90
+ > **Copy & Paste Prompt for Your AI Builder:**
91
+ > \`\`\`text
92
+ > Please add the Postbuild SEO & AI Launch files to my project:
93
+ > 1. Place robots.txt, sitemap.xml, and llms.txt into my static public/ folder so they are served at /robots.txt, /sitemap.xml, and /llms.txt.
94
+ > 2. Copy the meta tags, canonical link, OpenGraph tags, and JSON-LD structured data from head-seo-snippet.html into my main HTML <head> (or root layout metadata).
95
+ > 3. Place the 25 pre-rendered static HTML guides (under guides/, comparisons/, use-cases/, checklists/) into public/ so those routes resolve cleanly.
96
+ > \`\`\`
97
+
98
+ ---
99
+
100
+ ## Step 2: Submit to Curated Startup & AI Directories (No Personal Inbox Spam)
101
+
102
+ 1. Open \`directories-85.csv\` (or the **85 Directories** tab on \`https://www.postbuild.dev\`).
103
+ 2. Click any **Prefilled Launch URL** — your product name, URL, UTM tags, and description are already pre-populated in the link parameters.
104
+ 3. For directories with custom form fields, drag the **1-Click Auto-Fill Bookmarklet** on the Postbuild **85 Directories** tab to your browser bookmarks bar and click it on any directory page to fill the form automatically.
105
+ 4. **Always use your dedicated verification email** when a directory asks for an email address:
106
+ \`${botEmail}\`
107
+
108
+ ---
109
+
110
+ ## Step 3: Verify Directory Emails in 1 Click
111
+
112
+ 1. Every confirmation email or 6-digit OTP code sent to \`${botEmail}\` arrives in your **Account & Verification Inbox** on \`https://www.postbuild.dev\`.
113
+ 2. Click **Auto-Verify Links** (or copy the 1-click OTP badge) to confirm your listings without cluttering your personal inbox.
114
+
115
+ ---
116
+
117
+ ### Files Included in This Bundle
118
+ - \`START_HERE.md\` — This 3-step quickstart guide + copy-paste AI builder prompt
119
+ - \`public/robots.txt\` — Search & AI crawler rules (GPTBot, ClaudeBot, PerplexityBot)
120
+ - \`public/sitemap.xml\` — XML Sitemap indexing your main URL + 25 programmatic SEO guides
121
+ - \`public/llms.txt\` — Structured markdown manifest for ChatGPT Search, Claude, and Perplexity
122
+ - \`head-seo-snippet.html\` — Title, meta description, OpenGraph social tags, and Schema.org JSON-LD
123
+ - \`launch-kit.md\` — Ready-to-post copy for Show HN, Product Hunt, and startup directories
124
+ - \`directories-85.csv\` — All 85 curated directories ranked by Domain Rating (DR) with prefilled URLs
125
+ - \`public/{use-cases,comparisons,guides,checklists}/*.html\` — 25 static HTML SEO landing pages
126
+ `;
127
+ }
128
+
76
129
  export function applyLocalSeoFixes(report, options = {}) {
77
130
  const cwd = options.cwd || process.cwd();
78
131
  const overwritePublic = Boolean(options.overwritePublic);
@@ -84,6 +137,11 @@ export function applyLocalSeoFixes(report, options = {}) {
84
137
 
85
138
  const writtenFiles = [];
86
139
 
140
+ // 0. START_HERE.md in .postbuild/
141
+ const startHerePath = path.join(outDir, "START_HERE.md");
142
+ fs.writeFileSync(startHerePath, buildStartHereMd(report), "utf8");
143
+ writtenFiles.push({ path: path.relative(cwd, startHerePath), role: "3-Step Non-Technical Quickstart & AI Builder Prompt (START_HERE.md)" });
144
+
87
145
  // 1. robots.txt in public/ (if missing or overwritePublic)
88
146
  const robotsPath = path.join(publicDir, "robots.txt");
89
147
  if (!fs.existsSync(robotsPath) || overwritePublic) {