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/bin/git-paid.js +16 -6
- package/lib/auth.js +241 -24
- package/lib/directories.js +179 -123
- package/lib/engine.js +92 -48
- package/lib/fixer.js +128 -52
- package/lib/guard.js +203 -1
- package/lib/inbox.js +49 -4
- package/lib/mcp.js +15 -1
- package/lib/pseo.js +202 -8
- package/lib/safe-fetch.js +14 -5
- package/package.json +1 -1
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(
|
|
24
|
+
while ((match = regex.exec(stripped)) !== null && results.length < limit) {
|
|
20
25
|
const clean = match[1].replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
21
|
-
if (
|
|
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 (
|
|
32
|
-
if (
|
|
33
|
-
if (
|
|
34
|
-
if (
|
|
35
|
-
if (cats.length === 0) cats.push("
|
|
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
|
|
218
|
-
|
|
219
|
-
|
|
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
|
|
296
|
+
const cleanTagline = tagline.length > 64 ? truncateWordBoundary(tagline, 64) : tagline;
|
|
268
297
|
const shortPitch =
|
|
269
|
-
description && description.length >=
|
|
270
|
-
? description
|
|
271
|
-
: `${cleanName} (${domain})
|
|
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
|
-
`- **
|
|
278
|
-
`- **
|
|
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(`${
|
|
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: `${
|
|
311
|
-
{ path: `/use-cases/${slug}-automation-guide`, keyword: `
|
|
312
|
-
{ path: `/compare/${slug}-vs-manual-workflow`, keyword: `${cleanName} vs
|
|
313
|
-
{ path: `/templates/${slug}-starter-kit`, keyword: `${
|
|
314
|
-
{ path: `/alternatives/best-${slug}-alternatives-2026`, keyword:
|
|
315
|
-
{ path: `/calculators/${slug}-roi-calculator`, keyword: `${
|
|
316
|
-
{ path: `/guides/${slug}-api-integration`, keyword: `
|
|
317
|
-
{ path: `/use-cases/${slug}-for-agencies`, keyword: `${
|
|
318
|
-
{ path: `/checklists/${slug}-launch-checklist`, keyword: `
|
|
319
|
-
{ path: `/compare/${slug}-pricing-breakdown`, keyword: `${cleanName}
|
|
320
|
-
{ path: `/use-cases/${slug}-for-developers`, keyword: `${
|
|
321
|
-
{ path: `/use-cases/${slug}-for-indie-hackers`, keyword: `${cleanName} for
|
|
322
|
-
{ path: `/compare/${slug}-vs-legacy-tools`, keyword: `
|
|
323
|
-
{ path: `/guides/${slug}-programmatic-seo-setup`, keyword: `
|
|
324
|
-
{ path: `/templates/${slug}-nextjs-boilerplate`, keyword: `${
|
|
325
|
-
{ path: `/use-cases/${slug}-for-saas-teams`, keyword: `${cleanName} for
|
|
326
|
-
{ path: `/guides/${slug}-stripe-checkout-guide`, keyword: `
|
|
327
|
-
{ path: `/checklists/${slug}-seo-audit-checklist`, keyword: `
|
|
328
|
-
{ path: `/calculators/${slug}-conversion-estimator`, keyword:
|
|
329
|
-
{ path: `/alternatives/${slug}-open-source-comparison`, keyword: `${cleanName}
|
|
330
|
-
{ path: `/use-cases/${slug}-for-product-hunt-launch`, keyword: `
|
|
331
|
-
{ path: `/guides/${slug}-ai-search-indexing`, keyword: `
|
|
332
|
-
{ path: `/templates/${slug}-directory-submission-kit`, keyword:
|
|
333
|
-
{ path: `/use-cases/${slug}-for-ai-wrappers`, keyword: `
|
|
334
|
-
{ path: `/compare/${slug}-roi-case-study`, keyword: `${cleanName}
|
|
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
|
|
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, "&")
|
|
32
|
+
.replace(/</g, "<")
|
|
33
|
+
.replace(/>/g, ">")
|
|
34
|
+
.replace(/"/g, """);
|
|
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
|
-
`# ${
|
|
54
|
+
`# ${brand}`,
|
|
32
55
|
``,
|
|
33
|
-
`> ${
|
|
56
|
+
`> ${tagline}`,
|
|
34
57
|
``,
|
|
35
58
|
`- Canonical URL: ${report.targetUrl}`,
|
|
36
|
-
`-
|
|
37
|
-
`- Category: ${kit.categories ? kit.categories.join(", ") : "
|
|
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
|
-
`##
|
|
43
|
-
...(report.pseoPages || []).slice(0,
|
|
44
|
-
(p) => `- [${p.keyword || p.path}](${report.targetUrl.replace(/\/$/, "")}${p.path || p.slug}): ${p.
|
|
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
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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, """)}" />
|
|
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="${
|
|
94
|
+
<meta property="og:site_name" content="${brand.replace(/"/g, """)}" />
|
|
65
95
|
<meta property="og:title" content="${title.replace(/"/g, """)}" />
|
|
66
96
|
<meta property="og:description" content="${desc.replace(/"/g, """)}" />
|
|
67
97
|
<meta property="og:url" content="${report.targetUrl}" />
|
|
68
|
-
<meta property="og:image" content="${
|
|
98
|
+
<meta property="og:image" content="${ogImg.replace(/"/g, """)}" />
|
|
69
99
|
<meta name="twitter:card" content="summary_large_image" />
|
|
70
100
|
<meta name="twitter:title" content="${title.replace(/"/g, """)}" />
|
|
71
101
|
<meta name="twitter:description" content="${desc.replace(/"/g, """)}" />
|
|
72
|
-
<meta name="twitter:image" content="${
|
|
102
|
+
<meta name="twitter:image" content="${ogImg.replace(/"/g, """)}" />
|
|
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
|
|
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
|
|
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
|
|
96
|
-
> 1. Place robots.txt, sitemap.xml, and
|
|
97
|
-
> 2. Copy the meta tags, canonical link, OpenGraph tags, and JSON-LD structured data from head-seo-snippet.html into
|
|
98
|
-
> 3.
|
|
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:
|
|
136
|
+
## Step 2: Verify Domain Ownership on postbuild.dev (Unlocks Your \`@postbuild.dev\` Inbox)
|
|
104
137
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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:
|
|
146
|
+
## Step 3: Submit to 85 Curated Startup & AI Directories (No Personal Inbox Spam)
|
|
114
147
|
|
|
115
|
-
1.
|
|
116
|
-
2. Click **
|
|
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/
|
|
123
|
-
- \`public/
|
|
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
|
-
- \`
|
|
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
|
-
-
|
|
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/ (
|
|
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
|
-
|
|
159
|
-
|
|
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,
|
|
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: "
|
|
262
|
+
writtenFiles.push({ path: path.relative(cwd, pseoJsonOut), role: "Programmatic SEO Route Definitions (JSON)" });
|
|
191
263
|
|
|
192
|
-
// Render the
|
|
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
|
|
278
|
+
role: `${renderedPseo.length} Starter Static SEO HTML Drafts (review & copy selected routes to public/)`
|
|
203
279
|
});
|
|
204
280
|
|
|
205
|
-
if (
|
|
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.
|
|
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
|