Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIniciantecódigo testado

Links em HTML: href, target, âncora e link de e-mail

Link para outra página, para uma seção da mesma página, para e-mail e para telefone — e por que abrir em nova aba pede rel=noopener.

Rodolfo Mori12 min de leitura

Link em HTML é a tag <a> com o atributo href. O que vai dentro do href não é o endereço final: é uma instrução que o navegador resolve contra o endereço da página em que você está. Entender essa resolução é o que separa o site que funciona no seu computador do site que funciona depois de publicado.

Os exemplos deste artigo montam a página da Clínica Veterinária Pata Firme, que hoje só tem um perfil no Instagram e precisa de um site com serviços, horários e três formas de a pessoa chamar a clínica.

O processo técnico se chama resolução de URL. Em palavras simples, o navegador combina o href com a URL da página atual para descobrir o destino completo antes de navegar.

A rota calculada a partir do ponto onde você está

Uma orientação como “entre na próxima rua” muda conforme o lugar de onde a pessoa parte. servicos/vacinas.html funciona do mesmo modo: o endereço atual é a origem do cálculo. Uma barra inicial muda o ponto de partida para a raiz do site; uma URL absoluta já traz protocolo, domínio e caminho completos.

Antes de clicar no primeiro conjunto de links, escreva a URL final que espera para cada href. Depois passe o mouse, leia o destino mostrado pelo navegador e confira na barra de endereço. Repita imaginando a página movida para uma subpasta: os links que mudarem dependem da localização atual.

Este é o menu da clínica, com quatro links de tipos diferentes:

html
<nav>
  <a href="index.html">Início</a>
  <a href="servicos.html">Serviços</a>
  <a href="contato/">Contato</a>
  <a href="#horarios">Horários</a>
</nav>

O href é um atributo, como qualquer outro: um par nome/valor dentro da tag de abertura. Só que o valor dele passa por um processamento antes de virar navegação. Dá para ver esse processamento acontecendo com o jsdom, que é o parser de HTML do navegador rodando dentro do Node — a propriedade href do elemento devolve o endereço já resolvido, e getAttribute('href') devolve o valor do atributo antes dessa resolução:

js
import { JSDOM } from 'jsdom';

const html = `
  <a href="servicos.html">Serviços</a>
  <a href="/contato">Contato</a>
  <a href="../index.html">Voltar</a>
  <a href="https://wa.me/5511999998888">WhatsApp</a>
`;

const { document } = new JSDOM(html, {
  url: 'https://patafirme.com.br/vacinas/gatos.html',
}).window;

for (const a of document.querySelectorAll('a')) {
  console.log(a.getAttribute('href').padEnd(30), '->', a.href);
}
servicos.html -> https://patafirme.com.br/vacinas/servicos.html /contato -> https://patafirme.com.br/contato ../index.html -> https://patafirme.com.br/index.html https://wa.me/5511999998888 -> https://wa.me/5511999998888

Quatro entradas, quatro saídas — e só a última saiu igual ao que foi escrito. O mecanismo é sempre o mesmo:

href escrito servicos.html endereço da página /vacinas/gatos.html resolução de URL endereço final /vacinas/servicos.html

Relativo, absoluto e o dia em que a página muda de pasta

A diferença que mais custa caro é entre servicos.html e /servicos.html. A barra na frente muda tudo: sem ela, o caminho parte da pasta da página atual; com ela, parte da raiz do site. Enquanto o site tem uma pasta só, os dois se comportam igual — e é exatamente por isso que o problema aparece tarde:

js
const paginas = [
  'https://patafirme.com.br/index.html',
  'https://patafirme.com.br/vacinas/gatos.html',
];

for (const pagina of paginas) {
  console.log(pagina);
  for (const href of ['servicos.html', '/servicos.html']) {
    console.log('  ', href.padEnd(16), '->', new URL(href, pagina).href);
  }
}
https://patafirme.com.br/index.html servicos.html -> https://patafirme.com.br/servicos.html /servicos.html -> https://patafirme.com.br/servicos.html https://patafirme.com.br/vacinas/gatos.html servicos.html -> https://patafirme.com.br/vacinas/servicos.html /servicos.html -> https://patafirme.com.br/servicos.html

Na primeira página, resultado idêntico. Na segunda, o relativo foi parar em /vacinas/servicos.html, um arquivo que não existe. O menu não mudou; a página que carrega o menu mudou de pasta.

forma exemplo parte de quando usar
relativo servicos.html pasta da página atual arquivos que andam juntos, na mesma pasta
relativo com subida ../index.html uma pasta acima sair de uma subpasta
absoluto de raiz /servicos.html raiz do domínio menu, rodapé, tudo que se repete no site
absoluto completo https://wa.me/... lugar nenhum, já é o endereço outro domínio

A regra que eu sigo: navegação que se repete em várias páginas usa barra inicial. Ela é a única forma que continua certa quando alguém cria a pasta /vacinas/ seis meses depois.

O & no meio do endereço

Dentro de um atributo, o navegador ainda procura entidades HTML. Um & cru num endereço com parâmetros pode ser engolido:

js
import { JSDOM } from 'jsdom';

const html = `
  <a href="busca.html?tipo=vacina&copy;felinos">1</a>
  <a href="busca.html?a=1&para;b=2">2</a>
  <a href="busca.html?a=1&amp;para;b=2">3</a>
`;

const { document } = new JSDOM(html, { url: 'https://patafirme.com.br/' }).window;
for (const a of document.querySelectorAll('a')) {
  console.log(a.textContent, '->', a.getAttribute('href'));
}
1 -> busca.html?tipo=vacina©felinos 2 -> busca.html?a=1¶b=2 3 -> busca.html?a=1&para;b=2

&copy; virou © e &para; virou dentro do endereço. Nenhum dos dois é caractere exótico: copy e para são nomes de entidade que colidem com nomes de parâmetro perfeitamente comuns. O gatilho é o ; logo depois — dentro de um atributo, uma entidade sem ponto e vírgula seguida de = ou de letra fica como está, e por isso ?tipo=vacina&copy=felinos escapa ileso enquanto ?tipo=vacina&copy;felinos não. Só o terceiro link, escrito com &amp;, chegou íntegro. Na dúvida, & dentro de href vira sempre &amp; — custa quatro caracteres e nunca dá problema.

Um href que começa com # aponta para um elemento da própria página, achado pelo id:

html
<a href="#horarios">Horários</a>

<h2 id="horarios">Horários</h2>
<p>Segunda a sexta, das 8h às 19h. Sábado, das 8h às 13h.</p>

O casamento entre #horarios e id="horarios" tem que ser exato. Este teste percorre todas as âncoras da página e diz quais não têm destino:

js
import { JSDOM } from 'jsdom';

const html = `
  <a href="#horarios">Horários</a>
  <a href="#vacinas">Vacinas</a>
  <h2 id="horarios">Horários</h2>
  <h2 id="vacina">Vacinas</h2>
`;

const { document } = new JSDOM(html, { url: 'https://patafirme.com.br/' }).window;

for (const a of document.querySelectorAll('a[href^="#"]')) {
  const alvo = document.getElementById(a.getAttribute('href').slice(1));
  console.log(
    a.getAttribute('href').padEnd(12),
    alvo ? `achou <${alvo.tagName.toLowerCase()}>` : 'NAO EXISTE na pagina',
  );
}

const url = new URL('#horarios', 'https://patafirme.com.br/index.html');
console.log('\nurl completa :', url.href);
console.log('pathname     :', url.pathname);
console.log('hash         :', url.hash);
#horarios achou <h2> #vacinas NAO EXISTE na pagina

url completa : https://patafirme.com.br/index.html#horarios pathname : /index.html hash : #horarios

#vacinas no link, id="vacina" no título: uma letra de diferença e a página não sai do lugar. Sem erro no console, sem aviso — o navegador troca o endereço na barra, guarda a entrada no histórico e continua exatamente onde estava. É essa quietude que faz o defeito sobreviver semanas na página no ar.

Repare também que o hash é uma parte separada da URL. E ele nunca sai do navegador. Dá para comprovar isso subindo um servidor que imprime o que recebe. A pasta site/ aqui é o site da clínica como ele está no disco — index.html com o menu inteiro, servicos.html, contato/index.html e arquivos/tabela-precos.pdf — e é ela que vai servir de cobaia até o fim do artigo:

js
import express from 'express';

const app = express();
app.use((req, res, next) => {
  console.log('chegou no servidor ->', req.url);
  next();
});
app.use(express.static('site'));
app.listen(4321);
bash
curl -s -o /dev/null "http://localhost:4321/index.html#horarios"
curl -s -o /dev/null "http://localhost:4321/servicos.html?utm=whatsapp"
chegou no servidor -> /index.html chegou no servidor -> /servicos.html?utm=whatsapp

A query string ?utm=whatsapp chegou inteira. O #horarios não chegou. Por isso âncora não é rota: quem decide para onde a página rola é o navegador, sozinho, depois que o HTML já está na mão dele.

target="_blank": a aba nova e o rel que fecha a porta

target="_blank" faz o link abrir em outra aba. É o atributo mais usado errado do HTML, por dois motivos: gente coloca em link interno, onde ele só atrapalha, e gente esquece do rel.

html
<a href="https://vacinacao.sp.gov.br/" target="_blank" rel="noopener">
  calendário oficial de vacinação
</a>

O rel descreve a relação com o destino. noopener corta o acesso que a página aberta teria à aba de origem pela propriedade window.opener — sem ele, um site de terceiros consegue trocar o endereço da sua aba enquanto a pessoa está olhando para a outra. noreferrer vai além e também esconde de onde a pessoa veio.

Este teste separa os links de aba nova e mostra o que cada um declarou:

js
import { JSDOM } from 'jsdom';

const html = `
  <a href="https://vacinacao.sp.gov.br/" target="_blank">Calendário sem rel</a>
  <a href="https://vacinacao.sp.gov.br/" target="_blank" rel="noopener noreferrer">Calendário com rel</a>
  <a href="servicos.html" target="_blank">Serviços do próprio site</a>
`;

const { document } = new JSDOM(html, { url: 'https://patafirme.com.br/' }).window;

for (const a of document.querySelectorAll('a[target="_blank"]')) {
  console.log({
    texto: a.textContent,
    rel: a.rel || '(vazio)',
    temNoopener: a.relList.contains('noopener'),
    externo: new URL(a.href).origin !== 'https://patafirme.com.br',
  });
}
{ texto: 'Calendário sem rel', rel: '(vazio)', temNoopener: false, externo: true } { texto: 'Calendário com rel', rel: 'noopener noreferrer', temNoopener: true, externo: true } { texto: 'Serviços do próprio site', rel: '(vazio)', temNoopener: false, externo: false }

O terceiro caso é o que mais aparece em site de iniciante: target="_blank" num link para uma página do próprio site. Isso enche o navegador da pessoa de abas e quebra o botão de voltar, que é o botão mais usado da web. Aba nova só para destino de fora, e por escolha do leitor.

Nem todo href é uma página. Alguns esquemas mandam o sistema operacional abrir outro programa — e o do WhatsApp, apesar da fama, é um endereço https comum:

html
<a href="mailto:agenda@patafirme.com.br">Agendar por e-mail</a>
<a href="tel:+551140028922">(11) 4002-8922</a>
<a href="https://wa.me/5511999998888">Falar no WhatsApp</a>

O mailto aceita assunto e corpo prontos, na forma de parâmetros. Como esse texto tem espaço e acento, ele precisa ser codificado — e a função que faz isso é a mesma que você usa em JavaScript:

js
const assunto = 'Consulta para o Thor (labrador)';
const corpo = 'Olá! Queria agendar para quinta à tarde.';

const href =
  'mailto:agenda@patafirme.com.br' +
  `?subject=${encodeURIComponent(assunto)}` +
  `&body=${encodeURIComponent(corpo)}`;

console.log(href);

const url = new URL(href);
console.log('\nprotocol:', url.protocol);
console.log('pathname:', url.pathname);
console.log('subject :', url.searchParams.get('subject'));

const telefone = '(11) 4002-8922';
console.log('\ntel href:', 'tel:+55' + telefone.replace(/\D/g, ''));
console.log('wa href :', 'https://wa.me/55' + telefone.replace(/\D/g, ''));
mailto:agenda@patafirme.com.br?subject=Consulta%20para%20o%20Thor%20(labrador)&body=Ol%C3%A1!%20Queria%20agendar%20para%20quinta%20%C3%A0%20tarde.

protocol: mailto: pathname: agenda@patafirme.com.br subject : Consulta para o Thor (labrador)

tel href: tel:+551140028922 wa href : https://wa.me/551140028922

Três coisas que valem a pena guardar. O espaço virou %20 e o á virou %C3%A1 — endereço não carrega esses caracteres crus. O tel: precisa começar com + e o código do país: é o que a RFC 3966 chama de número global, e é a única forma que continua certa para quem clica de fora do Brasil. Hífen, ponto e parênteses são separadores visuais permitidos pela mesma RFC; espaço não é — e o espaço é justamente o que vem junto quando alguém copia o telefone do rodapé. Gerar o href com replace(/\D/g, ''), como no código acima, encerra a discussão. E o wa.me é um endereço https comum: funciona no celular chamando o aplicativo e no computador abrindo o WhatsApp Web, o que o esquema whatsapp:// não faz.

download: o atributo que o servidor nem vê

O atributo download pede ao navegador para salvar o arquivo em vez de abrir:

html
<a href="arquivos/tabela-precos.pdf" download="pata-firme-precos.pdf">
  Baixar a tabela de preços
</a>

O valor do atributo é o nome sugerido para o arquivo salvo. E “sugerido” é a palavra certa: nada disso viaja pela rede. Servindo o mesmo PDF pelo servidor de teste e olhando os cabeçalhos da resposta:

bash
curl -s -I "http://localhost:4321/arquivos/tabela-precos.pdf"
HTTP/1.1 200 OK X-Powered-By: Express Accept-Ranges: bytes Cache-Control: public, max-age=0 Last-Modified: Sat, 22 Aug 2026 19:28:44 GMT ETag: W/"28-1a02af2689c" Content-Type: application/pdf Content-Length: 40 Date: Sat, 22 Aug 2026 19:37:00 GMT Connection: keep-alive Keep-Alive: timeout=5

Nenhum Content-Disposition, nenhuma menção ao nome pata-firme-precos.pdf. O download é uma decisão que acontece só no navegador, e ele é ignorado quando o arquivo está em outro domínio — regra de segurança, para um site não conseguir forçar download de arquivo alheio com nome falso.

Quem usa leitor de tela costuma pedir a lista de links da página, para navegar sem ler tudo. Nessa lista o link aparece sozinho, fora do parágrafo. É por isso que “clique aqui” é problema, e não frescura:

js
import { JSDOM } from 'jsdom';

const pagina = `
  <p>Consulta de rotina. <a href="servicos.html">Clique aqui</a> para ver os preços.</p>
  <p>Vacina antirrábica. <a href="vacinas.html">Clique aqui</a> para o calendário.</p>
  <p>Castração. <a href="castracao.html">Saiba mais</a>.</p>
  <p>Endereço: <a href="mapa.html">https://patafirme.com.br/mapa.html</a></p>
`;

const { document } = new JSDOM(pagina, { url: 'https://patafirme.com.br/' }).window;

const nomes = [...document.querySelectorAll('a')].map((a) => a.textContent.trim());
console.log('Lista de links da página:');
nomes.forEach((n, i) => console.log(`  ${i + 1}. ${n}`));

const repetidos = nomes.filter((n, i) => nomes.indexOf(n) !== i);
console.log('\nTextos repetidos apontando para destinos diferentes:', [...new Set(repetidos)]);
Lista de links da página: 1. Clique aqui 2. Clique aqui 3. Saiba mais 4. https://patafirme.com.br/mapa.html

Textos repetidos apontando para destinos diferentes: [ ‘Clique aqui’ ]

Essa lista é inútil. Dois links com o mesmo nome levam a lugares diferentes, um terceiro não diz nada e o quarto obriga a pessoa a ouvir um endereço letra por letra. A versão que resolve tudo não é mais longa que a original:

html
<p>Consulta de rotina. <a href="servicos.html">Ver a tabela de preços</a>.</p>
<p>Vacina antirrábica. <a href="vacinas.html">Ver o calendário de vacinas</a>.</p>
<p>Castração. <a href="castracao.html">Como funciona a castração</a>.</p>
<p>Endereço: <a href="mapa.html">Rua das Acácias, 120, no mapa</a>.</p>

O buscador usa exatamente o mesmo sinal: o texto do link é a principal pista que ele tem sobre o conteúdo do destino. Escrever bem aqui melhora acessibilidade e busca com a mesma frase. O assunto tem mais camadas em acessibilidade em HTML.

Auditando a página inteira antes do deploy

Agora o pedaço que ninguém faz e devia. Este script abre o site/index.html da clínica — o mesmo do servidor de teste, com o menu e os links de contato —, resolve cada href contra o endereço de produção e confere, arquivo por arquivo, se o destino existe, respeitando maiúscula e minúscula:

js
import { readFileSync, readdirSync } from 'node:fs';
import { dirname, join, basename } from 'node:path';
import { JSDOM } from 'jsdom';

const PASTA = 'site';
const SITE = 'https://patafirme.com.br';
const PAGINA = `${SITE}/index.html`;

const { document } = new JSDOM(readFileSync(join(PASTA, 'index.html'), 'utf8'), {
  url: PAGINA,
}).window;

// existe um arquivo com ESTE nome, respeitando maiúscula e minúscula?
const existeExato = (caminho) => {
  try {
    return readdirSync(join(PASTA, dirname(caminho))).includes(basename(caminho));
  } catch {
    return false;
  }
};

const linhas = [];

for (const a of document.querySelectorAll('a[href]')) {
  const escrito = a.getAttribute('href');
  const url = new URL(escrito, PAGINA);
  let situacao;

  if (url.protocol !== 'https:' && url.protocol !== 'http:') {
    situacao = `outro app (${url.protocol.slice(0, -1)})`;
  } else if (url.origin !== SITE) {
    situacao = 'site externo';
  } else if (url.hash && url.pathname === '/index.html') {
    situacao = document.getElementById(url.hash.slice(1)) ? 'âncora ok' : 'ÂNCORA SEM ALVO';
  } else {
    let arquivo = url.pathname.slice(1);
    if (arquivo === '' || arquivo.endsWith('/')) arquivo += 'index.html';
    situacao = existeExato(arquivo) ? 'ok' : '404 EM PRODUÇÃO';
  }

  linhas.push([escrito, url.href.replace(SITE, '~'), situacao]);
}

const larg = (i) => Math.max(...linhas.map((l) => l[i].length));
console.log(`(~ = ${SITE})\n`);
for (const [escrito, virou, situacao] of linhas) {
  console.log(`${escrito.padEnd(larg(0))}  ${virou.padEnd(larg(1))}  ${situacao}`);
}
(~ = https://patafirme.com.br)

index.html ~/index.html ok servicos.html ~/servicos.html ok contato/ ~/contato/ ok #horarios ~/index.html#horarios âncora ok Servicos.html ~/Servicos.html 404 EM PRODUÇÃO https://wa.me/5511999998888 https://wa.me/5511999998888 site externo mailto:agenda@patafirme.com.br mailto:agenda@patafirme.com.br outro app (mailto) tel:+551140028922 tel:+551140028922 outro app (tel) arquivos/tabela-precos.pdf ~/arquivos/tabela-precos.pdf ok https://vacinacao.sp.gov.br/ https://vacinacao.sp.gov.br/ site externo /politica-de-privacidade ~/politica-de-privacidade 404 EM PRODUÇÃO equipe.html ~/equipe.html 404 EM PRODUÇÃO

Doze links, três problemas. equipe.html é a página que ainda não foi criada. /politica-de-privacidade é o link que alguém copiou de outro site. E Servicos.html, com S maiúsculo, é o mais perigoso dos três — porque na máquina de quem escreveu ele funciona.

Duas armadilhas moram aqui, e nenhuma delas aparece antes do deploy.

A primeira é abrir o site com dois cliques no arquivo, o que usa o esquema file://. Nesse esquema a “raiz” não é o site: é a raiz do computador.

js
const hrefs = ['servicos.html', '/politica-de-privacidade', 'contato/', '//vacinacao.sp.gov.br'];

const bases = {
  'na sua máquina': 'file:///Users/ana/pata-firme/index.html',
  'em produção   ': 'https://patafirme.com.br/index.html',
};

for (const href of hrefs) {
  console.log(href);
  for (const [nome, base] of Object.entries(bases)) {
    console.log(`  ${nome} -> ${new URL(href, base).href}`);
  }
}
servicos.html na sua máquina -> file:///Users/ana/pata-firme/servicos.html em produção -> https://patafirme.com.br/servicos.html /politica-de-privacidade na sua máquina -> file:///politica-de-privacidade em produção -> https://patafirme.com.br/politica-de-privacidade contato/ na sua máquina -> file:///Users/ana/pata-firme/contato/ em produção -> https://patafirme.com.br/contato/ //vacinacao.sp.gov.br na sua máquina -> file://vacinacao.sp.gov.br/ em produção -> https://vacinacao.sp.gov.br/

O caminho com barra inicial apontou para file:///politica-de-privacidade, a raiz do disco. E o endereço sem protocolo, //vacinacao.sp.gov.br, virou file://vacinacao.sp.gov.br/ — em produção ele funciona, aberto do disco ele não existe. É por isso que se testa com um servidor local, e não com dois cliques no arquivo.

A segunda armadilha é a caixa das letras, e ela é a mais cruel. O Servicos.html do menu foi para o servidor de teste e não deu erro nenhum:

bash
for p in /servicos.html /Servicos.html /equipe.html; do
  curl -s -o /dev/null -w "%{http_code}  $p\n" "http://localhost:4321$p"
done
200 /servicos.html 200 /Servicos.html 404 /equipe.html

Duzentos nos dois. O motivo é o disco: o macOS formata em APFS sem diferenciar maiúscula de minúscula, e o Linux do servidor de produção diferencia. O Node mostra a contradição de forma bem direta:

js
import { existsSync, readdirSync } from 'node:fs';

console.log("existsSync('Servicos.html')     :", existsSync('site/Servicos.html'));
console.log('nome exato na pasta             :', readdirSync('site').includes('Servicos.html'));
console.log('arquivos que existem de verdade :', readdirSync('site').filter((n) => n.endsWith('.html')));
existsSync('Servicos.html') : true nome exato na pasta : false arquivos que existem de verdade : [ 'index.html', 'servicos.html' ]

O sistema diz que o arquivo existe e a listagem da pasta diz que não. Quem resolve a diferença é o servidor de produção, publicando um Failed to load resource: 404 para o seu cliente. Por isso o auditor da seção anterior compara com readdirSync em vez de existsSync: rodando no seu Mac, ela é a única que reproduz o Linux.

A saída disso é uma convenção, não disciplina: nome de arquivo e de pasta sempre em minúsculo, sem acento e com hífen no lugar do espaço. tabela-precos.pdf, nunca Tabela de Preços.pdf.

O próximo passo

Rode o auditor na sua própria página antes do próximo deploy — são menos de cinquenta linhas e ele acha o que você não vê relendo o HTML. Depois disso, a próxima peça da página da clínica é a foto dos animais atendidos, que é imagens em HTML: a tag img usa o mesmo mecanismo de resolução de caminho que você acabou de ver, e sofre exatamente das mesmas armadilhas de caixa e de file://. Se quiser conferir onde esta lição entra na sequência, o guia de HTML mostra a ordem inteira e a trilha de HTML lista as lições publicadas.

  • html
  • links
  • href
  • ancora
  • navegacao

Perguntas frequentes

Quando usar caminho relativo e quando usar caminho começando com barra?
Use a barra inicial em tudo que é navegação do site — menu, rodapé, botões — porque esse caminho não muda quando a página troca de pasta. Deixe o relativo para links entre arquivos da mesma pasta, que costumam andar juntos. A exceção é o site aberto direto do disco, em file://, onde a barra inicial aponta para a raiz do computador e não funciona.
Posso colocar uma imagem dentro de um link?
Pode, e é o que se faz com logotipo e miniatura. A regra é que o link precisa continuar tendo um nome acessível: se dentro da tag a só existe uma img, o texto alternativo dessa imagem vira o texto do link. Imagem com alt vazio dentro de um link produz um link sem nome nenhum.
Preciso de rel="noopener" em link para uma página do meu próprio site?
Não. O risco que o noopener corta é a página de outro domínio conseguir mexer na aba de origem. Entre páginas do mesmo site isso não é ameaça, e os navegadores atuais já aplicam noopener sozinhos em target="_blank". Escrever mesmo assim custa nada e protege quem abre em navegador antigo.
Link de WhatsApp funciona em computador?
O endereço wa.me é uma página normal em https, então abre em qualquer lugar: no celular ele chama o aplicativo, no computador ele leva para o WhatsApp Web. É por isso que ele é mais seguro do que o esquema whatsapp://, que só resolve onde o aplicativo está instalado.
Por que meu link para uma seção não desce a página?
Na maioria das vezes o id do destino não existe com aquela grafia exata. O casamento entre o texto depois do # e o valor do id diferencia maiúscula de minúscula, e um espaço ou um caractere sobrando já derruba. Quando não bate, o navegador não avisa nada: ele troca o endereço na barra e continua onde estava. Rodar o teste de âncoras deste artigo aponta o culpado em um segundo.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

Todo o código deste artigo foi executado em Node 24.16.0 com jsdom 30.0.1 e Express 5.2.1, e as saídas exibidas são as reais — como produzimos este conteúdo.

Fontes consultadas

  1. MDN — O elemento <a> — developer.mozilla.org
  2. WHATWG HTML Standard — Links — html.spec.whatwg.org
  3. RFC 6068 — The 'mailto' URI Scheme — rfc-editor.org
  4. RFC 3966 — The 'tel' URI for Telephone Numbers — rfc-editor.org
  5. WHATWG URL Standard — URL parsing — url.spec.whatwg.org

Continue por aqui