sociallinks.dev

Blogg / Utvecklarguide

Så tolkar och normaliserar du profiladresser i sociala medier (Instagram, TikTok, YouTube, X)

Kreatörer klistrar in sina profillänkar i alla tänkbara former: med och utan www, från mobilappen med spårningsparametrar, som kortlänkar, som gamla twitter.com-adresser eller som en länk till ett inlägg i stället för till profilen. Om din produkt sparar strängarna precis som de kommer in dyker samma kreatör upp tre gånger och dina joins missar i tysthet. Den här guiden visar hur du gör om en profiladress från Instagram, TikTok, YouTube eller X till en plattform, ett handle eller kanal-ID och en kanonisk URL, med testad kod i JavaScript och Python.

Varför det är viktigt att normalisera profiladresser

En profiladress är en identifierare, och identifierare fungerar bara när de skrivs på ett enda sätt. https://www.instagram.com/natgeo/, instagram.com/NatGeo?igsh=MWQ1ZGUxMzBkMA== och https://m.instagram.com/natgeo pekar alla på samma konto, men som strängar är de tre olika värden.

Målet är en liten och strikt funktion: den accepterar det användare faktiskt klistrar in, returnerar ett normaliserat resultat för riktiga profiladresser och returnerar null för allt annat.

URL-formerna du kommer att stöta på

Formerna nedan kontrollerades mot de skarpa webbplatserna i oktober 2026. Plattformarna ändrar sin routing utan förvarning, så se det här som en ögonblicksbild och håll dina testfall uppdaterade.

Instagram

TikTok

YouTube

X (tidigare Twitter)

Teckenregler för handles per plattform

Att validera handlet fångar stavfel och de flesta sökvägar som inte är profiler. Använd plattformarnas egna regler där de publicerar dem, och håll kontrollerna generösa där de inte gör det, så att du aldrig avvisar ett riktigt konto.

PlattformTillåtna teckenLängdSkiftlägeKälla
InstagramBokstäver, siffror, punkter, understreck30 anges ofta, men finns inte i Instagrams hjälpsidorBehandla som skiftlägesokänsligt, spara i gemenerInstagrams hjälpcenter nämner punkter, siffror och understreck
TikTokBokstäver, siffror, understreck, punkter, får inte sluta med punkt2 till 24Skiftlägesokänsligt i adresser (@TikTok leder till tiktok)TikTok Support, byta användarnamn
YouTubeBokstäver eller siffror från språk som stöds, plus understreck, bindestreck, punkter och latinska mittpunkter (inte först eller sist)3 till 30 för de flesta skriftsystem, kortare gränser för han, hangul och några andra skriftsystemInte skiftlägeskänsligtYouTube-hjälp, riktlinjer för handles
XBokstäver, siffror och understreckUpp till 15, nya användarnamn måste vara längre än 4, men äldre korta handles finnsInte skiftlägeskänsligt (x.com/JACK kanoniseras till x.com/jack)X Help Center, regler för användarnamn

En algoritm för normalisering

  1. Step 1Trimma och lägg till ett schema

    Ta bort blanksteg och lösa vinkelparenteser. Om indatan saknar schema, lägg till https:// först så att instagram.com/natgeo går att tolka.

  2. Step 2Tolka med en riktig URL-tolk

    Använd new URL() i JavaScript eller urllib.parse.urlsplit i Python, aldrig ett reguljärt uttryck över hela strängen. En tolk hanterar portar, användarinformation och kodade tecken, och stoppar https://instagram.com@evil.com/natgeo från att passera som Instagram.

  3. Step 3Normalisera värden

    Gör om den till gemener och ta bort ett inledande www., m. eller mobile.. Slå sedan upp den i en exakt lista över tillåtna värdar. Använd aldrig includes("instagram.com"), som även matchar instagram.com.evil.com.

  4. Step 4Ta bort frågesträngen och fragmentet

    Spårningsparametrar som igsh, s, t och lang ändrar aldrig vilken profil en URL pekar på.

  5. Step 5Dela upp och avkoda sökvägen

    Dela på /, ta bort tomma segment (det tar bort avslutande snedstreck) och procentavkoda varje segment. Behandla felaktig kodning som ogiltig indata.

  6. Step 6Dirigera per plattform

    Tillämpa varje plattforms form: ett segment på Instagram och X, ett @-segment på TikTok, och @handle, channel/UC..., user/name eller c/name på YouTube. Avvisa reserverade ord.

  7. Step 7Validera handlet och gör om det till gemener

    Kontrollera handlet mot plattformens teckenregler och gör sedan om det till gemener. Gör inte om YouTube-kanal-ID:n till gemener: de är skiftlägeskänsliga.

  8. Step 8Bygg den kanoniska URL:en

    Bygg upp URL:en igen från delarna på en fast värd, så att varje likvärdig indata ger exakt samma sträng.

  9. Step 9Hantera kortlänkar för sig

    Känn igen TikToks kortlänkar, följ omdirigeringen med en HTTP-förfrågan och kör den slutliga URL:en genom tolken igen. Håll det här utanför den rena funktionen så att tolkningen förblir snabb och testbar.

Koden i JavaScript och Python

Båda funktionerna returnerar samma objekt för samma indata: { platform, handle, canonicalUrl } för handle-adresser, { platform, channelId, canonicalUrl } för YouTube-kanal-ID:n, username eller customName för de två äldre YouTube-formerna, och null för allt som inte är en profiladress. Ingen av dem kräver något beroende.

JavaScript (Node 18+)

Node.js

const HOSTS = {
  "instagram.com": "instagram",
  "tiktok.com": "tiktok",
  "youtube.com": "youtube",
  "x.com": "x",
  "twitter.com": "x",
};

const IG_RESERVED = new Set(["p", "reel", "reels", "tv", "stories", "explore", "accounts", "direct"]);
const X_RESERVED = new Set([
  "home", "explore", "search", "i", "settings", "notifications", "messages",
  "compose", "intent", "hashtag", "login", "signup", "logout", "tos", "privacy",
]);

const IG_HANDLE = /^[a-z0-9._]{1,30}$/;
const TIKTOK_HANDLE = /^[a-z0-9._]{2,24}$/;
const X_HANDLE = /^[a-z0-9_]{1,15}$/;
const YT_HANDLE = /^[\p{L}\p{M}\p{N}._\-·]{1,30}$/u;
const YT_CHANNEL_ID = /^UC[A-Za-z0-9_-]{22}$/;
const YT_LEGACY_NAME = /^[A-Za-z0-9._-]{1,100}$/;

export function parseProfileUrl(input) {
  if (typeof input !== "string") return null;
  let raw = input.trim().replace(/^<|>$/g, "");
  if (!raw) return null;
  if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(raw)) raw = "https://" + raw;

  let url;
  try {
    url = new URL(raw);
  } catch {
    return null;
  }
  if (url.protocol !== "https:" && url.protocol !== "http:") return null;

  const host = url.hostname.toLowerCase().replace(/^(www|m|mobile)\./, "");
  const platform = HOSTS[host];
  if (!platform) return null;

  let segments;
  try {
    segments = url.pathname.split("/").filter(Boolean).map(decodeURIComponent);
  } catch {
    return null; // malformed percent-encoding
  }
  const [first = "", second] = segments;

  if (platform === "instagram") {
    const handle = first.toLowerCase();
    if (segments.length !== 1 || IG_RESERVED.has(handle) || !IG_HANDLE.test(handle)) return null;
    return { platform, handle, canonicalUrl: `https://www.instagram.com/${handle}/` };
  }

  if (platform === "tiktok") {
    if (segments.length !== 1 || !first.startsWith("@")) return null;
    const handle = first.slice(1).toLowerCase();
    if (!TIKTOK_HANDLE.test(handle) || handle.endsWith(".")) return null;
    return { platform, handle, canonicalUrl: `https://www.tiktok.com/@${handle}` };
  }

  if (platform === "x") {
    const handle = first.toLowerCase();
    if (segments.length !== 1 || X_RESERVED.has(handle) || !X_HANDLE.test(handle)) return null;
    return { platform, handle, canonicalUrl: `https://x.com/${handle}` };
  }

  // YouTube: /@handle[/tab], /channel/UC...[/tab], /user/name, /c/name
  if (first.startsWith("@")) {
    const handle = first.slice(1).toLowerCase();
    if (!YT_HANDLE.test(handle) || /^[._\-·]|[._\-·]$/.test(handle)) return null;
    return { platform, handle, canonicalUrl: `https://www.youtube.com/@${encodeURIComponent(handle)}` };
  }
  if (first === "channel" && second && YT_CHANNEL_ID.test(second)) {
    return { platform, channelId: second, canonicalUrl: `https://www.youtube.com/channel/${second}` };
  }
  if ((first === "user" || first === "c") && second && segments.length === 2 && YT_LEGACY_NAME.test(second)) {
    const key = first === "user" ? "username" : "customName";
    return { platform, [key]: second, canonicalUrl: `https://www.youtube.com/${first}/${second}` };
  }
  return null; // videos, shorts, playlists, search and anything else
}

const SHORT_HOSTS = new Set(["vm.tiktok.com", "vt.tiktok.com"]);

export function isShortLink(input) {
  try {
    const url = new URL(/^https?:\/\//i.test(input) ? input : "https://" + input);
    const host = url.hostname.toLowerCase();
    return SHORT_HOSTS.has(host) || (host.replace(/^www\./, "") === "tiktok.com" && url.pathname.startsWith("/t/"));
  } catch {
    return false;
  }
}

export async function resolveShortLink(input) {
  const res = await fetch(/^https?:\/\//i.test(input) ? input : "https://" + input, {
    redirect: "follow",
    signal: AbortSignal.timeout(10_000),
  });
  await res.body?.cancel();
  return res.url; // the final URL after every redirect
}

resolveShortLink använder den inbyggda fetch, som följer omdirigeringar och exponerar den slutliga adressen som res.url. Anropa parseProfileUrl(await resolveShortLink(link)) när isShortLink(link) är sant. En kortlänk kan också leda till en video i stället för en profil, och då returnerar tolken korrekt null.

Python 3

Python

import re
import unicodedata
import urllib.request
from urllib.parse import quote, unquote, urlsplit

HOSTS = {
    "instagram.com": "instagram",
    "tiktok.com": "tiktok",
    "youtube.com": "youtube",
    "x.com": "x",
    "twitter.com": "x",
}

IG_RESERVED = {"p", "reel", "reels", "tv", "stories", "explore", "accounts", "direct"}
X_RESERVED = {
    "home", "explore", "search", "i", "settings", "notifications", "messages",
    "compose", "intent", "hashtag", "login", "signup", "logout", "tos", "privacy",
}

IG_HANDLE = re.compile(r"[a-z0-9._]{1,30}")
TIKTOK_HANDLE = re.compile(r"[a-z0-9._]{2,24}")
X_HANDLE = re.compile(r"[a-z0-9_]{1,15}")
YT_CHANNEL_ID = re.compile(r"UC[A-Za-z0-9_-]{22}")
YT_LEGACY_NAME = re.compile(r"[A-Za-z0-9._-]{1,100}")
YT_PUNCT = "._-·"


def _is_youtube_handle(handle):
    if not 1 <= len(handle) <= 30 or handle[0] in YT_PUNCT or handle[-1] in YT_PUNCT:
        return False
    return all(
        ch.isalnum() or ch in YT_PUNCT or unicodedata.category(ch).startswith("M")
        for ch in handle
    )


def parse_profile_url(value):
    if not isinstance(value, str):
        return None
    raw = value.strip().strip("<>")
    if not raw:
        return None
    if not re.match(r"^[a-z][a-z0-9+.-]*://", raw, re.I):
        raw = "https://" + raw

    try:
        url = urlsplit(raw)
        host = (url.hostname or "").lower()
    except ValueError:
        return None
    if url.scheme.lower() not in ("http", "https"):
        return None

    host = re.sub(r"^(www|m|mobile)\.", "", host)
    platform = HOSTS.get(host)
    if not platform:
        return None

    try:
        segments = [unquote(s, errors="strict") for s in url.path.split("/") if s]
    except UnicodeDecodeError:
        return None  # malformed percent-encoding
    first = segments[0] if segments else ""
    second = segments[1] if len(segments) > 1 else None

    if platform == "instagram":
        handle = first.lower()
        if len(segments) != 1 or handle in IG_RESERVED or not IG_HANDLE.fullmatch(handle):
            return None
        return {"platform": platform, "handle": handle,
                "canonicalUrl": f"https://www.instagram.com/{handle}/"}

    if platform == "tiktok":
        if len(segments) != 1 or not first.startswith("@"):
            return None
        handle = first[1:].lower()
        if not TIKTOK_HANDLE.fullmatch(handle) or handle.endswith("."):
            return None
        return {"platform": platform, "handle": handle,
                "canonicalUrl": f"https://www.tiktok.com/@{handle}"}

    if platform == "x":
        handle = first.lower()
        if len(segments) != 1 or handle in X_RESERVED or not X_HANDLE.fullmatch(handle):
            return None
        return {"platform": platform, "handle": handle, "canonicalUrl": f"https://x.com/{handle}"}

    # YouTube: /@handle[/tab], /channel/UC...[/tab], /user/name, /c/name
    if first.startswith("@"):
        handle = first[1:].lower()
        if not _is_youtube_handle(handle):
            return None
        return {"platform": platform, "handle": handle,
                "canonicalUrl": f"https://www.youtube.com/@{quote(handle, safe='._-')}"}
    if first == "channel" and second and YT_CHANNEL_ID.fullmatch(second):
        return {"platform": platform, "channelId": second,
                "canonicalUrl": f"https://www.youtube.com/channel/{second}"}
    if first in ("user", "c") and second and len(segments) == 2 and YT_LEGACY_NAME.fullmatch(second):
        key = "username" if first == "user" else "customName"
        return {"platform": platform, key: second,
                "canonicalUrl": f"https://www.youtube.com/{first}/{second}"}
    return None  # videos, shorts, playlists, search and anything else


SHORT_HOSTS = {"vm.tiktok.com", "vt.tiktok.com"}


def is_short_link(value):
    url = urlsplit(value if re.match(r"^https?://", value, re.I) else "https://" + value)
    host = (url.hostname or "").lower()
    return host in SHORT_HOSTS or (
        re.sub(r"^www\.", "", host) == "tiktok.com" and url.path.startswith("/t/")
    )


def resolve_short_link(value, timeout=10):
    if not re.match(r"^https?://", value, re.I):
        value = "https://" + value
    request = urllib.request.Request(value, headers={"User-Agent": "Mozilla/5.0"})
    with urllib.request.urlopen(request, timeout=timeout) as response:
        return response.geturl()  # the final URL after every redirect

Pythons \w och str.isalnum() täcker Unicode-bokstäver och siffror men inte de kombinerande tecken som skriftsystem som devanagari använder. Därför accepterar YouTube-kontrollen även tecken i Unicode-kategorierna M.

Kantfall och tester

Varje rad nedan ingår i testsviten som båda implementationerna klarar.

IndataUtdataVarför
instagram.com/NatGeo?igsh=MWQ1ZGUxMzBkMA==instagram, natgeo, https://www.instagram.com/natgeo/Inget schema, blandat skiftläge, delningsparameter
https://m.instagram.com/nat.geo_/#bioinstagram, nat.geo_Mobilvärd och fragment
https://www.instagram.com/p/C9xYzAbCdEf/nullEtt inlägg, inte en profil
https://www.instagram.com/stories/natgeo/1234567890/nullEn story, trots att handlet finns i sökvägen
tiktok.com/@Khaby.Lame/tiktok, khaby.lame, https://www.tiktok.com/@khaby.lameAvslutande snedstreck och skiftläge
https://www.tiktok.com/@khaby.lame/video/7106594312292453675nullEn videoadress
https://vm.tiktok.com/ZMabc123/null (följ omdirigeringen först)Kortlänk utan handle
https://m.youtube.com/@GoogleDevelopers/videos?app=desktopyoutube, googledevelopersMobilvärd, kanalflik, frågesträng
https://www.youtube.com/channel/UC_x5XG1OV2P6uZZ5FSM9Ttw/featuredyoutube, channelId UC_x5XG1OV2P6uZZ5FSM9TtwKanal-ID:t behåller sitt skiftläge
https://www.youtube.com/c/GoogleDevelopersyoutube, customName GoogleDevelopersÄldre anpassad URL, kräver ett uppslag för ID:t
https://www.youtube.com/@%E3%83%86%E3%82%B9%E3%83%88youtube, handle テストProcentkodat Unicode-handle
https://youtu.be/dQw4w9WgXcQnullEn video, inte en kanal
https://mobile.twitter.com/jack?s=21&t=abcx, jack, https://x.com/jackGammal domän och delningsparametrar
https://x.com/i/flow/loginnullApp-rutt
https://x.com/this_handle_is_too_longnullFler än 15 tecken
https://instagram.com.evil.com/natgeonullVärd som ser likadan ut
https://instagram.com@evil.com/natgeonullTrick med användarinformation, den riktiga värden är evil.com
@natgeonullEtt handle utan URL har ingen plattform

Ha testfallen i ditt repo och kör dem i CI. När en plattform lägger till en rutt eller en delningsparameter lägger du till en rad, och lösningen blir uppenbar.

Node.js-test

import assert from "node:assert/strict";
import { parseProfileUrl } from "./parse.mjs";

const cases = [
  ["instagram.com/NatGeo?igsh=MWQ1ZGUxMzBkMA==", { platform: "instagram", handle: "natgeo", canonicalUrl: "https://www.instagram.com/natgeo/" }],
  ["https://www.instagram.com/reel/C9xYzAbCdEf/", null],
  ["https://www.instagram.com/explore/", null],
  ["tiktok.com/@Khaby.Lame/", { platform: "tiktok", handle: "khaby.lame", canonicalUrl: "https://www.tiktok.com/@khaby.lame" }],
  ["https://www.tiktok.com/@khaby.lame/video/7106594312292453675", null],
  ["https://vm.tiktok.com/ZMabc123/", null],
  ["https://m.youtube.com/@GoogleDevelopers/videos", { platform: "youtube", handle: "googledevelopers", canonicalUrl: "https://www.youtube.com/@googledevelopers" }],
  ["https://www.youtube.com/channel/UC_x5XG1OV2P6uZZ5FSM9Ttw/featured", { platform: "youtube", channelId: "UC_x5XG1OV2P6uZZ5FSM9Ttw", canonicalUrl: "https://www.youtube.com/channel/UC_x5XG1OV2P6uZZ5FSM9Ttw" }],
  ["https://www.youtube.com/user/GoogleDevelopers", { platform: "youtube", username: "GoogleDevelopers", canonicalUrl: "https://www.youtube.com/user/GoogleDevelopers" }],
  ["https://youtu.be/dQw4w9WgXcQ", null],
  ["https://twitter.com/Jack/", { platform: "x", handle: "jack", canonicalUrl: "https://x.com/jack" }],
  ["https://mobile.twitter.com/jack?s=21", { platform: "x", handle: "jack", canonicalUrl: "https://x.com/jack" }],
  ["https://x.com/jack/status/20", null],
  ["https://x.com/i/flow/login", null],
  ["https://instagram.com.evil.com/natgeo", null],
];

for (const [input, expected] of cases) {
  assert.deepEqual(parseProfileUrl(input), expected, input);
}
console.log(`${cases.length} cases passed`);

Python-test

from parse_profile import parse_profile_url

CASES = [
    ("instagram.com/NatGeo?igsh=MWQ1ZGUxMzBkMA==", {"platform": "instagram", "handle": "natgeo", "canonicalUrl": "https://www.instagram.com/natgeo/"}),
    ("https://www.instagram.com/reel/C9xYzAbCdEf/", None),
    ("tiktok.com/@Khaby.Lame/", {"platform": "tiktok", "handle": "khaby.lame", "canonicalUrl": "https://www.tiktok.com/@khaby.lame"}),
    ("https://vm.tiktok.com/ZMabc123/", None),
    ("https://m.youtube.com/@GoogleDevelopers/videos", {"platform": "youtube", "handle": "googledevelopers", "canonicalUrl": "https://www.youtube.com/@googledevelopers"}),
    ("https://www.youtube.com/channel/UC_x5XG1OV2P6uZZ5FSM9Ttw/featured", {"platform": "youtube", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "canonicalUrl": "https://www.youtube.com/channel/UC_x5XG1OV2P6uZZ5FSM9Ttw"}),
    ("https://youtu.be/dQw4w9WgXcQ", None),
    ("https://twitter.com/Jack/", {"platform": "x", "handle": "jack", "canonicalUrl": "https://x.com/jack"}),
    ("https://x.com/i/flow/login", None),
    ("https://instagram.com.evil.com/natgeo", None),
]

for value, expected in CASES:
    assert parse_profile_url(value) == expected, value
print(f"{len(CASES)} cases passed")

Spara både rå indata och den kanoniska formen

Normalisering förlorar information med flit, och dina regler kommer att bli bättre med tiden. Spara båda värdena:

Handles är inte permanenta. Kreatörer byter namn på sina konton på alla plattformar, och ett handle du sparade förra året kan nu tillhöra någon annan. Där ett officiellt API ger dig ett stabilt ID, som ett YouTube-kanal-ID, spara det som långsiktig nyckel och behandla handlet som ett visningsvärde som du uppdaterar. Den kompletterande guiden om att få fram ett YouTube-kanal-ID från ett handle visar hur.

Från en profil till de andra

När en profiladress är normaliserad är nästa fråga oftast vilka andra konton som tillhör samma kreatör. sociallinks.dev är ett API som tar en profiladress från Instagram, TikTok, YouTube eller X och returnerar kreatörens konton på andra plattformar, med en konfidenspoäng och bevisen för varje matchning.

Vanliga frågor

Ska jag göra om handles till gemener innan jag sparar dem?

Ja, för uppslagsnyckeln. YouTube och X säger att handles inte är skiftlägeskänsliga, och TikTok leder @TikTok till samma konto som @tiktok. Instagram dokumenterar ingen regel för skiftläge, men användarnamn visas med gemener, så gemener är det säkra standardvalet. Behåll den ursprungliga stavningen i den råa URL:en om du vill visa den. Gör inte om YouTube-kanal-ID:n till gemener, eftersom de är skiftlägeskänsliga.

Kan jag plocka ut handlet ur en adress till ett inlägg eller en video?

För TikTok-videor (/@handle/video/id) och inlägg på X (/handle/status/id) är handlet i sökvägen upphovspersonen, så du kan plocka ut det om det är vad du vill. Adresser till inlägg och reels på Instagram (/p/ och /reel/) innehåller inte handlet. Funktionerna i den här guiden returnerar null för alla dessa eftersom de inte är profillänkar.

Hur hanterar jag vm.tiktok.com-länkar?

Kortlänkar innehåller bara en kod. Gör en HTTP-förfrågan, följ omdirigeringarna och tolka den slutliga URL:en. Gör det i ett separat steg med timeout och cache, eftersom det är ett nätverksanrop och koden kan gå ut.

Är twitter.com fortfarande en giltig profiladress?

Ja. Profiladresser på twitter.com omdirigerar till x.com, så behandla båda domänerna som plattformen X och spara x.com-formen som kanonisk.

Varför inte använda ett enda stort reguljärt uttryck?

Ett enda reguljärt uttryck över hela strängen är svårläst och lätt att lura med värdar som ser likadana ut, användarinformation eller kodade tecken. Tolka först URL:en med standardbiblioteket och använd sedan små reguljära uttryck bara på handlet.

Hur gör jag om en YouTube-adress med /c/ eller /user/ till ett kanal-ID?

Adresser med /user/<name> motsvarar filtret forUsername i YouTube Data API. Det finns inget API-filter för anpassade /c/-namn. Läs guiden om att få fram ett YouTube-kanal-ID från ett handle för båda fallen.

Hitta en kreatörs konton