sociallinks.dev

Blog / Guide développeur

Comment analyser et normaliser les URL de profils de réseaux sociaux (Instagram, TikTok, YouTube, X)

Les créateurs collent leurs liens de profil sous toutes les formes possibles : avec ou sans www, depuis l’application mobile avec des paramètres de suivi, en liens courts, en anciennes URL twitter.com, ou en lien vers une publication plutôt que vers le profil. Si votre produit stocke ces chaînes telles quelles, le même créateur apparaît trois fois et vos jointures échouent sans bruit. Ce guide montre comment transformer n’importe quelle URL de profil Instagram, TikTok, YouTube ou X en une plateforme, un pseudo ou un ID de chaîne, et une URL canonique unique, avec du code testé en JavaScript et en Python.

Pourquoi normaliser les URL de profils

Une URL de profil est un identifiant, et un identifiant ne fonctionne que s’il s’écrit d’une seule façon. https://www.instagram.com/natgeo/, instagram.com/NatGeo?igsh=MWQ1ZGUxMzBkMA== et https://m.instagram.com/natgeo pointent toutes vers le même compte, mais en tant que chaînes ce sont trois valeurs différentes.

L’objectif est une petite fonction stricte : elle accepte ce que les utilisateurs collent réellement, renvoie un résultat normalisé pour les vraies URL de profil, et null pour tout le reste.

Les formes d’URL que vous rencontrerez

Les formes ci-dessous ont été vérifiées sur les sites en ligne en octobre 2026. Les plateformes modifient leur routage sans prévenir : voyez-y un instantané et tenez vos cas de test à jour.

Instagram

TikTok

YouTube

X (anciennement Twitter)

Règles de caractères des pseudos par plateforme

Valider le pseudo permet d’attraper les fautes de frappe et la plupart des chemins qui ne sont pas des profils. Appliquez les règles des plateformes là où elles les publient, et gardez des vérifications souples là où elles ne le font pas, pour ne jamais rejeter un vrai compte.

PlateformeCaractères autorisésLongueurCasseSource
InstagramLettres, chiffres, points, tirets bas30 est souvent cité ; introuvable dans les pages d’aide d’InstagramÀ traiter comme insensible à la casse ; stocker en minusculesLe centre d’aide Instagram mentionne points, chiffres et tirets bas
TikTokLettres, chiffres, tirets bas, points ; ne peut pas finir par un point2 à 24Insensible à la casse dans les URL (@TikTok renvoie vers tiktok)Assistance TikTok, modifier son nom d’utilisateur
YouTubeLettres ou chiffres des langues prises en charge, plus tirets bas, traits d’union, points et points médians latins (ni en premier ni en dernier)3 à 30 pour la plupart des écritures ; limites plus courtes pour le han, le hangûl et quelques autres écrituresInsensible à la casseAide YouTube, règles sur les pseudos
XLettres, chiffres et tirets basJusqu’à 15 ; les nouveaux noms doivent dépasser 4 caractères, mais d’anciens pseudos courts existentInsensible à la casse (x.com/JACK devient x.com/jack)Centre d’aide X, règles des noms d’utilisateur

Un algorithme de normalisation

  1. Step 1Nettoyez et ajoutez un schéma

    Retirez les espaces et les chevrons parasites. Si la saisie n’a pas de schéma, ajoutez https:// devant pour que instagram.com/natgeo soit analysable.

  2. Step 2Analysez avec un vrai analyseur d’URL

    Utilisez new URL() en JavaScript ou urllib.parse.urlsplit en Python, jamais une expression régulière sur toute la chaîne. Un analyseur gère les ports, les informations d’utilisateur et les caractères encodés, et empêche https://instagram.com@evil.com/natgeo de passer pour Instagram.

  3. Step 3Normalisez l’hôte

    Passez-le en minuscules et retirez un éventuel préfixe www., m. ou mobile.. Cherchez-le ensuite dans une liste blanche exacte d’hôtes. N’utilisez jamais includes("instagram.com"), qui accepte aussi instagram.com.evil.com.

  4. Step 4Supprimez la chaîne de requête et le fragment

    Les paramètres de suivi comme igsh, s, t et lang ne changent jamais le profil vers lequel pointe une URL.

  5. Step 5Découpez et décodez le chemin

    Découpez sur /, supprimez les segments vides (ce qui retire les barres obliques finales) et décodez chacun en pourcentage. Traitez un encodage mal formé comme une saisie invalide.

  6. Step 6Aiguillez selon la plateforme

    Appliquez la forme de chaque plateforme : un segment sur Instagram et X, un segment @ sur TikTok, et @handle, channel/UC..., user/name ou c/name sur YouTube. Rejetez les mots réservés.

  7. Step 7Validez le pseudo et passez-le en minuscules

    Vérifiez le pseudo selon les règles de caractères de la plateforme, puis passez-le en minuscules. Ne mettez pas en minuscules les ID de chaîne YouTube : ils sont sensibles à la casse.

  8. Step 8Construisez l’URL canonique

    Reconstruisez l’URL à partir de ses parties sur un hôte fixe, pour que toutes les saisies équivalentes produisent exactement la même chaîne.

  9. Step 9Traitez les liens courts à part

    Détectez les liens courts TikTok, suivez la redirection avec une requête HTTP, et repassez l’URL finale dans l’analyseur. Gardez cela hors de la fonction pure pour que l’analyse reste rapide et testable.

Le code en JavaScript et en Python

Les deux fonctions renvoient les mêmes objets pour la même saisie : { platform, handle, canonicalUrl } pour les URL de pseudo, { platform, channelId, canonicalUrl } pour les ID de chaîne YouTube, username ou customName pour les deux anciennes formes YouTube, et null pour tout ce qui n’est pas une URL de profil. Aucune n’a besoin de dépendance.

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 utilise le fetch intégré, qui suit les redirections et expose l’adresse finale dans res.url. Appelez parseProfileUrl(await resolveShortLink(link)) quand isShortLink(link) est vrai. Un lien court peut aussi mener à une vidéo plutôt qu’à un profil ; l’analyseur renvoie alors à juste titre 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

En Python, \w et str.isalnum() couvrent les lettres et chiffres Unicode, mais pas les signes combinants utilisés par des écritures comme le devanagari : c’est pourquoi la vérification YouTube accepte aussi les caractères des catégories Unicode M.

Cas limites et tests

Chaque ligne ci-dessous fait partie de la suite de tests que les deux implémentations réussissent.

SaisieRésultatPourquoi
instagram.com/NatGeo?igsh=MWQ1ZGUxMzBkMA==instagram, natgeo, https://www.instagram.com/natgeo/Pas de schéma, casse mixte, paramètre de partage
https://m.instagram.com/nat.geo_/#bioinstagram, nat.geo_Hôte mobile et fragment
https://www.instagram.com/p/C9xYzAbCdEf/nullUne publication, pas un profil
https://www.instagram.com/stories/natgeo/1234567890/nullUne story, même si le pseudo est dans le chemin
tiktok.com/@Khaby.Lame/tiktok, khaby.lame, https://www.tiktok.com/@khaby.lameBarre oblique finale et casse
https://www.tiktok.com/@khaby.lame/video/7106594312292453675nullUne URL de vidéo
https://vm.tiktok.com/ZMabc123/null (suivez d’abord la redirection)Lien court sans pseudo
https://m.youtube.com/@GoogleDevelopers/videos?app=desktopyoutube, googledevelopersHôte mobile, onglet de chaîne, requête
https://www.youtube.com/channel/UC_x5XG1OV2P6uZZ5FSM9Ttw/featuredyoutube, channelId UC_x5XG1OV2P6uZZ5FSM9TtwL’ID de chaîne garde sa casse
https://www.youtube.com/c/GoogleDevelopersyoutube, customName GoogleDevelopersAncienne URL personnalisée ; une recherche est nécessaire pour l’ID
https://www.youtube.com/@%E3%83%86%E3%82%B9%E3%83%88youtube, pseudo テストPseudo Unicode encodé en pourcentage
https://youtu.be/dQw4w9WgXcQnullUne vidéo, pas une chaîne
https://mobile.twitter.com/jack?s=21&t=abcx, jack, https://x.com/jackAncien domaine et paramètres de partage
https://x.com/i/flow/loginnullRoute de l’application
https://x.com/this_handle_is_too_longnullPlus de 15 caractères
https://instagram.com.evil.com/natgeonullHôte imitant le vrai
https://instagram.com@evil.com/natgeonullPiège des informations d’utilisateur ; le vrai hôte est evil.com
@natgeonullUn pseudo seul n’a pas de plateforme

Gardez ces cas dans votre dépôt et exécutez-les en CI. Quand une plateforme ajoute une route ou un paramètre de partage, vous ajoutez une ligne et la correction devient évidente.

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

Stockez la saisie brute et la forme canonique

La normalisation perd de l’information par nature, et vos règles s’amélioreront avec le temps. Stockez les deux valeurs :

Les pseudos ne sont pas permanents. Les créateurs renomment leurs comptes sur toutes les plateformes, et un pseudo stocké l’an dernier peut appartenir aujourd’hui à quelqu’un d’autre. Quand une API officielle vous donne un ID stable, comme un ID de chaîne YouTube, stockez-le comme clé à long terme et traitez le pseudo comme une valeur d’affichage à mettre à jour. Le guide complémentaire sur l’obtention de l’ID d’une chaîne YouTube à partir d’un pseudo explique comment.

D’un profil aux autres

Une fois une URL de profil normalisée, la question suivante est en général : quels autres comptes appartiennent au même créateur ? sociallinks.dev est une API qui prend une URL de profil Instagram, TikTok, YouTube ou X et renvoie les comptes du créateur sur les autres plateformes, avec un score de confiance et la preuve de chaque correspondance.

FAQ

Faut-il mettre les pseudos en minuscules avant de les stocker ?

Oui pour la clé de recherche. YouTube et X indiquent que les pseudos sont insensibles à la casse, et TikTok renvoie @TikTok vers le même compte que @tiktok. Instagram ne documente pas de règle de casse, mais les noms d’utilisateur s’affichent en minuscules : les mettre en minuscules est donc le choix sûr par défaut. Gardez la casse d’origine dans l’URL brute si vous voulez l’afficher. Ne mettez pas en minuscules les ID de chaîne YouTube, qui sont sensibles à la casse.

Peut-on extraire le pseudo d’une URL de publication ou de vidéo ?

Pour les vidéos TikTok (/@handle/video/id) et les publications X (/handle/status/id), le pseudo du chemin est celui de l’auteur : vous pouvez donc l’extraire si c’est ce que vous voulez. Les URL de publications et de Reels Instagram (/p/ et /reel/) ne contiennent pas le pseudo. Les fonctions de ce guide renvoient null pour tous ces cas, car ce ne sont pas des liens de profil.

Comment traiter les liens vm.tiktok.com ?

Les liens courts ne contiennent qu’un code. Faites une requête HTTP, suivez les redirections et analysez l’URL finale. Faites-le dans une étape séparée, avec un délai d’expiration et un cache, car c’est un appel réseau et le code peut expirer.

twitter.com est-elle encore une URL de profil valide ?

Oui. Les URL de profil twitter.com redirigent vers x.com : traitez les deux domaines comme la plateforme X et stockez la forme x.com comme canonique.

Pourquoi ne pas utiliser une seule grande expression régulière ?

Une seule regex sur toute la chaîne est difficile à lire et facile à tromper avec des hôtes imitant le vrai, des informations d’utilisateur ou des caractères encodés. Analysez d’abord l’URL avec la bibliothèque standard, puis n’utilisez de petites regex que sur le pseudo.

Comment transformer une URL YouTube /c/ ou /user/ en ID de chaîne ?

Les URL /user/<name> correspondent au filtre forUsername de l’API YouTube Data. Il n’existe aucun filtre d’API pour les noms personnalisés /c/. Consultez le guide sur l’obtention de l’ID d’une chaîne YouTube à partir d’un pseudo pour les deux cas.

Trouver les comptes d’un créateur