git-paid 2.1.6 → 2.1.7

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
@@ -14,25 +14,47 @@ function extractTag(html, regex) {
14
14
  }
15
15
 
16
16
  function extractAllTags(html, regex, limit = 6) {
17
+ const stripped = String(html || "")
18
+ .replace(/<!--[\s\S]*?-->/g, " ")
19
+ .replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, " ")
20
+ .replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, " ")
21
+ .replace(/<template\b[^>]*>[\s\S]*?<\/template>/gi, " ");
17
22
  const results = [];
18
23
  let match;
19
- while ((match = regex.exec(html)) !== null && results.length < limit) {
24
+ while ((match = regex.exec(stripped)) !== null && results.length < limit) {
20
25
  const clean = match[1].replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
21
- if (clean && clean.length > 3 && !results.includes(clean)) {
26
+ if (
27
+ clean &&
28
+ clean.length > 3 &&
29
+ !clean.includes("${") &&
30
+ !clean.includes("{") &&
31
+ !results.includes(clean)
32
+ ) {
22
33
  results.push(clean);
23
34
  }
24
35
  }
25
36
  return results;
26
37
  }
27
38
 
39
+ function truncateWordBoundary(text, maxLen = 160) {
40
+ const clean = String(text || "").replace(/\s+/g, " ").trim();
41
+ if (clean.length <= maxLen) return clean;
42
+ const slice = clean.slice(0, maxLen - 3);
43
+ const lastSpace = slice.lastIndexOf(" ");
44
+ return (lastSpace > maxLen * 0.6 ? slice.slice(0, lastSpace) : slice).replace(/[.,;:\s-]+$/, "") + "...";
45
+ }
46
+
28
47
  function inferCategories(text) {
29
- const lower = (text || "").toLowerCase();
48
+ const lower = (text || "").toLowerCase().trim();
49
+ if (lower.length < 25) {
50
+ return ["Web Application"];
51
+ }
30
52
  const cats = [];
31
- if (/cli|git|sdk|api|developer|code|terminal|repo|deploy/.test(lower)) cats.push("Developer Tools");
32
- if (/ai|llm|agent|gpt|claude|model|prompt|automation/.test(lower)) cats.push("AI & Automation");
33
- if (/seo|marketing|directory|launch|growth|leads|traffic/.test(lower)) cats.push("Marketing & SEO");
34
- if (/stripe|pay|revenue|billing|invoice|saas|pricing/.test(lower)) cats.push("SaaS & Monetization");
35
- if (cats.length === 0) cats.push("SaaS", "Productivity", "Software");
53
+ if (/\b(cli|git|sdk|api|developer|code|terminal|repo|deploy)\b/.test(lower)) cats.push("Developer Tools");
54
+ if (/\b(ai|llm|agent|gpt|claude|model|prompt)\b/.test(lower)) cats.push("AI & Automation");
55
+ if (/\b(seo|marketing|directory|launch|growth|leads|traffic)\b/.test(lower)) cats.push("Marketing & SEO");
56
+ if (/\b(stripe|pay|revenue|billing|invoice|saas|pricing)\b/.test(lower)) cats.push("SaaS & Monetization");
57
+ if (cats.length === 0) cats.push("Web Application");
36
58
  return cats.slice(0, 3);
37
59
  }
38
60
 
@@ -213,10 +235,16 @@ export async function scanTargetUrl(rawInput, options = {}) {
213
235
 
214
236
  const titleParts = rawTitle.split(/\s*[|—–-]\s*/);
215
237
  const cleanName = (titleParts[0] || slug).trim();
238
+ const h1Candidate =
239
+ h1List[0] && h1List[0].length <= 75 && h1List[0].toLowerCase() !== cleanName.toLowerCase()
240
+ ? h1List[0]
241
+ : null;
216
242
  const extractedTagline =
217
- (titleParts[1] && titleParts[1].length >= 12 ? titleParts[1].trim() : null) ||
218
- (h1List[0] && h1List[0].length <= 75 ? h1List[0] : null) ||
219
- (description ? description.slice(0, 60).trim() : `${cleanName} (${domain})`);
243
+ (titleParts[1] && titleParts[1].length >= 12 && titleParts[1].toLowerCase() !== "web application"
244
+ ? titleParts[1].trim()
245
+ : null) ||
246
+ h1Candidate ||
247
+ (description ? description.slice(0, 60).trim() : domain);
220
248
 
221
249
  const launchKit = generateLaunchKit({
222
250
  origin: parsedUrl.origin,
@@ -253,6 +281,7 @@ export async function scanTargetUrl(rawInput, options = {}) {
253
281
  cleanName,
254
282
  reachable,
255
283
  botEmail,
284
+ ogImage: ogImage || "",
256
285
  readinessScore,
257
286
  checks,
258
287
  launchKit,
@@ -264,23 +293,22 @@ export async function scanTargetUrl(rawInput, options = {}) {
264
293
  }
265
294
 
266
295
  function generateLaunchKit({ origin, domain, slug, cleanName, tagline, description, h1List, h2List, botEmail }) {
267
- const cleanTagline = tagline.length > 64 ? tagline.slice(0, 61).trim() + "..." : tagline;
296
+ const cleanTagline = tagline.length > 64 ? truncateWordBoundary(tagline, 64) : tagline;
268
297
  const shortPitch =
269
- description && description.length >= 40
270
- ? description.slice(0, 158)
271
- : `${cleanName} (${domain}) helps teams and solo builders automate their workflow, eliminate manual bottlenecks, and ship faster.`;
298
+ description && description.length >= 25
299
+ ? truncateWordBoundary(description, 160)
300
+ : `${cleanName} (${domain}) — web application and online workspace available at ${origin}.`;
272
301
 
273
302
  const featureBullets =
274
303
  h2List.length >= 2
275
304
  ? h2List.slice(0, 4).map((h) => `- **${h}**`).join("\n")
276
305
  : [
277
- `- **Instant Setup**: Point ${cleanName} at your live workflow and start in under 60 seconds.`,
278
- `- **Automated Execution**: Replaces repetitive manual checklists with deterministic automation.`,
279
- `- **Built for Builders**: Clean CLI and web workspace designed for high-velocity shipping.`
306
+ `- **Web Workspace**: Access ${cleanName} directly at ${origin}.`,
307
+ `- **Focused Workflow**: Built for teams and individuals using ${domain}.`
280
308
  ].join("\n");
281
309
 
282
310
  const utmUrl = `${origin}/?utm_source=postbuild&utm_medium=directory&utm_campaign=${slug}_launch`;
283
- const categories = inferCategories(`${cleanName} ${cleanTagline} ${shortPitch} ${h1List.join(" ")} ${h2List.join(" ")}`);
311
+ const categories = inferCategories(`${description || ""} ${h1List.join(" ")} ${h2List.join(" ")}`);
284
312
 
285
313
  const longDescription = `${cleanName} — ${cleanTagline}
286
314
 
@@ -305,33 +333,34 @@ Try it live: ${utmUrl}`;
305
333
  };
306
334
  }
307
335
 
308
- function generatePseoPages(origin, slug, cleanName) {
336
+ export function generatePseoPages(origin, slug, cleanName) {
337
+ const isPostbuildSelf = slug === "postbuild";
309
338
  const templates = [
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" }
339
+ { path: `/use-cases/${slug}-for-solo-founders`, keyword: `${cleanName} for Solo Founders — Setup & Workflow Guide`, category: "Use Case", schema: "FAQPage + SoftwareApplication", intent: "Transactional" },
340
+ { path: `/use-cases/${slug}-automation-guide`, keyword: `How to Streamline Daily Workflows with ${cleanName}`, category: "Use Case", schema: "HowTo + FAQPage", intent: "High Intent" },
341
+ { path: `/compare/${slug}-vs-manual-workflow`, keyword: `${cleanName} vs. Manual Workflows — Operational Comparison`, category: "Comparison", schema: "Article + FAQPage", intent: "Comparison" },
342
+ { path: `/templates/${slug}-starter-kit`, keyword: `${cleanName} Starter Checklist & Initial Configuration`, category: "Template", schema: "SoftwareSourceCode", intent: "Lead Magnet" },
343
+ { path: isPostbuildSelf ? `/alternatives/best-${slug}-alternatives-2026` : `/guides/${slug}-evaluation-criteria`, keyword: `How to Evaluate ${cleanName} & Workflow Approaches (2026)`, category: "Comparison", schema: "ItemList + FAQPage", intent: "Comparison" },
344
+ { path: isPostbuildSelf ? `/calculators/${slug}-roi-calculator` : `/calculators/${slug}-workflow-time-estimator`, keyword: `${cleanName} Workflow Time & Adoption Framework`, category: "Calculator", schema: "WebApplication", intent: "Interactive" },
345
+ { path: `/guides/${slug}-api-integration`, keyword: `Integrating ${cleanName} Into Your Team Stack`, category: "Guide", schema: "TechArticle", intent: "Developer" },
346
+ { path: `/use-cases/${slug}-for-agencies`, keyword: `${cleanName} for Agencies & Multi-Project Teams`, category: "Use Case", schema: "FAQPage + SoftwareApplication", intent: "B2B" },
347
+ { path: `/checklists/${slug}-launch-checklist`, keyword: `Complete ${cleanName} Rollout & Launch Checklist`, category: "Checklist", schema: "HowTo + ItemList", intent: "Lead Magnet" },
348
+ { path: isPostbuildSelf ? `/compare/${slug}-pricing-breakdown` : `/compare/${slug}-plan-evaluation`, keyword: `${cleanName} Plan Evaluation & Buyer Checklist`, category: "Comparison", schema: "Product + Offer", intent: "Bottom Funnel" },
349
+ { path: `/use-cases/${slug}-for-developers`, keyword: `${cleanName} for Developers & Technical Teams`, category: "Use Case", schema: "TechArticle", intent: "Transactional" },
350
+ { path: `/use-cases/${slug}-for-indie-hackers`, keyword: `${cleanName} for Indie SaaS Founders & Bootstrappers`, category: "Use Case", schema: "FAQPage + SoftwareApplication", intent: "Transactional" },
351
+ { path: `/compare/${slug}-vs-legacy-tools`, keyword: `Transitioning to ${cleanName} from Legacy Processes`, category: "Comparison", schema: "Article + FAQPage", intent: "Comparison" },
352
+ { path: `/guides/${slug}-programmatic-seo-setup`, keyword: `Structured Technical SEO Setup with ${cleanName}`, category: "Guide", schema: "TechArticle", intent: "High Intent" },
353
+ { path: isPostbuildSelf ? `/templates/${slug}-nextjs-boilerplate` : `/templates/${slug}-deployment-guide`, keyword: `${cleanName} Modern Web Workflow & Deployment Guide`, category: "Template", schema: "TechArticle", intent: "Developer" },
354
+ { path: `/use-cases/${slug}-for-saas-teams`, keyword: `${cleanName} for Product & Engineering Teams`, category: "Use Case", schema: "FAQPage + SoftwareApplication", intent: "B2B" },
355
+ { path: isPostbuildSelf ? `/guides/${slug}-stripe-checkout-guide` : `/guides/${slug}-onboarding-workflow`, keyword: `Self-Serve Onboarding Workflow with ${cleanName}`, category: "Guide", schema: "HowTo + TechArticle", intent: "Transactional" },
356
+ { path: `/checklists/${slug}-seo-audit-checklist`, keyword: `Technical Readiness & Audit Checklist for ${cleanName}`, category: "Checklist", schema: "ItemList + FAQPage", intent: "Lead Magnet" },
357
+ { path: `/calculators/${slug}-conversion-estimator`, keyword: `${cleanName} Funnel & Adoption Review Framework`, category: "Calculator", schema: "WebApplication", intent: "Interactive" },
358
+ { path: isPostbuildSelf ? `/alternatives/${slug}-open-source-comparison` : `/guides/${slug}-managed-vs-diy`, keyword: `${cleanName} Managed Workflow vs. DIY Maintenance`, category: "Comparison", schema: "TechArticle", intent: "Comparison" },
359
+ { path: `/use-cases/${slug}-for-product-hunt-launch`, keyword: `Preparing a Product Launch with ${cleanName}`, category: "Use Case", schema: "HowTo + FAQPage", intent: "High Intent" },
360
+ { path: `/guides/${slug}-ai-search-indexing`, keyword: `Configuring AI Search Indexing (ChatGPT & Perplexity) for ${cleanName}`, category: "Guide", schema: "TechArticle", intent: "High Intent" },
361
+ { path: `/templates/${slug}-directory-submission-kit`, keyword: `${cleanName} Distribution & Directory Launch Kit`, category: "Template", schema: "ItemList", intent: "Lead Magnet" },
362
+ { path: `/use-cases/${slug}-for-ai-wrappers`, keyword: `Shipping & Scaling AI Applications with ${cleanName}`, category: "Use Case", schema: "FAQPage + SoftwareApplication", intent: "Transactional" },
363
+ { path: isPostbuildSelf ? `/compare/${slug}-roi-case-study` : `/compare/${slug}-architecture-walkthrough`, keyword: `${cleanName} Architecture & Implementation Walkthrough`, category: "Walkthrough", schema: "Article", intent: "Bottom Funnel" }
335
364
  ];
336
365
 
337
366
  return templates.map((t, idx) => ({
@@ -339,6 +368,8 @@ function generatePseoPages(origin, slug, cleanName) {
339
368
  url: `${origin}${t.path}`,
340
369
  path: t.path,
341
370
  keyword: t.keyword,
371
+ category: t.category,
372
+ schemaType: t.schema,
342
373
  volume: t.category,
343
374
  kd: t.schema,
344
375
  intent: t.intent,
@@ -348,6 +379,7 @@ function generatePseoPages(origin, slug, cleanName) {
348
379
 
349
380
  function generateArtifacts(origin, slug, cleanName, description, pseoPages) {
350
381
  const today = new Date().toISOString().split("T")[0];
382
+ const isPostbuildSelf = origin.includes("postbuild.dev");
351
383
 
352
384
  const routeEntries = pseoPages
353
385
  .map(
@@ -360,7 +392,17 @@ function generateArtifacts(origin, slug, cleanName, description, pseoPages) {
360
392
  )
361
393
  .join("\n");
362
394
 
363
- const sitemapXml = `<?xml version="1.0" encoding="UTF-8"?>
395
+ const rootSitemapXml = `<?xml version="1.0" encoding="UTF-8"?>
396
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
397
+ <url>
398
+ <loc>${origin}/</loc>
399
+ <lastmod>${today}</lastmod>
400
+ <changefreq>daily</changefreq>
401
+ <priority>1.0</priority>
402
+ </url>
403
+ </urlset>`;
404
+
405
+ const sitemapAllRoutesXml = `<?xml version="1.0" encoding="UTF-8"?>
364
406
  <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
365
407
  <url>
366
408
  <loc>${origin}/</loc>
@@ -371,6 +413,8 @@ function generateArtifacts(origin, slug, cleanName, description, pseoPages) {
371
413
  ${routeEntries}
372
414
  </urlset>`;
373
415
 
416
+ const sitemapXml = isPostbuildSelf ? sitemapAllRoutesXml : rootSitemapXml;
417
+
374
418
  const robotsTxt = `User-agent: *
375
419
  Allow: /
376
420
 
@@ -384,7 +428,6 @@ Allow: /
384
428
 
385
429
  Sitemap: ${origin}/sitemap.xml`;
386
430
 
387
- const isPostbuildSelf = origin.includes("postbuild.dev");
388
431
  const offersBlock = isPostbuildSelf
389
432
  ? `,\n "offers": {\n "@type": "Offer",\n "price": "29.00",\n "priceCurrency": "USD"\n }`
390
433
  : "";
@@ -453,7 +496,7 @@ export function CheckoutGate({ priceId = "price_YOUR_STRIPE_PRICE_ID", label = "
453
496
  // res.status(200).json({ ok: true, checkoutUrl: session.url });
454
497
  // }`;
455
498
 
456
- return { sitemapXml, robotsTxt, jsonLd, paywallCode };
499
+ return { sitemapXml, rootSitemapXml, sitemapAllRoutesXml, robotsTxt, jsonLd, paywallCode };
457
500
  }
458
501
 
459
502
  export const INDEXNOW_KEY = "pb7f9a2e4c8b1d6f3a5e0c9b2d4f6a8e";
@@ -533,6 +576,7 @@ export async function dispatchDirectories({ targetUrl = "https://www.postbuild.d
533
576
  category: dir.category,
534
577
  dr: dir.dr,
535
578
  authType: dir.authType,
579
+ prefillMode: dir.prefillMode || "clipboard_form",
536
580
  autoSubmit: Boolean(dir.autoSubmit),
537
581
  submissionUrl: dir.url,
538
582
  prefilledUrl: dir.prefilledUrl,
package/lib/fixer.js CHANGED
@@ -25,113 +25,155 @@ export function detectPublicDir(cwd = process.cwd()) {
25
25
  return { framework, publicDir };
26
26
  }
27
27
 
28
+ export function buildOgCardSvg(brandName = "Web Application", domain = "example.com", tagline = "") {
29
+ const esc = (s) =>
30
+ String(s || "")
31
+ .replace(/&/g, "&amp;")
32
+ .replace(/</g, "&lt;")
33
+ .replace(/>/g, "&gt;")
34
+ .replace(/"/g, "&quot;");
35
+ const sub = tagline && tagline.toLowerCase() !== brandName.toLowerCase() ? tagline.slice(0, 72) : domain;
36
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630">
37
+ <rect width="1200" height="630" fill="#F8FAFC" />
38
+ <rect x="48" y="48" width="1104" height="534" rx="28" fill="#FFFFFF" stroke="#E2E8F0" stroke-width="2" />
39
+ <text x="108" y="270" fill="#0F172A" font-family="-apple-system, BlinkMacSystemFont, Segoe UI, Inter, sans-serif" font-size="64" font-weight="800">${esc(brandName.slice(0, 32))}</text>
40
+ <text x="108" y="346" fill="#475569" font-family="-apple-system, BlinkMacSystemFont, Segoe UI, Inter, sans-serif" font-size="30" font-weight="500">${esc(sub)}</text>
41
+ <text x="108" y="488" fill="#047857" font-family="ui-monospace, SFMono-Regular, Menlo, monospace" font-size="24" font-weight="700">${esc(domain)}</text>
42
+ </svg>
43
+ `;
44
+ }
45
+
28
46
  export function buildLlmsTxt(report) {
29
47
  const kit = report.launchKit || {};
48
+ const brand = kit.brandName || kit.productName || report.domain;
49
+ const tagline =
50
+ kit.tagline && kit.tagline.toLowerCase() !== brand.toLowerCase()
51
+ ? kit.tagline
52
+ : report.meta?.description || `${brand} (${report.domain})`;
30
53
  const lines = [
31
- `# ${kit.brandName || kit.productName || report.domain}`,
54
+ `# ${brand}`,
32
55
  ``,
33
- `> ${kit.tagline || report.meta?.description || "Software platform and developer workflow tool."}`,
56
+ `> ${tagline}`,
34
57
  ``,
35
58
  `- Canonical URL: ${report.targetUrl}`,
36
- `- Verification & Press Contact: ${report.botEmail}`,
37
- `- Category: ${kit.categories ? kit.categories.join(", ") : "Developer Tools, SaaS"}`,
59
+ `- Contact: ${report.botEmail}`,
60
+ `- Category: ${kit.categories && kit.categories.length ? kit.categories.join(", ") : "Web Application"}`,
38
61
  ``,
39
62
  `## Overview`,
40
- `${kit.shortPitch || kit.longDescription || ""}`,
63
+ `${kit.shortPitch || kit.longDescription || `${brand} at ${report.targetUrl}`}`,
41
64
  ``,
42
- `## Key Comparison & Programmatic SEO Guides`,
43
- ...(report.pseoPages || []).slice(0, 15).map(
44
- (p) => `- [${p.keyword || p.path}](${report.targetUrl.replace(/\/$/, "")}${p.path || p.slug}): ${p.intent} guide (${p.volume || p.estMonthlySearches})`
65
+ `## Guides & Resources (Add published routes here)`,
66
+ ...(report.pseoPages || []).slice(0, 8).map(
67
+ (p) => `- [${p.keyword || p.path}](${report.targetUrl.replace(/\/$/, "")}${p.path || p.slug}): ${p.category || p.volume || "Guide"}`
45
68
  ),
46
69
  ``
47
70
  ];
48
71
  return lines.join("\n");
49
72
  }
50
73
 
51
- export function buildHeadSeoSnippet(report) {
52
- const title = report.launchKit?.tagline
53
- ? `${report.launchKit.brandName || report.launchKit.productName || report.domain} — ${report.launchKit.tagline}`
54
- : report.meta?.title || report.domain;
55
- const desc = report.launchKit?.shortPitch || report.meta?.description || "";
74
+ export function buildHeadSeoSnippet(report, verificationToken = "") {
75
+ const brand = report.launchKit?.brandName || report.launchKit?.productName || report.domain;
76
+ const tagline = report.launchKit?.tagline || "";
77
+ const title =
78
+ tagline && tagline.toLowerCase() !== brand.toLowerCase() && !tagline.includes(report.domain)
79
+ ? `${brand} — ${tagline}`
80
+ : report.meta?.title || `${brand} (${report.domain})`;
81
+ const desc = report.launchKit?.shortPitch || report.meta?.description || `${brand} (${report.domain})`;
56
82
  const cleanOrigin = String(report.targetUrl || `https://${report.domain}`).replace(/\/$/, "");
57
- return `<!-- Generated by postbuild (git paid fix) for ${report.domain} -->
83
+ const ogImg = report.ogImage || `${cleanOrigin}/og-card.svg`;
84
+ const verifyTag =
85
+ verificationToken || report.verificationToken
86
+ ? `\n<!-- Domain Ownership Verification -->\n<meta name="postbuild-verification" content="${verificationToken || report.verificationToken}" />\n`
87
+ : "";
88
+ return `<!-- SEO & OpenGraph <head> tags for ${report.domain} -->
58
89
  <title>${title}</title>
59
90
  <meta name="description" content="${desc.replace(/"/g, "&quot;")}" />
60
- <link rel="canonical" href="${report.targetUrl}" />
61
-
91
+ <link rel="canonical" href="${report.targetUrl}" />${verifyTag}
62
92
  <!-- OpenGraph / Social Cards -->
63
93
  <meta property="og:type" content="website" />
64
- <meta property="og:site_name" content="${report.launchKit?.brandName || report.launchKit?.productName || report.domain}" />
94
+ <meta property="og:site_name" content="${brand.replace(/"/g, "&quot;")}" />
65
95
  <meta property="og:title" content="${title.replace(/"/g, "&quot;")}" />
66
96
  <meta property="og:description" content="${desc.replace(/"/g, "&quot;")}" />
67
97
  <meta property="og:url" content="${report.targetUrl}" />
68
- <meta property="og:image" content="${cleanOrigin}/og-card.png" />
98
+ <meta property="og:image" content="${ogImg.replace(/"/g, "&quot;")}" />
69
99
  <meta name="twitter:card" content="summary_large_image" />
70
100
  <meta name="twitter:title" content="${title.replace(/"/g, "&quot;")}" />
71
101
  <meta name="twitter:description" content="${desc.replace(/"/g, "&quot;")}" />
72
- <meta name="twitter:image" content="${cleanOrigin}/og-card.png" />
102
+ <meta name="twitter:image" content="${ogImg.replace(/"/g, "&quot;")}" />
73
103
 
74
104
  <!-- Structured Data (JSON-LD) -->
75
105
  ${report.artifacts?.jsonLd || ""}
76
106
  `;
77
107
  }
78
108
 
79
- export function buildStartHereMd(report) {
109
+ export function buildStartHereMd(report, verificationToken = "") {
80
110
  const domain = report.domain || "your-project.com";
81
111
  const brand = report.launchKit?.brandName || report.launchKit?.productName || domain;
82
112
  const botEmail = report.botEmail || `${domain.replace(/\./g, "-")}-bot@postbuild.dev`;
113
+ const token = verificationToken || report.verificationToken || "";
83
114
  return `# START HERE — 3-Step Launch Guide for ${brand} (${domain})
84
115
 
85
- 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:
116
+ 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 steps in order:
86
117
 
87
118
  ---
88
119
 
89
- ## Step 1: Drop the SEO & AI Crawler Files Into Your App (60 Seconds)
120
+ ## Step 1: Drop the SEO, AI Crawler & Verification Files Into Your App
90
121
 
91
122
  Upload or drag this folder into your AI builder (Lovable, Bolt, Replit, Cursor, Claude Code) and paste this exact prompt:
92
123
 
93
124
  > **Copy & Paste Prompt for Your AI Builder:**
94
125
  > \`\`\`text
95
- > Please add the Postbuild SEO & AI Launch files to my project:
96
- > 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.
97
- > 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).
98
- > 3. Place the 25 pre-rendered static HTML guides (under guides/, comparisons/, use-cases/, checklists/) into public/ so those routes resolve cleanly.
126
+ > Please add the SEO & Launch files to my project:
127
+ > 1. Place public/robots.txt, public/sitemap.xml, public/llms.txt, public/og-card.svg, and public/.well-known/postbuild-verify.txt into my static public/ folder so they are served at /robots.txt, /sitemap.xml, /llms.txt, /og-card.svg, and /.well-known/postbuild-verify.txt.
128
+ > 2. Copy the meta tags (including <meta name="postbuild-verification" ...>), canonical link, OpenGraph tags, and JSON-LD structured data from head-seo-snippet.html directly into the static HTML <head> of my root index.html (do not inject via client-only JavaScript so crawlers and domain verification can read them on initial HTTP fetch).
129
+ > 3. Review the starter HTML guides in .postbuild/pseo/ (or starter-pages/). Copy only the 5–8 routes that match our product into public/, customize their copy with our real features/pricing, and add only those published routes from sitemap-starter-routes.xml into public/sitemap.xml and public/llms.txt (remove any unpublished route links so nothing 404s).
99
130
  > \`\`\`
100
131
 
132
+ > **Editorial Note on Starter HTML Pages:** The static HTML pages in \`.postbuild/pseo/\` are **starter drafts** synthesized from your homepage scan. \`public/sitemap.xml\` lists only your root URL (\`https://${domain}/\`) by default so you never publish broken 404 URLs. Copy and customize only the pages you want to publish, then add those URLs from \`sitemap-starter-routes.xml\` into \`public/sitemap.xml\`.
133
+
101
134
  ---
102
135
 
103
- ## Step 2: Submit to Curated Startup & AI Directories (No Personal Inbox Spam)
136
+ ## Step 2: Verify Domain Ownership on postbuild.dev (Unlocks Your \`@postbuild.dev\` Inbox)
104
137
 
105
- 1. Open \`directories-85.csv\` (or the **85 Directories** tab on \`https://www.postbuild.dev\`).
106
- 2. Click any **Prefilled Launch URL** — your product name, URL, UTM tags, and description are already pre-populated in the link parameters.
107
- 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.
108
- 4. **Always use your dedicated verification email** when a directory asks for an email address:
109
- \`${botEmail}\`
138
+ Before submitting to directories with \`${botEmail}\`, confirm your inbox is unlocked:
139
+
140
+ 1. Deploy the Step 1 changes to \`https://${domain}\` (so \`https://${domain}/.well-known/postbuild-verify.txt\` or your \`<meta name="postbuild-verification">\` tag is live).
141
+ 2. Open **\`https://www.postbuild.dev\` → Account tab** and click **Verify Domain Now**${token ? ` (your verification token is \`${token}\`)` : ""}.
142
+ 3. Keep the verification file or meta tag in place so your \`${botEmail}\` inbox stays unlocked for all future visits.
110
143
 
111
144
  ---
112
145
 
113
- ## Step 3: Verify Directory Emails in 1 Click
146
+ ## Step 3: Submit to 85 Curated Startup & AI Directories (No Personal Inbox Spam)
114
147
 
115
- 1. Every confirmation email or 6-digit OTP code sent to \`${botEmail}\` arrives in your **Account & Verification Inbox** on \`https://www.postbuild.dev\`.
116
- 2. Click **Auto-Verify Links** (or copy the 1-click OTP badge) to confirm your listings without cluttering your personal inbox.
148
+ 1. Open \`directories-85.csv\` (or the **Dispatch (85)** tab on \`https://www.postbuild.dev\`).
149
+ 2. Click any **Prefilled Launch URL** — your product name, URL, UTM tags, and description are already pre-populated in the link parameters.
150
+ 3. For directories with custom form fields, drag the **1-Click Auto-Fill Bookmarklet** on the Postbuild **Dispatch (85)** tab to your browser bookmarks bar and click it on any directory page to fill the form automatically.
151
+ 4. **Use your verified inbox address** when a directory asks for an email address:
152
+ \`${botEmail}\`
153
+ 5. Every confirmation email or 6-digit OTP code sent to \`${botEmail}\` arrives in your **Account & Verification Inbox** on \`https://www.postbuild.dev\`.
117
154
 
118
155
  ---
119
156
 
120
157
  ### Files Included in This Bundle
121
158
  - \`START_HERE.md\` — This 3-step quickstart guide + copy-paste AI builder prompt
122
- - \`public/robots.txt\` — Search & AI crawler rules (GPTBot, ClaudeBot, PerplexityBot)
123
- - \`public/sitemap.xml\` — XML Sitemap indexing your main URL + 25 programmatic SEO guides
159
+ - \`public/.well-known/postbuild-verify.txt\` — Domain ownership verification file for \`${botEmail}\`
160
+ - \`public/robots.txt\` — Search & AI crawler rules (OAI-SearchBot, ClaudeBot, PerplexityBot)
161
+ - \`public/sitemap.xml\` — Clean XML Sitemap indexing your root URL (\`https://${domain}/\`)
162
+ - \`sitemap-starter-routes.xml\` — Full XML Sitemap entries for the optional starter HTML routes
124
163
  - \`public/llms.txt\` — Structured markdown manifest for ChatGPT Search, Claude, and Perplexity
125
- - \`head-seo-snippet.html\` — Title, meta description, OpenGraph social tags, and Schema.org JSON-LD
164
+ - \`public/og-card.svg\` — Social preview card fallback image
165
+ - \`head-seo-snippet.html\` — Title, meta description, domain verification tag, OpenGraph tags, and Schema.org JSON-LD
126
166
  - \`launch-kit.md\` — Ready-to-post copy for Show HN, Product Hunt, and startup directories
127
167
  - \`directories-85.csv\` — All 85 curated directories ranked by Domain Rating (DR) with prefilled URLs
128
- - \`public/{use-cases,comparisons,guides,checklists}/*.html\` — 25 static HTML SEO landing pages
168
+ - \`.postbuild/pseo/{use-cases,compare,guides,checklists,templates,calculators}/**/index.html\` — Starter static HTML SEO drafts to review before publishing
129
169
  `;
130
170
  }
131
171
 
132
172
  export function applyLocalSeoFixes(report, options = {}) {
133
173
  const cwd = options.cwd || process.cwd();
134
174
  const overwritePublic = Boolean(options.overwritePublic);
175
+ const publishPseoToPublic = Boolean(options.publishPseoToPublic);
176
+ const verificationToken = options.verificationToken || report.verificationToken || "";
135
177
  const { framework, publicDir } = detectPublicDir(cwd);
136
178
  fs.mkdirSync(publicDir, { recursive: true });
137
179
 
@@ -142,9 +184,36 @@ export function applyLocalSeoFixes(report, options = {}) {
142
184
 
143
185
  // 0. START_HERE.md in .postbuild/
144
186
  const startHerePath = path.join(outDir, "START_HERE.md");
145
- fs.writeFileSync(startHerePath, buildStartHereMd(report), "utf8");
187
+ fs.writeFileSync(startHerePath, buildStartHereMd(report, verificationToken), "utf8");
146
188
  writtenFiles.push({ path: path.relative(cwd, startHerePath), role: "3-Step Non-Technical Quickstart & AI Builder Prompt (START_HERE.md)" });
147
189
 
190
+ // 0b. .well-known/postbuild-verify.txt in public/ and .postbuild/ when verificationToken is available
191
+ if (verificationToken) {
192
+ const publicWellKnownDir = path.join(publicDir, ".well-known");
193
+ fs.mkdirSync(publicWellKnownDir, { recursive: true });
194
+ const publicVerifyPath = path.join(publicWellKnownDir, "postbuild-verify.txt");
195
+ fs.writeFileSync(publicVerifyPath, `${verificationToken}\n`, "utf8");
196
+ writtenFiles.push({ path: path.relative(cwd, publicVerifyPath), role: "Domain Ownership Verification File (.well-known/postbuild-verify.txt)" });
197
+
198
+ const outWellKnownDir = path.join(outDir, ".well-known");
199
+ fs.mkdirSync(outWellKnownDir, { recursive: true });
200
+ fs.writeFileSync(path.join(outWellKnownDir, "postbuild-verify.txt"), `${verificationToken}\n`, "utf8");
201
+ }
202
+
203
+ // 0c. Default og-card.svg in public/ (if no og-card.svg or og-card.png exists yet)
204
+ const ogSvgPath = path.join(publicDir, "og-card.svg");
205
+ const ogPngPath = path.join(publicDir, "og-card.png");
206
+ const ogSvgContent = buildOgCardSvg(
207
+ report.cleanName || report.launchKit?.productName || report.domain,
208
+ report.domain,
209
+ report.launchKit?.tagline || ""
210
+ );
211
+ if (!fs.existsSync(ogSvgPath) && !fs.existsSync(ogPngPath)) {
212
+ fs.writeFileSync(ogSvgPath, ogSvgContent, "utf8");
213
+ writtenFiles.push({ path: path.relative(cwd, ogSvgPath), role: "OpenGraph Social Preview Card (og-card.svg)" });
214
+ }
215
+ fs.writeFileSync(path.join(outDir, "og-card.svg"), ogSvgContent, "utf8");
216
+
148
217
  // 1. robots.txt in public/ (if missing or overwritePublic)
149
218
  const robotsPath = path.join(publicDir, "robots.txt");
150
219
  if (!fs.existsSync(robotsPath) || overwritePublic) {
@@ -152,11 +221,14 @@ export function applyLocalSeoFixes(report, options = {}) {
152
221
  writtenFiles.push({ path: path.relative(cwd, robotsPath), role: "AI + Search Crawler Rules (robots.txt)" });
153
222
  }
154
223
 
155
- // 2. sitemap.xml in public/ (if missing or overwritePublic)
224
+ // 2. sitemap.xml in public/ (root URL only by default unless publishPseoToPublic or postbuild.dev)
156
225
  const sitemapPath = path.join(publicDir, "sitemap.xml");
226
+ const rootSitemapXml = report.artifacts.rootSitemapXml || report.artifacts.sitemapXml;
227
+ const fullSitemapXml = report.artifacts.sitemapAllRoutesXml || report.artifacts.sitemapXml;
157
228
  if (!fs.existsSync(sitemapPath) || overwritePublic) {
158
- fs.writeFileSync(sitemapPath, report.artifacts.sitemapXml, "utf8");
159
- writtenFiles.push({ path: path.relative(cwd, sitemapPath), role: "XML Sitemap with 25 pSEO Routes" });
229
+ const publicSitemapContent = publishPseoToPublic || report.domain === "postbuild.dev" ? fullSitemapXml : rootSitemapXml;
230
+ fs.writeFileSync(sitemapPath, publicSitemapContent, "utf8");
231
+ writtenFiles.push({ path: path.relative(cwd, sitemapPath), role: "XML Sitemap (sitemap.xml)" });
160
232
  }
161
233
 
162
234
  // 3. llms.txt in public/ for AI Answer Engines (ChatGPT, Claude, Perplexity)
@@ -169,11 +241,11 @@ export function applyLocalSeoFixes(report, options = {}) {
169
241
 
170
242
  // 4. Complete .postbuild bundle
171
243
  const headSnippetPath = path.join(outDir, "head-seo-snippet.html");
172
- fs.writeFileSync(headSnippetPath, buildHeadSeoSnippet(report), "utf8");
244
+ fs.writeFileSync(headSnippetPath, buildHeadSeoSnippet(report, verificationToken), "utf8");
173
245
  writtenFiles.push({ path: path.relative(cwd, headSnippetPath), role: "OpenGraph + Canonical + JSON-LD <head> Snippet" });
174
246
 
175
- const sitemapOut = path.join(outDir, "sitemap.xml");
176
- fs.writeFileSync(sitemapOut, report.artifacts.sitemapXml, "utf8");
247
+ const sitemapOut = path.join(outDir, "sitemap-starter-routes.xml");
248
+ fs.writeFileSync(sitemapOut, fullSitemapXml, "utf8");
177
249
 
178
250
  const robotsOut = path.join(outDir, "robots.txt");
179
251
  fs.writeFileSync(robotsOut, report.artifacts.robotsTxt, "utf8");
@@ -187,27 +259,31 @@ export function applyLocalSeoFixes(report, options = {}) {
187
259
 
188
260
  const pseoJsonOut = path.join(outDir, "pseo-routes.json");
189
261
  fs.writeFileSync(pseoJsonOut, JSON.stringify(report.pseoPages, null, 2), "utf8");
190
- writtenFiles.push({ path: path.relative(cwd, pseoJsonOut), role: "25 Programmatic SEO Route Definitions (JSON)" });
262
+ writtenFiles.push({ path: path.relative(cwd, pseoJsonOut), role: "Programmatic SEO Route Definitions (JSON)" });
191
263
 
192
- // Render the 25 static HTML landing pages into .postbuild/pseo (and public/ if overwritePublic)
264
+ // Render the starter HTML landing pages into .postbuild/pseo (and public/ only if explicitly requested via publishPseoToPublic)
193
265
  const pseoOutDir = path.join(outDir, "pseo");
194
266
  const pseoRenderOpts = {
195
267
  origin: report.targetUrl,
196
268
  cleanName: report.cleanName || report.launchKit?.productName || report.domain,
197
- domain: report.domain
269
+ domain: report.domain,
270
+ tagline: report.launchKit?.tagline || "",
271
+ shortPitch: report.launchKit?.shortPitch || "",
272
+ categories: report.launchKit?.categories || [],
273
+ ogImage: report.ogImage || ""
198
274
  };
199
275
  const renderedPseo = writePseoHtmlFiles(report.pseoPages || [], pseoOutDir, pseoRenderOpts);
200
276
  writtenFiles.push({
201
277
  path: path.relative(cwd, pseoOutDir),
202
- role: `${renderedPseo.length} Static Programmatic SEO HTML Landing Pages`
278
+ role: `${renderedPseo.length} Starter Static SEO HTML Drafts (review & copy selected routes to public/)`
203
279
  });
204
280
 
205
- if (overwritePublic) {
281
+ if (publishPseoToPublic) {
206
282
  writePseoHtmlFiles(report.pseoPages || [], publicDir, pseoRenderOpts);
207
283
  }
208
284
 
209
285
  const launchKitOut = path.join(outDir, "launch-kit.md");
210
- fs.writeFileSync(launchKitOut, report.launchKit.longDescription, "utf8");
286
+ fs.writeFileSync(launchKitOut, report.launchKit?.longDescription || report.launchKit?.shortPitch || "", "utf8");
211
287
  writtenFiles.push({ path: path.relative(cwd, launchKitOut), role: "Synthesized Launch Copy (HN, Product Hunt, Directories)" });
212
288
 
213
289
  // 5. CSV export of all 85 directories with prefilled submission links