From 9ab01a9fe69e163d0ff00f8ed3a7dfc2801cd73b Mon Sep 17 00:00:00 2001 From: "cade.sarkin" Date: Tue, 4 Aug 2026 17:02:02 +0000 Subject: [PATCH] fix: collapse duplicate leaderboard entries for the same docs URL Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- app/api/score/route.ts | 11 ++++++++-- lib/slug-aliases.ts | 3 +++ lib/supabase.ts | 20 +++++++++++++++++ test/score-slug-by-docs-url.test.ts | 34 +++++++++++++++++++++++++++++ test/slug-aliases.test.ts | 4 ++++ 5 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 test/score-slug-by-docs-url.test.ts diff --git a/app/api/score/route.ts b/app/api/score/route.ts index c43059a..9d134c3 100644 --- a/app/api/score/route.ts +++ b/app/api/score/route.ts @@ -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"; @@ -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). diff --git a/lib/slug-aliases.ts b/lib/slug-aliases.ts index 417781a..a07922c 100644 --- a/lib/slug-aliases.ts +++ b/lib/slug-aliases.ts @@ -6,6 +6,9 @@ export const SLUG_ALIASES: Record = { // 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 { diff --git a/lib/supabase.ts b/lib/supabase.ts index 1b2f24a..214b412 100644 --- a/lib/supabase.ts +++ b/lib/supabase.ts @@ -116,6 +116,26 @@ export async function getScoreBySlug(slug: string): Promise } } +// 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 { + 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 { if (filter.slugs?.length) { try { diff --git a/test/score-slug-by-docs-url.test.ts b/test/score-slug-by-docs-url.test.ts new file mode 100644 index 0000000..81207a0 --- /dev/null +++ b/test/score-slug-by-docs-url.test.ts @@ -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(); + }); +}); diff --git a/test/slug-aliases.test.ts b/test/slug-aliases.test.ts index 109628b..ac843a9 100644 --- a/test/slug-aliases.test.ts +++ b/test/slug-aliases.test.ts @@ -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");