Partilhar: WhatsApp
aulify
UC UC00622 · T. Desenv. Software

Ficha 02 · JavaScript, DOM, projecto

Eventos, manipulação, fetch, deploy
Versão · Aluno
Tempo · 60 minutos
Cotação · 100 pontos
Aluno(a)
Turma
Data
Objectivos da ficha

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. varconst / 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:

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:

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: wwwcname.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:


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>&copy; 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):

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:

<!-- 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:

``` - 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:

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:

10. Database / API:

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

14. Third-party scripts:

Limitar (analytics, ads, widgets) — frequentemente o maior gargalo.

15. Database HTML:


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


Cuidados:

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 &nbsp; ✓ Sem cartão de crédito &nbsp; ✓ 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>&copy; 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:

  1. Acima da fold: H1, subtitle, CTA, screenshot.
  2. Pricing visível sem fricção.
  3. Trust signals múltiplos: clientes, testemunhos, garantias.
  4. CTAs claros e repetidos.
  5. Objecções respondidas no FAQ.
  6. 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:

Retorno:

ROI: facilmente 100x se conseguir 1 emprego/projecto extra graças ao portfolio.


Princípios:

  1. Simplicidade: < é mais. Não inundar.
  2. Conteúdo > Forma: projects valem mais que animações.
  3. Mobile-first: 60% visitas em mobile.
  4. Performance: > 90 Lighthouse.
  5. Updated: portfolio desactualizado é pior que nada.
  6. Únicos: que te distingue?
  7. Honest: não exagerar skills.

Inspirações:

Adaptar ao teu estilo e target audience (PMEs portuguesas vs startups americanas têm expectativas diferentes).