sociallinks.dev

Blog / Guia para desenvolvedores

Como interpretar e padronizar URLs de perfis de redes sociais (Instagram, TikTok, YouTube, X)

Os criadores colam os links de perfil em todos os formatos imagináveis: com e sem www, vindos do app com parâmetros de rastreamento, como links curtos, como URLs antigas do twitter.com ou como link para um post em vez do perfil. Se o seu produto guarda esses textos do jeito que chegam, o mesmo criador aparece três vezes e seus cruzamentos falham sem aviso. Este guia mostra como transformar qualquer URL de perfil do Instagram, TikTok, YouTube ou X em uma plataforma, um nome de usuário ou ID de canal e uma URL canônica, com código testado em JavaScript e Python.

Por que padronizar URLs de perfil importa

Uma URL de perfil é um identificador, e identificadores só funcionam quando são escritos de um único jeito. https://www.instagram.com/natgeo/, instagram.com/NatGeo?igsh=MWQ1ZGUxMzBkMA== e https://m.instagram.com/natgeo apontam todos para a mesma conta, mas como texto são três valores diferentes.

O objetivo é uma função pequena e rigorosa: aceita o que os usuários realmente colam, retorna um resultado padronizado para URLs de perfil reais e retorna null para todo o resto.

Os formatos de URL que você vai encontrar

Os formatos abaixo foram conferidos nos sites ao vivo em outubro de 2026. As plataformas mudam suas rotas sem aviso, então trate isto como um retrato do momento e mantenha seus casos de teste atualizados.

Instagram

TikTok

YouTube

X (antigo Twitter)

Regras de caracteres do nome de usuário por plataforma

Validar o nome de usuário pega erros de digitação e a maioria dos caminhos que não são perfis. Use as regras das próprias plataformas quando elas as publicam e deixe as checagens flexíveis quando não publicam, para nunca rejeitar uma conta real.

PlataformaCaracteres permitidosTamanhoMaiúsculas e minúsculasFonte
InstagramLetras, números, pontos, sublinhados30 é muito citado; não encontrado nas páginas de ajuda do InstagramTrate como indiferente a maiúsculas; guarde em minúsculasA Central de Ajuda do Instagram menciona pontos, números e sublinhados
TikTokLetras, números, sublinhados, pontos; não pode terminar com ponto2 a 24Indiferente a maiúsculas nas URLs (@TikTok leva a tiktok)Suporte do TikTok, alteração do nome de usuário
YouTubeLetras ou números dos idiomas suportados, mais sublinhados, hifens, pontos e pontos médios latinos (não no início nem no fim)3 a 30 na maioria dos sistemas de escrita; limites menores para Han, Hangul e alguns outrosNão diferencia maiúsculasAjuda do YouTube, diretrizes de identificadores
XLetras, números e sublinhadosAté 15; nomes novos precisam ter mais de 4, mas existem nomes curtos antigosNão diferencia maiúsculas (x.com/JACK vira x.com/jack)Central de Ajuda do X, regras de nome de usuário

Um algoritmo de padronização

  1. Step 1Limpe e adicione um esquema

    Remova espaços e sinais de menor e maior soltos. Se a entrada não tiver esquema, acrescente https:// no início para que instagram.com/natgeo possa ser interpretado.

  2. Step 2Use um parser de URL de verdade

    Use new URL() em JavaScript ou urllib.parse.urlsplit em Python, nunca uma regex sobre o texto inteiro. Um parser lida com portas, dados de usuário e caracteres codificados, e impede que https://instagram.com@evil.com/natgeo passe como Instagram.

  3. Step 3Padronize o host

    Passe para minúsculas e remova um www., m. ou mobile. do início. Depois procure o host numa lista exata de hosts permitidos. Nunca use includes("instagram.com"), que também aceita instagram.com.evil.com.

  4. Step 4Descarte a query string e o fragmento

    Parâmetros de rastreamento como igsh, s, t e lang nunca mudam o perfil para o qual a URL aponta.

  5. Step 5Divida e decodifique o caminho

    Divida em /, descarte os segmentos vazios (isso remove as barras finais) e aplique a decodificação percentual em cada um. Trate codificação malformada como entrada inválida.

  6. Step 6Encaminhe por plataforma

    Aplique o formato de cada plataforma: um segmento no Instagram e no X, um segmento com @ no TikTok e @handle, channel/UC..., user/name ou c/name no YouTube. Rejeite palavras reservadas.

  7. Step 7Valide e passe o nome para minúsculas

    Confira o nome de usuário com as regras de caracteres da plataforma e depois passe para minúsculas. Não passe IDs de canal do YouTube para minúsculas: eles diferenciam maiúsculas.

  8. Step 8Monte a URL canônica

    Reconstrua a URL a partir das partes, num host fixo, para que toda entrada equivalente produza exatamente o mesmo texto.

  9. Step 9Trate links curtos à parte

    Detecte links curtos do TikTok, siga o redirecionamento com uma requisição HTTP e passe a URL final de novo pelo parser. Deixe isso fora da função pura, para que a interpretação continue rápida e testável.

O código em JavaScript e Python

As duas funções retornam os mesmos objetos para a mesma entrada: { platform, handle, canonicalUrl } para URLs com nome de usuário, { platform, channelId, canonicalUrl } para IDs de canal do YouTube, username ou customName para as duas formas antigas do YouTube e null para qualquer coisa que não seja URL de perfil. Nenhuma precisa de dependência.

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 o fetch nativo, que segue redirecionamentos e expõe o endereço final em res.url. Chame parseProfileUrl(await resolveShortLink(link)) quando isShortLink(link) for verdadeiro. Um link curto também pode levar a um vídeo em vez de um perfil, e nesse caso o parser retorna null, como deve.

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

Em Python, \w e str.isalnum() cobrem letras e dígitos Unicode, mas não as marcas combinantes usadas por sistemas de escrita como o devanágari, e é por isso que a checagem do YouTube também aceita caracteres das categorias Unicode M.

Casos extremos e testes

Cada linha abaixo faz parte da suíte de testes em que as duas implementações passam.

EntradaSaídaPor quê
instagram.com/NatGeo?igsh=MWQ1ZGUxMzBkMA==instagram, natgeo, https://www.instagram.com/natgeo/Sem esquema, maiúsculas misturadas, parâmetro de compartilhamento
https://m.instagram.com/nat.geo_/#bioinstagram, nat.geo_Host mobile e fragmento
https://www.instagram.com/p/C9xYzAbCdEf/nullUm post, não um perfil
https://www.instagram.com/stories/natgeo/1234567890/nullUm story, mesmo com o nome de usuário no caminho
tiktok.com/@Khaby.Lame/tiktok, khaby.lame, https://www.tiktok.com/@khaby.lameBarra final e maiúsculas
https://www.tiktok.com/@khaby.lame/video/7106594312292453675nullUma URL de vídeo
https://vm.tiktok.com/ZMabc123/null (siga o redirecionamento antes)Link curto sem nome de usuário
https://m.youtube.com/@GoogleDevelopers/videos?app=desktopyoutube, googledevelopersHost mobile, aba do canal, query
https://www.youtube.com/channel/UC_x5XG1OV2P6uZZ5FSM9Ttw/featuredyoutube, channelId UC_x5XG1OV2P6uZZ5FSM9TtwO ID do canal mantém as maiúsculas
https://www.youtube.com/c/GoogleDevelopersyoutube, customName GoogleDevelopersURL personalizado antigo; precisa de uma consulta para obter o ID
https://www.youtube.com/@%E3%83%86%E3%82%B9%E3%83%88youtube, handle テストIdentificador Unicode com codificação percentual
https://youtu.be/dQw4w9WgXcQnullUm vídeo, não um canal
https://mobile.twitter.com/jack?s=21&t=abcx, jack, https://x.com/jackDomínio antigo e parâmetros de compartilhamento
https://x.com/i/flow/loginnullRota do app
https://x.com/this_handle_is_too_longnullMais de 15 caracteres
https://instagram.com.evil.com/natgeonullHost imitador
https://instagram.com@evil.com/natgeonullTruque com dados de usuário; o host real é evil.com
@natgeonullUm nome de usuário solto não tem plataforma

Mantenha os casos no seu repositório e rode-os na CI. Quando uma plataforma adicionar uma rota ou um parâmetro de compartilhamento, você acrescenta uma linha e a correção fica óbvia.

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

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

Guarde a entrada bruta e a forma canônica

A padronização perde informação de propósito, e suas regras vão melhorar com o tempo. Guarde os dois valores:

Nomes de usuário não são permanentes. Criadores renomeiam contas em todas as plataformas, e um nome que você guardou no ano passado pode hoje ser de outra pessoa. Quando uma API oficial fornece um ID estável, como o ID de canal do YouTube, guarde-o como chave de longo prazo e trate o nome de usuário como um valor de exibição que você atualiza. O guia complementar sobre como obter o ID de um canal do YouTube a partir do identificador mostra como.

De um perfil para os outros

Depois que uma URL de perfil está padronizada, a próxima pergunta costuma ser quais outras contas pertencem ao mesmo criador. O sociallinks.dev é uma API que recebe a URL de um perfil do Instagram, TikTok, YouTube ou X e retorna as contas do criador em outras plataformas, com uma pontuação de confiança e as evidências de cada correspondência.

Perguntas frequentes

Devo passar os nomes de usuário para minúsculas antes de guardar?

Sim, para a chave de busca. O YouTube e o X dizem que os nomes de usuário não diferenciam maiúsculas, e o TikTok leva @TikTok à mesma conta que @tiktok. O Instagram não documenta uma regra, mas os nomes de usuário são exibidos em minúsculas, então usar minúsculas é o padrão seguro. Mantenha a grafia original na URL bruta se quiser exibi-la. Não passe IDs de canal do YouTube para minúsculas, porque eles diferenciam maiúsculas.

Dá para extrair o nome de usuário de uma URL de post ou vídeo?

Em vídeos do TikTok (/@handle/video/id) e posts do X (/handle/status/id), o nome de usuário no caminho é o do autor, então você pode extraí-lo se for isso que quer. URLs de posts e reels do Instagram (/p/ e /reel/) não incluem o nome de usuário. As funções deste guia retornam null para todas elas porque não são links de perfil.

Como lidar com links vm.tiktok.com?

Links curtos contêm só um código. Faça uma requisição HTTP, siga os redirecionamentos e interprete a URL final. Faça isso numa etapa separada, com timeout e cache, porque é uma chamada de rede e o código pode expirar.

twitter.com ainda é uma URL de perfil válida?

Sim. URLs de perfil do twitter.com redirecionam para o x.com, então trate os dois domínios como a plataforma X e guarde a forma x.com como canônica.

Por que não usar uma única expressão regular grande?

Uma única regex sobre o texto inteiro é difícil de ler e fácil de enganar com hosts imitadores, dados de usuário ou caracteres codificados. Interprete a URL primeiro com a biblioteca padrão e depois use regexes pequenas só no nome de usuário.

Como transformar uma URL /c/ ou /user/ do YouTube em ID do canal?

URLs /user/<name> correspondem ao filtro forUsername da YouTube Data API. Não existe filtro na API para nomes personalizados /c/. Veja o guia sobre como obter o ID de um canal do YouTube a partir do identificador para os dois casos.

Encontre as contas de um criador