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/bin/git-paid.js +25 -12
- package/lib/auth.js +289 -29
- package/lib/directories.js +179 -123
- package/lib/engine.js +97 -53
- package/lib/fixer.js +129 -50
- package/lib/guard.js +402 -0
- package/lib/inbox.js +56 -10
- package/lib/mcp.js +35 -11
- package/lib/pseo.js +244 -44
- package/lib/safe-fetch.js +14 -5
- package/package.json +2 -1
package/lib/pseo.js
CHANGED
|
@@ -68,19 +68,19 @@ function renderTopicSpecificSection(pageSlug, cleanName, domain) {
|
|
|
68
68
|
<td><strong>Free Audit & Preview</strong></td>
|
|
69
69
|
<td><strong>$0</strong></td>
|
|
70
70
|
<td>Any public URL</td>
|
|
71
|
-
<td>8-point SEO & AI crawler readiness scan, copy-paste <code>robots.txt</code> / <code>llms.txt</code> / JSON-LD snippets, and 5-directory preview.</td>
|
|
71
|
+
<td>8-point SEO & AI crawler readiness scan, copy-paste <code>robots.txt</code> / <code>llms.txt</code> / JSON-LD snippets, Auto-Fill Bookmarklet, and 5-directory preview.</td>
|
|
72
72
|
</tr>
|
|
73
73
|
<tr>
|
|
74
74
|
<td><strong>Launch Pass</strong> <span class="pill emerald" style="margin-left:6px;">Recommended</span></td>
|
|
75
75
|
<td><strong>$29</strong> (one-time)</td>
|
|
76
76
|
<td>1 project domain</td>
|
|
77
|
-
<td>All 85 prefilled directory launch links + CSV export, 25 static HTML SEO
|
|
77
|
+
<td>All 85 prefilled directory launch links + CSV export, 1-click <code>.zip</code> Launch Bundle with 25 static HTML SEO routes (<code>npx git-paid fix</code>), dedicated <code><domain>-bot@postbuild.dev</code> verification inbox (30-day retention), and CLI/MCP key.</td>
|
|
78
78
|
</tr>
|
|
79
79
|
<tr>
|
|
80
|
-
<td><strong>Studio</strong></td>
|
|
81
|
-
<td><strong
|
|
82
|
-
<td>Multi-project</td>
|
|
83
|
-
<td>
|
|
80
|
+
<td><strong>Done-For-You & Studio</strong></td>
|
|
81
|
+
<td><strong>Waitlist</strong></td>
|
|
82
|
+
<td>Multi-project / Concierge</td>
|
|
83
|
+
<td>Done-for-you manual & CAPTCHA directory submissions, weekly AI search visibility checks, and Buyer Intent Radar alerts (contact <code>support@postbuild.dev</code>).</td>
|
|
84
84
|
</tr>
|
|
85
85
|
</tbody>
|
|
86
86
|
</table>
|
|
@@ -91,39 +91,39 @@ function renderTopicSpecificSection(pageSlug, cleanName, domain) {
|
|
|
91
91
|
if (pageSlug.includes("roi-case-study")) {
|
|
92
92
|
return `
|
|
93
93
|
<section class="card" style="margin-bottom:28px;">
|
|
94
|
-
<h2>
|
|
94
|
+
<h2>Live Self-Audit Walkthrough: How <code>www.postbuild.dev</code> Uses <code>npx git-paid</code></h2>
|
|
95
95
|
<p style="font-size:14px;color:var(--muted);margin-top:4px;">
|
|
96
|
-
|
|
96
|
+
Every deliverable generated for <code>www.postbuild.dev</code> is publicly deployed and verifiable via live HTTP inspection:
|
|
97
97
|
</p>
|
|
98
|
-
<div class="table-wrap" role="region" aria-label="
|
|
98
|
+
<div class="table-wrap" role="region" aria-label="Live self-audit verification" tabindex="0">
|
|
99
99
|
<table>
|
|
100
100
|
<thead>
|
|
101
101
|
<tr>
|
|
102
102
|
<th>Deliverable</th>
|
|
103
|
-
<th>
|
|
104
|
-
<th>
|
|
103
|
+
<th>Without Postbuild</th>
|
|
104
|
+
<th>Live on <code>www.postbuild.dev</code> (Verifiable Now)</th>
|
|
105
105
|
</tr>
|
|
106
106
|
</thead>
|
|
107
107
|
<tbody>
|
|
108
108
|
<tr>
|
|
109
|
-
<td><strong>Technical
|
|
110
|
-
<td>
|
|
111
|
-
<td>100 / 100
|
|
109
|
+
<td><strong>8-Point Technical Audit</strong></td>
|
|
110
|
+
<td>Manual inspection of headers, schema, and crawler rules</td>
|
|
111
|
+
<td><strong>100 / 100</strong> across all 8 checks (run <code>npx git-paid https://www.postbuild.dev</code> to verify live)</td>
|
|
112
112
|
</tr>
|
|
113
113
|
<tr>
|
|
114
114
|
<td><strong>Static SEO Landing Pages</strong></td>
|
|
115
|
-
<td>
|
|
116
|
-
<td>26 routes indexed in <code
|
|
115
|
+
<td>Single-page app root (<code>/</code>) only</td>
|
|
116
|
+
<td>26 routes indexed in <a href="/sitemap.xml"><code>/sitemap.xml</code></a> (<code>/</code> + 25 pre-rendered static HTML guides)</td>
|
|
117
117
|
</tr>
|
|
118
118
|
<tr>
|
|
119
|
-
<td><strong>
|
|
120
|
-
<td>
|
|
121
|
-
<td>
|
|
119
|
+
<td><strong>AI Search Crawler Manifest</strong></td>
|
|
120
|
+
<td>No <code>llms.txt</code> or explicit AI bot rules</td>
|
|
121
|
+
<td>Live <a href="/llms.txt"><code>/llms.txt</code></a> and <a href="/robots.txt"><code>/robots.txt</code></a> allowing <code>OAI-SearchBot</code>, <code>PerplexityBot</code>, and <code>ClaudeBot</code></td>
|
|
122
122
|
</tr>
|
|
123
123
|
<tr>
|
|
124
|
-
<td><strong>
|
|
125
|
-
<td
|
|
126
|
-
<td>
|
|
124
|
+
<td><strong>Directory Launch Queue</strong></td>
|
|
125
|
+
<td>Manual spreadsheet copy-pasting & personal inbox clutter</td>
|
|
126
|
+
<td>85 curated directories ranked by DR with prefilled UTM links + dedicated <code>postbuild-bot@postbuild.dev</code> inbox</td>
|
|
127
127
|
</tr>
|
|
128
128
|
</tbody>
|
|
129
129
|
</table>
|
|
@@ -151,7 +151,7 @@ function renderTopicSpecificSection(pageSlug, cleanName, domain) {
|
|
|
151
151
|
<tbody>
|
|
152
152
|
<tr>
|
|
153
153
|
<td><strong>Manual Spreadsheets</strong></td>
|
|
154
|
-
<td>$0 (
|
|
154
|
+
<td>$0 (manual form entry)</td>
|
|
155
155
|
<td>Clutters personal email</td>
|
|
156
156
|
<td>Manual authoring required</td>
|
|
157
157
|
</tr>
|
|
@@ -192,22 +192,28 @@ export function renderSinglePseoPage(page, allPages = [], options = {}) {
|
|
|
192
192
|
const relatedPages = allPages.filter((p) => p.path !== page.path).slice(0, 8);
|
|
193
193
|
const topicSection = renderTopicSpecificSection(pageSlug, cleanName, domain);
|
|
194
194
|
|
|
195
|
+
const softwareAppNode = {
|
|
196
|
+
"@type": "SoftwareApplication",
|
|
197
|
+
name: cleanName,
|
|
198
|
+
url: origin,
|
|
199
|
+
applicationCategory: "DeveloperApplication",
|
|
200
|
+
operatingSystem: "Web, macOS, Windows, Linux",
|
|
201
|
+
description: metaDescription,
|
|
202
|
+
...(domain === "postbuild.dev"
|
|
203
|
+
? {
|
|
204
|
+
offers: {
|
|
205
|
+
"@type": "Offer",
|
|
206
|
+
price: "29.00",
|
|
207
|
+
priceCurrency: "USD"
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
: {})
|
|
211
|
+
};
|
|
212
|
+
|
|
195
213
|
const jsonLd = {
|
|
196
214
|
"@context": "https://schema.org",
|
|
197
215
|
"@graph": [
|
|
198
|
-
|
|
199
|
-
"@type": "SoftwareApplication",
|
|
200
|
-
name: cleanName,
|
|
201
|
-
url: origin,
|
|
202
|
-
applicationCategory: "DeveloperApplication",
|
|
203
|
-
operatingSystem: "Web, macOS, Windows, Linux",
|
|
204
|
-
description: metaDescription,
|
|
205
|
-
offers: {
|
|
206
|
-
"@type": "Offer",
|
|
207
|
-
price: "29.00",
|
|
208
|
-
priceCurrency: "USD"
|
|
209
|
-
}
|
|
210
|
-
},
|
|
216
|
+
softwareAppNode,
|
|
211
217
|
{
|
|
212
218
|
"@type": "BreadcrumbList",
|
|
213
219
|
itemListElement: [
|
|
@@ -257,7 +263,7 @@ export function renderSinglePseoPage(page, allPages = [], options = {}) {
|
|
|
257
263
|
<meta name="twitter:title" content="${escapeHtml(fullTitle)}" />
|
|
258
264
|
<meta name="twitter:description" content="${escapeHtml(metaDescription)}" />
|
|
259
265
|
<link rel="icon" type="image/svg+xml" href="/brand/postbuild-icon.svg" />
|
|
260
|
-
<script type="application/ld+json">${JSON.stringify(jsonLd)}</script>
|
|
266
|
+
<script type="application/ld+json">${JSON.stringify(jsonLd).replace(/</g, "\\u003c")}</script>
|
|
261
267
|
<script>
|
|
262
268
|
window.si = window.si || function () { (window.siq = window.siq || []).push(arguments); };
|
|
263
269
|
</script>
|
|
@@ -632,7 +638,7 @@ export function renderSinglePseoPage(page, allPages = [], options = {}) {
|
|
|
632
638
|
|
|
633
639
|
<footer class="site-footer">
|
|
634
640
|
<div>
|
|
635
|
-
<strong>postbuild.dev</strong> (<code>git-paid</code> v2.1.
|
|
641
|
+
<strong>postbuild.dev</strong> (<code>git-paid</code> v2.1.6) — Post-build SEO, directory launch kit & verification inbox.
|
|
636
642
|
</div>
|
|
637
643
|
<div style="display:flex;gap:14px;flex-wrap:wrap;">
|
|
638
644
|
<a href="/">Home & Audit</a>
|
|
@@ -657,19 +663,213 @@ export function renderSinglePseoPage(page, allPages = [], options = {}) {
|
|
|
657
663
|
</html>`;
|
|
658
664
|
}
|
|
659
665
|
|
|
666
|
+
export function renderBuyerStarterPseoPage(page, allPages = [], options = {}) {
|
|
667
|
+
const cleanDomain = String(options.domain || "example.com")
|
|
668
|
+
.toLowerCase()
|
|
669
|
+
.trim()
|
|
670
|
+
.replace(/^https?:\/\//, "")
|
|
671
|
+
.replace(/^www\./, "")
|
|
672
|
+
.split("/")[0];
|
|
673
|
+
const cleanOrig = String(options.origin || `https://${cleanDomain}`).replace(/\/$/, "");
|
|
674
|
+
const prodName = String(options.cleanName || cleanDomain).trim();
|
|
675
|
+
const canonicalUrl = `${cleanOrig}${page.path}`;
|
|
676
|
+
const cat = page.category || page.volume || "Guide";
|
|
677
|
+
const cleanHeading = String(page.keyword || humanizeSlug(page.path, prodName).title);
|
|
678
|
+
const pageTitle = cleanHeading.toLowerCase().includes(prodName.toLowerCase())
|
|
679
|
+
? cleanHeading
|
|
680
|
+
: `${cleanHeading} | ${prodName}`;
|
|
681
|
+
|
|
682
|
+
const rawTagline = String(options.tagline || "").trim();
|
|
683
|
+
const tagline =
|
|
684
|
+
rawTagline &&
|
|
685
|
+
rawTagline.toLowerCase() !== prodName.toLowerCase() &&
|
|
686
|
+
!rawTagline.toLowerCase().includes(`(${cleanDomain})`)
|
|
687
|
+
? rawTagline
|
|
688
|
+
: "";
|
|
689
|
+
|
|
690
|
+
const shortPitch = String(options.shortPitch || options.description || "").trim();
|
|
691
|
+
const summaryText =
|
|
692
|
+
shortPitch ||
|
|
693
|
+
`${prodName} is available at ${cleanOrig}. Customize this starter draft with your specific product workflows, screenshots, and customer examples before publishing.`;
|
|
694
|
+
|
|
695
|
+
const cats =
|
|
696
|
+
Array.isArray(options.categories) && options.categories.length > 0
|
|
697
|
+
? options.categories.join(", ")
|
|
698
|
+
: "Web Application";
|
|
699
|
+
|
|
700
|
+
const metaDesc = `${cleanHeading}. ${summaryText}`.slice(0, 158).trim();
|
|
701
|
+
const ogImgUrl = options.ogImage || `${cleanOrig}/og-card.svg`;
|
|
702
|
+
const related = (allPages || []).filter((p) => p.path !== page.path).slice(0, 5);
|
|
703
|
+
const headingTopic = cleanHeading
|
|
704
|
+
.replace(new RegExp(`^${prodName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*[—–:-]?\\s*`, "i"), "")
|
|
705
|
+
.trim() || cleanHeading;
|
|
706
|
+
|
|
707
|
+
const hasFaqSchema = String(page.schemaType || page.kd || "").includes("FAQPage");
|
|
708
|
+
const faqItems = [
|
|
709
|
+
{
|
|
710
|
+
q: `What is covered in this ${headingTopic} guide?`,
|
|
711
|
+
a: `${summaryText}`
|
|
712
|
+
},
|
|
713
|
+
{
|
|
714
|
+
q: `Where can I access ${prodName}?`,
|
|
715
|
+
a: `Visit ${cleanOrig} to explore ${prodName} directly and review current product documentation.`
|
|
716
|
+
}
|
|
717
|
+
];
|
|
718
|
+
|
|
719
|
+
const graph = [
|
|
720
|
+
{
|
|
721
|
+
"@type": "SoftwareApplication",
|
|
722
|
+
name: prodName,
|
|
723
|
+
url: cleanOrig,
|
|
724
|
+
applicationCategory: "BusinessApplication",
|
|
725
|
+
operatingSystem: "Web",
|
|
726
|
+
description: summaryText
|
|
727
|
+
},
|
|
728
|
+
{
|
|
729
|
+
"@type": "BreadcrumbList",
|
|
730
|
+
itemListElement: [
|
|
731
|
+
{ "@type": "ListItem", position: 1, name: prodName, item: cleanOrig },
|
|
732
|
+
{ "@type": "ListItem", position: 2, name: cat, item: canonicalUrl }
|
|
733
|
+
]
|
|
734
|
+
}
|
|
735
|
+
];
|
|
736
|
+
if (hasFaqSchema) {
|
|
737
|
+
graph.push({
|
|
738
|
+
"@type": "FAQPage",
|
|
739
|
+
mainEntity: faqItems.map((f) => ({
|
|
740
|
+
"@type": "Question",
|
|
741
|
+
name: f.q,
|
|
742
|
+
acceptedAnswer: { "@type": "Answer", text: f.a }
|
|
743
|
+
}))
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
const ldJson = JSON.stringify({ "@context": "https://schema.org", "@graph": graph }).replace(/</g, "\\u003c");
|
|
747
|
+
|
|
748
|
+
let bodySectionHtml = "";
|
|
749
|
+
if (cat === "Comparison" || cat === "Walkthrough") {
|
|
750
|
+
bodySectionHtml = ` <section class="card">
|
|
751
|
+
<h2>Evaluation Criteria: ${escapeHtml(headingTopic)}</h2>
|
|
752
|
+
<p>Use the worksheet below when comparing <strong>${escapeHtml(prodName)}</strong> against existing internal workflows:</p>
|
|
753
|
+
<table class="cmp-table">
|
|
754
|
+
<thead><tr><th>Criterion</th><th>${escapeHtml(prodName)} (${escapeHtml(cleanDomain)})</th><th>Current / Baseline Process</th></tr></thead>
|
|
755
|
+
<tbody>
|
|
756
|
+
<tr><td><strong>Primary Scope</strong></td><td>${escapeHtml(tagline || prodName)}</td><td>Document current team baseline</td></tr>
|
|
757
|
+
<tr><td><strong>Overview</strong></td><td>${escapeHtml(summaryText)}</td><td>Record current steps and handoffs</td></tr>
|
|
758
|
+
<tr><td><strong>Direct Access</strong></td><td><a href="${escapeHtml(cleanOrig)}">${escapeHtml(cleanOrig)}</a></td><td>Internal docs / spreadsheets</td></tr>
|
|
759
|
+
</tbody>
|
|
760
|
+
</table>
|
|
761
|
+
</section>`;
|
|
762
|
+
} else if (cat === "Calculator") {
|
|
763
|
+
bodySectionHtml = ` <section class="card">
|
|
764
|
+
<h2>Workflow Time Worksheet — ${escapeHtml(headingTopic)}</h2>
|
|
765
|
+
<p>Enter your team's monthly frequency and average minutes per task to estimate total monthly time spent on this workflow:</p>
|
|
766
|
+
<div style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:10px;padding:18px;margin-top:12px;">
|
|
767
|
+
<label style="display:block;font-weight:600;margin-bottom:6px;">Monthly runs: <span id="runsVal">10 runs/mo</span></label>
|
|
768
|
+
<input id="runsInput" type="range" min="1" max="60" value="10" style="width:100%;margin-bottom:12px;" oninput="var r=parseInt(this.value,10)||1;var m=parseInt(document.getElementById('minsInput').value,10)||15;document.getElementById('runsVal').textContent=r+' runs/mo';document.getElementById('hrsTotal').textContent=((r*m)/60).toFixed(1)+' hrs/mo';" />
|
|
769
|
+
<label style="display:block;font-weight:600;margin-bottom:6px;">Minutes per run: <span id="minsVal">30 mins/run</span></label>
|
|
770
|
+
<input id="minsInput" type="range" min="5" max="120" step="5" value="30" style="width:100%;" oninput="var m=parseInt(this.value,10)||15;var r=parseInt(document.getElementById('runsInput').value,10)||1;document.getElementById('minsVal').textContent=m+' mins/run';document.getElementById('hrsTotal').textContent=((r*m)/60).toFixed(1)+' hrs/mo';" />
|
|
771
|
+
<div style="margin-top:14px;background:#fff;border:1px solid #e2e8f0;border-radius:8px;padding:12px;">
|
|
772
|
+
<div style="font-size:12px;color:#64748b;">Estimated Monthly Workflow Time (Runs × Minutes)</div>
|
|
773
|
+
<div id="hrsTotal" style="font-size:22px;font-weight:800;color:#047857;">5.0 hrs/mo</div>
|
|
774
|
+
</div>
|
|
775
|
+
</div>
|
|
776
|
+
</section>`;
|
|
777
|
+
} else {
|
|
778
|
+
bodySectionHtml = ` <section class="card">
|
|
779
|
+
<h2>Key Notes: ${escapeHtml(headingTopic)}</h2>
|
|
780
|
+
<ul style="padding-left:20px;margin:12px 0;">
|
|
781
|
+
<li style="margin-bottom:10px;"><strong>Objective:</strong> Define how ${escapeHtml(prodName)} fits into your ${escapeHtml(headingTopic.toLowerCase())} workflow.</li>
|
|
782
|
+
<li style="margin-bottom:10px;"><strong>Live Environment:</strong> Visit <a href="${escapeHtml(cleanOrig)}">${escapeHtml(cleanOrig)}</a> to inspect current features and setup options.</li>
|
|
783
|
+
<li style="margin-bottom:10px;"><strong>Customization Note:</strong> Replace this starter section with your product's specific steps, screenshots, or code examples before publishing.</li>
|
|
784
|
+
</ul>
|
|
785
|
+
</section>`;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
return `<!DOCTYPE html>
|
|
789
|
+
<!-- Starter SEO draft for ${escapeHtml(cleanDomain)} (${escapeHtml(page.path)}). Review and customize with your product details before publishing to production. -->
|
|
790
|
+
<html lang="en">
|
|
791
|
+
<head>
|
|
792
|
+
<meta charset="UTF-8" />
|
|
793
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
794
|
+
<title>${escapeHtml(pageTitle)}</title>
|
|
795
|
+
<meta name="description" content="${escapeHtml(metaDesc)}" />
|
|
796
|
+
<link rel="canonical" href="${escapeHtml(canonicalUrl)}" />
|
|
797
|
+
<meta property="og:type" content="article" />
|
|
798
|
+
<meta property="og:title" content="${escapeHtml(pageTitle)}" />
|
|
799
|
+
<meta property="og:description" content="${escapeHtml(metaDesc)}" />
|
|
800
|
+
<meta property="og:url" content="${escapeHtml(canonicalUrl)}" />
|
|
801
|
+
<meta property="og:image" content="${escapeHtml(ogImgUrl)}" />
|
|
802
|
+
<meta name="twitter:card" content="summary_large_image" />
|
|
803
|
+
<meta name="twitter:title" content="${escapeHtml(pageTitle)}" />
|
|
804
|
+
<meta name="twitter:description" content="${escapeHtml(metaDesc)}" />
|
|
805
|
+
<meta name="twitter:image" content="${escapeHtml(ogImgUrl)}" />
|
|
806
|
+
<script type="application/ld+json">${ldJson}</script>
|
|
807
|
+
<style>body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;max-width:880px;margin:0 auto;padding:36px 20px 64px;line-height:1.65;color:#0f172a;background:#f8fafc}.card{background:#fff;border:1px solid #e2e8f0;border-radius:12px;padding:26px;margin-bottom:20px}.cta{display:inline-block;background:#047857;color:#fff;padding:10px 18px;border-radius:8px;text-decoration:none;font-weight:600}.cmp-table{width:100%;border-collapse:collapse;margin-top:14px;font-size:14px}.cmp-table th,.cmp-table td{border:1px solid #e2e8f0;padding:10px 12px;text-align:left}.cmp-table th{background:#f1f5f9;font-weight:700}.faq-item{border-top:1px solid #e2e8f0;padding-top:14px;margin-top:14px}</style>
|
|
808
|
+
</head>
|
|
809
|
+
<body>
|
|
810
|
+
<header style="display:flex;justify-content:space-between;align-items:center;margin-bottom:24px;padding-bottom:14px;border-bottom:1px solid #e2e8f0;">
|
|
811
|
+
<nav aria-label="Breadcrumb" style="font-size:13px;color:#64748b;"><a href="${escapeHtml(cleanOrig)}" style="font-weight:700;color:#0f172a;text-decoration:none;">${escapeHtml(prodName)}</a> / <span>${escapeHtml(cat)}</span></nav>
|
|
812
|
+
<a class="cta" href="${escapeHtml(cleanOrig)}" style="font-size:13px;padding:7px 14px;">Open ${escapeHtml(prodName)} →</a>
|
|
813
|
+
</header>
|
|
814
|
+
<main>
|
|
815
|
+
<article class="card">
|
|
816
|
+
<div style="font-size:12px;font-weight:700;color:#047857;text-transform:uppercase;letter-spacing:0.04em;">${escapeHtml(cat)} · ${escapeHtml(cats)}</div>
|
|
817
|
+
<h1 style="margin:8px 0 12px;font-size:28px;line-height:1.25;">${escapeHtml(cleanHeading)}</h1>
|
|
818
|
+
${tagline ? `<p style="font-size:16px;color:#334155;margin-bottom:14px;"><strong>${escapeHtml(prodName)}</strong> — ${escapeHtml(tagline)}</p>` : ""}
|
|
819
|
+
<p style="color:#475569;">${escapeHtml(summaryText)}</p>
|
|
820
|
+
<p style="margin-top:18px;"><a class="cta" href="${escapeHtml(cleanOrig)}">Explore ${escapeHtml(prodName)} on ${escapeHtml(cleanDomain)} →</a></p>
|
|
821
|
+
</article>
|
|
822
|
+
${bodySectionHtml}
|
|
823
|
+
${
|
|
824
|
+
hasFaqSchema
|
|
825
|
+
? ` <section class="card">
|
|
826
|
+
<h2>Frequently Asked Questions</h2>
|
|
827
|
+
${faqItems.map((f) => ` <div class="faq-item"><h3 style="margin:0 0 6px;font-size:16px;">${escapeHtml(f.q)}</h3><p style="margin:0;color:#475569;font-size:14px;">${escapeHtml(f.a)}</p></div>`).join("\n")}
|
|
828
|
+
</section>`
|
|
829
|
+
: ""
|
|
830
|
+
}
|
|
831
|
+
<section class="card">
|
|
832
|
+
<h2>Related ${escapeHtml(prodName)} Guides</h2>
|
|
833
|
+
<ul style="padding-left:20px;margin:10px 0;">
|
|
834
|
+
${related.map((r) => ` <li style="margin-bottom:6px;"><a href="${escapeHtml(r.path)}" style="color:#0284c7;text-decoration:none;font-weight:500;">${escapeHtml(r.keyword || r.path)}</a> <span style="color:#64748b;font-size:12px;">(${escapeHtml(r.category || r.volume || "Guide")})</span></li>`).join("\n")}
|
|
835
|
+
</ul>
|
|
836
|
+
</section>
|
|
837
|
+
</main>
|
|
838
|
+
</body>
|
|
839
|
+
</html>`;
|
|
840
|
+
}
|
|
841
|
+
|
|
660
842
|
/**
|
|
661
|
-
* Writes
|
|
843
|
+
* Writes Programmatic SEO HTML files into targetBaseDir so cleanUrls (/use-cases/...) resolve with 200 OK.
|
|
844
|
+
* Uses the buyer-safe starter template for all customer domains, and only uses postbuild.dev's own marketing
|
|
845
|
+
* template when building postbuild.dev itself.
|
|
662
846
|
*/
|
|
663
847
|
export function writePseoHtmlFiles(pseoPages = [], targetBaseDir, options = {}) {
|
|
848
|
+
const cleanDomain = String(options.domain || "postbuild.dev")
|
|
849
|
+
.toLowerCase()
|
|
850
|
+
.trim()
|
|
851
|
+
.replace(/^https?:\/\//, "")
|
|
852
|
+
.replace(/^www\./, "")
|
|
853
|
+
.split("/")[0];
|
|
854
|
+
const isPostbuildSelf = cleanDomain === "postbuild.dev";
|
|
664
855
|
const written = [];
|
|
665
856
|
for (const page of pseoPages) {
|
|
666
857
|
const cleanRel = String(page.path || "").replace(/^\/+/, "");
|
|
667
858
|
if (!cleanRel) continue;
|
|
668
|
-
const
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
859
|
+
const html = isPostbuildSelf
|
|
860
|
+
? renderSinglePseoPage(page, pseoPages, options)
|
|
861
|
+
: renderBuyerStarterPseoPage(page, pseoPages, options);
|
|
862
|
+
const indexOutFile = path.join(targetBaseDir, cleanRel, "index.html");
|
|
863
|
+
fs.mkdirSync(path.dirname(indexOutFile), { recursive: true });
|
|
864
|
+
fs.writeFileSync(indexOutFile, html, "utf8");
|
|
865
|
+
written.push(indexOutFile);
|
|
866
|
+
|
|
867
|
+
if (isPostbuildSelf) {
|
|
868
|
+
const flatOutFile = path.join(targetBaseDir, `${cleanRel}.html`);
|
|
869
|
+
fs.mkdirSync(path.dirname(flatOutFile), { recursive: true });
|
|
870
|
+
fs.writeFileSync(flatOutFile, html, "utf8");
|
|
871
|
+
}
|
|
673
872
|
}
|
|
674
873
|
return written;
|
|
675
874
|
}
|
|
875
|
+
|
package/lib/safe-fetch.js
CHANGED
|
@@ -145,7 +145,7 @@ export async function assertSafeUrl(rawUrl, options = {}) {
|
|
|
145
145
|
return parsed;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
async function readResponseWithCap(response, maxBytes = MAX_RESPONSE_BYTES) {
|
|
148
|
+
async function readResponseWithCap(response, maxBytes = MAX_RESPONSE_BYTES, truncateOnMaxBytes = false) {
|
|
149
149
|
if (!response.body) {
|
|
150
150
|
return Buffer.alloc(0);
|
|
151
151
|
}
|
|
@@ -157,11 +157,19 @@ async function readResponseWithCap(response, maxBytes = MAX_RESPONSE_BYTES) {
|
|
|
157
157
|
const { done, value } = await reader.read();
|
|
158
158
|
if (done) break;
|
|
159
159
|
const buf = Buffer.from(value);
|
|
160
|
-
total
|
|
161
|
-
|
|
162
|
-
|
|
160
|
+
if (total + buf.length > maxBytes) {
|
|
161
|
+
if (truncateOnMaxBytes) {
|
|
162
|
+
const remaining = Math.max(0, maxBytes - total);
|
|
163
|
+
if (remaining > 0) {
|
|
164
|
+
chunks.push(buf.subarray(0, remaining));
|
|
165
|
+
}
|
|
166
|
+
await reader.cancel().catch(() => {});
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
await reader.cancel().catch(() => {});
|
|
163
170
|
throw new Error(`Response exceeded maximum size limit of ${maxBytes} bytes`);
|
|
164
171
|
}
|
|
172
|
+
total += buf.length;
|
|
165
173
|
chunks.push(buf);
|
|
166
174
|
}
|
|
167
175
|
|
|
@@ -176,6 +184,7 @@ export async function safeFetch(rawUrl, options = {}) {
|
|
|
176
184
|
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
177
185
|
maxRedirects = MAX_REDIRECTS,
|
|
178
186
|
maxBytes = MAX_RESPONSE_BYTES,
|
|
187
|
+
truncateOnMaxBytes = false,
|
|
179
188
|
allowedDomains = null,
|
|
180
189
|
lookupFn
|
|
181
190
|
} = options;
|
|
@@ -220,7 +229,7 @@ export async function safeFetch(rawUrl, options = {}) {
|
|
|
220
229
|
continue;
|
|
221
230
|
}
|
|
222
231
|
|
|
223
|
-
const bodyBuffer = await readResponseWithCap(resp, maxBytes);
|
|
232
|
+
const bodyBuffer = await readResponseWithCap(resp, maxBytes, truncateOnMaxBytes);
|
|
224
233
|
return {
|
|
225
234
|
ok: resp.ok,
|
|
226
235
|
status: resp.status,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "git-paid",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.7",
|
|
4
4
|
"description": "git push ships your code. git paid sells it. Autonomous SEO, Paywall, MCP & Directory Launch Agent (www.postbuild.dev).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "bin/git-paid.js",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"lib/directories.js",
|
|
14
14
|
"lib/engine.js",
|
|
15
15
|
"lib/fixer.js",
|
|
16
|
+
"lib/guard.js",
|
|
16
17
|
"lib/inbox.js",
|
|
17
18
|
"lib/mcp.js",
|
|
18
19
|
"lib/pseo.js",
|