diff --git a/scripts/generate-seo.ts b/scripts/generate-seo.ts index 743aff26..ddd0ee66 100644 --- a/scripts/generate-seo.ts +++ b/scripts/generate-seo.ts @@ -47,9 +47,11 @@ async function generateSeo() { const desc = escapeHtml(item.description || "Zero-paywall developer tool curated on DevShelf."); const category = escapeHtml(item.category || "Developer Tools"); const lang = escapeHtml(item.language || ""); - const license = escapeHtml(item.license || "Free / FOSS"); + const license = escapeHtml(item.license || ""); const auth = escapeHtml(item.auth || ""); - const freeTier = escapeHtml(item.freeTier || ""); + const pricing = + item.freeTier || item.rateLimit || "See the official site for free access and limits."; + const freeTier = escapeHtml(pricing); const alternativeTo = escapeHtml(item.alternativeTo || ""); const dockerCompose = item.dockerCompose || ""; @@ -68,8 +70,8 @@ async function generateSeo() { const faqs = [ { - question: `Is ${name} really free to use?`, - answer: `Yes. ${name} is verified on DevShelf under the ${license || "open-source"} license. Pricing: ${freeTier || "100% Free / FOSS"} with no upfront credit card requirement.`, + question: `What free access does ${item.name} offer?`, + answer: `${pricing}${item.license ? ` License: ${item.license}.` : ""}`, }, ...(alternativeTo ? [ @@ -80,10 +82,10 @@ async function generateSeo() { ] : []), { - question: `What are the authentication and access requirements for ${name}?`, - answer: auth - ? `Authentication requirement: ${auth}.` - : "No mandatory credit card or paywalled account needed to get started.", + question: `What are the authentication and access requirements for ${item.name}?`, + answer: item.auth + ? `Authentication requirement: ${item.auth}.` + : "See the official site for authentication and access requirements.", }, ]; @@ -104,7 +106,7 @@ async function generateSeo() { price: "0", priceCurrency: "USD", availability: "https://schema.org/InStock", - description: item.freeTier || "Verified permanent free or open-source tier", + description: pricing, }, ...(item.license ? { license: item.license } : {}), ...(item.language ? { programmingLanguage: item.language } : {}), @@ -319,7 +321,7 @@ async function generateSeo() {
Pricing Model - 100% Free / FOSS + Free access; limits may apply
${lang ? `
Language${lang}
` : ""} ${license ? `
License${license}
` : ""} @@ -483,7 +485,7 @@ async function generateSeo() {

${escapeHtml(tool.description)}

- 💎 Free: ${escapeHtml(tool.freeTier || "100% Free / FOSS")} + 💎 Free: ${escapeHtml(tool.freeTier || tool.rateLimit || "See official site for free access and limits")} ${tool.language ? `${escapeHtml(tool.language)}` : ""} ${tool.license ? `${escapeHtml(tool.license)}` : ""}
@@ -512,7 +514,7 @@ async function generateSeo() { ${escapeHtml(t.name)} ${escapeHtml(t.category || "Tool")} - ${escapeHtml(t.freeTier || "100% Free")} + ${escapeHtml(t.freeTier || t.rateLimit || "See official site for free access and limits")} ${escapeHtml(t.language || "Multi-platform")} Specs → @@ -798,7 +800,7 @@ async function generateSeo() {

${escapeHtml(tool.description)}

- 💎 Free: ${escapeHtml(tool.freeTier || "100% Free / FOSS")} + 💎 Free: ${escapeHtml(tool.freeTier || tool.rateLimit || "See official site for free access and limits")} ${tool.auth ? `🔑 ${escapeHtml(tool.auth)}` : ""} ${tool.language ? `${escapeHtml(tool.language)}` : ""}
@@ -967,7 +969,7 @@ async function generateSeo() {

${escapeHtml(item.description || "")}

- 💎 ${escapeHtml(item.freeTier || "100% Free")} + 💎 ${escapeHtml(item.freeTier || item.rateLimit || "See official site for free access and limits")} Details →
`, @@ -1164,7 +1166,9 @@ ${topAltsMarkdown} for (const item of resources) { llmsFullLines.push(`### ${item.name}`); llmsFullLines.push(`- **Category**: ${item.category || item.section || "Tool"}`); - llmsFullLines.push(`- **Pricing Model**: ${item.freeTier || "100% Free / FOSS"}`); + llmsFullLines.push( + `- **Pricing Model**: ${item.freeTier || item.rateLimit || "See official site for free access and limits"}`, + ); if (item.alternativeTo) llmsFullLines.push(`- **Alternative To**: ${item.alternativeTo}`); if (item.license) llmsFullLines.push(`- **License**: ${item.license}`); if (item.language) llmsFullLines.push(`- **Language**: ${item.language}`); diff --git a/tests/seo-pricing.test.ts b/tests/seo-pricing.test.ts new file mode 100644 index 00000000..0e894374 --- /dev/null +++ b/tests/seo-pricing.test.ts @@ -0,0 +1,77 @@ +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; + +test("generated pages preserve API allowances without inventing a license", async () => { + const root = process.cwd(); + const directory = await fs.mkdtemp(path.join(os.tmpdir(), "devshelf-seo-")); + try { + const apis = JSON.parse(await fs.readFile(path.join(root, "shelf/apis.json"), "utf8")); + const arcmira = apis.find( + (item: { name: string }) => item.name === "Arcmira: YouTube Transcript Search", + ); + assert.ok(arcmira, "Arcmira fixture must exist in the catalog"); + await fs.mkdir(path.join(directory, "site")); + await fs.writeFile( + path.join(directory, "site/data.json"), + JSON.stringify({ + resources: [ + { ...arcmira, type: "api" }, + { + name: "Example & tool", + description: "A test tool", + category: "Testing", + url: "https://example.com", + license: "MIT", + freeTier: "10 requests & no charge", + }, + { + name: "Unspecified", + description: "A test tool", + category: "Testing", + url: "https://example.com", + }, + ], + }), + ); + execFileSync( + process.execPath, + [ + ...process.execArgv.filter((arg) => arg !== "--test"), + path.join(root, "scripts/generate-seo.ts"), + ], + { cwd: directory }, + ); + const page = await fs.readFile( + path.join(directory, "site/tools/arcmira-youtube-transcript-search.html"), + "utf8", + ); + assert.match(page, /1,000 included credits per month/); + assert.match(page, /newest 30 days and later result pages/); + assert.match(page, /Premium transcripts and sponsor research use paid plans/); + assert.doesNotMatch(page, /100% Free|Free \/ FOSS|under the .* license/); + const structured = JSON.parse( + page.match(/