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
31 changes: 29 additions & 2 deletions lib/socials/__tests__/normalizeProfileUrl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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");
});
});
11 changes: 9 additions & 2 deletions lib/socials/normalizeProfileUrl.ts
Original file line number Diff line number Diff line change
@@ -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");
}
Loading