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/README.md +22 -34
- package/bin/git-paid.js +14 -14
- package/lib/auth.js +866 -282
- package/lib/directories.js +123 -123
- package/lib/engine.js +228 -112
- package/lib/fixer.js +58 -0
- package/lib/inbox.js +359 -72
- package/lib/mcp.js +139 -90
- package/lib/pseo.js +290 -106
- package/lib/safe-fetch.js +239 -0
- package/lib/stateful-mcp.js +16 -4
- package/lib/submitter.js +89 -28
- package/package.json +53 -44
- package/brand/README.md +0 -54
- package/brand/postbuild-icon.png +0 -0
- package/brand/postbuild-icon.svg +0 -14
- package/brand/postbuild-logo.png +0 -0
- package/brand/postbuild-logo.svg +0 -17
- package/lib/stripe.js +0 -554
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
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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 =
|
|
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
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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 =
|
|
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
|
|
82
|
+
const rRes = await safeFetch(`${parsedUrl.origin}/robots.txt`, {
|
|
83
|
+
timeoutMs: 3500,
|
|
84
|
+
lookupFn: options.lookupFn
|
|
85
|
+
});
|
|
79
86
|
if (rRes.ok) {
|
|
80
|
-
robotsText =
|
|
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
|
|
95
|
+
const sRes = await safeFetch(`${parsedUrl.origin}/sitemap.xml`, {
|
|
96
|
+
timeoutMs: 3500,
|
|
97
|
+
lookupFn: options.lookupFn
|
|
98
|
+
});
|
|
89
99
|
if (sRes.ok) {
|
|
90
|
-
sitemapText =
|
|
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
|
-
|
|
99
|
-
`${slug.charAt(0).toUpperCase() + slug.slice(1)} —
|
|
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
|
-
|
|
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 &
|
|
131
|
-
passed: Boolean(
|
|
132
|
-
detail:
|
|
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 (
|
|
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
|
|
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
|
|
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 =
|
|
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() :
|
|
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
|
-
|
|
229
|
-
const enrichedDirectories = DIRECTORIES.map(dir => ({
|
|
243
|
+
const enrichedDirectories = DIRECTORIES.map((dir) => ({
|
|
230
244
|
...dir,
|
|
231
|
-
status: "
|
|
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:
|
|
297
|
-
{ path: `/use-cases/${slug}-automation-guide`, keyword: `how to automate tasks with ${cleanName}`,
|
|
298
|
-
{ path: `/compare/${slug}-vs-manual-workflow`, keyword: `${cleanName} vs manual
|
|
299
|
-
{ path: `/templates/${slug}-starter-kit`, keyword:
|
|
300
|
-
{ path: `/alternatives/best-${slug}-alternatives-2026`, keyword:
|
|
301
|
-
{ path: `/calculators/${slug}-roi-calculator`, keyword: `${slug} time & cost
|
|
302
|
-
{ path: `/guides/${slug}-api-integration`, keyword: `connect ${cleanName} to webhook & API`,
|
|
303
|
-
{ path: `/use-cases/${slug}-for-agencies`, keyword: `${slug}
|
|
304
|
-
{ path: `/checklists/${slug}-launch-checklist`, keyword: `complete ${slug}
|
|
305
|
-
{ path: `/compare/${slug}-pricing-breakdown`, keyword:
|
|
306
|
-
{ path: `/use-cases/${slug}-for-developers`, keyword: `${slug} CLI and developer workflow`,
|
|
307
|
-
{ path: `/use-cases/${slug}-for-indie-hackers`, keyword: `${cleanName} for indie SaaS launches`,
|
|
308
|
-
{ path: `/compare/${slug}-vs-legacy-tools`, keyword: `
|
|
309
|
-
{ path: `/guides/${slug}-programmatic-seo-setup`, keyword: `
|
|
310
|
-
{ path: `/templates/${slug}-nextjs-boilerplate`, keyword: `${slug} nextjs integration
|
|
311
|
-
{ path: `/use-cases/${slug}-for-saas-teams`, keyword:
|
|
312
|
-
{ path: `/guides/${slug}-stripe-checkout-guide`, keyword: `add self-serve checkout with ${cleanName}`,
|
|
313
|
-
{ path: `/checklists/${slug}-seo-audit-checklist`, keyword: `technical seo audit checklist ${slug}`,
|
|
314
|
-
{ path: `/calculators/${slug}-conversion-estimator`, keyword: `saas launch conversion
|
|
315
|
-
{ path: `/alternatives/${slug}-open-source-comparison`, keyword: `${cleanName}
|
|
316
|
-
{ path: `/use-cases/${slug}-for-product-hunt-launch`, keyword: `prepare product hunt launch with ${cleanName}`,
|
|
317
|
-
{ path: `/guides/${slug}-ai-search-indexing`, keyword: `
|
|
318
|
-
{ path: `/templates/${slug}-directory-submission-kit`, keyword: `85 startup directory
|
|
319
|
-
{ path: `/use-cases/${slug}-for-ai-wrappers`, keyword: `monetize ai apps with ${cleanName}`,
|
|
320
|
-
{ path: `/compare/${slug}-roi-case-study`, keyword: `${cleanName}
|
|
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.
|
|
329
|
-
kd:
|
|
342
|
+
volume: t.category,
|
|
343
|
+
kd: t.schema,
|
|
330
344
|
intent: t.intent,
|
|
331
|
-
status: "
|
|
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 = `//
|
|
391
|
-
export async function
|
|
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,
|
|
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
|
|
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={() =>
|
|
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: "#
|
|
427
|
+
background: "#059669",
|
|
414
428
|
color: "#ffffff",
|
|
415
429
|
fontWeight: 600,
|
|
416
430
|
fontSize: "13px",
|
|
417
|
-
border: "1px solid #
|
|
431
|
+
border: "1px solid #047857",
|
|
418
432
|
cursor: "pointer"
|
|
419
433
|
}}
|
|
420
434
|
>
|
|
421
|
-
<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
|
|
430
|
-
|
|
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: "
|
|
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 || "
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
|
|
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
|
-
|
|
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
|
|
472
|
-
author: h.author || "
|
|
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:
|
|
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
|
-
|
|
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) {
|