git-paid 2.1.5 → 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,6 +428,10 @@ Allow: /
384
428
 
385
429
  Sitemap: ${origin}/sitemap.xml`;
386
430
 
431
+ const offersBlock = isPostbuildSelf
432
+ ? `,\n "offers": {\n "@type": "Offer",\n "price": "29.00",\n "priceCurrency": "USD"\n }`
433
+ : "";
434
+
387
435
  const jsonLd = `<script type="application/ld+json">
388
436
  {
389
437
  "@context": "https://schema.org",
@@ -392,12 +440,7 @@ Sitemap: ${origin}/sitemap.xml`;
392
440
  "url": "${origin}",
393
441
  "applicationCategory": "BusinessApplication",
394
442
  "operatingSystem": "Web",
395
- "description": "${description.replace(/"/g, '\\"')}",
396
- "offers": {
397
- "@type": "Offer",
398
- "price": "29.00",
399
- "priceCurrency": "USD"
400
- }
443
+ "description": "${description.replace(/"/g, '\\"')}"${offersBlock}
401
444
  }
402
445
  </script>`;
403
446
 
@@ -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,110 +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 || "";
56
- return `<!-- Generated by postbuild (git paid fix) for ${report.domain} -->
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})`;
82
+ const cleanOrigin = String(report.targetUrl || `https://${report.domain}`).replace(/\/$/, "");
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} -->
57
89
  <title>${title}</title>
58
90
  <meta name="description" content="${desc.replace(/"/g, "&quot;")}" />
59
- <link rel="canonical" href="${report.targetUrl}" />
60
-
91
+ <link rel="canonical" href="${report.targetUrl}" />${verifyTag}
61
92
  <!-- OpenGraph / Social Cards -->
62
93
  <meta property="og:type" content="website" />
63
- <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;")}" />
64
95
  <meta property="og:title" content="${title.replace(/"/g, "&quot;")}" />
65
96
  <meta property="og:description" content="${desc.replace(/"/g, "&quot;")}" />
66
97
  <meta property="og:url" content="${report.targetUrl}" />
98
+ <meta property="og:image" content="${ogImg.replace(/"/g, "&quot;")}" />
67
99
  <meta name="twitter:card" content="summary_large_image" />
68
100
  <meta name="twitter:title" content="${title.replace(/"/g, "&quot;")}" />
69
101
  <meta name="twitter:description" content="${desc.replace(/"/g, "&quot;")}" />
102
+ <meta name="twitter:image" content="${ogImg.replace(/"/g, "&quot;")}" />
70
103
 
71
104
  <!-- Structured Data (JSON-LD) -->
72
105
  ${report.artifacts?.jsonLd || ""}
73
106
  `;
74
107
  }
75
108
 
76
- export function buildStartHereMd(report) {
109
+ export function buildStartHereMd(report, verificationToken = "") {
77
110
  const domain = report.domain || "your-project.com";
78
111
  const brand = report.launchKit?.brandName || report.launchKit?.productName || domain;
79
112
  const botEmail = report.botEmail || `${domain.replace(/\./g, "-")}-bot@postbuild.dev`;
113
+ const token = verificationToken || report.verificationToken || "";
80
114
  return `# START HERE — 3-Step Launch Guide for ${brand} (${domain})
81
115
 
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:
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:
83
117
 
84
118
  ---
85
119
 
86
- ## 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
87
121
 
88
122
  Upload or drag this folder into your AI builder (Lovable, Bolt, Replit, Cursor, Claude Code) and paste this exact prompt:
89
123
 
90
124
  > **Copy & Paste Prompt for Your AI Builder:**
91
125
  > \`\`\`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.
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).
96
130
  > \`\`\`
97
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
+
98
134
  ---
99
135
 
100
- ## 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)
101
137
 
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}\`
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.
107
143
 
108
144
  ---
109
145
 
110
- ## Step 3: Verify Directory Emails in 1 Click
146
+ ## Step 3: Submit to 85 Curated Startup & AI Directories (No Personal Inbox Spam)
111
147
 
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.
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\`.
114
154
 
115
155
  ---
116
156
 
117
157
  ### Files Included in This Bundle
118
158
  - \`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
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
121
163
  - \`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
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
123
166
  - \`launch-kit.md\` — Ready-to-post copy for Show HN, Product Hunt, and startup directories
124
167
  - \`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
168
+ - \`.postbuild/pseo/{use-cases,compare,guides,checklists,templates,calculators}/**/index.html\` — Starter static HTML SEO drafts to review before publishing
126
169
  `;
127
170
  }
128
171
 
129
172
  export function applyLocalSeoFixes(report, options = {}) {
130
173
  const cwd = options.cwd || process.cwd();
131
174
  const overwritePublic = Boolean(options.overwritePublic);
175
+ const publishPseoToPublic = Boolean(options.publishPseoToPublic);
176
+ const verificationToken = options.verificationToken || report.verificationToken || "";
132
177
  const { framework, publicDir } = detectPublicDir(cwd);
133
178
  fs.mkdirSync(publicDir, { recursive: true });
134
179
 
@@ -139,9 +184,36 @@ export function applyLocalSeoFixes(report, options = {}) {
139
184
 
140
185
  // 0. START_HERE.md in .postbuild/
141
186
  const startHerePath = path.join(outDir, "START_HERE.md");
142
- fs.writeFileSync(startHerePath, buildStartHereMd(report), "utf8");
187
+ fs.writeFileSync(startHerePath, buildStartHereMd(report, verificationToken), "utf8");
143
188
  writtenFiles.push({ path: path.relative(cwd, startHerePath), role: "3-Step Non-Technical Quickstart & AI Builder Prompt (START_HERE.md)" });
144
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
+
145
217
  // 1. robots.txt in public/ (if missing or overwritePublic)
146
218
  const robotsPath = path.join(publicDir, "robots.txt");
147
219
  if (!fs.existsSync(robotsPath) || overwritePublic) {
@@ -149,11 +221,14 @@ export function applyLocalSeoFixes(report, options = {}) {
149
221
  writtenFiles.push({ path: path.relative(cwd, robotsPath), role: "AI + Search Crawler Rules (robots.txt)" });
150
222
  }
151
223
 
152
- // 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)
153
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;
154
228
  if (!fs.existsSync(sitemapPath) || overwritePublic) {
155
- fs.writeFileSync(sitemapPath, report.artifacts.sitemapXml, "utf8");
156
- 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)" });
157
232
  }
158
233
 
159
234
  // 3. llms.txt in public/ for AI Answer Engines (ChatGPT, Claude, Perplexity)
@@ -166,11 +241,11 @@ export function applyLocalSeoFixes(report, options = {}) {
166
241
 
167
242
  // 4. Complete .postbuild bundle
168
243
  const headSnippetPath = path.join(outDir, "head-seo-snippet.html");
169
- fs.writeFileSync(headSnippetPath, buildHeadSeoSnippet(report), "utf8");
244
+ fs.writeFileSync(headSnippetPath, buildHeadSeoSnippet(report, verificationToken), "utf8");
170
245
  writtenFiles.push({ path: path.relative(cwd, headSnippetPath), role: "OpenGraph + Canonical + JSON-LD <head> Snippet" });
171
246
 
172
- const sitemapOut = path.join(outDir, "sitemap.xml");
173
- fs.writeFileSync(sitemapOut, report.artifacts.sitemapXml, "utf8");
247
+ const sitemapOut = path.join(outDir, "sitemap-starter-routes.xml");
248
+ fs.writeFileSync(sitemapOut, fullSitemapXml, "utf8");
174
249
 
175
250
  const robotsOut = path.join(outDir, "robots.txt");
176
251
  fs.writeFileSync(robotsOut, report.artifacts.robotsTxt, "utf8");
@@ -184,27 +259,31 @@ export function applyLocalSeoFixes(report, options = {}) {
184
259
 
185
260
  const pseoJsonOut = path.join(outDir, "pseo-routes.json");
186
261
  fs.writeFileSync(pseoJsonOut, JSON.stringify(report.pseoPages, null, 2), "utf8");
187
- 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)" });
188
263
 
189
- // 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)
190
265
  const pseoOutDir = path.join(outDir, "pseo");
191
266
  const pseoRenderOpts = {
192
267
  origin: report.targetUrl,
193
268
  cleanName: report.cleanName || report.launchKit?.productName || report.domain,
194
- 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 || ""
195
274
  };
196
275
  const renderedPseo = writePseoHtmlFiles(report.pseoPages || [], pseoOutDir, pseoRenderOpts);
197
276
  writtenFiles.push({
198
277
  path: path.relative(cwd, pseoOutDir),
199
- role: `${renderedPseo.length} Static Programmatic SEO HTML Landing Pages`
278
+ role: `${renderedPseo.length} Starter Static SEO HTML Drafts (review & copy selected routes to public/)`
200
279
  });
201
280
 
202
- if (overwritePublic) {
281
+ if (publishPseoToPublic) {
203
282
  writePseoHtmlFiles(report.pseoPages || [], publicDir, pseoRenderOpts);
204
283
  }
205
284
 
206
285
  const launchKitOut = path.join(outDir, "launch-kit.md");
207
- fs.writeFileSync(launchKitOut, report.launchKit.longDescription, "utf8");
286
+ fs.writeFileSync(launchKitOut, report.launchKit?.longDescription || report.launchKit?.shortPitch || "", "utf8");
208
287
  writtenFiles.push({ path: path.relative(cwd, launchKitOut), role: "Synthesized Launch Copy (HN, Product Hunt, Directories)" });
209
288
 
210
289
  // 5. CSV export of all 85 directories with prefilled submission links