sociallinks.dev

Blog / Guida per sviluppatori

Come analizzare e normalizzare gli URL dei profili social (Instagram, TikTok, YouTube, X)

I creator incollano i link ai loro profili in tutte le forme possibili: con e senza www, dall’app mobile con parametri di tracciamento, come short link, come vecchi URL twitter.com, o come link a un post invece che al profilo. Se il tuo prodotto salva queste stringhe così come arrivano, lo stesso creator compare tre volte e i tuoi join falliscono senza avvisare. Questa guida mostra come trasformare qualsiasi URL di profilo Instagram, TikTok, YouTube o X in una piattaforma, un handle o un ID canale e un unico URL canonico, con codice testato in JavaScript e Python.

Perché normalizzare gli URL dei profili

Un URL di profilo è un identificatore, e gli identificatori funzionano solo quando si scrivono in un unico modo. https://www.instagram.com/natgeo/, instagram.com/NatGeo?igsh=MWQ1ZGUxMzBkMA== e https://m.instagram.com/natgeo puntano tutti allo stesso account, ma come stringhe sono tre valori diversi.

L’obiettivo è una funzione piccola e rigorosa: accetta ciò che gli utenti incollano davvero, restituisce un risultato normalizzato per gli URL di profilo reali e restituisce null per tutto il resto.

Le forme di URL che incontrerai

Le forme qui sotto sono state verificate sui siti reali a ottobre 2026. Le piattaforme cambiano il routing senza preavviso, quindi considera questo elenco una fotografia del momento e tieni aggiornati i tuoi casi di test.

Instagram

TikTok

YouTube

X (ex Twitter)

Regole sui caratteri degli handle per piattaforma

Validare l’handle intercetta gli errori di battitura e la maggior parte dei percorsi che non sono profili. Usa le regole delle piattaforme dove le pubblicano, e tieni i controlli più permissivi dove non lo fanno, così non scarti mai un account reale.

PiattaformaCaratteri ammessiLunghezzaMaiuscole e minuscoleFonte
InstagramLettere, numeri, punti, trattini bassi30 è il valore citato più spesso; non compare nelle pagine di assistenza di InstagramConsidera che non distingua maiuscole e minuscole; salva in minuscoloIl Centro assistenza di Instagram cita punti, numeri e trattini bassi
TikTokLettere, numeri, trattini bassi, punti; non può finire con un puntoDa 2 a 24Non distingue maiuscole e minuscole negli URL (@TikTok porta a tiktok)Assistenza TikTok, modifica del nome utente
YouTubeLettere o numeri delle lingue supportate, più trattini bassi, trattini, punti e punti mediani latini (non all’inizio né alla fine)Da 3 a 30 per la maggior parte degli alfabeti; limiti più brevi per han, hangul e alcuni altri alfabetiNon distingue maiuscole e minuscoleAssistenza YouTube, linee guida sugli handle
XLettere, numeri e trattini bassiFino a 15; i nuovi username devono superare i 4 caratteri, ma esistono vecchi handle più cortiNon distingue maiuscole e minuscole (x.com/JACK diventa x.com/jack)Centro assistenza di X, regole sui nomi utente

Un algoritmo di normalizzazione

  1. Step 1Ripulisci e aggiungi uno schema

    Togli spazi e parentesi angolari di troppo. Se l’input non ha uno schema, anteponi https:// così anche instagram.com/natgeo viene analizzato.

  2. Step 2Analizza con un vero parser di URL

    Usa new URL() in JavaScript o urllib.parse.urlsplit in Python, mai una regex sull’intera stringa. Un parser gestisce porte, informazioni utente e caratteri codificati, e impedisce che https://instagram.com@evil.com/natgeo passi per Instagram.

  3. Step 3Normalizza l’host

    Mettilo in minuscolo e togli un solo prefisso iniziale www., m. o mobile.. Poi cercalo in una allow-list esatta di host. Non usare mai includes("instagram.com"), che corrisponde anche a instagram.com.evil.com.

  4. Step 4Elimina query string e frammento

    Parametri di tracciamento come igsh, s, t e lang non cambiano mai il profilo a cui punta un URL.

  5. Step 5Dividi e decodifica il percorso

    Dividi su /, elimina i segmenti vuoti (così sparisce la barra finale) e decodifica il percent-encoding di ciascuno. Considera input non valido una codifica malformata.

  6. Step 6Smista per piattaforma

    Applica la forma di ogni piattaforma: un segmento su Instagram e X, un segmento con @ su TikTok, e @handle, channel/UC..., user/name o c/name su YouTube. Scarta le parole riservate.

  7. Step 7Valida l’handle e mettilo in minuscolo

    Controlla l’handle con le regole sui caratteri della piattaforma, poi mettilo in minuscolo. Non mettere in minuscolo gli ID dei canali YouTube: distinguono maiuscole e minuscole.

  8. Step 8Costruisci l’URL canonico

    Ricostruisci l’URL dalle sue parti su un unico host fisso, così ogni input equivalente produce esattamente la stessa stringa.

  9. Step 9Gestisci gli short link a parte

    Riconosci gli short link di TikTok, segui il redirect con una richiesta HTTP e ripassa l’URL finale nel parser. Tieni questo passaggio fuori dalla funzione pura, così l’analisi resta veloce e testabile.

Il codice in JavaScript e Python

Entrambe le funzioni restituiscono gli stessi oggetti per lo stesso input: { platform, handle, canonicalUrl } per gli URL con handle, { platform, channelId, canonicalUrl } per gli ID dei canali YouTube, username o customName per le due forme legacy di YouTube, e null per tutto ciò che non è un URL di profilo. Nessuna delle due richiede dipendenze.

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 usa il fetch integrato, che segue i redirect ed espone l’indirizzo finale come res.url. Chiama parseProfileUrl(await resolveShortLink(link)) quando isShortLink(link) è vero. Uno short link può anche portare a un video invece che a un profilo: in quel caso il parser restituisce correttamente 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

In Python \w e str.isalnum() coprono lettere e cifre Unicode ma non i segni combinanti usati da alfabeti come il devanagari, ed è per questo che il controllo per YouTube accetta anche i caratteri delle categorie Unicode M.

Casi limite e test

Ogni riga qui sotto fa parte della suite di test che entrambe le implementazioni superano.

InputOutputPerché
instagram.com/NatGeo?igsh=MWQ1ZGUxMzBkMA==instagram, natgeo, https://www.instagram.com/natgeo/Nessuno schema, maiuscole e minuscole miste, parametro di condivisione
https://m.instagram.com/nat.geo_/#bioinstagram, nat.geo_Host mobile e frammento
https://www.instagram.com/p/C9xYzAbCdEf/nullUn post, non un profilo
https://www.instagram.com/stories/natgeo/1234567890/nullUna storia, anche se l’handle è nel percorso
tiktok.com/@Khaby.Lame/tiktok, khaby.lame, https://www.tiktok.com/@khaby.lameBarra finale e maiuscole
https://www.tiktok.com/@khaby.lame/video/7106594312292453675nullUn URL di video
https://vm.tiktok.com/ZMabc123/null (prima segui il redirect)Short link senza handle
https://m.youtube.com/@GoogleDevelopers/videos?app=desktopyoutube, googledevelopersHost mobile, scheda del canale, query
https://www.youtube.com/channel/UC_x5XG1OV2P6uZZ5FSM9Ttw/featuredyoutube, channelId UC_x5XG1OV2P6uZZ5FSM9TtwL’ID canale mantiene maiuscole e minuscole
https://www.youtube.com/c/GoogleDevelopersyoutube, customName GoogleDevelopersURL personalizzato legacy; serve una ricerca per l’ID
https://www.youtube.com/@%E3%83%86%E3%82%B9%E3%83%88youtube, handle テストHandle Unicode con percent-encoding
https://youtu.be/dQw4w9WgXcQnullUn video, non un canale
https://mobile.twitter.com/jack?s=21&t=abcx, jack, https://x.com/jackVecchio dominio e parametri di condivisione
https://x.com/i/flow/loginnullRoute dell’app
https://x.com/this_handle_is_too_longnullPiù di 15 caratteri
https://instagram.com.evil.com/natgeonullHost che imita quello vero
https://instagram.com@evil.com/natgeonullTrucco con le informazioni utente; l’host reale è evil.com
@natgeonullUn handle da solo non ha piattaforma

Tieni i casi nel tuo repository ed eseguili in CI. Quando una piattaforma aggiunge una route o un parametro di condivisione, aggiungi una riga e la correzione diventa ovvia.

Test Node.js

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`);

Test Python

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")

Salva l’input grezzo e la forma canonica

La normalizzazione perde informazioni per definizione, e le tue regole miglioreranno nel tempo. Salva entrambi i valori:

Gli handle non sono permanenti. I creator rinominano i propri account su ogni piattaforma, e un handle che hai salvato l’anno scorso ora potrebbe appartenere a qualcun altro. Dove un’API ufficiale ti dà un ID stabile, come l’ID di un canale YouTube, salvalo come chiave a lungo termine e considera l’handle un valore da mostrare, da aggiornare periodicamente. La guida collegata su come ottenere l’ID di un canale YouTube da un handle mostra come fare.

Da un profilo agli altri

Una volta normalizzato un URL di profilo, la domanda successiva di solito è quali altri account appartengono allo stesso creator. sociallinks.dev è un’API che prende un URL di profilo Instagram, TikTok, YouTube o X e restituisce gli account del creator sulle altre piattaforme, con un punteggio di affidabilità e le prove di ogni collegamento.

Domande frequenti

Devo mettere in minuscolo gli handle prima di salvarli?

Sì, per la chiave di ricerca. YouTube e X dicono che gli handle non distinguono maiuscole e minuscole, e TikTok porta @TikTok allo stesso account di @tiktok. Instagram non documenta una regola, ma gli username vengono mostrati in minuscolo, quindi il minuscolo è la scelta sicura. Conserva le maiuscole originali nell’URL grezzo se vuoi mostrarle. Non mettere in minuscolo gli ID dei canali YouTube, che distinguono maiuscole e minuscole.

Posso estrarre l’handle dall’URL di un post o di un video?

Per i video TikTok (/@handle/video/id) e i post X (/handle/status/id) l’handle nel percorso è l’autore, quindi puoi estrarlo se è ciò che ti serve. Gli URL di post e reel di Instagram (/p/ e /reel/) non contengono l’handle. Le funzioni di questa guida restituiscono null per tutti questi casi, perché non sono link a profili.

Come gestisco i link vm.tiktok.com?

Gli short link contengono solo un codice. Fai una richiesta HTTP, segui i redirect e analizza l’URL finale. Fallo in un passaggio separato con un timeout e una cache, perché è una chiamata di rete e il codice può scadere.

twitter.com è ancora un URL di profilo valido?

Sì. Gli URL di profilo twitter.com reindirizzano a x.com, quindi tratta entrambi i domini come la piattaforma X e salva come canonica la forma x.com.

Perché non usare un’unica grande espressione regolare?

Una sola regex sull’intera stringa è difficile da leggere e facile da ingannare con host che imitano quelli veri, informazioni utente o caratteri codificati. Prima analizza l’URL con la libreria standard, poi usa piccole regex solo sull’handle.

Come trasformo un URL YouTube /c/ o /user/ in un ID canale?

Gli URL /user/<name> corrispondono al filtro forUsername della YouTube Data API. Non esiste un filtro API per i nomi personalizzati /c/. Consulta la guida su come ottenere l’ID di un canale YouTube da un handle per entrambi i casi.

Trova gli account di un creator