diff --git a/lib/socials/__tests__/normalizeProfileUrl.test.ts b/lib/socials/__tests__/normalizeProfileUrl.test.ts index f501e75f..1184feee 100644 --- a/lib/socials/__tests__/normalizeProfileUrl.test.ts +++ b/lib/socials/__tests__/normalizeProfileUrl.test.ts @@ -9,7 +9,7 @@ describe("normalizeProfileUrl", () => { }); it("removes http:// protocol from URL", () => { - expect(normalizeProfileUrl("http://twitter.com/user")).toBe("twitter.com/user"); + expect(normalizeProfileUrl("http://twitter.com/user")).toBe("x.com/user"); }); it("returns URL unchanged if no protocol", () => { @@ -40,6 +40,33 @@ describe("normalizeProfileUrl", () => { }); it("handles URL with www. but without protocol", () => { - expect(normalizeProfileUrl("www.twitter.com/user")).toBe("twitter.com/user"); + expect(normalizeProfileUrl("www.twitter.com/user")).toBe("x.com/user"); + }); + + it("canonicalizes twitter.com to x.com (chat#1851)", () => { + expect(normalizeProfileUrl("https://twitter.com/ashnikko")).toBe("x.com/ashnikko"); + expect(normalizeProfileUrl("https://www.twitter.com/KETTAMA_/")).toBe("x.com/KETTAMA_"); + expect(normalizeProfileUrl("twitter.com/disclosure")).toBe("x.com/disclosure"); + }); + + it("produces the same key for twitter.com and x.com spellings", () => { + expect(normalizeProfileUrl("https://x.com/Foo")).toBe( + normalizeProfileUrl("https://twitter.com/Foo"), + ); + }); + + it("leaves x.com URLs unchanged", () => { + expect(normalizeProfileUrl("https://x.com/ashnikko")).toBe("x.com/ashnikko"); + }); + + it("does not rewrite domains that merely contain twitter.com", () => { + expect(normalizeProfileUrl("https://nottwitter.com/user")).toBe("nottwitter.com/user"); + expect(normalizeProfileUrl("https://example.com/twitter.com/user")).toBe( + "example.com/twitter.com/user", + ); + }); + + it("canonicalizes a bare twitter.com domain with no path", () => { + expect(normalizeProfileUrl("https://twitter.com")).toBe("x.com"); }); }); diff --git a/lib/socials/normalizeProfileUrl.ts b/lib/socials/normalizeProfileUrl.ts index 2d45a231..667913e1 100644 --- a/lib/socials/normalizeProfileUrl.ts +++ b/lib/socials/normalizeProfileUrl.ts @@ -1,15 +1,22 @@ /** - * Normalizes a profile URL by removing the protocol, www. prefix, and trailing slash. + * Normalizes a profile URL by removing the protocol, www. prefix, and trailing slash, + * and canonicalizing domain aliases so equivalent profiles produce one upsert key. * This ensures consistent URL format for database queries and storage. * + * Domain aliases: `twitter.com` → `x.com` (official domain, and what the Apify + * actor echoes). Without this, scrape write-backs land on an `x.com/*` twin row + * while artists stay connected to a stale `twitter.com/*` row (chat#1851). + * * @param url - The profile URL to normalize * @returns The normalized URL without protocol, www., or trailing slash */ export function normalizeProfileUrl(url: string | null | undefined): string { if (!url) return ""; - return url + const stripped = url .replace(/^https?:\/\//, "") .replace(/^www\./, "") .replace(/\/$/, ""); + + return stripped.replace(/^twitter\.com(\/|$)/i, "x.com$1"); }