Ficha 02 · JavaScript, DOM, projecto
- JavaScript
- DOM
- Fetch
- Deploy
Parte I · JavaScript
Exercício 1 · Sintaxe moderna (15 pts)
Reescreve este código JS para ES6+ moderno:
var nome = "Ana";
var idade = 25;
function saudar(nome, saudacao) {
if (saudacao === undefined) saudacao = "Olá";
return saudacao + ", " + nome + "!";
}
var numeros = [1, 2, 3];
var dobros = [];
for (var i = 0; i < numeros.length; i++) {
dobros.push(numeros[i] * 2);
}
var pessoa = { nome: nome, idade: idade };
Versão ES6+ moderna:
const nome = "Ana";
const idade = 25;
// Arrow function + default parameter
const saudar = (nome, saudacao = "Olá") => `${saudacao}, ${nome}!`;
// map em vez de for loop
const numeros = [1, 2, 3];
const dobros = numeros.map(n => n * 2);
// Shorthand property
const pessoa = { nome, idade };
Comparação detalhada:
1. var → const / let:
❌ Antigo:
var nome = "Ana"; // function-scoped, hoisting, redeclarável
✅ Moderno:
const nome = "Ana"; // block-scoped, não redeclarável
let idade = 25; // se precisa mudar
Por que:
- var tem behavior estranho (hoisting, function scope).
- const é default; let quando precisa mudar.
- Evita bugs.
2. Function declaration → Arrow function:
❌ Antigo:
function saudar(nome, saudacao) {
if (saudacao === undefined) saudacao = "Olá";
return saudacao + ", " + nome + "!";
}
✅ Moderno:
const saudar = (nome, saudacao = "Olá") => `${saudacao}, ${nome}!`;
Benefícios:
- Sintaxe mais curta.
- Default params built-in.
- Template literals em vez de concatenação.
- this léxico (importante em callbacks).
3. For loop → Métodos funcionais:
❌ Antigo:
var dobros = [];
for (var i = 0; i < numeros.length; i++) {
dobros.push(numeros[i] * 2);
}
✅ Moderno:
const dobros = numeros.map(n => n * 2);
Outros métodos úteis:
// filter — filtrar
const pares = numeros.filter(n => n % 2 === 0);
// reduce — acumular
const soma = numeros.reduce((acc, n) => acc + n, 0);
// find — encontrar primeiro
const primeiro = numeros.find(n => n > 1);
// some / every — verificar
const algumGrande = numeros.some(n => n > 10);
const todosPositivos = numeros.every(n => n > 0);
// forEach — iterar
numeros.forEach(n => console.log(n));
4. Object shorthand:
❌ Antigo:
var pessoa = { nome: nome, idade: idade };
✅ Moderno:
const pessoa = { nome, idade }; // shorthand quando key === value name
5. Outras melhorias ES6+ úteis:
Destructuring:
// Object
const { nome, idade } = pessoa;
const { nome: n } = pessoa; // rename
// Array
const [primeiro, segundo, ...resto] = [1, 2, 3, 4, 5];
Spread / Rest:
// Array
const arr1 = [1, 2, 3];
const arr2 = [...arr1, 4, 5]; // [1, 2, 3, 4, 5]
// Object
const obj1 = { a: 1 };
const obj2 = { ...obj1, b: 2 }; // { a: 1, b: 2 }
// Rest params
const sum = (...nums) => nums.reduce((a, b) => a + b);
sum(1, 2, 3); // 6
Optional chaining (?.):
// Antigo
if (user && user.address && user.address.city) {
console.log(user.address.city);
}
// Moderno
console.log(user?.address?.city); // undefined se algo é null
Nullish coalescing (??):
// Antigo: || considera 0 e "" como falsy
const valor = input || "default";
// Moderno: ?? só considera null/undefined
const valor = input ?? "default";
Async / await (em vez de callbacks/Promises chain):
// Promises chain
fetch(url)
.then(r => r.json())
.then(data => console.log(data))
.catch(err => console.error(err));
// Async/await (mais legível)
try {
const r = await fetch(url);
const data = await r.json();
console.log(data);
} catch (err) {
console.error(err);
}
Resumo: ES6+ torna o código mais conciso, legível, e menos error-prone. Em projectos modernos, sempre usar moderno.
Exercício 2 · DOM (15 pts)
Implementa funcionalidade: - Input para nome. - Botão "Adicionar à lista". - Lista que se actualiza. - Cada item tem botão "Remover". - Persistência no localStorage.
HTML:
<!DOCTYPE html>
<html lang="pt">
<head>
<meta charset="UTF-8">
<title>Lista de Tarefas</title>
<style>
body {
font-family: sans-serif;
max-width: 500px;
margin: 2rem auto;
padding: 1rem;
}
.add-form {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
.add-form input {
flex: 1;
padding: 0.5rem;
font-size: 1rem;
border: 1px solid #ccc;
border-radius: 4px;
}
.add-form button {
padding: 0.5rem 1rem;
background: #6366f1;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: 600;
}
.lista {
list-style: none;
padding: 0;
}
.lista li {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
background: white;
border: 1px solid #e5e7eb;
border-radius: 4px;
margin-bottom: 0.5rem;
}
.lista button {
background: #ef4444;
color: white;
border: none;
padding: 0.25rem 0.75rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.875rem;
}
.lista button:hover {
background: #dc2626;
}
.vazio {
text-align: center;
color: #999;
padding: 2rem;
}
</style>
</head>
<body>
<h1>Lista de Tarefas</h1>
<form class="add-form" id="form">
<input
type="text"
id="input"
placeholder="O que precisas fazer?"
required
maxlength="100">
<button type="submit">Adicionar</button>
</form>
<ul class="lista" id="lista">
<!-- Items adicionados aqui -->
</ul>
<script src="app.js"></script>
</body>
</html>
JavaScript (app.js):
// ============== Elementos do DOM ==============
const form = document.getElementById('form');
const input = document.getElementById('input');
const lista = document.getElementById('lista');
// ============== Estado (carregado do localStorage) ==============
const STORAGE_KEY = 'tarefas';
let tarefas = carregarTarefas();
// ============== Funções ==============
/**
* Carrega tarefas do localStorage.
*/
function carregarTarefas() {
try {
const dados = localStorage.getItem(STORAGE_KEY);
return dados ? JSON.parse(dados) : [];
} catch (e) {
console.error('Erro ao carregar:', e);
return [];
}
}
/**
* Guarda tarefas no localStorage.
*/
function guardarTarefas() {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(tarefas));
} catch (e) {
console.error('Erro ao guardar:', e);
alert('Erro ao guardar. Localstorage cheio?');
}
}
/**
* Renderiza a lista no DOM.
*/
function renderizar() {
// Limpar lista actual
lista.innerHTML = '';
if (tarefas.length === 0) {
lista.innerHTML = '<li class="vazio">Sem tarefas. Adiciona uma! 🎯</li>';
return;
}
// Criar items
tarefas.forEach((tarefa, index) => {
const li = document.createElement('li');
const span = document.createElement('span');
span.textContent = tarefa;
const btn = document.createElement('button');
btn.textContent = 'Remover';
btn.addEventListener('click', () => remover(index));
li.appendChild(span);
li.appendChild(btn);
lista.appendChild(li);
});
}
/**
* Adiciona nova tarefa.
*/
function adicionar(texto) {
const tarefa = texto.trim();
if (!tarefa) {
return;
}
// Evitar duplicados
if (tarefas.includes(tarefa)) {
alert('Esta tarefa já existe!');
return;
}
tarefas.push(tarefa);
guardarTarefas();
renderizar();
}
/**
* Remove tarefa por índice.
*/
function remover(index) {
if (confirm(`Remover "${tarefas[index]}"?`)) {
tarefas.splice(index, 1);
guardarTarefas();
renderizar();
}
}
// ============== Event listeners ==============
form.addEventListener('submit', (e) => {
e.preventDefault();
adicionar(input.value);
input.value = '';
input.focus(); // foco volta ao input
});
// ============== Inicializar ==============
renderizar();
input.focus();
Funcionalidades demonstradas:
✅ DOM manipulation:
- getElementById, createElement, appendChild, innerHTML.
✅ Events:
- addEventListener para submit e click.
- e.preventDefault() para impedir refresh.
✅ localStorage:
- getItem, setItem, JSON.parse/stringify.
- Try/catch para erros (quota, JSON inválido).
✅ State management:
- Variável tarefas como source of truth.
- Funções modificam state + chamam renderizar().
✅ UX: - Confirmação ao remover. - Aviso em duplicados. - Input focus após adicionar. - Estado vazio com mensagem.
Melhorias possíveis:
1. Marcar como concluído:
// Mudar tarefas para array de objects
let tarefas = [
{ texto: "Comprar leite", concluida: false }
];
// Botão extra para toggle
btnToggle.addEventListener('click', () => {
tarefa.concluida = !tarefa.concluida;
guardarTarefas();
renderizar();
});
// CSS para concluídas
.tarefa.concluida {
text-decoration: line-through;
opacity: 0.5;
}
2. Editar tarefa:
const btnEditar = document.createElement('button');
btnEditar.textContent = '✏️';
btnEditar.addEventListener('click', () => {
const novoTexto = prompt('Editar:', tarefa);
if (novoTexto) {
tarefas[index] = novoTexto;
guardarTarefas();
renderizar();
}
});
3. Filtrar (todas/activas/concluídas):
let filtro = 'todas';
function renderizar() {
let visible = tarefas;
if (filtro === 'activas') visible = tarefas.filter(t => !t.concluida);
if (filtro === 'concluidas') visible = tarefas.filter(t => t.concluida);
// ...
}
4. Drag & drop para reordenar.
5. Sincronizar com servidor em vez de localStorage.
6. PWA com service worker para offline.
Princípios:
- Separação de concerns: render, state, events em funções separadas.
- DRY: chamar
renderizar()em vez de duplicar código. - Validation: confirmar antes de remover.
- Error handling: try/catch em localStorage.
- Accessibility: form + submit, não apenas click.
Parte II · Fetch e APIs
Exercício 3 · Fetch API (15 pts)
Faz request GET a API pública e mostra resultado.
Exemplo: buscar info de utilizador GitHub:
<!DOCTYPE html>
<html lang="pt">
<head>
<meta charset="UTF-8">
<title>GitHub User</title>
<style>
body {
font-family: sans-serif;
max-width: 600px;
margin: 2rem auto;
padding: 1rem;
}
.search {
display: flex;
gap: 0.5rem;
margin-bottom: 2rem;
}
.search input {
flex: 1;
padding: 0.5rem;
font-size: 1rem;
}
.user-card {
background: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
text-align: center;
}
.user-card img {
width: 120px;
height: 120px;
border-radius: 50%;
margin-bottom: 1rem;
}
.user-card h2 {
margin: 0.5rem 0;
}
.user-card .bio {
color: #666;
margin: 1rem 0;
}
.stats {
display: flex;
justify-content: space-around;
padding-top: 1rem;
border-top: 1px solid #e5e7eb;
}
.stat {
text-align: center;
}
.stat .number {
font-size: 1.5rem;
font-weight: bold;
color: #6366f1;
}
.stat .label {
font-size: 0.875rem;
color: #999;
}
.loading, .error {
text-align: center;
padding: 2rem;
}
.error {
color: #ef4444;
}
</style>
</head>
<body>
<h1>GitHub User Lookup</h1>
<form class="search" id="form">
<input
type="text"
id="username"
placeholder="Username GitHub (ex: torvalds)"
required>
<button type="submit">Buscar</button>
</form>
<div id="resultado"></div>
<script>
const form = document.getElementById('form');
const usernameInput = document.getElementById('username');
const resultado = document.getElementById('resultado');
/**
* Busca utilizador GitHub.
*/
async function buscarUtilizador(username) {
const url = `https://api.github.com/users/${username}`;
try {
const response = await fetch(url);
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Utilizador "${username}" não encontrado`);
}
throw new Error(`Erro ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
if (error.name === 'TypeError') {
throw new Error('Sem conexão à internet');
}
throw error;
}
}
/**
* Renderiza utilizador no DOM.
*/
function renderizarUtilizador(user) {
resultado.innerHTML = `
<div class="user-card">
<img src="${user.avatar_url}" alt="${user.login}">
<h2>${user.name || user.login}</h2>
<p class="bio">${user.bio || '(sem bio)'}</p>
${user.location ? `<p>📍 ${user.location}</p>` : ''}
${user.company ? `<p>🏢 ${user.company}</p>` : ''}
<div class="stats">
<div class="stat">
<div class="number">${user.public_repos}</div>
<div class="label">Repos</div>
</div>
<div class="stat">
<div class="number">${user.followers}</div>
<div class="label">Followers</div>
</div>
<div class="stat">
<div class="number">${user.following}</div>
<div class="label">Following</div>
</div>
</div>
<p style="margin-top: 1rem;">
<a href="${user.html_url}" target="_blank">Ver no GitHub →</a>
</p>
</div>
`;
}
/**
* Mostra mensagem de erro.
*/
function mostrarErro(mensagem) {
resultado.innerHTML = `<div class="error">❌ ${mensagem}</div>`;
}
/**
* Mostra loading.
*/
function mostrarLoading() {
resultado.innerHTML = '<div class="loading">⏳ A buscar...</div>';
}
// Event listener
form.addEventListener('submit', async (e) => {
e.preventDefault();
const username = usernameInput.value.trim();
if (!username) return;
mostrarLoading();
try {
const user = await buscarUtilizador(username);
renderizarUtilizador(user);
} catch (error) {
mostrarErro(error.message);
}
});
</script>
</body>
</html>
Conceitos demonstrados:
1. fetch() (built-in moderno):
const response = await fetch(url);
2. Async/await:
- async function declara função assíncrona.
- await espera Promise resolver.
- Mais legível que .then() chain.
3. Response handling:
if (!response.ok) {
// Erro HTTP (4xx, 5xx)
}
const data = await response.json(); // parse JSON
4. Try/catch:
try {
await fetch(...);
} catch (error) {
// Network error ou erro lançado
}
5. Estados UI: - Loading. - Sucesso. - Erro.
6. Template literals para HTML:
resultado.innerHTML = `
<div class="card">
<h2>${user.name}</h2>
</div>
`;
Cuidado XSS: para conteúdo de utilizador (não de API confiável), usar textContent em vez de innerHTML, ou escapar.
Outras opções HTTP:
POST:
const response = await fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'Ana',
email: 'ana@x.pt'
})
});
PUT (update):
const response = await fetch(`https://api.example.com/users/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
DELETE:
const response = await fetch(`https://api.example.com/users/${id}`, {
method: 'DELETE'
});
Authentication:
const response = await fetch(url, {
headers: {
'Authorization': `Bearer ${token}`
}
});
Múltiplos requests em paralelo:
const [users, posts, comments] = await Promise.all([
fetch('/users').then(r => r.json()),
fetch('/posts').then(r => r.json()),
fetch('/comments').then(r => r.json())
]);
APIs públicas para praticar:
- JSONPlaceholder: jsonplaceholder.typicode.com (teste).
- OpenWeather: weather data.
- NASA APOD: foto astronómica diária.
- PokéAPI: pokemon.
- CoinGecko: crypto prices.
- Rest Countries: info de países.
Exercício 4 · Deploy (10 pts)
Como fazer deploy do site para a internet?
Deploy de Site Estático (HTML/CSS/JS)
Opções gratuitas modernas:
1. Vercel (recomendado para projectos modernos):
Setup:
# 1. Instalar CLI
npm install -g vercel
# 2. No directório do projecto
cd meu-site
# 3. Deploy
vercel
# Segue prompts: login, link to project, etc.
# 4. Deploy production
vercel --prod
Vantagens: - Gratuito para projectos pessoais. - HTTPS automático. - CDN global (rápido em qualquer lugar). - Preview deployments para cada commit/PR. - Custom domains gratuitos. - Integração GitHub automática.
2. Netlify (alternativa muito popular):
Drag & drop: - netlify.com → arrastar pasta do projecto. - Site online em segundos.
Ou via CLI:
npm install -g netlify-cli
netlify deploy --prod
Vantagens semelhantes a Vercel.
3. GitHub Pages (se já usas GitHub):
Setup:
1. Push código para repo GitHub.
2. Settings → Pages.
3. Source: main branch (root ou /docs).
4. Save.
5. URL: https://username.github.io/repo.
Limites: - Apenas sites estáticos (sem backend). - 1 GB de storage. - 100 GB de bandwidth/mês.
4. Cloudflare Pages:
Similar a Vercel/Netlify, com edge global da Cloudflare.
5. AWS S3 + CloudFront (para mais controlo):
Mais complexo mas standard em empresas.
Para sites com backend (Node.js, Python, etc.):
1. Render (moderno, gratuito tier): - render.com. - Suporta Node, Python, Ruby, Go. - Free tier dorme após inatividade (ok para projectos pessoais).
2. Railway: - railway.app. - $5/mês gratuito. - Deploy directo de GitHub.
3. Heroku (clássico, agora pago): - Pioneer em PaaS. - Free tier removido em 2022.
4. DigitalOcean App Platform: - Boa relação custo-qualidade.
5. Fly.io: - Deploy global em edge.
Workflow recomendado:
1. Criar conta GitHub (se não tem).
2. Criar repo + push código:
git init
git add .
git commit -m "Initial commit"
git remote add origin git@github.com:user/projecto.git
git branch -M main
git push -u origin main
3. Conectar Vercel ao GitHub: - vercel.com → Add New Project. - Import from GitHub. - Seleccionar repo. - Deploy.
4. Configurações (se necessário): - Domain: adicionar custom domain. - Environment variables: APIs keys, etc. - Build settings: se usa framework.
5. Push = deploy automático:
- Cada push para main → produção.
- Cada PR → preview deployment.
Custom domain:
1. Comprar domínio (~10€/ano): - Namecheap. - Cloudflare Registrar (mais barato). - GoDaddy.
2. Configurar DNS:
- No dashboard do registrar:
- A record: @ → IP do Vercel.
- CNAME: www → cname.vercel-dns.com.
3. Adicionar no Vercel:
- Settings → Domains → Add meusite.pt.
- Vercel detecta + emite SSL automaticamente.
4. Aguardar propagação (até 48h, normalmente 1h).
SSL/HTTPS: - Automático em Vercel, Netlify, etc. - Let's Encrypt por baixo. - Renovação automática.
Checklist antes de deploy:
- [ ] HTML válido (validator.w3.org).
- [ ] CSS sem erros (jigsaw.w3.org).
- [ ] Mobile responsive (testar em devtools).
- [ ] Imagens optimizadas (WebP, lazy loading).
- [ ] Meta tags SEO (title, description).
- [ ] Open Graph para social sharing.
- [ ] favicon.ico.
- [ ] robots.txt.
- [ ] sitemap.xml (para SEO).
- [ ] Analytics (Google Analytics, Plausible).
- [ ] Lighthouse score > 90.
- [ ] 404 page customizada.
Análise pós-deploy:
Lighthouse (Chrome DevTools): - Performance. - Accessibility. - Best Practices. - SEO.
PageSpeed Insights: - pagespeed.web.dev. - Análise + sugestões.
Google Search Console: - Submeter sitemap. - Monitorizar indexação. - Ver search analytics.
Analytics: - Google Analytics 4: visitas, fontes, comportamento. - Plausible: privacy-friendly, pago (mas barato). - Cloudflare Web Analytics: gratuito, sem cookies.
Continuous Deployment:
Setup recomendado:
GitHub repo
│
│ Push to main
▼
GitHub Actions (CI: test, lint, build)
│
│ Sucesso
▼
Vercel/Netlify (deploy)
│
▼
Production site (https://meusite.pt)
Vantagens: - Zero downtime deployments. - Rollback instantâneo se algo correr mal. - Preview de cada mudança antes de merge. - Histórico de deploys. - Notifications Slack/Discord quando deploy.
Resumo:
Para sites estáticos (HTML/CSS/JS): - Vercel ou Netlify = melhor escolha 2026. - 5 minutos para ter site online. - Gratuito para projectos pessoais.
Para sites dinâmicos (com backend): - Render ou Railway = boa escolha. - Mais complexo mas flexível.
Princípio: hoje é trivial colocar sites online. Não há desculpa para ter site só no localhost.
Parte III · Acessibilidade e SEO
Exercício 5 · Auditoria (15 pts)
Audita este HTML e lista problemas:
<html>
<head>
<title></title>
</head>
<body>
<div>
<div>logo</div>
<div>
<div onclick="menu()">menu</div>
</div>
</div>
<div>
<div>texto importante</div>
<div>
<img src="foto.jpg">
<div>clica aqui</div>
</div>
</div>
<div>copyright 2026</div>
</body>
</html>
Problemas identificados:
1. Faltam metadados essenciais:
- ❌ <!DOCTYPE html> em falta (modo quirks).
- ❌ lang attribute no html em falta (acessibilidade).
- ❌ charset em falta (encoding).
- ❌ viewport em falta (não responsive).
- ❌ <title> vazio (SEO + UX desastrosos).
- ❌ meta description em falta (SEO).
2. HTML não semântico:
- ❌ <div> por todo o lado em vez de tags semânticas.
- ❌ Sem <header>, <nav>, <main>, <footer>.
- ❌ Sem hierarquia de headings (h1, h2, etc.).
3. Acessibilidade (a11y) péssima:
- ❌ Imagem sem alt.
- ❌ "Menu" é <div> em vez de <button> (não acessível por teclado).
- ❌ "clica aqui" é <div> em vez de <a> ou <button>.
- ❌ "texto importante" sem heading apropriado.
- ❌ onclick em <div> (sem suporte teclado).
4. SEO problemas:
- ❌ Sem <h1>.
- ❌ Sem meta tags.
- ❌ Sem estrutura semântica.
- ❌ Link "clica aqui" sem contexto.
5. JavaScript inline obsoleto:
- ❌ onclick="menu()" (inline JS).
- ❌ Mistura HTML/JS.
HTML CORRIGIDO:
<!DOCTYPE html>
<html lang="pt-PT">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nome do Site | Tagline descritiva</title>
<meta name="description" content="Descrição do site para SEO (150-160 chars).">
<!-- Open Graph -->
<meta property="og:title" content="Nome do Site">
<meta property="og:description" content="Descrição">
<meta property="og:image" content="https://example.com/og-image.jpg">
<meta property="og:url" content="https://example.com">
<link rel="canonical" href="https://example.com">
<link rel="icon" type="image/png" href="favicon.png">
</head>
<body>
<header>
<a href="/" class="logo">
<img src="logo.svg" alt="Nome do Site">
</a>
<nav aria-label="Principal">
<button class="menu-toggle" aria-label="Abrir menu" aria-expanded="false">
<span class="hamburger" aria-hidden="true"></span>
</button>
<ul class="menu">
<li><a href="/">Home</a></li>
<li><a href="/sobre">Sobre</a></li>
<li><a href="/contacto">Contacto</a></li>
</ul>
</nav>
</header>
<main>
<article>
<h1>Título Principal Descritivo</h1>
<section>
<h2>Subsecção com Heading Apropriado</h2>
<p>Conteúdo do artigo com parágrafos bem estruturados.</p>
<figure>
<img
src="foto.jpg"
alt="Descrição detalhada do que está na imagem"
loading="lazy"
width="800"
height="600">
<figcaption>Legenda opcional da imagem</figcaption>
</figure>
<p>Mais conteúdo. Para mais informação sobre o tópico X,
<a href="/saber-mais">visite a nossa página de saber mais</a>.
</p>
</section>
</article>
</main>
<footer>
<p>© 2026 Nome do Site. Todos os direitos reservados.</p>
<nav aria-label="Footer">
<ul>
<li><a href="/privacidade">Política de Privacidade</a></li>
<li><a href="/termos">Termos de Uso</a></li>
<li><a href="/contacto">Contacto</a></li>
</ul>
</nav>
</footer>
<script src="app.js" defer></script>
</body>
</html>
Melhorias aplicadas:
✅ Semantic HTML:
- <header>, <nav>, <main>, <article>, <section>, <footer>.
- <h1> único na página + hierarquia.
- <figure> + <figcaption> para imagens.
✅ Acessibilidade:
- lang="pt-PT".
- alt em todas as imagens.
- aria-label em nav e button.
- aria-expanded no toggle.
- <button> em vez de <div> clicável.
- <a> para links com texto descritivo.
- loading="lazy" em imagens.
- width/height em imagens (evita layout shift).
✅ SEO:
- <title> descritivo.
- meta description.
- Open Graph tags.
- Canonical URL.
- <h1> claro.
- Texto de link descritivo ("visite a nossa página" não "clica aqui").
✅ Performance:
- loading="lazy" em imagens.
- defer no script.
- viewport meta.
✅ JavaScript separado:
- Sem onclick inline.
- addEventListener no script externo.
Antes vs Depois:
| Aspecto | Antes | Depois |
|---|---|---|
| Lighthouse SEO | 30/100 | 95/100 |
| Lighthouse a11y | 40/100 | 95/100 |
| Lighthouse Performance | 60/100 | 90/100 |
| Indexável | Não | Sim |
| Mobile-friendly | Não | Sim |
| Screen reader friendly | Não | Sim |
| Keyboard navigation | Não | Sim |
Ferramentas de auditoria: - Chrome DevTools → Lighthouse. - axe DevTools (extensão). - WAVE. - Google Search Console. - PageSpeed Insights.
Exercício 6 · Performance (10 pts)
Como optimizar performance de site lento?
Optimização de Performance Web
Métricas-chave (Core Web Vitals):
- LCP (Largest Contentful Paint): tempo para mostrar conteúdo principal. Target: < 2.5s.
- FID (First Input Delay): tempo para responder a primeira interacção. Target: < 100ms.
- CLS (Cumulative Layout Shift): instabilidade visual. Target: < 0.1.
Auditar com: Lighthouse, PageSpeed Insights, WebPageTest.
Optimizações:
1. Imagens (geralmente 50%+ do peso):
<!-- Formats modernos -->
<picture>
<source srcset="hero.avif" type="image/avif">
<source srcset="hero.webp" type="image/webp">
<img src="hero.jpg" alt="" width="800" height="600">
</picture>
<!-- Lazy loading -->
<img src="foto.jpg" loading="lazy" alt="">
<!-- Responsive -->
<img srcset="small.jpg 480w, large.jpg 1200w"
sizes="(max-width: 600px) 480px, 1200px"
src="large.jpg" alt="">
<!-- Sempre incluir width/height (evita CLS) -->
<img src="foto.jpg" width="800" height="600" alt="">
Comprimir com: - Squoosh.app. - ImageOptim (Mac). - TinyPNG. - Sharp (CLI).
WebP/AVIF: 30-50% menores que JPEG/PNG.
2. CSS:
- Minificar (remover whitespace/comments).
- Inline critical CSS (above the fold).
- Lazy load CSS não-crítico.
- Remove unused CSS (PurgeCSS).
<!-- Critical inline -->
<style>
/* CSS para above-the-fold */
</style>
<!-- Resto async -->
<link rel="preload" href="style.css" as="style"
onload="this.onload=null;this.rel='stylesheet'">
3. JavaScript:
- Minificar (Terser).
- Code splitting (carregar só o necessário).
- Defer / async: ```html
``` - Tree shaking (remover código não usado). - Modules ES6 (carregamento mais eficiente).
4. Fontes:
<!-- Preload critical fonts -->
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>
<!-- font-display: swap (mostra fallback enquanto carrega) -->
<style>
@font-face {
font-family: 'Inter';
src: url('inter.woff2') format('woff2');
font-display: swap;
}
</style>
Limitar fontes: 1-2 famílias, 2-3 pesos máximo.
Subset (apenas caracteres usados).
5. Cache HTTP:
Configurar headers (no servidor / Vercel):
Cache-Control: public, max-age=31536000, immutable # assets estáticos
Cache-Control: no-cache # HTML
Service Worker para offline (PWA):
// sw.js
self.addEventListener('fetch', (e) => {
e.respondWith(
caches.match(e.request).then(r => r || fetch(e.request))
);
});
6. CDN:
Servir assets de CDN global: - Cloudflare (gratuito). - CloudFront (AWS). - Vercel (incluído). - Netlify (incluído).
Reduz latência geográfica.
7. Reduzir requests:
- Combine CSS files.
- Sprites para ícones pequenos.
- Inline SVGs se pequenos.
- HTTP/2 multiplexing (server-side).
8. Lazy load:
Imagens:
<img src="foto.jpg" loading="lazy">
iframes:
<iframe src="youtube.com/..." loading="lazy"></iframe>
Componentes JS (em SPAs):
const Comp = lazy(() => import('./HeavyComponent'));
9. Reduzir DOM size:
- Pagination em vez de listar tudo.
- Virtual scrolling para listas longas.
- Limpar event listeners não usados.
10. Database / API:
- Cache responses (Redis).
- Pagination + cursor-based.
- GraphQL ou tRPC para evitar over-fetching.
- Connection pooling.
11. Reduce render-blocking:
<!-- CSS render-blocking — minimizar -->
<link rel="stylesheet" href="critical.css">
<!-- Defer non-critical -->
<link rel="preload" href="extra.css" as="style" onload="this.rel='stylesheet'">
<!-- Scripts não-críticos no fim do body ou com defer -->
<script src="app.js" defer></script>
12. Preload críticos:
<link rel="preload" href="hero.jpg" as="image">
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>
<link rel="dns-prefetch" href="https://api.example.com">
<link rel="preconnect" href="https://api.example.com" crossorigin>
13. Compression (servidor):
- Gzip (default na maioria dos servers).
- Brotli (melhor que gzip, suportado por browsers modernos).
- Vercel/Netlify aplicam automaticamente.
14. Third-party scripts:
Limitar (analytics, ads, widgets) — frequentemente o maior gargalo.
- Async load.
- Tag manager consolidado.
- Self-host se possível.
15. Database HTML:
- Sem inline scripts/styles grandes.
- Minimal initial HTML (lazy load resto).
- SSG (Static Site Generation) se possível.
Plano de optimização típico:
Fase 1 — Quick wins (1 dia, 70% melhoria): 1. Comprimir imagens. 2. Lazy loading. 3. Minificar CSS/JS. 4. Habilitar gzip/brotli.
Fase 2 — Médio prazo (1 semana): 1. WebP/AVIF. 2. Critical CSS inline. 3. Defer non-critical scripts. 4. Optimize fonts. 5. CDN.
Fase 3 — Longo prazo (1 mês+): 1. Service Worker / PWA. 2. Code splitting (SPA). 3. Server-side rendering. 4. Database optimization.
Targets realistas (sites bem optimizados):
- Total page size: < 1 MB.
- Time to Interactive: < 3s em 3G.
- Lighthouse Performance: > 90.
- LCP: < 2s.
- CLS: < 0.05.
Cuidados:
- ⚠️ Não over-optimize: pode complicar manutenção.
- ⚠️ Mede antes e depois: optimização sem dados é guessing.
- ⚠️ Foca no que importa: 80/20 — algumas optimizações trazem 80% do ganho.
Parte IV · Projecto
Exercício 7 · Landing page (15 pts)
Projecta estrutura HTML de landing page para um produto SaaS:
Estrutura HTML completa de Landing Page SaaS:
<!DOCTYPE html>
<html lang="pt-PT">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CRM Pro · Gestão Simples para Pequenos Negócios</title>
<meta name="description" content="Software CRM intuitivo que ajuda PMEs a gerir clientes, vendas e marketing num só lugar. Teste grátis 14 dias.">
<!-- Open Graph -->
<meta property="og:title" content="CRM Pro · Gestão Simples para Pequenos Negócios">
<meta property="og:description" content="Software CRM intuitivo...">
<meta property="og:image" content="https://crmpro.com/og.jpg">
<meta property="og:url" content="https://crmpro.com">
<meta property="og:type" content="website">
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image">
<link rel="canonical" href="https://crmpro.com">
<link rel="icon" href="/favicon.ico">
<link rel="stylesheet" href="/style.css">
</head>
<body>
<!-- Header / Nav -->
<header class="header">
<div class="container">
<a href="/" class="logo">
<img src="/logo.svg" alt="CRM Pro" width="120" height="32">
</a>
<nav aria-label="Principal">
<ul class="nav-menu">
<li><a href="#features">Funcionalidades</a></li>
<li><a href="#pricing">Preços</a></li>
<li><a href="#testimonials">Testemunhos</a></li>
<li><a href="/blog">Blog</a></li>
<li><a href="/contacto">Contacto</a></li>
</ul>
</nav>
<div class="header-cta">
<a href="/login" class="btn btn-text">Entrar</a>
<a href="/registar" class="btn btn-primary">Começar grátis</a>
</div>
</div>
</header>
<main>
<!-- Hero -->
<section class="hero" id="hero">
<div class="container">
<h1>O CRM mais simples para PMEs portuguesas</h1>
<p class="hero-subtitle">
Gerir clientes, vendas e marketing num só lugar.
Comece grátis em 30 segundos.
</p>
<div class="hero-cta">
<a href="/registar" class="btn btn-primary btn-large">
Começar grátis →
</a>
<a href="/demo" class="btn btn-secondary btn-large">
Ver demo (2 min)
</a>
</div>
<p class="hero-trust">
✓ 14 dias grátis ✓ Sem cartão de crédito ✓ Setup em 5 minutos
</p>
<figure class="hero-image">
<img
src="/hero-screenshot.webp"
alt="Screenshot da dashboard CRM Pro"
width="1200"
height="700"
loading="eager">
</figure>
</div>
</section>
<!-- Social proof / Logos -->
<section class="social-proof">
<div class="container">
<p>Mais de 5.000 empresas portuguesas confiam em nós</p>
<div class="logo-grid">
<img src="/cliente1.svg" alt="Cliente 1" loading="lazy">
<img src="/cliente2.svg" alt="Cliente 2" loading="lazy">
<img src="/cliente3.svg" alt="Cliente 3" loading="lazy">
<img src="/cliente4.svg" alt="Cliente 4" loading="lazy">
<img src="/cliente5.svg" alt="Cliente 5" loading="lazy">
</div>
</div>
</section>
<!-- Features -->
<section class="features" id="features">
<div class="container">
<h2>Tudo o que precisa para crescer</h2>
<p class="section-subtitle">
Ferramentas profissionais sem complicação.
</p>
<div class="features-grid">
<article class="feature">
<div class="feature-icon">📊</div>
<h3>Dashboard intuitivo</h3>
<p>Veja todos os indicadores essenciais num único ecrã.</p>
</article>
<article class="feature">
<div class="feature-icon">👥</div>
<h3>Gestão de clientes</h3>
<p>Toda a informação dos seus clientes organizada e acessível.</p>
</article>
<article class="feature">
<div class="feature-icon">💰</div>
<h3>Pipeline de vendas</h3>
<p>Acompanhe oportunidades do primeiro contacto ao fecho.</p>
</article>
<article class="feature">
<div class="feature-icon">📧</div>
<h3>Email marketing</h3>
<p>Campanhas profissionais com templates prontos a usar.</p>
</article>
<article class="feature">
<div class="feature-icon">📈</div>
<h3>Relatórios automáticos</h3>
<p>Insights semanais e mensais enviados para o seu email.</p>
</article>
<article class="feature">
<div class="feature-icon">🔒</div>
<h3>Seguro e GDPR</h3>
<p>Os seus dados em servidores europeus, em conformidade total.</p>
</article>
</div>
</div>
</section>
<!-- How it works -->
<section class="how-it-works">
<div class="container">
<h2>Comece em 3 passos</h2>
<ol class="steps">
<li class="step">
<span class="step-number">1</span>
<h3>Crie a sua conta</h3>
<p>Registo em 30 segundos com email. Sem cartão de crédito.</p>
</li>
<li class="step">
<span class="step-number">2</span>
<h3>Importe os seus dados</h3>
<p>Excel, CSV, ou conecte directamente com Gmail/Outlook.</p>
</li>
<li class="step">
<span class="step-number">3</span>
<h3>Comece a vender</h3>
<p>Dashboard pronto, equipa configurada. Vamos!</p>
</li>
</ol>
</div>
</section>
<!-- Testimonials -->
<section class="testimonials" id="testimonials">
<div class="container">
<h2>O que dizem os nossos clientes</h2>
<div class="testimonial-grid">
<blockquote class="testimonial">
<p>"Triplicámos as vendas em 6 meses usando o CRM Pro."</p>
<footer>
<img src="/avatar1.jpg" alt="Ana Silva" loading="lazy">
<cite>
<strong>Ana Silva</strong>
<span>CEO, Empresa A</span>
</cite>
</footer>
</blockquote>
<blockquote class="testimonial">
<p>"A interface mais simples que já usei. Equipa adoptou em 1 dia."</p>
<footer>
<img src="/avatar2.jpg" alt="João Costa" loading="lazy">
<cite>
<strong>João Costa</strong>
<span>Director Comercial, Empresa B</span>
</cite>
</footer>
</blockquote>
<blockquote class="testimonial">
<p>"Suporte em português, em horário português. Inestimável!"</p>
<footer>
<img src="/avatar3.jpg" alt="Maria Santos" loading="lazy">
<cite>
<strong>Maria Santos</strong>
<span>Fundadora, Empresa C</span>
</cite>
</footer>
</blockquote>
</div>
</div>
</section>
<!-- Pricing -->
<section class="pricing" id="pricing">
<div class="container">
<h2>Preços simples e transparentes</h2>
<p class="section-subtitle">Comece grátis. Pague apenas quando crescer.</p>
<div class="pricing-grid">
<article class="plan">
<h3>Free</h3>
<p class="plan-price">
<span class="price">0€</span>
<span class="period">/mês</span>
</p>
<ul class="plan-features">
<li>✓ Até 100 contactos</li>
<li>✓ 1 utilizador</li>
<li>✓ Funcionalidades essenciais</li>
<li>✓ Suporte por email</li>
</ul>
<a href="/registar?plan=free" class="btn btn-secondary">Começar grátis</a>
</article>
<article class="plan plan-featured">
<span class="plan-badge">Mais Popular</span>
<h3>Pro</h3>
<p class="plan-price">
<span class="price">29€</span>
<span class="period">/mês</span>
</p>
<ul class="plan-features">
<li>✓ Contactos ilimitados</li>
<li>✓ Até 5 utilizadores</li>
<li>✓ Todas as funcionalidades</li>
<li>✓ Email marketing</li>
<li>✓ Suporte prioritário</li>
</ul>
<a href="/registar?plan=pro" class="btn btn-primary">Teste 14 dias grátis</a>
</article>
<article class="plan">
<h3>Enterprise</h3>
<p class="plan-price">
<span class="price">99€</span>
<span class="period">/mês</span>
</p>
<ul class="plan-features">
<li>✓ Tudo do Pro</li>
<li>✓ Utilizadores ilimitados</li>
<li>✓ API access</li>
<li>✓ Account manager dedicado</li>
<li>✓ SLA 99.9%</li>
</ul>
<a href="/contacto" class="btn btn-secondary">Falar com vendas</a>
</article>
</div>
</div>
</section>
<!-- FAQ -->
<section class="faq">
<div class="container">
<h2>Perguntas frequentes</h2>
<details>
<summary>Quanto tempo posso testar gratuitamente?</summary>
<p>14 dias completos com todas as funcionalidades do plano Pro.</p>
</details>
<details>
<summary>Posso cancelar a qualquer momento?</summary>
<p>Sim. Sem fidelizações ou taxas de cancelamento.</p>
</details>
<details>
<summary>Os meus dados estão seguros?</summary>
<p>Servidores europeus, encriptação SSL, GDPR-compliant, backups diários.</p>
</details>
<details>
<summary>Posso migrar de outro CRM?</summary>
<p>Sim, oferecemos migração gratuita do seu CRM actual.</p>
</details>
</div>
</section>
<!-- Final CTA -->
<section class="cta-final">
<div class="container">
<h2>Pronto para transformar o seu negócio?</h2>
<p>Junte-se a milhares de empresas portuguesas.</p>
<a href="/registar" class="btn btn-primary btn-large">
Começar grátis →
</a>
<p class="cta-note">14 dias grátis · Sem cartão · Cancele quando quiser</p>
</div>
</section>
</main>
<!-- Footer -->
<footer class="footer">
<div class="container">
<div class="footer-grid">
<div class="footer-col">
<img src="/logo-white.svg" alt="CRM Pro" width="120">
<p>O CRM mais simples para PMEs portuguesas.</p>
</div>
<div class="footer-col">
<h4>Produto</h4>
<ul>
<li><a href="/funcionalidades">Funcionalidades</a></li>
<li><a href="/precos">Preços</a></li>
<li><a href="/integracoes">Integrações</a></li>
<li><a href="/api">API</a></li>
</ul>
</div>
<div class="footer-col">
<h4>Empresa</h4>
<ul>
<li><a href="/sobre">Sobre</a></li>
<li><a href="/blog">Blog</a></li>
<li><a href="/carreiras">Carreiras</a></li>
<li><a href="/contacto">Contacto</a></li>
</ul>
</div>
<div class="footer-col">
<h4>Recursos</h4>
<ul>
<li><a href="/documentacao">Documentação</a></li>
<li><a href="/ajuda">Ajuda</a></li>
<li><a href="/status">Status</a></li>
</ul>
</div>
<div class="footer-col">
<h4>Legal</h4>
<ul>
<li><a href="/privacidade">Privacidade</a></li>
<li><a href="/termos">Termos</a></li>
<li><a href="/cookies">Cookies</a></li>
</ul>
</div>
</div>
<div class="footer-bottom">
<p>© 2026 CRM Pro Lda. Todos os direitos reservados.</p>
<ul class="social-links">
<li><a href="https://twitter.com/crmpro" aria-label="Twitter">Twitter</a></li>
<li><a href="https://linkedin.com/company/crmpro" aria-label="LinkedIn">LinkedIn</a></li>
<li><a href="https://github.com/crmpro" aria-label="GitHub">GitHub</a></li>
</ul>
</div>
</div>
</footer>
<script src="/app.js" defer></script>
</body>
</html>
Estrutura demonstrada:
✅ Header com nav + CTAs: branding + navegação + acções.
✅ Hero: H1 + subtitle + CTAs + social proof.
✅ Social proof: logos de clientes (trust).
✅ Features: 6 cards com ícones (visualizar oferta).
✅ How it works: 3 passos numerados (reduz fricção).
✅ Testimonials: 3 quotes com foto + cargo (credibilidade).
✅ Pricing: 3 planos (Free, Pro destacado, Enterprise).
✅ FAQ: <details> para expansão nativa.
✅ Final CTA: última oportunidade.
✅ Footer: links organizados, social, copyright.
Princípios de conversão aplicados:
- Acima da fold: H1, subtitle, CTA, screenshot.
- Pricing visível sem fricção.
- Trust signals múltiplos: clientes, testemunhos, garantias.
- CTAs claros e repetidos.
- Objecções respondidas no FAQ.
- Mobile-first semântico.
Seguir: CSS para visual + JavaScript para interactividade (FAQ toggle, smooth scroll, etc.).
Exercício 8 · Projecto plano (10 pts)
Define plano de projecto pessoal para portfolio.
Plano de Portfolio Pessoal — 4 Semanas
Objectivo: criar site portfolio profissional para apresentar trabalhos e competências.
Stack escolhida: - HTML5 semântico. - CSS com Flexbox/Grid. - JavaScript vanilla (sem framework — mais leve). - Deploy: Vercel. - Domínio: meunome.dev ou similar.
Semana 1 — Design + estrutura:
Dia 1-2: - Inspiração: ver portfolios em awwwards.com, dribbble.com. - Wireframes das páginas (Figma ou papel). - Decisões: - Quantas páginas? (Home, Sobre, Projectos, Blog, Contacto). - Paleta de cores (2-3 cores). - Tipografia (1-2 fontes). - Tom (formal vs casual).
Dia 3-4: - Mood board com referências. - Design alta-fidelidade no Figma. - Iterar baseado em feedback.
Dia 5-7: - HTML semântico de todas as páginas. - CSS base (reset, variables, typography). - Mobile-first desde o início.
Semana 2 — Desenvolvimento:
Páginas a desenvolver:
Home: - Hero com nome, role, tagline. - 3-4 projects destacados. - CTA para sobre/contacto.
Sobre: - Foto profissional. - Bio (3-4 parágrafos). - Skills (visual: bars, tags). - Experiência (timeline). - Educação. - Download CV PDF.
Projectos: - Grid de projects. - Cada project com: thumbnail, título, tech stack, descrição curta. - Filter por categoria.
Cada project (sub-página): - Hero com imagem grande. - Overview (problema, solução). - Tech stack. - Screenshots / demos. - Links: live demo, GitHub.
Blog (opcional): - Lista de posts. - Markdown → HTML (simples). - Categorias / tags.
Contacto: - Form (Netlify Forms ou Formspree). - Links: email, LinkedIn, GitHub, Twitter. - Localização.
Semana 3 — Conteúdo + polish:
Conteúdo: - Bio bem escrita (mostrar personalidade). - Project case studies (3-5 melhores). - Screenshots / GIFs dos projects. - CV PDF alinhado com site. - Foto profissional (não selfie).
Polish técnico: - Animations subtis (hover, scroll). - Smooth scrolling. - Dark mode toggle. - Loading states. - 404 page customizada. - Open Graph images para social. - Favicon.
Performance: - Optimizar imagens (WebP). - Lazy load todas as imagens. - Minificar CSS/JS. - Lighthouse score > 95.
SEO: - Meta tags em todas as páginas. - Sitemap.xml. - robots.txt. - Structured data (JSON-LD para Person).
Acessibilidade: - WCAG AA compliance. - Keyboard navigation funcional. - Screen reader test. - Contraste suficiente.
Semana 4 — Deploy + lançamento:
Setup: - Repo GitHub privado primeiro (depois público). - README profissional. - License (MIT).
Domínio: - Comprar (Namecheap, ~10€). - Configurar DNS para Vercel.
Deploy: - Vercel conectado ao GitHub. - Custom domain configurado. - HTTPS automático.
Analytics: - Plausible ou Google Analytics 4. - Search Console submeter sitemap.
Lançamento:
Social media: - LinkedIn post: "Lancei o meu novo portfolio em meunome.dev — feedback bem-vindo!". - Twitter (se usa). - Comunidades (Hacker News, Reddit r/webdev, indiehackers).
Pessoas: - Família e amigos para feedback. - Mentores para critique. - Colegas devs para review técnico.
Pós-lançamento (contínuo):
Manutenção: - Atualizar projects à medida que faz novos. - Blog novo post mensal. - Skills atualizadas conforme aprende.
Métricas: - Visitantes: cresce com tempo + posts blog. - Tempo no site: > 1 min é bom. - Contactos: ideal 1-2/mês.
Iterações: - A/B testing de CTAs. - User feedback via formulário. - Lighthouse mensal.
Investimento:
- Tempo: 80-160h (1 mês full-time vs 2 meses part-time).
- Domínio: 10€/ano.
- Hosting: gratuito (Vercel).
- Domínio + email profissional: ~30€/ano.
- Foto profissional: 100€ (sessão fotográfica).
- Total: ~150€ no primeiro ano.
Retorno:
- Visibilidade profissional.
- Networking facilitado.
- Confiança dos recrutadores.
- Oportunidades que aparecem.
- Skill demonstrada (não apenas dita no CV).
ROI: facilmente 100x se conseguir 1 emprego/projecto extra graças ao portfolio.
Princípios:
- Simplicidade: < é mais. Não inundar.
- Conteúdo > Forma: projects valem mais que animações.
- Mobile-first: 60% visitas em mobile.
- Performance: > 90 Lighthouse.
- Updated: portfolio desactualizado é pior que nada.
- Únicos: que te distingue?
- Honest: não exagerar skills.
Inspirações:
- brittanychiang.com: minimalista, dev.
- bruno-simon.com: 3D criativo.
- rauno.me: animations subtis.
- steveruiz.me: blog + portfolio.
Adaptar ao teu estilo e target audience (PMEs portuguesas vs startups americanas têm expectativas diferentes).