A trilha HTML puro ensina as peças — tag, seletor, flexbox, evento. Esta ensina a juntar as peças num site inteiro: em que ordem construir, onde cada arquivo mora, o esqueleto do HTML, o CSS em camadas, os padrões de layout que se repetem, os nomes que mantêm tudo legível, os ícones em SVG, os vídeos e mapas que não pesam, e o que o CSS já resolve sozinho. No fim, o projeto-base pronto para copiar, com os mesmos tokens que as ferramentas geram.
Revisão já conversamos sobre issoNovo ainda não vimos9 partes · 38 tópicos
Mostrar
Nenhum tópico com esse filtro.
PARTE 1
Antes do primeiro arquivo
Todo site começa igual, e é por isso que vale ter um jeito fixo de começar. A ordem em que as coisas são construídas, onde cada arquivo mora e quando separar em vários.
A ordem em que um site se constrói
Novo
O erro mais caro é começar pelo que dá mais vontade: a animação da hero, a cor do botão. Cada camada depende da de baixo, e mexer em baixo depois obriga a refazer o que está em cima.
Ordem
Camada
Pronta quando
1
Conteúdo
o texto de cada seção existe — de verdade ou marcado como [PREENCHER]
2
HTML
a página lê bem sem CSS nenhum: títulos na ordem, listas como lista, botões como botão
3
Tokens
cor, fonte, tamanhos e espaços decididos e em variáveis
4
Layout
as seções se arrumam do celular à tela grande
5
Componentes
botão, card, formulário, com todos os estados
6
Movimento
só depois que o resto está certo — e só onde ajuda
A camada 2 é a que mais se pula e a que mais protege: HTML que faz sentido sem estilo é HTML que o Google, o leitor de tela e a IA que resume o site entendem. O teste é desligar o CSS no DevTools e ler a página de cima a baixo.
As camadas 3 e 5 já têm ferramenta: Paleta, Tipografia e Escala decidem os tokens, e o Design system junta tudo num tokens.css. O conteúdo da camada 1 sai da estratégia da trilha Landing pages.
Onde cada arquivo mora
Novo
Não existe estrutura "certa", mas existe uma que não atrapalha: previsível, com nomes que dizem o que o arquivo é, e rasa — ninguém precisa abrir cinco pastas para achar a foto da fachada.
o servidor diferencia Foto.jpg de foto.jpg; o Windows, não — funciona no seu computador e quebra no ar
sem acento, sem espaço, sem ç
vira %C3%A7 na URL e quebra em algum lugar
palavras separadas por hífen
reforma-cozinha.webp se lê; reformacozinha não
o nome diz o que é
img_0231.jpg obriga a abrir para saber
Os arquivos que ficam na raiz — robots, sitemap, favicon, 404 — e para que cada um serve estão em Os arquivos da raiz, na trilha Depois do ar.
Um arquivo só ou vários?
Novo
As duas formas estão certas; o que decide é quantas páginas o site tem.
Um HTML com tudo dentro
HTML + css/ + js/
Bom para
landing page de uma página só, entrega por arquivo, protótipo
duas páginas ou mais, site que vai crescer
Vantagem
um arquivo abre em qualquer lugar, sem servidor
o CSS é baixado uma vez e reaproveitado em todas as páginas
Custo
cada página nova copia o estilo inteiro
mais arquivos para organizar
A regra prática: a partir da segunda página, separe. Duas páginas com o mesmo CSS colado dentro viram duas versões diferentes do site na primeira correção que for feita só numa delas.
Por que o formato de arquivo único funciona tão bem para landing page está em HTML único autocontido. E quando o que se repete é o HTML — o mesmo cabeçalho em dez páginas —, as saídas sem build estão em Reuso sem build.
PARTE 2
O esqueleto do HTML
O <head> que toda página precisa, o <body> com as regiões certas, e um molde de seção que se repete igual do começo ao fim.
O head completo, em ordem
NovoHTML
O <head> não aparece na tela, mas decide como o site aparece em todo o resto: na aba, no Google, no WhatsApp, na tela do celular. A ordem abaixo não é enfeite — o charset e o viewport vêm primeiro porque o navegador precisa deles antes de ler qualquer outra coisa.
head
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Nome do negócio — o que faz, onde</title>
<meta name="description" content="Uma frase que responde a busca e convida a clicar.">
<link rel="canonical" href="https://seusite.com.br/">
<meta property="og:title" content="Nome do negócio — o que faz">
<meta property="og:description" content="A mesma ideia da description, para quem recebe o link.">
<meta property="og:image" content="https://seusite.com.br/og.jpg">
<meta property="og:url" content="https://seusite.com.br/">
<meta property="og:type" content="website">
<meta name="twitter:card" content="summary_large_image">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="icon" href="/favicon.ico" sizes="32x32">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<meta name="theme-color" content="#0D6B62">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=...&display=swap">
<link rel="stylesheet" href="css/tokens.css">
<link rel="stylesheet" href="css/estilo.css">
<script src="js/app.js" defer></script>
</head>
fontes antes do CSS: o preconnect abre a conexão com o Google Fonts enquanto o resto ainda está sendo lido
defer no script: ele baixa em paralelo e só roda depois que o HTML inteiro foi lido — pode ficar no <head> sem travar nada
tokens.css antes do estilo.css: as variáveis precisam existir antes de alguém usá-las
Quatro regiões resolvem quase todo site: <header>, <main>, <footer> e, dentro do header, um <nav>. Leitor de tela lista essas regiões e deixa pular direto para elas — por isso cada uma aparece uma vez só e o <main> tem o conteúdo, não o menu.
Uma landing page é uma sequência de seções, e o que faz ela parecer feita com cuidado é todas seguirem o mesmo molde: a mesma margem nas laterais, o mesmo respiro em cima e embaixo, o título sempre no mesmo lugar. Quando o molde é um só, seção nova custa minutos.
o molde
<section class="secao" id="depoimentos" aria-labelledby="depoimentos-titulo">
<div class="container pilha">
<h2 id="depoimentos-titulo">O que dizem os clientes</h2>
<!-- o conteúdo da seção -->
</div>
</section>
Peça
O que resolve
.secao
o respiro vertical, igual em todas
id
o menu e os botões levam direto para ela: href="#depoimentos"
.container
a largura máxima e a margem lateral, iguais em todas
.pilha
o espaço entre título, texto e conteúdo
Seção com fundo de cor diferente continua no molde: a cor vai na <section>, que ocupa a largura toda, e o .container dentro dela mantém o texto alinhado com o resto da página.
Onze seções já prontas neste molde — topo com menu no celular, hero, serviços, quem faz, como funciona, depoimentos, planos, dúvidas, chamada final, contato e rodapé — estão na categoria Seções dos Modelos, para copiar direto no projeto-base. Que seções uma landing page precisa, e em que ordem, é estratégia: está em O catálogo de seções e A sequência lógica. Aqui é só o molde onde elas entram.
PARTE 3
O CSS em camadas
Um arquivo de estilo que cresce sem virar briga: em que ordem as coisas entram, o reset curto que todo projeto usa, onde moram os tokens e o que é estilo de base.
A ordem do arquivo de estilo
NovoCSS
CSS sem ordem vira uma sequência de correções: a regra de baixo vence a de cima, alguém põe !important, e seis meses depois ninguém sabe por que o botão está azul. O @layer resolve isso declarando, logo na primeira linha, quem vence quem.
Com as camadas declaradas, a de baixo na lista sempre vence a de cima, não importa a especificidade: um .botao em componentes vence um a em base, mesmo que a regra do a esteja escrita depois.
O detalhe que pega: CSS escrito fora de qualquer camada vence todas elas. Isso é útil para uma correção de emergência, e é uma armadilha se um arquivo de terceiro for incluído sem camada — ele passa por cima de tudo. O resto da sintaxe está em Aninhamento nativo e @layer.
Um reset curto
NovoCSS
Cada navegador vem com estilos próprios: margens nos títulos, imagens que estouram a coluna, botões com outra fonte. O reset zera o que atrapalha e mais nada — não precisa de biblioteca, cabe em quinze linhas.
largura de 300px com padding continua 300px, não 340px
margin: 0
as margens do navegador somem; o espaço vem da escala, não de um padrão invisível
img { max-width: 100% }
foto grande não estoura a coluna no celular
font: inherit nos campos
botão e campo usam a fonte do site, não a do sistema
ul[class]
lista usada como layout perde os marcadores; lista de texto corrido mantém
prefers-reduced-motion
quem pediu menos movimento no sistema recebe menos movimento
O que é o box model, e por que o border-box muda a conta, está em Box model. O porquê do movimento reduzido, em Movimento.
Os tokens no :root
NovoCSS
Token é uma decisão de design com nome: --cor-destaque em vez de #0D6B62 espalhado em vinte lugares. Mudar a cor do site vira mudar uma linha — e o modo escuro vira redefinir as mesmas variáveis com outros valores.
Os nomes acima são exatamente os que a ferramenta Design system exporta. O fluxo é: decidir cor, fonte e escala nas ferramentas da fase 2, baixar o tokens.css e colocar no lugar deste. O estilo.css não muda — ele só usa os nomes.
Nenhum valor solto no estilo.css. Se aparecer #333 ou margin: 13px no meio de um componente, ou é um token que faltou criar, ou é um erro. É essa regra que faz o site inteiro parecer da mesma mão. O porquê está em Variáveis CSS e as três camadas de token.
O estilo de base
NovoCSS
A camada de base estiliza as tags sozinhas, sem classe: o texto corrido, os títulos, os links, o foco. Feita direito, um parágrafo novo já nasce certo — sem ninguém lembrar de pôr classe nele.
base
@layer base {
html { scroll-behavior: smooth; }
body {
background: var(--cor-fundo); color: var(--cor-texto);
font-family: var(--fonte-corpo); font-weight: var(--peso-corpo);
font-size: var(--t-corpo); line-height: var(--alt-corpo);
}
h1, h2, h3 {
font-family: var(--fonte-titulo); font-weight: var(--peso-titulo);
line-height: var(--alt-titulo); text-wrap: balance;
}
h1 { font-size: var(--t-h1); }
h2 { font-size: var(--t-h2); }
h3 { font-size: var(--t-h3); }
p { max-width: var(--medida); text-wrap: pretty; }
a { color: var(--cor-destaque-texto); text-underline-offset: .15em; }
:root { accent-color: var(--cor-destaque); } /* marcar, opção e deslizar na cor da marca (parte 8) */
:focus-visible { outline: 2px solid var(--cor-foco); outline-offset: 3px; }
[id] { scroll-margin-top: var(--e9); } /* a âncora não some atrás do topo fixo */
}
text-wrap: balance nos títulos evita a última linha com uma palavra só; pretty nos parágrafos evita a palavra órfã no fim. Navegador que não entende simplesmente ignora
max-width: var(--medida) no parágrafo segura a linha numa largura confortável de ler, mesmo em tela grande
:focus-visible mostra o foco para quem usa teclado, sem aparecer no clique do mouse
scroll-margin-top faz o link #servicos parar abaixo do topo fixo, e não com o título escondido atrás dele
Quase todo layout de site de serviço é a combinação de poucos padrões. Ter cada um como uma classe pronta é o que faz a página se montar rápido e continuar alinhada.
O container
NovoCSS
A largura máxima do conteúdo e a margem lateral no celular, numa linha só:
Leia assim: "a largura é o menor entre a tela menos 24px de cada lado e 72rem". No celular, vale a primeira — sobra margem dos dois lados. Na tela grande, vale a segunda — o conteúdo para de crescer e fica centralizado pelo margin-inline: auto. Nenhuma media query.
Uma largura para o site todo, não uma por seção. Texto corrido fica ainda mais estreito pela --medida do parágrafo; o que é mais largo — uma galeria, um mapa — sai do container de propósito, e isso é uma decisão, não um acidente.
Pilha, fila e grade
NovoCSS
Três classes cobrem quase todo arranjo de uma página de serviço. Cada uma resolve uma pergunta só.
os três padrões
@layer layout {
.container { width: min(100% - 2 * var(--e5), 72rem); margin-inline: auto; }
.secao { padding-block: clamp(var(--e7), 8vw, var(--e9)); }
/* pilha: coisas uma embaixo da outra, com o mesmo respiro */
.pilha { display: flex; flex-direction: column; gap: var(--e5); }
/* fila: coisas lado a lado que quebram linha quando não cabem */
.fila { display: flex; flex-wrap: wrap; align-items: center; gap: var(--e3); }
/* grade: quantas colunas couberem, nenhuma mais estreita que 16rem */
.grade {
display: grid; gap: var(--e5);
grid-template-columns: repeat(auto-fit, minmax(min(16rem, 100%), 1fr));
}
}
Padrão
A pergunta
Onde aparece
.pilha
coisas uma embaixo da outra, com o mesmo espaço entre elas
título + texto + botão; o conteúdo de um card
.fila
coisas lado a lado que quebram linha quando não cabem
os links do menu, os botões, as etiquetas
.grade
quantas colunas couberem, todas iguais
cards de serviço, depoimentos, fotos
A .grade é a que mais economiza: auto-fit com minmax(min(16rem, 100%), 1fr) dá uma coluna no celular, duas no tablet e três ou quatro na tela grande — sem uma media query sequer. O min(16rem, 100%) é o que impede a coluna de estourar numa tela menor que 16rem.
A ideia de montar layout com poucos padrões pequenos e combináveis é o centro do livro Every Layout, de Heydon Pickering e Andy Bell. O funcionamento de cada propriedade está em Flexbox e Grid.
Do celular para cima
NovoCSS
A maior parte das visitas de site de serviço chega pelo celular, e o layout de celular é o mais simples: uma coluna. Por isso o CSS começa por ele, e a tela grande é que ganha regras a mais — com min-width.
celular primeiro
/* o padrão já é o celular: uma coluna */
.hero { display: grid; gap: var(--e6); }
/* a partir de onde o conteúdo pede, duas colunas */
@media (min-width: 52rem) {
.hero { grid-template-columns: 1.2fr 1fr; align-items: center; }
}
o ponto de quebra vem do conteúdo, não de um aparelho: abra a página, alargue a janela devagar e ponha a media query onde o layout começa a ficar ruim
poucos pontos de quebra: com .grade e clamp() fazendo o trabalho, um ou dois costumam bastar
em rem, não em px: quem aumentou a fonte no celular recebe o layout de uma coluna mais cedo, que é o que ele precisa
O que mantém um projeto legível daqui a seis meses: nomes de classe que dizem o que a coisa é, componentes pensados como peças, e um JavaScript que não derruba a página quando uma parte falha.
Nomes de classe que não viram bagunça
NovoCSS
O nome de classe descreve o que a coisa é, não como ela parece: .card-servico continua certo quando o card deixar de ser azul; .caixa-azul, não.
Uma convenção leve, a mesma usada neste site, resolve quase tudo: o bloco, o elemento de dentro com dois sublinhados, a variação com dois hífens.
bloco, elemento, variação
<article class="card card--destaque">
<h3 class="card__titulo">Reforma completa</h3>
<p class="card__texto">Do projeto à entrega da chave.</p>
</article>
Parte
Formato
Quer dizer
bloco
.card
uma peça que faz sentido sozinha
elemento
.card__titulo
uma parte que só existe dentro do bloco
variação
.card--destaque
o mesmo bloco, um pouco diferente
O que ganha com isso: olhando só o HTML, dá para saber onde está o estilo de cada coisa; e nenhuma regra de um componente vaza para outro, porque ninguém escreve .card h3 — escreve .card__titulo. A convenção se chama BEM; aqui ela é usada sem cerimônia.
Pensar em componentes sem framework
NovoCSS
Componente não é coisa de React. É uma peça com nome, que funciona em qualquer lugar da página e que tem todos os estados previstos: o botão normal, com o mouse em cima, com foco, pressionado e desabilitado.
Um componente está pronto quando
Teste
funciona em qualquer seção
copiar para outra seção e não precisar mexer em nada
não depende de onde está
nenhuma regra do tipo .hero .botao
tem todos os estados
Tab até ele, passar o mouse, apertar, desabilitar
só usa tokens
nenhum valor solto no bloco dele
A variação vem pela classe --, nunca pelo lugar: o botão da hero é um .botao--grande que qualquer outra seção pode usar, não um .hero .botao que só existe ali.
A mesma ideia nos frameworks está em Pensar em componentes. Os componentes com todos os estados, já nos tokens do projeto, saem da ferramenta Design system; efeitos prontos para copiar estão em Modelos.
O JavaScript de um site de serviço
NovoJS
Site de serviço precisa de pouco JavaScript: o menu no celular, um acordeão, talvez um formulário. O risco não é o tamanho — é um erro num pedaço derrubar todos os outros.
js/app.js
/* app.js — cada bloco se guarda sozinho: se o elemento não existe, sai */
/* ano do rodapé sempre atual */
(function () {
var ano = document.querySelector('[data-ano]');
if (!ano) return;
ano.textContent = new Date().getFullYear();
})();
/* o próximo recurso entra aqui, no mesmo formato */
cada recurso num bloco próprio, que começa verificando se o elemento existe — a página sem aquele elemento simplesmente pula o bloco
um arquivo, com defer: roda depois que o HTML está pronto, sem esperar DOMContentLoaded
o conteúdo não depende dele: com o JavaScript desligado, o texto continua lá e os links continuam funcionando
localStorage dentro de try: aba anônima e navegador bloqueado não podem quebrar a página
Quase todo site tem ícone: no botão do WhatsApp, na lista de serviços, no menu do celular, no logo. SVG resolve todos — nítido em qualquer tela, leve, e com a cor controlada pelo CSS. O que muda é como ele entra na página.
Três jeitos de pôr um SVG na página
NovoHTML
O mesmo arquivo .svg pode entrar de três formas, e cada uma decide o que o CSS consegue fazer com ele.
viewBox é a folha de desenho — "0 0 24 24" quer dizer um quadrado de 24 unidades. Com ele, o ícone escala para qualquer tamanho sem perder nitidez
o tamanho vem do CSS, não de width e height no código: o mesmo ícone serve no botão pequeno e no card grande
fonte de ícones (um arquivo de fonte cheio de símbolos) era o jeito antigo: baixa o conjunto inteiro para usar cinco ícones, e aparece como quadradinho quando a fonte falha. SVG inline substituiu com vantagem
currentColor: o ícone segue o texto
NovoCSS
currentColor é uma palavra do CSS que quer dizer "a cor do texto aqui". Um ícone desenhado com ela muda de cor junto com o link ou o botão onde está — no hover, no foco, no modo escuro — sem nenhuma regra a mais.
a classe de todo ícone
.icone {
display: inline-block; /* o reset põe todo svg em bloco */
vertical-align: -.25em; /* alinha com a linha do texto */
width: 1.25em; height: 1.25em; /* em: acompanha o tamanho do texto ao lado */
flex: none; /* não encolhe dentro de um flex */
fill: none;
stroke: currentColor; /* a cor do texto em volta */
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
A classe .icone já está no estilo.css do projeto-base. O botão já tem a cor do texto definida; o ícone herda. Troque o color do botão no hover e o ícone troca junto. O 1.25em faz o ícone crescer quando o texto cresce, e o .botao do projeto-base já tem gap para separar os dois.
Ícone de traço (linha) usa stroke; ícone cheio usa fill: currentColor e stroke: none. Não misture os dois estilos no mesmo site — é o tipo de detalhe que faz a página parecer montada com pedaços de lugares diferentes.
Sprite: desenhar uma vez, usar em todo lugar
NovoHTML
Colar o mesmo SVG em doze cards repete o desenho doze vezes no HTML. O sprite guarda cada ícone uma vez, com um nome, e cada uso só aponta para ele.
O <use> copia o desenho do <symbol>, e o viewBox vem junto. A classe .icone continua valendo: cor, tamanho e espessura do traço passam para dentro da cópia.
Sprite em arquivo separado (<use href="img/icones.svg#i-check"/>) economiza ainda mais, porque o navegador guarda o arquivo em cache — mas só funciona servido por um servidor, do mesmo domínio. Abrindo o HTML direto do computador, com dois cliques, os ícones somem no Chrome. Para landing page de uma página, o sprite dentro do HTML é o mais seguro.
Ícone que é botão precisa de nome
NovoHTML
Quando o ícone acompanha um texto — "✓ Entrega no prazo" —, ele é enfeite, e o aria-hidden="true" o esconde do leitor de tela para não ser lido duas vezes. Quando o ícone é o botão inteiro, é o contrário: sem texto, o leitor de tela anuncia só "botão", e ninguém sabe o que ele faz.
botão só com ícone
<button class="botao-icone" type="button" aria-label="Fechar">
<svg class="icone" aria-hidden="true"><use href="#i-fechar"/></svg>
</button>
<!-- ou, com texto que só o leitor de tela lê -->
<a class="botao-icone" href="https://instagram.com/perfil">
<svg class="icone" aria-hidden="true"><use href="#i-instagram"/></svg>
<span class="so-leitor">Instagram</span>
</a>
Caso
O que fazer
ícone ao lado de texto
aria-hidden="true" no <svg>
botão ou link só com ícone
nome no aria-label ou num .so-leitor, e o SVG com aria-hidden
ícone que é informação sozinho (um selo, um gráfico)
role="img" e aria-label no <svg>
E o tamanho do toque: um ícone de 20px é pequeno demais para o dedo. O botão em volta dele precisa ter pelo menos 44 por 44 pixels, com o ícone no centro. A classe .so-leitor é a do projeto-base; o porquê de tudo isso está em ARIA — e quando não usar.
O logo em SVG
NovoHTML
Logo em PNG fica borrado na tela de alta densidade e pesa mais do que precisa. Em SVG, fica nítido em qualquer tamanho e costuma pesar poucos KB. Peça ao cliente o arquivo vetorial — .svg, .ai, .pdf ou .eps —, e não a imagem que ele tem no celular.
Onde
Como entra
Por quê
No topo, com as cores da marca
<img src="img/logo.svg" alt="Nome do negócio">
as cores são do desenho; o CSS não precisa mexer
No rodapé ou sobre foto, em uma cor só
inline, com fill: currentColor
a mesma marca em branco, em preto ou na cor do fundo, sem outro arquivo
O alt do logo é o nome do negócio, não "logo". E o width e height no <img> reservam o espaço antes do arquivo chegar — sem eles, o topo pula quando o logo carrega.
Quando o cliente não tem o arquivo vetorial, redesenhar o logo é um serviço à parte, não um detalhe da entrega. O favicon e a imagem de compartilhamento estão em Favicon e imagem de compartilhamento.
Limpar o SVG antes de usar
NovoHTML
SVG exportado do Figma, do Illustrator ou baixado de um site vem com coisa que não serve para nada na página: comentários do programa, metadados, ids gerados, números com oito casas decimais. Um ícone de 400 bytes chega com 3 KB.
passe pelo SVGOMG, a versão no navegador do otimizador SVGO: arraste o arquivo e baixe o limpo
mantenha o viewBox — é ele que deixa o SVG escalar
tire o fill fixo dos ícones (#1D1D1B) para o currentColor do CSS funcionar
confira o resultado a olho: a otimização agressiva às vezes arredonda demais e entorta uma curva
SVG é código, não só imagem. Um SVG pode ter <script> dentro, e colado inline ele roda na sua página. Só cole SVG de fonte em que você confia, e passe pelo otimizador — ele remove o que não é desenho. O risco está explicado em XSS — quando texto vira código.
Onde achar ícones — e o que olhar antes
Novo
Desenhar ícone não é o seu trabalho. Existem conjuntos gratuitos, bem desenhados e com licença que permite uso em site de cliente.
Conjunto
Estilo
Licença
Lucide
traço, simples
ISC
Heroicons
traço e cheio
MIT
Phosphor
vários pesos do mesmo ícone
MIT
Tabler Icons
traço, milhares de ícones
MIT
Material Symbols
o do Google, ajustável
Apache 2.0
um conjunto por site: ícones de conjuntos diferentes têm espessura e cantos diferentes, e isso aparece
copie o SVG do site do conjunto, só dos ícones que vai usar — não instale um pacote de mil ícones para usar oito
confira a licença antes: todas acima permitem uso comercial, mas a licença está sempre no site do conjunto e é ela que vale
logo de rede social e do WhatsApp é marca registrada: use o arquivo oficial da central de marca de cada empresa, sem mudar proporção nem cor além do que as regras deles permitem
Para o ícone que não existe em lugar nenhum — o símbolo do serviço específico do cliente —, a saída é uma ilustração encomendada ou uma foto, não um ícone improvisado. Onde procurar referência visual está em Onde procurar, na trilha Vibecoding.
PARTE 7
Vídeo, mapa e outros embeds
O vídeo do YouTube, o mapa do Google, o feed do Instagram e a agenda de horários são o que o cliente mais pede — e o que mais pesa na página. O padrão que resolve quase todos é o mesmo: mostrar uma imagem ou um link e só carregar o de verdade quando a pessoa pede.
O que um embed custa
Novo
Colar o código de incorporação do YouTube parece colar uma imagem. Não é: o <iframe> abre uma segunda página dentro da sua, com o player inteiro, os scripts de medição e as fontes dele — tudo baixado no carregamento, antes de qualquer pessoa pensar em apertar o play.
Embed
O que chega junto
O que fazer
Vídeo do YouTube ou Vimeo
o player e os scripts dele, centenas de KB
fachada: capa com botão, o player só no clique
Mapa do Google
o mapa interativo inteiro
link de como chegar; ou o mapa com loading="lazy"
Post ou feed do Instagram
o script de incorporação da rede
um print com link para o post; ou o feed pela API, sem o script
Avaliações do Google em widget
script de terceiro, às vezes pago
as avaliações escritas na página, com link para o perfil
Para ver o custo real, abra o DevTools na aba Network, recarregue com e sem o embed e compare o total transferido. A diferença costuma ser maior que a página inteira sem ele.
Embed pesado no topo da página derruba o LCP, a métrica de carregamento que o Google mede — está em Consertar o LCP. O aviso curto já aparecia em Performance; aqui está o como.
Vídeo do YouTube que carrega no clique
NovoHTMLJS
A fachada é uma imagem de capa com um botão de play por cima. Parece o player, pesa só a imagem, e no clique o botão é trocado pelo <iframe> de verdade, já tocando. Quem nunca aperta o play nunca baixa o player.
/* troca a capa pelo player só quando a pessoa pede */
document.querySelectorAll('.video[data-video]').forEach(function (caixa) {
var botao = caixa.querySelector('.video__play');
if (!botao) return;
botao.addEventListener('click', function () {
var f = document.createElement('iframe');
f.src = 'https://www.youtube-nocookie.com/embed/' + caixa.dataset.video + '?autoplay=1';
f.title = botao.textContent.trim();
f.allow = 'autoplay; encrypted-media; picture-in-picture; fullscreen';
caixa.innerHTML = '';
caixa.appendChild(f);
f.focus();
});
});
a capa é sua, salva em img/: um quadro bonito do vídeo, em WebP. Dá para usar a miniatura que o YouTube gera, mas aí a página já fala com o servidor dele antes do clique
youtube-nocookie.com é o "modo de privacidade aprimorado" do próprio YouTube: segundo o Google, ele não guarda informação de quem visita até a pessoa dar play
o title do iframe é o que o leitor de tela anuncia; e o foco vai para o player, para quem usa teclado não se perder
o aspect-ratio reserva o espaço do vídeo antes de tudo carregar — a página não pula
No celular, alguns navegadores só tocam de verdade no segundo toque, dentro do próprio player. É uma regra do navegador, não um erro da fachada.
Vídeo próprio no site
NovoHTML
Quando o vídeo é curto e é do cliente — a vitrine girando, o antes e depois, um fundo discreto na hero —, ele pode morar no próprio site com a tag <video>, sem player de ninguém.
vídeo com controles
<video controls preload="none" poster="img/capa-obra.webp" width="1280" height="720">
<source src="video/obra.mp4" type="video/mp4">
Seu navegador não mostra vídeo. <a href="video/obra.mp4">Baixe o arquivo</a>.
</video>
a imagem que aparece antes — sem ela, fica um retângulo preto
muted + playsinline
sem os dois, o iPhone não toca sozinho e abre em tela cheia
aria-hidden no fundo
vídeo decorativo não é conteúdo; o leitor de tela pula
Vídeo longo não mora no site. Arquivo de vídeo pesa dezenas de megabytes, e cada visita consome banda da hospedagem — no plano gratuito, isso acaba. Até uns 30 segundos, comprimido e sem som, tudo bem; acima disso, YouTube ou Vimeo com a fachada. E vídeo que se mexe sozinho precisa parar para quem pediu menos movimento no sistema: pause-o com prefers-reduced-motion ou mostre só o poster.
Mapa: link, embed ou imagem
NovoHTML
O mapa interativo raramente é o que a pessoa precisa. No celular, o que ela quer é tocar e abrir o aplicativo de mapas já com a rota — e isso é um link.
Opção
Peso
Melhor para
Link "Como chegar"
nenhum
quase sempre: abre o app de mapas no celular
Imagem do mapa + link
uma imagem
quando a localização é argumento (perto do centro, fácil de estacionar)
Mapa incorporado com loading="lazy"
o mapa inteiro, mas só ao chegar perto
quando o cliente faz questão do mapa interativo
o link que abre o app
<a href="https://www.google.com/maps/search/?api=1&query=Rua+Exemplo+123+Cidade" target="_blank" rel="noopener">
Como chegar ↗
</a>
o mapa incorporado, sem pesar no carregamento
<iframe
src="https://www.google.com/maps/embed?pb=..."
title="Mapa: Nome do negócio, Rua Exemplo, 123"
width="600" height="400" style="border: 0; width: 100%; height: auto; aspect-ratio: 3 / 2"
loading="lazy" referrerpolicy="no-referrer-when-downgrade"
allowfullscreen></iframe>
O endereço do src sai do próprio Google Maps: Compartilhar, depois Incorporar um mapa. O loading="lazy" faz o navegador só baixar o mapa quando a pessoa rola até perto dele — quem não chega ao rodapé nunca paga o custo.
"Quero o Instagram no site, atualizando sozinho" é pedido comum — e é o item que mais quebra depois da entrega. Antes de prometer, escolha o caminho de propósito:
Caminho
Atualiza sozinho
O que custa
Print dos posts com link para o perfil
não
nada; alguém troca as imagens de vez em quando
Widget pronto de um serviço de terceiro
sim
o script dele na página, o que ele coleta de quem visita e o plano dele — leia os três antes
Integração própria pela API da Meta
sim
uma função no servidor e um token que vence a cada 60 dias
A API mudou em 4 de dezembro de 2024: a Meta desligou a antiga Basic Display API, a única que aceitava conta pessoal. Hoje o caminho oficial é a Instagram API com login do Instagram, que só funciona com conta profissional (Comercial ou Criador de conteúdo). Se o cliente usa conta pessoal, a primeira conversa é essa. A preparação, em resumo: um app do tipo Business no painel de desenvolvedor da Meta, a conta do cliente ligada a ele com a permissão instagram_business_basic e um token de acesso de longa duração gerado ali. O painel da Meta muda de lugar com frequência — siga o guia oficial no dia, não um print antigo.
O token nunca vai para o JavaScript da página: quem o tiver lê a conta. Ele fica numa variável de ambiente da Vercel, e uma função no servidor busca os posts e devolve só o que a página precisa:
api/instagram.mjs — a função na Vercel
/* INSTAGRAM_TOKEN fica em Settings → Environment Variables, nunca no código */
export async function GET() {
const campos = 'caption,media_type,media_url,thumbnail_url,permalink';
const url = 'https://graph.instagram.com/me/media?fields=' + campos +
'&limit=9&access_token=' + process.env.INSTAGRAM_TOKEN;
const r = await fetch(url);
if (!r.ok) return Response.json([], { status: 502 });
const { data = [] } = await r.json();
const posts = data
.map((p) => ({
link: p.permalink,
/* vídeo usa a miniatura; post com música protegida vem sem media_url */
imagem: p.media_type === 'VIDEO' ? p.thumbnail_url : p.media_url,
texto: (p.caption || '').slice(0, 120),
}))
.filter((p) => p.imagem);
/* a CDN da Vercel guarda a resposta por uma hora: a Meta não é chamada a cada visita */
return Response.json(posts, {
headers: { 'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400' },
});
}
na página: o link já funciona sem JavaScript
<div class="insta" data-insta>
<a href="https://instagram.com/perfil_do_cliente" target="_blank" rel="noopener">Ver no Instagram</a>
</div>
O token vence em 60 dias. Ele pode ser renovado a partir de 24 horas de vida, antes de vencer, e o novo vale mais 60 dias a partir da renovação — é a chamada acima. Ela devolve um token novo: troque a variável na Vercel e publique de novo. Esqueceu, o feed some e volta o link. Por isso esse item é manutenção de verdade, com data: ponha no calendário da Depois de publicar e cobre dentro do contrato de manutenção — é exatamente o que justifica a mensalidade.
Agendamento pelo site
NovoHTML
Consulta, sessão, aula experimental, visita com hora marcada: quando o negócio vive de horário, a pessoa quer escolher o dela sem esperar resposta. Mas agendamento só funciona se a agenda do cliente for a verdade — se ele marca no caderno e esquece de bloquear no sistema, o site oferece um horário que não existe, e o WhatsApp era melhor. Pergunte como ele marca hoje antes de propor.
No Google Agenda, a página de agendamento fica em Páginas de agendamento; o código sai de Opções → Opções de compartilhamento → Incorporar no site, em duas formas: a página inteira dentro do site, ou um botão que abre um pop-up. O jeito mais leve é nenhum dos dois — um link:
o botão que só abre a agenda: não pesa nada
<a class="botao" href="LINK_DA_PAGINA_DE_AGENDAMENTO" target="_blank" rel="noopener">
Escolher um horário
</a>
a agenda dentro da página, só quando a pessoa chega nela
<iframe
src="ENDERECO_DO_CODIGO_DE_INCORPORACAO"
title="Agendar horário com Nome do negócio"
loading="lazy"
style="border: 0; width: 100%; height: 640px"></iframe>
a conta da agenda é do cliente, como o domínio: é nela que ficam nome, telefone e horário de cada pessoa que marcou
o pop-up e a página incorporada carregam o script do serviço; o link não carrega nada. Incorporado, ponha numa página própria (agendar.html) ou perto do fim, com loading="lazy", para não pesar a abertura
o iframe precisa de title, que é o que o leitor de tela anuncia; e teste a marcação inteira no celular, porque é lá que ela acontece
Todo embed de terceiro — YouTube, Maps, Instagram, widget de chat — faz o navegador de quem visita conversar com o servidor daquela empresa, e muitos gravam cookies. Para a lei, é coleta de dado pessoal, mesmo que você não tenha instalado nenhum "rastreamento".
a fachada resolve o peso e a privacidade de uma vez: nada é chamado até a pessoa clicar
o link no lugar do mapa não coleta nada
o que continuar incorporado entra na política de privacidade, com o nome da empresa
Quais dados cada ferramenta coleta e o que isso pede do site estão em O que você coleta sem perceber, na trilha Negócio.
PARTE 8
O que o CSS já resolve
Coisas que até pouco tempo pediam JavaScript, biblioteca ou gambiarra e hoje são uma ou duas linhas de CSS — e as duas situações que quase todo site esquece: a tabela no celular e a página impressa.
Carrossel sem biblioteca
NovoCSS
A maior parte do que se chama carrossel é uma fila de cards que se arrasta para o lado. O navegador já faz isso — com a inércia do dedo, o encaixe em cada card e a barra de rolagem — sem uma linha de JavaScript.
css
.carrossel {
display: flex; gap: var(--e4);
overflow-x: auto; overscroll-behavior-x: contain;
scroll-snap-type: x mandatory; /* para sempre num card, nunca no meio */
padding-bottom: var(--e3); /* espaço para a barra de rolagem */
}
.carrossel > * {
flex: 0 0 min(80%, 20rem); /* o pedaço do próximo aparece: mostra que dá para arrastar */
scroll-snap-align: start;
}
o pedaço do próximo card aparecendo na borda é o que avisa que existe mais — seta escondida não avisa ninguém
tabindex="0" deixa rolar pelo teclado, com as setas; o aria-label diz ao leitor de tela o que é a região
carrossel que passa sozinho é outra coisa, e quase sempre piora: ninguém consegue ler o que sai da tela enquanto lê
A versão com os pontinhos que acompanham e levam a cada card está pronta em Carrossel sem biblioteca, na galeria de Modelos.
Cor nos controles do formulário
NovoCSS
Caixa de marcar, botão de opção, barra deslizante e barra de progresso vêm com a cor do sistema — azul, quase sempre. Estilizar cada um na mão era um trabalho inteiro. Hoje é uma linha.
css
:root {
accent-color: var(--cor-destaque); /* marcar, opção, deslizar e progresso na cor da marca */
color-scheme: light dark; /* os controles nativos acompanham o modo escuro */
}
O accent-color já está no projeto-base. O color-scheme entra quando o site tiver modo escuro — ele faz a barra de rolagem, os campos e o calendário do <input type="date"> ficarem escuros quando o site está no modo escuro. Sem ele, aparece um campo branco brilhando no meio da página escura.
O navegador ajusta o tom da marca dentro do controle para manter contraste. Se a cor de destaque for muito clara, o controle fica com um visto escuro — é proposital. Os formulários em si estão em O que o navegador já faz de graça.
Cards alinhados entre si
NovoCSS
Três cards lado a lado, cada um com um título de tamanho diferente: os textos e os botões ficam desalinhados, cada card na sua altura. O subgrid faz as linhas de dentro de cada card seguirem as mesmas linhas da grade — título com título, texto com texto, botão com botão.
css
.cards {
display: grid; gap: var(--e5);
grid-template-columns: repeat(auto-fit, minmax(min(16rem, 100%), 1fr));
}
.cards > .card {
display: grid;
grid-row: span 3; /* cada card ocupa três linhas: título, texto, botão */
grid-template-rows: subgrid; /* e usa as linhas da grade de fora */
gap: var(--e3); /* o espaço entre as partes de dentro */
}
O número do span é o número de partes do card. Card com imagem, título, texto e botão é span 4. Com o subgrid, o botão de todos os cards para na mesma altura sem ninguém medir nada.
Funciona nos navegadores atuais desde 2023. Onde não funcionar, o card cai para uma grade comum — continua certo, só não fica alinhado. Grid em geral está em Grid.
Bloquear o fundo com inert
NovoHTML
Com o menu do celular aberto por cima da página, o Tab continua passando pelos links que estão atrás dele — invisíveis para quem vê, mas não para quem navega pelo teclado. O atributo inert desliga um pedaço inteiro da página: não recebe clique, nem foco, nem é lido.
js
/* ao abrir o menu, o resto da página sai de alcance; ao fechar, volta */
function menuAberto(aberto) {
document.querySelector('main').inert = aberto;
document.querySelector('footer').inert = aberto;
}
A janela feita com <dialog> e showModal() já faz isso sozinha — é um dos motivos para usá-la. O inert é para o que não é <dialog>: um menu lateral, um painel que desliza.
O <dialog> e o que ele resolve estão em <dialog>. O topo com menu da galeria esconde o painel fechado com visibility, que também o tira do Tab: Topo com menu no celular.
Tabela no celular
NovoCSS
Tabela de preços, de horários, de especificações: no celular, ela ou espreme o texto até ficar ilegível, ou estoura a largura da página e faz tudo rolar para o lado. A saída mais simples e mais segura é deixar a tabela rolar sozinha, dentro de uma caixa.
html
<div class="tabela-rolagem" role="region" aria-label="Tabela de preços" tabindex="0">
<table>...</table>
</div>
só a tabela rola; a página continua parada — nada de rolagem lateral na página inteira
tabindex="0" para rolar pelo teclado, e o aria-label para o leitor de tela saber o que é
se a tabela tem duas colunas — dia e horário, item e preço —, ela provavelmente cabe no celular sem rolar: teste antes de pôr a caixa
O truque de virar cada linha num card no celular (trocando o display das células) funciona na tela, mas em alguns navegadores faz a tabela deixar de ser tabela para o leitor de tela — as relações entre cabeçalho e dado se perdem. Se usar, teste com leitor de tela; a caixa com rolagem não tem esse risco.
A página impressa
NovoCSS
Cardápio, tabela de preços, política, orçamento: alguém vai imprimir, ou salvar em PDF pelo "Imprimir" do navegador. Sem estilo de impressão, sai o menu, o botão flutuante do WhatsApp, o fundo escuro gastando tinta e os links sem endereço.
no fim do estilo.css
@media print {
.topo, .botao, .zap-flutuante, video, iframe { display: none; }
body { background: #fff; color: #000; }
a[href^="http"]::after { content: " (" attr(href) ")"; font-size: .85em; }
.card, figure, tr { break-inside: avoid; } /* não corta um card no meio da folha */
h2, h3 { break-after: avoid; } /* título não fica sozinho no pé da página */
}
Para testar sem gastar papel: Ctrl+P (Cmd+P no Mac) e olhar a prévia, ou o DevTools, que simula a impressão na tela (Rendering → Emulate CSS media type → print).
Três linhas de impressão resolvem a maior parte, e ninguém lembra delas até o cliente mandar o PDF torto. Por isso esse bloco já está no estilo.css do projeto-base, na camada de utilitários.
PARTE 9
O projeto-base
Tudo desta trilha junto, pronto para copiar: é o ponto de partida de todo site novo. Troque o tokens.css pelo do cliente e comece pela primeira seção.
O projeto-base inteiro
NovoHTMLCSS
Três arquivos e as pastas. O index.html é o esqueleto das partes 2; o tokens.css e o estilo.css são as partes 3 e 4 montadas; o app.js é o formato da parte 5.
/* app.js — cada bloco se guarda sozinho: se o elemento não existe, sai */
/* ano do rodapé sempre atual */
(function () {
var ano = document.querySelector('[data-ano]');
if (!ano) return;
ano.textContent = new Date().getFullYear();
})();
/* o próximo recurso entra aqui, no mesmo formato */
Com o projeto-base copiado, a sequência de um site novo fica sempre a mesma:
criar a pasta com o nome do cliente e iniciar o Git (Começar um projeto)
gerar os tokens nas ferramentas da fase 2 e trocar o tokens.css
gerar o <head> na ferramenta Cabeçalho da página e colar no lugar do de exemplo
escrever o conteúdo de cada seção, mesmo que com [PREENCHER]
montar as seções no molde da parte 2, uma por vez, testando no celular a cada uma — as mais comuns já estão prontas, no formato do projeto-base, na categoria Seções dos Modelos
Depois da construção vêm a Qualidade — imagem, acessibilidade, velocidade e teste — e a trilha Depois do ar, com o que fazer quando o site é publicado.