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
11 changes: 9 additions & 2 deletions app/api/score/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { NextResponse } from "next/server";
import fs from "fs";
import { waitUntil } from "@vercel/functions";
import { upsertScore, getScoreBySlug } from "@/lib/supabase";
import { upsertScore, getScoreBySlug, getScoreSlugByDocsUrl } from "@/lib/supabase";
import { fetchOgName, domainToName } from "@/lib/og-name";
import { computeScore } from "afdocs";
import { AFDOCS_VERSION } from "@/lib/scoring";
Expand Down Expand Up @@ -340,7 +340,14 @@ export async function POST(request: Request) {
// Fern preview/staging hosts (*.ferndocs.com) always slug by URL so they stay distinct from the
// canonical live company entry — otherwise e.g. docusign.ferndocs.com collapses onto the "docusign" slug.
const isFernHost = (() => { try { return /(^|\.)ferndocs\.com$/i.test(new URL(url).hostname); } catch { return false; } })();
const rawSlug = slugParam || (effectiveName && !urlPath && !isFernHost ? nameToSlug(effectiveName) : urlToSlug(url));
// A URL that is already on the leaderboard keeps its stored slug, so re-submitting it
// updates that entry instead of creating a second one under a URL-derived slug (which is
// what curated entries with a path — e.g. developer.salesforce.com/docs — would otherwise get).
const storedSlugForUrl = slugParam ? null : await getScoreSlugByDocsUrl(url);
const rawSlug =
slugParam ||
storedSlugForUrl ||
(effectiveName && !urlPath && !isFernHost ? nameToSlug(effectiveName) : urlToSlug(url));
// Alias a likely-typed domain (e.g. "monday" → "developer-monday-com-api-reference") to a curated
// leaderboard entry. This is a *redirect for lookups only*: we surface the existing canonical entry
// but never score/overwrite it. Actual scoring always stores under the raw slug (see runJob below).
Expand Down
3 changes: 3 additions & 0 deletions lib/slug-aliases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ export const SLUG_ALIASES: Record<string, string> = {
// entry instead of scoring (or rejecting) the apex. Submitting monday.com navigates
// here rather than showing a "not eligible" error.
monday: 'developer-monday-com-api-reference',
// URL-derived duplicate of the curated `salesforce` entry — same docs site, so
// surface the curated entry instead of a second grade for the same URL.
'developer-salesforce-com-docs': 'salesforce',
};

export function resolveSlugAlias(slug: string): string {
Expand Down
20 changes: 20 additions & 0 deletions lib/supabase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,26 @@ export async function getScoreBySlug(slug: string): Promise<CompanyScore | null>
}
}

// Finds the slug an already-scored docs URL is stored under, ignoring trailing-slash
// and case differences. Visible entries win over hidden ones so a curated slug is
// preferred over a previously auto-generated duplicate.
export async function getScoreSlugByDocsUrl(docsUrl: string): Promise<string | null> {
const normalized = docsUrl.trim().replace(/\/+$/, '').toLowerCase();
try {
const { rows } = await query<{ slug: string }>(
`SELECT slug FROM public.scores
WHERE lower(regexp_replace(docs_url, '/+$', '')) = $1
ORDER BY hidden ASC, scored_at DESC
LIMIT 1`,
[normalized]
);
return rows[0]?.slug ?? null;
} catch (err) {
console.error('[scores] getScoreSlugByDocsUrl error:', err instanceof Error ? err.message : err);
return null;
}
}

export async function deleteScoresByFilter(filter: { slugs?: string[]; docsUrls?: string[] }): Promise<void> {
if (filter.slugs?.length) {
try {
Expand Down
34 changes: 34 additions & 0 deletions test/score-slug-by-docs-url.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { afterEach, describe, expect, it, vi } from "vitest";

const query = vi.fn();
vi.mock("@/lib/db", () => ({ query: (...args: unknown[]) => query(...args) }));

afterEach(() => query.mockReset());

async function getScoreSlugByDocsUrl(url: string) {
const mod = await import("@/lib/supabase");
return mod.getScoreSlugByDocsUrl(url);
}

describe("getScoreSlugByDocsUrl", () => {
it("returns the slug an already-scored URL is stored under", async () => {
query.mockResolvedValue({ rows: [{ slug: "salesforce" }] });
await expect(getScoreSlugByDocsUrl("https://developer.salesforce.com/docs")).resolves.toBe("salesforce");
});

it("normalizes trailing slashes and case before matching", async () => {
query.mockResolvedValue({ rows: [] });
await getScoreSlugByDocsUrl("https://Developer.Salesforce.com/docs//");
expect(query.mock.calls[0][1]).toEqual(["https://developer.salesforce.com/docs"]);
});

it("returns null for URLs with no entry", async () => {
query.mockResolvedValue({ rows: [] });
await expect(getScoreSlugByDocsUrl("https://docs.example.com")).resolves.toBeNull();
});

it("returns null when the lookup fails", async () => {
query.mockRejectedValue(new Error("connection refused"));
await expect(getScoreSlugByDocsUrl("https://docs.example.com")).resolves.toBeNull();
});
});
4 changes: 4 additions & 0 deletions test/slug-aliases.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ describe("resolveSlugAlias", () => {
expect(resolveSlugAlias("monday")).toBe("developer-monday-com-api-reference");
});

it("maps the URL-derived salesforce duplicate to the curated entry", () => {
expect(resolveSlugAlias("developer-salesforce-com-docs")).toBe("salesforce");
});

it("passes through slugs that have no alias", () => {
expect(resolveSlugAlias("stripe")).toBe("stripe");
expect(resolveSlugAlias("developer-monday-com-api-reference")).toBe("developer-monday-com-api-reference");
Expand Down