Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 19 additions & 15 deletions scripts/generate-seo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 || "";
Expand All @@ -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
? [
Expand All @@ -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.",
},
];

Expand All @@ -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 } : {}),
Expand Down Expand Up @@ -319,7 +321,7 @@ async function generateSeo() {
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4 p-5 rounded-xl bg-slate-950/80 border border-slate-800 text-xs font-mono">
<div>
<span class="text-slate-500 block mb-1">Pricing Model</span>
<span class="text-emerald-400 font-semibold">100% Free / FOSS</span>
<span class="text-emerald-400 font-semibold">Free access; limits may apply</span>
</div>
${lang ? `<div><span class="text-slate-500 block mb-1">Language</span><span class="text-cyan-300 font-semibold">${lang}</span></div>` : ""}
${license ? `<div><span class="text-slate-500 block mb-1">License</span><span class="text-purple-300 font-semibold">${license}</span></div>` : ""}
Expand Down Expand Up @@ -483,7 +485,7 @@ async function generateSeo() {
</div>
<p class="text-sm text-slate-300 leading-relaxed mb-4">${escapeHtml(tool.description)}</p>
<div class="flex flex-wrap gap-2 text-xs font-mono">
<span class="px-2.5 py-1 rounded-lg bg-emerald-950/80 text-emerald-300 border border-emerald-500/30">💎 Free: ${escapeHtml(tool.freeTier || "100% Free / FOSS")}</span>
<span class="px-2.5 py-1 rounded-lg bg-emerald-950/80 text-emerald-300 border border-emerald-500/30">💎 Free: ${escapeHtml(tool.freeTier || tool.rateLimit || "See official site for free access and limits")}</span>
${tool.language ? `<span class="px-2.5 py-1 rounded-lg bg-cyan-950/80 text-cyan-300 border border-cyan-500/30">${escapeHtml(tool.language)}</span>` : ""}
${tool.license ? `<span class="px-2.5 py-1 rounded-lg bg-slate-800 text-slate-300 border border-slate-700">${escapeHtml(tool.license)}</span>` : ""}
</div>
Expand Down Expand Up @@ -512,7 +514,7 @@ async function generateSeo() {
<a href="../tools/${slugify(t.name)}.html" class="hover:text-purple-300 transition">${escapeHtml(t.name)}</a>
</td>
<td class="p-3.5 text-slate-400">${escapeHtml(t.category || "Tool")}</td>
<td class="p-3.5 text-emerald-400">${escapeHtml(t.freeTier || "100% Free")}</td>
<td class="p-3.5 text-emerald-400">${escapeHtml(t.freeTier || t.rateLimit || "See official site for free access and limits")}</td>
<td class="p-3.5 text-cyan-300">${escapeHtml(t.language || "Multi-platform")}</td>
<td class="p-3.5 text-right font-sans">
<a href="../tools/${slugify(t.name)}.html" class="text-purple-400 hover:text-purple-300 font-bold">Specs →</a>
Expand Down Expand Up @@ -798,7 +800,7 @@ async function generateSeo() {
</div>
<p class="text-sm text-slate-300 leading-relaxed mb-4">${escapeHtml(tool.description)}</p>
<div class="flex flex-wrap gap-2 text-xs font-mono">
<span class="px-2.5 py-1 rounded-lg bg-emerald-950/80 text-emerald-300 border border-emerald-500/30">💎 Free: ${escapeHtml(tool.freeTier || "100% Free / FOSS")}</span>
<span class="px-2.5 py-1 rounded-lg bg-emerald-950/80 text-emerald-300 border border-emerald-500/30">💎 Free: ${escapeHtml(tool.freeTier || tool.rateLimit || "See official site for free access and limits")}</span>
${tool.auth ? `<span class="px-2.5 py-1 rounded-lg bg-amber-950/80 text-amber-300 border border-amber-500/30">🔑 ${escapeHtml(tool.auth)}</span>` : ""}
${tool.language ? `<span class="px-2.5 py-1 rounded-lg bg-cyan-950/80 text-cyan-300 border border-cyan-500/30">${escapeHtml(tool.language)}</span>` : ""}
</div>
Expand Down Expand Up @@ -967,7 +969,7 @@ async function generateSeo() {
<p class="text-xs text-slate-400 line-clamp-2 mb-3 leading-relaxed">${escapeHtml(item.description || "")}</p>
</div>
<div class="pt-2 border-t border-slate-800/60 flex items-center justify-between text-[11px] font-mono">
<span class="text-emerald-400 truncate max-w-[150px]">💎 ${escapeHtml(item.freeTier || "100% Free")}</span>
<span class="text-emerald-400 truncate max-w-[150px]">💎 ${escapeHtml(item.freeTier || item.rateLimit || "See official site for free access and limits")}</span>
<a href="tools/${slugify(item.name)}.html" class="text-purple-400 hover:text-purple-300 font-bold">Details →</a>
</div>
</div>`,
Expand Down Expand Up @@ -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}`);
Expand Down
77 changes: 77 additions & 0 deletions tests/seo-pricing.test.ts
Original file line number Diff line number Diff line change
@@ -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(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/)?.[1] || "{}",
);
assert.equal(structured["@graph"][0].license, undefined);
assert.match(structured["@graph"][0].offers.description, /1,000 included credits/);
assert.match(structured["@graph"][1].mainEntity[0].acceptedAnswer.text, /newest 30 days/);
const llms = await fs.readFile(path.join(directory, "site/llms-full.txt"), "utf8");
assert.match(llms, /Pricing Model\*\*: 60 requests per minute/);
const licensed = await fs.readFile(
path.join(directory, "site/tools/example-tool.html"),
"utf8",
);
assert.match(licensed, /10 requests &amp; no charge/);
assert.match(licensed, /License: MIT/);
assert.doesNotMatch(licensed, /&amp;amp;/);
const unknown = await fs.readFile(path.join(directory, "site/tools/unspecified.html"), "utf8");
assert.match(unknown, /See the official site for free access and limits/);
assert.doesNotMatch(unknown, /100% Free|Free \/ FOSS/);
} finally {
await fs.rm(directory, { recursive: true, force: true });
}
});
Loading