Skip to content
Open
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
51 changes: 51 additions & 0 deletions lib/__tests__/jsonLd.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { describe, it, expect } from "vitest";
import { getTool, TOOLS, SITE_URL } from "../tools";
import { toolJsonLd, breadcrumbJsonLd, faqJsonLd, homeJsonLd } from "../jsonLd";

// jwt-decoder always ships with FAQs, so it exercises every builder without
// coupling the test to copy that changes across branches.
const jwt = getTool("jwt-decoder")!;

describe("toolJsonLd", () => {
it("marks the tool as a free, English SoftwareApplication", () => {
const ld = toolJsonLd(jwt);
expect(ld["@type"]).toBe("SoftwareApplication");
expect(ld.inLanguage).toBe("en");
expect(ld.isAccessibleForFree).toBe(true);
expect(ld.url).toBe(`${SITE_URL}${jwt.path}`);
expect(ld.offers).toMatchObject({ price: "0", priceCurrency: "USD" });
});
});

describe("breadcrumbJsonLd", () => {
it("is a two-level Home > Tool trail with no item on the current page", () => {
const ld = breadcrumbJsonLd(jwt);
expect(ld.itemListElement).toHaveLength(2);
expect(ld.itemListElement[0]).toMatchObject({ position: 1, name: "Home", item: SITE_URL });
expect(ld.itemListElement[1].position).toBe(2);
expect(ld.itemListElement[1]).not.toHaveProperty("item");
});
});

describe("faqJsonLd", () => {
it("maps every FAQ to a Question/Answer pair", () => {
const ld = faqJsonLd(jwt);
expect(ld["@type"]).toBe("FAQPage");
expect(ld.mainEntity).toHaveLength(jwt.faqs.length);
expect(ld.mainEntity[0]).toMatchObject({
"@type": "Question",
name: jwt.faqs[0].q,
acceptedAnswer: { "@type": "Answer", text: jwt.faqs[0].a },
});
});
});

describe("homeJsonLd", () => {
it("emits Organization, an English WebSite, and an ItemList of every tool", () => {
const graph = homeJsonLd()["@graph"];
const byType = (t: string) => graph.find((n) => n["@type"] === t) as Record<string, unknown> | undefined;
expect(byType("Organization")).toBeTruthy();
expect(byType("WebSite")?.inLanguage).toBe("en");
expect((byType("ItemList")?.itemListElement as unknown[]).length).toBe(TOOLS.length);
});
});
7 changes: 7 additions & 0 deletions lib/jsonLd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ export function toolJsonLd(tool: Tool) {
operatingSystem: "Any (Browser)",
description: tool.jsonLdDescription,
url: `${SITE_URL}${tool.path}`,
inLanguage: "en",
// Every tool is free and needs no account, so this is truthful and helps
// Google understand the offer. No datePublished — we have no authoritative
// publish date to state, and fabricating one is against the same policy that
// keeps aggregateRating off these nodes.
isAccessibleForFree: true,
offers: { "@type": "Offer", price: "0", priceCurrency: "USD" },
publisher: { "@type": "Organization", name: "CODERCOPS", url: "https://www.codercops.com" },
};
Expand Down Expand Up @@ -80,6 +86,7 @@ export function homeJsonLd() {
url: SITE_URL,
name: "CODERCOPS Tools",
description: "Free, fast, privacy-first developer tools that run entirely in your browser.",
inLanguage: "en",
publisher: { "@id": ORG_ID },
},
{
Expand Down