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(/ |