Apostila Completa — Projeto Prático

Do VS Code ao Ar

Landing Page de Cafeteria

VS Code + Remote SSH + Hostinger + React + Tailwind + Node.js + MySQL + Nginx
Do zero ao site no ar com domínio e HTTPS

8
Capítulos
1
Projeto
100%
Prático

Desenvolvido por PiTech Sistemas

pitech.com.br @pitechsistemas

Desenvolvedor: Paulo Inacio

Março 2026

Sumário

Sobre esta apostila: Guia 100% prático para criar uma landing page profissional de cafeteria e colocar no ar. Você vai aprender a configurar o VS Code com Remote SSH, contratar e preparar uma VPS na Hostinger, criar banco de dados, desenvolver uma landing page bonita com React + Tailwind, e fazer o deploy completo com Nginx, domínio e HTTPS.
S1   VS Code — Seu Ambiente de Trabalho
Instalação, extensões essenciais, Remote SSH, terminal integrado
S2   Hostinger — Comprando e Acessando sua VPS
Escolhendo o plano, primeiro acesso SSH, conectando com Remote SSH
S3   Preparando o Servidor
Node.js, MySQL, Nginx, firewall — tudo que o servidor precisa
S4   Banco de Dados — MySQL
Criando o banco, tabelas do cardápio e mensagens de contato
S5   Backend — API Express
Rotas do cardápio, formulário de contato, conexão com MySQL
S6   Landing Page — React + Tailwind
Design (paleta, tipografia, responsividade, estrutura de LP), Tailwind fundamentals, Navbar, Hero, Destaques (cards), Cardápio (filtro), Sobre, Contato (formulário), Footer, personalização para outros negócios
S7   Build e Deploy — Colocando no Ar
Build do React, PM2, configuração do Nginx, portas
S8   Domínio e SSL — HTTPS Gratuito
Registro de domínio, DNS, Let's Encrypt, site 100% seguro
S1 VS Code — Seu Ambiente de Trabalho

O Visual Studio Code (VS Code) é o editor de código mais usado no mundo. Gratuito, leve e cheio de extensões. Vamos instalá-lo e configurá-lo para ser seu ambiente completo de desenvolvimento e deploy.

S1.1 Instalando o VS Code

Acesse code.visualstudio.com e baixe a versão para o seu sistema (Windows, Mac ou Linux). A instalação é "next, next, finish".

Por que VS Code e não outro editor? O VS Code tem um ecossistema gigante de extensões, terminal integrado, e — o mais importante para nós — a extensão Remote - SSH que permite editar arquivos do servidor diretamente, como se fossem locais. Nenhum outro editor gratuito faz isso tão bem.

S1.2 Extensões Essenciais

Abra o VS Code e instale estas extensões (ícone de quadrado na barra lateral ou Ctrl+Shift+X):

Extensão Para que serve
Remote - SSH (Microsoft) Conectar ao servidor e editar arquivos remotos
ES7+ React/Redux Snippets Atalhos para criar componentes React rapidamente
Tailwind CSS IntelliSense Autocomplete de classes Tailwind
Prettier Formata código automaticamente ao salvar
ESLint Mostra erros de JavaScript/TypeScript em tempo real
Thunder Client Testar APIs sem sair do VS Code (alternativa ao Postman)
Como instalar uma extensão 1. Aperte Ctrl+Shift+X (abre a barra de extensões)
2. Digite o nome da extensão na busca
3. Clique em Install
4. Pronto — aparece na barra lateral

S1.3 Terminal Integrado

O VS Code tem um terminal embutido. Você não precisa abrir o Prompt de Comando separadamente.

Abrindo o terminal Aperte Ctrl+' (crase) ou vá em Terminal → New Terminal. Ele abre na parte inferior do VS Code.
💻 Windows — Configure o Git Bash como terminal padrão O terminal padrão do VS Code no Windows é o PowerShell. Para usar o Git Bash (que entende comandos como curl, cp, cat):

1. Aperte Ctrl+Shift+P → digite Terminal: Select Default Profile
2. Selecione Git Bash
3. Feche e reabra o terminal (Ctrl+')

Agora todos os comandos desta apostila funcionam sem problemas.

S1.4 Atalhos Essenciais

Atalho O que faz
Ctrl+S Salvar arquivo
Ctrl+' Abrir/fechar terminal
Ctrl+Shift+P Paleta de comandos (busca qualquer ação)
Ctrl+P Buscar arquivo pelo nome
Ctrl+Shift+X Abrir extensões
Ctrl+B Abrir/fechar barra lateral
Ctrl+D Selecionar próxima ocorrência da palavra
Alt+↑/↓ Mover linha para cima/baixo
Ctrl+/ Comentar/descomentar linha
Checkpoint S1 ✅ VS Code instalado
✅ 6 extensões instaladas (Remote SSH, ES7 Snippets, Tailwind, Prettier, ESLint, Thunder Client)
✅ Terminal integrado funcionando com Git Bash (Windows) ou bash (Mac/Linux)
✅ Conhece os atalhos essenciais
S2 Hostinger — Comprando e Acessando sua VPS

Uma VPS (Virtual Private Server) é um computador na nuvem que fica ligado 24/7. É onde seu site vai morar. A Hostinger oferece VPS baratas e com bom desempenho.

S2.1 Comprando a VPS na Hostinger

O que é uma VPS? Imagine que você alugou um quarto em um prédio. O prédio é o servidor físico, e seu quarto é a VPS — um pedaço isolado com seus próprios recursos (RAM, CPU, disco). Você tem controle total sobre ele: instala o que quiser, configura como quiser.

Passo a passo:

1. Acesse hostinger.com.br e clique em VPS Hosting
2. Escolha o plano KVM 1 (o mais barato) — 1 vCPU, 4GB RAM, suficiente para começar
3. No sistema operacional, selecione Ubuntu 22.04 (ou 24.04 se disponível)
4. Defina uma senha de root forte (anote em lugar seguro!)
5. Finalize a compra
6. No painel da Hostinger, copie o endereço IP da VPS (ex: 154.62.xxx.xxx)
Por que Ubuntu? Ubuntu é a distribuição Linux mais usada em servidores. Maior comunidade, mais tutoriais, mais compatibilidade. Se der erro, é mais fácil encontrar a solução no Google.

S2.2 Primeiro Acesso — SSH pelo Terminal

SSH é como um "controle remoto" para o servidor. Você digita comandos no seu computador, e eles executam na VPS.

ssh root@SEU_IP

Exemplo real:

ssh root@154.62.109.73

Na primeira vez, ele pergunta "Are you sure you want to continue connecting?" — digite yes e aperte Enter. Depois digite a senha que você definiu na Hostinger.

💻 SSH no Windows No Windows 10/11, o SSH já vem instalado — abra o Prompt de Comando, Windows Terminal ou Git Bash e digite ssh root@IP normalmente. Em versões mais antigas, use o PuTTY (gratuito).
O que é "root"? root é o superusuário do Linux — tem permissão para fazer tudo. É como o "Administrador" do Windows. Usamos root para configurar o servidor.

S2.3 Remote SSH — VS Code Conectado na VPS

Aqui está a mágica: em vez de usar o terminal puro, vamos conectar o VS Code direto na VPS. Você edita arquivos, navega pastas e usa o terminal, tudo dentro do VS Code — como se o servidor fosse seu computador local.

Passo a passo — Configurando o Remote SSH

1. No VS Code, aperte Ctrl+Shift+P
2. Digite Remote-SSH: Connect to Host... e selecione
3. Digite: root@SEU_IP (ex: root@154.62.109.73)
4. Selecione Linux quando perguntar o tipo do servidor
5. Digite a senha do root
6. Espere o VS Code instalar os componentes no servidor (só na primeira vez)
7. Pronto! O VS Code abre uma nova janela conectada à VPS
Como saber se estou conectado? Olhe no canto inferior esquerdo do VS Code. Se aparecer SSH: SEU_IP (em verde/azul), você está conectado na VPS. Se aparecer vazio, você está no computador local.

Abrindo uma pasta do servidor

Depois de conectar, vá em File → Open Folder e escolha /root. Agora o VS Code mostra todas as pastas do servidor na barra lateral, exatamente como faria com um projeto local.

Salvando a conexão (para não digitar toda vez)

1. Aperte Ctrl+Shift+PRemote-SSH: Open SSH Configuration File
2. Selecione o arquivo (geralmente ~/.ssh/config)
3. Adicione:
Host minha-vps
    HostName 154.62.109.73
    User root
O que cada linha faz? Host minha-vps — apelido que você escolhe (pode ser qualquer nome)
HostName — o IP real da VPS
User root — o usuário que vai conectar

Agora, quando fizer Remote-SSH: Connect to Host, aparece "minha-vps" na lista — é só clicar e digitar a senha.
Recurso Terminal SSH puro VS Code + Remote SSH
Editar arquivos nano (difícil, sem cores) Clica no arquivo, edita com syntax highlighting
Navegar pastas ls, cd (cego) Barra lateral visual com árvore de pastas
Buscar no projeto grep (complexo) Ctrl+Shift+F — busca global
Terminal Janela separada Embutido no VS Code (Ctrl+')
Autocomplete Nenhum IntelliSense completo
Arrastar arquivos scp (comando) Drag and drop na barra lateral
Checkpoint S2 ✅ VPS comprada na Hostinger com Ubuntu
✅ IP anotado e senha do root guardada
✅ Primeiro acesso SSH funcionando
✅ Remote SSH configurado — VS Code abrindo arquivos da VPS
✅ Conexão salva no SSH config (apelido)
S3 Preparando o Servidor

Agora que estamos conectados na VPS pelo VS Code, vamos instalar tudo que o servidor precisa. Abra o terminal do VS Code (Ctrl+') — ele já está executando na VPS.

S3.1 Atualizando o Sistema

# Atualiza a lista de pacotes e instala atualizações
apt update && apt upgrade -y
O que é apt? apt é o gerenciador de pacotes do Ubuntu — como uma loja de apps para o servidor. apt update atualiza a lista de "apps disponíveis". apt upgrade -y instala as atualizações pendentes. O -y diz "sim" automaticamente para todas as perguntas.

S3.2 Instalando o Node.js

# Adiciona o repositório oficial do Node.js 20
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -

# Instala o Node.js (npm vem junto)
apt install -y nodejs

# Verifica as versões
node -v    # v20.x.x
npm -v     # 10.x.x
Comando O que faz
curl -fsSL ... | bash - Baixa o script de instalação e executa direto
apt install -y nodejs Instala o Node.js e o npm
node -v Mostra a versão do Node.js instalada

S3.3 Instalando o MySQL

# Instala o servidor MySQL
apt install -y mysql-server

# Verifica se está rodando
systemctl status mysql

Se aparecer active (running) em verde, o MySQL está funcionando.

# Entra no MySQL
mysql
Por que não precisa de senha? No Ubuntu, o MySQL usa autenticação por socket — se você é o root do sistema, entra direto no MySQL sem senha. É seguro porque só o root do servidor tem acesso. Em produção, vamos configurar senha para o banco.

Configurando senha para o MySQL

Dentro do MySQL, execute:

ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'SuaSenhaForte123!';
FLUSH PRIVILEGES;
exit;
Anote essa senha! Você vai precisar dela no arquivo .env do backend. Use uma senha forte (letras, números, símbolos).

S3.4 Instalando o Nginx

apt install -y nginx
systemctl status nginx
O que é Nginx? Nginx (pronuncia "engine-x") é o software que recebe as visitas do site. Quando alguém digita cafeteria.com.br no navegador, o Nginx é quem responde. Ele serve os arquivos HTML/CSS/JS e redireciona as chamadas de API para o Node.js.

Teste acessando http://SEU_IP no navegador. Se aparecer "Welcome to nginx!", está funcionando.

S3.5 Instalando o PM2

npm install -g pm2
O que é PM2? PM2 é um gerenciador de processos para Node.js. Ele mantém seu backend rodando 24/7 — se o servidor reiniciar ou o processo morrer, o PM2 levanta de novo automaticamente. Sem ele, se o terminal fechar, o Node.js morre junto.

S3.6 Configurando o Firewall

# Permite SSH (para não perder o acesso!)
ufw allow 22

# Permite HTTP e HTTPS (para o site funcionar)
ufw allow 80
ufw allow 443

# Ativa o firewall
ufw enable

# Verifica
ufw status
Porta Protocolo Para que
22 SSH Seu acesso remoto ao servidor
80 HTTP Site sem HTTPS (redireciona para 443)
443 HTTPS Site com certificado SSL (seguro)
NUNCA abra a porta 3306 (MySQL) ou 3737 (seu backend)! Se abrir a porta do MySQL, qualquer pessoa na internet pode tentar conectar no seu banco. O backend fala com o MySQL localmente — não precisa de porta aberta. O Nginx faz a ponte entre a internet e o backend (proxy reverso).
Checkpoint S3 ✅ Sistema atualizado (apt update && apt upgrade)
✅ Node.js 20 instalado (node -v)
✅ MySQL instalado e rodando com senha configurada
✅ Nginx instalado — "Welcome to nginx" no navegador
✅ PM2 instalado globalmente
✅ Firewall ativo (portas 22, 80, 443)
S4 Banco de Dados — MySQL

Vamos criar o banco de dados da cafeteria. Precisamos de duas tabelas: uma para o cardápio e outra para as mensagens de contato do formulário.

S4.1 Criando o Banco

mysql -u root -p

Digite a senha que você configurou no S3.3. Dentro do MySQL:

CREATE DATABASE cafeteria;
USE cafeteria;

S4.2 Tabela do Cardápio

Vamos criar a tabela que guarda os itens do cardápio. Analise cada coluna:

CREATE TABLE menu_items (
  id INT AUTO_INCREMENT PRIMARY KEY,
  nome VARCHAR(100) NOT NULL,
  descricao VARCHAR(255),
  preco DECIMAL(10,2) NOT NULL,
  categoria ENUM('cafes', 'doces', 'salgados', 'bebidas') NOT NULL,
  imagem_url VARCHAR(500),
  destaque TINYINT(1) DEFAULT 0,
  ativo TINYINT(1) DEFAULT 1,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Explicando cada coluna
id INT AUTO_INCREMENTNúmero único que aumenta sozinho (1, 2, 3...). PRIMARY KEY = identifica cada registro de forma única
VARCHAR(100) NOT NULLTexto de até 100 caracteres. NOT NULL = campo obrigatório (não aceita vazio)
DECIMAL(10,2)Número com 2 casas decimais. Nunca use FLOAT para dinheiro! FLOAT perde precisão (R$ 10.00 pode virar 9.999...)
ENUM('cafes', ...)Só aceita valores pré-definidos. Se tentar inserir 'pizza', o MySQL recusa — funciona como validação no banco
imagem_url VARCHAR(500)URLs são longas, por isso 500 caracteres. Vamos usar imagens gratuitas do Unsplash
destaque TINYINT(1)1 = aparece na seção "Destaques" da landing page, 0 = não. TINYINT(1) funciona como boolean
ativo TINYINT(1) DEFAULT 11 = visível no site, 0 = oculto. É o conceito de soft delete — em vez de deletar o registro, marcamos como inativo
CURRENT_TIMESTAMPData/hora em que o registro foi criado. O MySQL preenche automaticamente

S4.3 Tabela de Contatos

Quando alguém preencher o formulário de contato no site, a mensagem será salva nesta tabela:

CREATE TABLE contatos (
  id INT AUTO_INCREMENT PRIMARY KEY,
  nome VARCHAR(100) NOT NULL,
  email VARCHAR(150) NOT NULL,
  telefone VARCHAR(20),
  mensagem TEXT NOT NULL,
  lido TINYINT(1) DEFAULT 0,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Detalhes importantes desta tabela
telefone VARCHAR(20)Sem NOT NULL — telefone é opcional no formulário. Quando não preenchido, fica NULL
mensagem TEXTVARCHAR(255) tem limite de 255 chars. TEXT aceita até 65.535 caracteres — ideal para mensagens longas
lido TINYINT(1) DEFAULT 0Começa como 0 (não lido). Quando o dono ler a mensagem, atualiza para 1. Útil para um futuro painel admin

S4.4 Inserindo Dados do Cardápio

Vamos popular o cardápio com 10 itens de exemplo. O INSERT usa a sintaxe VALUES (...), (...), (...) para inserir vários registros de uma vez:

-- Formato: (nome, descrição, preço, categoria, URL da imagem, destaque?)
INSERT INTO menu_items (nome, descricao, preco, categoria, imagem_url, destaque) VALUES
('Espresso', 'Café puro e encorpado, extraído na hora', 8.90, 'cafes', 'https://images.unsplash.com/photo-1510707577719-ae7c14805e3a?w=400', 1),
('Cappuccino', 'Espresso com leite vaporizado e espuma cremosa', 12.90, 'cafes', 'https://images.unsplash.com/photo-1572442388796-11668a67e53d?w=400', 1),
('Latte', 'Café com leite cremoso e toque de baunilha', 14.90, 'cafes', 'https://images.unsplash.com/photo-1461023058943-07fcbe16d735?w=400', 1),
('Mocha', 'Espresso com chocolate e chantilly', 16.90, 'cafes', 'https://images.unsplash.com/photo-1578314675249-a6910f80cc4e?w=400', 0),
('Pão de Queijo', 'Quentinho, direto do forno mineiro', 6.90, 'salgados', 'https://images.unsplash.com/photo-1598142982901-df6cec890505?w=400', 1),
('Croissant', 'Folhado amanteigado crocante por fora, macio por dentro', 9.90, 'salgados', 'https://images.unsplash.com/photo-1555507036-ab1f4038024a?w=400', 0),
('Bolo de Cenoura', 'Com cobertura generosa de chocolate', 11.90, 'doces', 'https://images.unsplash.com/photo-1621303837174-89787a7d4729?w=400', 1),
('Brownie', 'Chocolate intenso com nozes crocantes', 13.90, 'doces', 'https://images.unsplash.com/photo-1606313564200-e75d5e30476c?w=400', 0),
('Suco Natural', 'Laranja, limão ou maracujá — feito na hora', 10.90, 'bebidas', 'https://images.unsplash.com/photo-1621506289937-a8e4df240d0b?w=400', 0),
('Chá Gelado', 'Pêssego ou frutas vermelhas, refrescante', 9.90, 'bebidas', 'https://images.unsplash.com/photo-1556679343-c7306c1976bc?w=400', 0);
Entendendo o INSERT
SintaxeINSERT INTO tabela (colunas) VALUES (valores) — as colunas e valores devem estar na mesma ordem
Múltiplos registrosSeparamos com vírgula: (reg1), (reg2), (reg3); — mais eficiente que 10 INSERTs separados
destaque = 1Espresso, Cappuccino, Latte, Pão de Queijo e Bolo de Cenoura estão marcados como destaque — vão aparecer na seção "Nossos Destaques"
Imagens UnsplashO Unsplash oferece imagens gratuitas. O ?w=400 redimensiona para 400px de largura — carrega mais rápido
Colunas omitidasNão informamos id (auto_increment), ativo (default 1), nem created_at (default agora) — o MySQL preenche sozinho

Verifique se os dados foram inseridos:

SELECT nome, preco, categoria, destaque FROM menu_items;
O que esse SELECT faz? Seleciona apenas 4 colunas (não precisa ver todas) de todos os registros. Você deve ver uma tabela com 10 linhas. Se aparecer vazia, revise o INSERT acima — provavelmente faltou o ponto e vírgula no final.
Checkpoint S4 ✅ Banco cafeteria criado
✅ Entendeu cada tipo de coluna (VARCHAR, DECIMAL, ENUM, TINYINT, TEXT, TIMESTAMP)
✅ Tabela menu_items com 10 itens do cardápio
✅ Tabela contatos pronta para receber mensagens do formulário
✅ Dados de teste inseridos e verificados com SELECT
S5 Backend — API Express

O backend é o "garçom" entre o site e o banco de dados. O React (frontend) pede os dados do cardápio, o Express (backend) busca no MySQL e devolve.

S5.1 Criando o Projeto

No terminal do VS Code (conectado na VPS):

mkdir -p /root/cafeteria/backend
cd /root/cafeteria/backend
npm init -y
npm install express mysql2 cors dotenv
Pacote Para que serve
express Framework web — cria rotas (GET, POST) e gerencia requisições
mysql2 Driver para conectar ao MySQL com suporte a Promises
cors Permite que o frontend (porta 5173) fale com o backend (porta 3737)
dotenv Carrega variáveis do arquivo .env (senhas, configs)

S5.2 Arquivo .env

Crie o arquivo /root/cafeteria/backend/.env:

PORT=3737
DB_HOST=localhost
DB_USER=root
DB_PASSWORD=SuaSenhaForte123!
DB_NAME=cafeteria
Nunca commite o .env! O .env contém senhas. Se publicar no GitHub, qualquer pessoa pode acessar seu banco. Adicione .env ao .gitignore.

S5.3 server.js — O Coração do Backend

Crie /root/cafeteria/backend/server.js. Vamos construir trecho por trecho, explicando cada parte.

Passo 1 — Imports e configuração inicial

require('dotenv').config();
const express = require('express');
const cors = require('cors');
const mysql = require('mysql2/promise');
O que cada linha faz?
require('dotenv').config()Lê o arquivo .env e carrega as variáveis (DB_HOST, DB_PASSWORD, etc.) para process.env
require('express')Importa o framework Express — ele cria o servidor web e gerencia as rotas
require('cors')Cross-Origin Resource Sharing — permite que o frontend (porta 5173) fale com o backend (porta 3737)
require('mysql2/promise')Driver MySQL com suporte a async/await — o /promise é importante para usar await

Passo 2 — Criando o app e ativando middlewares

const app = express();
app.use(cors());
app.use(express.json());
O que são middlewares? Middlewares são funções que rodam antes de cada requisição. Pense neles como porteiros:
express()Cria a aplicação — é o "servidor" que vai ouvir as requisições
app.use(cors())Porteiro 1: permite requisições de outros domínios (sem isso, o React não consegue chamar a API)
app.use(express.json())Porteiro 2: converte o corpo da requisição de JSON para objeto JavaScript. Sem isso, req.body seria undefined

Passo 3 — Conectando ao MySQL (Pool)

const pool = mysql.createPool({
  host: process.env.DB_HOST,         // localhost
  user: process.env.DB_USER,         // root
  password: process.env.DB_PASSWORD, // do .env
  database: process.env.DB_NAME,     // cafeteria
  waitForConnections: true,
  connectionLimit: 10,
});
Por que createPool e não createConnection?
createConnectionAbre UMA conexão. Se cair, o server trava. Se 10 pessoas acessam ao mesmo tempo, esperam na fila.
createPoolCria um grupo de 10 conexões reutilizáveis. Quando uma requisição termina, a conexão volta pro pool. Muito mais eficiente!
process.env.DB_HOSTLê do arquivo .env. Assim a senha não fica exposta no código — fica num arquivo separado.

Passo 4 — Rota de Health Check

app.get('/health', (req, res) => {
  res.json({ status: 'ok', timestamp: new Date() });
});
Para que serve o health check? É uma rota simples que responde "estou vivo". Usamos para: 1) Verificar se o servidor está rodando: curl http://localhost:3737/health 2) Monitoramento: ferramentas como PM2 e Nginx podem chamar essa rota para verificar se o backend está saudável. O app.get('/health', ...) significa: quando alguém acessar /health com método GET, execute essa função.

Passo 5 — Rota GET /api/menu (listar cardápio)

app.get('/api/menu', async (req, res) => {
  try {
    const [rows] = await pool.query(
      'SELECT id, nome, descricao, preco, categoria, imagem_url, destaque FROM menu_items WHERE ativo = 1 ORDER BY categoria, nome'
    );
    res.json({ success: true, data: rows });
  } catch (error) {
    res.status(500).json({ success: false, error: 'Erro ao buscar cardápio' });
  }
});
Entendendo linha por linha
async (req, res)async permite usar await dentro da função. req = requisição (o que o cliente enviou), res = resposta (o que vamos devolver)
try { ... } catchSe der erro no banco (ex: MySQL offline), o catch pega o erro e retorna 500 ao invés de travar o servidor
const [rows] = awaitpool.query retorna [rows, fields]. Usamos desestruturação [rows] para pegar só os dados (ignoramos fields)
WHERE ativo = 1Filtra: só mostra itens com ativo = 1. Para "deletar" um item, basta fazer UPDATE SET ativo = 0 — é o conceito de soft delete
ORDER BY categoria, nomeOrdena primeiro por categoria (bebidas, cafes...) e depois por nome dentro de cada categoria
res.json({ success, data })Envia a resposta como JSON. O React vai receber isso com fetch('/api/menu')
res.status(500)500 = Internal Server Error. Códigos: 200 = OK, 201 = Criado, 400 = Erro do cliente, 500 = Erro do servidor

Passo 6 — Rota GET /api/menu/destaques

app.get('/api/menu/destaques', async (req, res) => {
  try {
    const [rows] = await pool.query(
      'SELECT id, nome, descricao, preco, categoria, imagem_url FROM menu_items WHERE ativo = 1 AND destaque = 1'
    );
    res.json({ success: true, data: rows });
  } catch (error) {
    res.status(500).json({ success: false, error: 'Erro ao buscar destaques' });
  }
});
Mesma estrutura, filtro diferente A diferença dessa rota para a anterior é o AND destaque = 1. Só retorna os itens que foram marcados como destaque no banco — esses aparecem na seção "Nossos Destaques" da landing page. Note que a estrutura é a mesma: try/catch + pool.query + res.json. Esse padrão se repete em toda rota de leitura.

Passo 7 — Rota POST /api/contato (formulário)

app.post('/api/contato', async (req, res) => {
  try {
    const { nome, email, telefone, mensagem } = req.body;
Diferenças do GET para o POST GET = buscar dados (cardápio). POST = enviar dados (formulário). No POST, os dados vêm no corpo da requisição (req.body). O express.json() que ativamos no Passo 2 converte o JSON do corpo para um objeto JavaScript. A desestruturação { nome, email, telefone, mensagem } = req.body extrai cada campo em uma variável separada.
    // Validação — campos obrigatórios
    if (!nome || !email || !mensagem) {
      return res.status(400).json({
        success: false,
        error: 'Nome, email e mensagem são obrigatórios',
      });
    }
Por que validar no backend? O frontend também tem validação (required nos inputs), mas nunca confie só no frontend. Qualquer pessoa pode chamar sua API diretamente (via curl, Postman, etc.), pulando o formulário. A validação no backend é a última barreira. O status 400 = Bad Request (erro do cliente — enviou dados incompletos).
    // Insere no banco com placeholders (?) contra SQL injection
    await pool.query(
      'INSERT INTO contatos (nome, email, telefone, mensagem) VALUES (?, ?, ?, ?)',
      [nome, email, telefone || null, mensagem]
    );

    res.status(201).json({ success: true, message: 'Mensagem enviada com sucesso!' });
  } catch (error) {
    res.status(500).json({ success: false, error: 'Erro ao enviar mensagem' });
  }
});
O que são os ? (placeholders)? NUNCA concatene variáveis direto no SQL assim: 'INSERT ... VALUES ("' + nome + '")'. Um hacker pode enviar nome = "'; DROP TABLE contatos; --" e deletar sua tabela inteira (SQL Injection). Os ? são substituídos pelo mysql2 de forma segura — ele escapa caracteres especiais automaticamente. O telefone || null significa: se o telefone estiver vazio, insere NULL no banco.
Códigos HTTP que usamos
200OK — tudo certo (usado automaticamente pelo res.json())
201Created — algo foi criado no banco (usamos no POST de contato)
400Bad Request — o cliente enviou dados inválidos ou incompletos
500Internal Server Error — algo deu errado no servidor (banco offline, erro no SQL, etc.)

Passo 8 — Iniciando o servidor

const PORT = process.env.PORT || 3737;
app.listen(PORT, () => {
  console.log(`Servidor rodando na porta ${PORT}`);
});
Entendendo a última parte process.env.PORT lê a porta do .env (3737). O || 3737 é um fallback: se o .env não tiver a variável, usa 3737. app.listen(PORT, callback) inicia o servidor e fica "ouvindo" requisições na porta especificada. Quando estiver rodando, imprime a mensagem no terminal.
Arquivo completo! Os 8 passos acima formam o server.js inteiro. Cole todos os trechos na ordem dentro de um único arquivo. O servidor tem: 2 middlewares (cors, json), 1 pool MySQL, 3 rotas (health, menu, contato) e 1 listen.

S5.4 Testando o Backend

cd /root/cafeteria/backend
node server.js
# Em outro terminal (Ctrl+Shift+' no VS Code)
curl http://localhost:3737/health
curl http://localhost:3737/api/menu
curl http://localhost:3737/api/menu/destaques
Alternativa ao curl: Use o Thunder Client no VS Code (extensão que instalamos no S1). Abra pela barra lateral, crie uma request GET para http://localhost:3737/api/menu e clique Send. Vê os dados formatados com cores.

Teste o formulário de contato:

curl -X POST http://localhost:3737/api/contato \
  -H "Content-Type: application/json" \
  -d '{"nome":"João","email":"joao@teste.com","mensagem":"Adorei o café!"}'
Checkpoint S5 ✅ Backend criado com Express + MySQL
✅ 3 rotas funcionando: /health, /api/menu, /api/contato
✅ Dados do cardápio retornando do banco
✅ Formulário de contato salvando no banco
S6 Landing Page — React + Tailwind

Agora vamos criar a parte visual — uma landing page profissional e bonita para a cafeteria. Vamos construir seção por seção, explicando cada componente, cada biblioteca e cada técnica usada.

O que vamos usar nesta seção
ReactBiblioteca para criar interfaces — dividimos a tela em componentes reutilizáveis
TypeScriptJavaScript com tipagem — evita erros e melhora o autocompletar do VS Code
Tailwind CSSFramework de estilos por classes utilitárias — estilo direto no HTML, sem criar CSS separado
ViteFerramenta de build ultrarrápida — cria o projeto, roda o dev server e faz o build para produção
Lucide ReactBiblioteca de ícones SVG prontos — Coffee, Phone, Mail, etc. Mais de 1.500 ícones gratuitos

S6.1 Criando o Projeto React

cd /root/cafeteria
npm create vite@latest frontend -- --template react-ts
cd frontend
npm install
O que cada comando faz? npm create vite@latest — cria um projeto novo usando o Vite como base
--template react-ts — usa o template React com TypeScript (não JavaScript puro)
npm install — instala todas as dependências listadas no package.json

Instalando as Bibliotecas

# Tailwind CSS — estilização por classes utilitárias
npm install -D tailwindcss @tailwindcss/vite

# Lucide React — biblioteca de ícones SVG
npm install lucide-react
Por que -D no Tailwind e não no Lucide? O -D instala como dependência de desenvolvimento (devDependencies). O Tailwind só é usado no build — ele gera o CSS final e depois não é mais necessário. Já o Lucide precisa estar no código em produção, porque os ícones são componentes React que rodam no navegador.

Configurando o Vite

Edite vite.config.ts:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
  plugins: [react(), tailwindcss()],
  server: {
    proxy: {
      '/api': 'http://localhost:3737',  // redireciona /api para o backend
    },
  },
})
O que é o proxy? O React roda na porta 5173, o backend na 3737. Quando o React chama fetch('/api/menu'), o Vite intercepta e redireciona para http://localhost:3737/api/menu. Isso evita erros de CORS durante o desenvolvimento.

Substitua o conteúdo de src/index.css:

@import "tailwindcss";
O que o @import "tailwindcss" faz? Importa as 3 camadas do Tailwind: base (reset CSS — remove estilos padrão do navegador), components (estilos de componentes) e utilities (as classes utilitárias como bg-red-500, flex, etc.). Em produção, o Vite remove automaticamente todas as classes que você não usou — o CSS final fica pequeno.

S6.2 Design — Paleta de Cores, Tipografia e Estrutura

Antes de escrever código, vamos entender os princípios de design que fazem uma landing page ser bonita e profissional.

Paleta de Cores

Toda landing page profissional usa uma paleta de cores consistente. Escolhemos cores que transmitam a identidade do negócio:

stone-900
#1c1917
Fundo escuro
stone-700
#44403c
Bordas
stone-50
#fafaf9
Fundo claro
amber-600
#d97706
Destaque/CTA
amber-400
#fbbf24
Detalhes
Por que essas cores?
Psicologia das coresAmber/dourado = calor, aconchego, sofisticação. Stone/marrom = terra, café, conforto. Cores quentes combinam com cafeteria.
ContrasteTexto claro em fundo escuro (ou vice-versa) — garante legibilidade. O amber-400 sobre stone-900 tem contraste 7:1 (excelente)
Cor de ação (CTA)O botão "Ver Cardápio" é amber-600 — a cor mais vibrante da página, chama atenção para a ação principal
Regra 60-30-1060% cor neutra (stone/branco), 30% cor secundária (stone escuro), 10% cor de destaque (amber). Proporção clássica do design.
Para adaptar a outro negócio: Troque amber e stone por outras cores do Tailwind. Exemplos: Clínica médica → blue + slate | Loja de plantas → green + stone | Tech/SaaS → violet + zinc | Restaurante → red + neutral. O Tailwind oferece 22 paletas completas (50 a 950). Veja todas em tailwindcss.com/docs/colors.

Tipografia — Hierarquia Visual

O tamanho e peso da fonte criam uma hierarquia que guia o olho do visitante:

Classe TailwindTamanhoPesoOnde usamos
text-5xl md:text-7xl font-bold72pxBold (700)Título principal do Hero — maior texto da página
text-3xl font-bold30pxBold (700)Títulos de seção (Destaques, Cardápio, Sobre, Contato)
text-xl font-bold20pxBold (700)Logo "Café Aroma" na navbar
text-lg font-bold18pxBold (700)Nomes dos produtos nos cards
text-sm14pxNormal (400)Links, descrições, subtítulos
text-xs uppercase tracking-[3px]12pxNormalLabels de seção ("Favoritos da Casa", "Explore")
Princípios de tipografia
HierarquiaTítulo > subtítulo > texto > detalhe. O visitante deve entender a estrutura sem ler — só pelo tamanho
Peso (font-weight)font-bold (700) para títulos, font-semibold (600) para botões, font-medium (500) para labels
Espaçamento entre letrastracking-[3px] estica as letras — usado em subtítulos UPPERCASE para um visual elegante
Altura da linhaleading-relaxed (1.625) para textos longos — mais espaço entre linhas facilita a leitura
Máximo 2-3 tamanhosNão use 10 tamanhos diferentes. Mantenha consistência — mesma classe para mesma função em toda a página

Entendendo as Classes do Tailwind

No Tailwind, cada classe faz uma coisa só. Vamos aprender a ler uma combinação complexa:

// Exemplo: botão CTA do Hero
className="inline-flex items-center gap-2 bg-amber-600 hover:bg-amber-700 text-white px-8 py-3.5 rounded-full text-sm font-semibold transition shadow-lg"
Lendo classe por classe
inline-flexFlexbox inline — coloca os filhos (texto + ícone) lado a lado
items-centerAlinha verticalmente no centro (ícone e texto na mesma linha)
gap-2Espaço de 8px entre texto e ícone (gap-1 = 4px, gap-2 = 8px, gap-4 = 16px)
bg-amber-600Cor de fundo amber intenso. Os números vão de 50 (claro) a 950 (escuro)
hover:bg-amber-700Ao passar o mouse, escurece para amber-700. O prefixo hover: aplica a classe só no hover
px-8 py-3.5Padding: 32px horizontal, 14px vertical. p = padding, x = eixo X, y = eixo Y
rounded-fullBordas 100% arredondadas — transforma o retângulo em pílula
transitionAnima suavemente qualquer mudança (cor, tamanho). Sem isso, o hover seria instantâneo
shadow-lgSombra grande — dá sensação de "flutuação", destaca o botão do fundo
Escala de espaçamento do Tailwind O Tailwind usa uma escala baseada em 4px: 1 = 4px | 2 = 8px | 3 = 12px | 4 = 16px | 6 = 24px | 8 = 32px | 10 = 40px | 12 = 48px | 16 = 64px | 20 = 80px Funciona para: p (padding), m (margin), gap, w (width), h (height), space

Responsividade — Mobile First

O Tailwind usa a abordagem mobile first: as classes sem prefixo valem para celular, e os prefixos (md:, lg:) adicionam regras para telas maiores:

PrefixoLargura mínimaEquivale aDispositivo
(nenhum)0pxPadrãoCelular
sm:640px@media (min-width: 640px)Celular grande
md:768px@media (min-width: 768px)Tablet
lg:1024px@media (min-width: 1024px)Desktop
xl:1280px@media (min-width: 1280px)Desktop grande

Exemplo prático dos nossos cards de destaque:

// Grid que se adapta ao tamanho da tela
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"

// Leitura:
// Celular (< 768px):  1 coluna   → cards empilhados
// Tablet  (≥ 768px):  2 colunas  → cards lado a lado
// Desktop (≥ 1024px): 4 colunas  → todos visíveis
// Texto que muda de tamanho
className="text-5xl md:text-7xl font-bold"

// Celular: text-5xl (48px) — cabe na tela pequena
// Desktop: text-7xl (72px) — aproveita o espaço maior
Dicas de responsividade
Sempre comece pelo celularEscreva a classe sem prefixo (mobile), depois adicione md: e lg: para telas maiores
hidden md:flexEsconde no celular, mostra no desktop. Usamos na navbar para esconder os links (em telas pequenas não cabem)
max-w-6xl mx-autoLargura máxima de 1152px + centralizado. No celular ocupa 100%, no desktop fica centralizado com margem
px-6Padding lateral de 24px — evita que o texto encoste nas bordas da tela do celular

Hover, Transições e Animações

Efeitos visuais dão feedback ao usuário e tornam a página viva. O Tailwind facilita com prefixos:

ClasseO que fazOnde usamos
hover:bg-amber-700Muda a cor ao passar o mouseBotões
hover:shadow-xlAumenta a sombra — sensação de "levantar"Cards
hover:text-amber-400Muda cor do texto no hoverLinks da navbar
group-hover:scale-110Zoom de 110% quando o card pai recebe hoverImagens dos destaques
transitionAnima a mudança suavemente (150ms padrão)Tudo que tem hover
transition-all duration-300Anima tudo com duração de 300msCards com múltiplos efeitos
transition-transform duration-500Anima só o transform (zoom) em 500msZoom lento da imagem
O que é group + group-hover? Normalmente, hover: ativa quando você passa o mouse no próprio elemento. Mas queremos que, ao passar o mouse no card inteiro, a imagem dentro dele faça zoom. Solução: coloque className="group" no card pai e className="group-hover:scale-110" na imagem filha. O Tailwind entende: "quando o grupo receber hover, aplique scale-110 neste filho".

Anatomia de uma Landing Page Profissional

Uma LP eficiente segue uma estrutura psicológica que guia o visitante do interesse até a ação:

┌─────────────────────────────────┐ │ NAVBAR — Logo + links âncora │ Navegação fixa (sempre visível) ├─────────────────────────────────┤ │ HERO — Título + CTA │ Primeira impressão (5 segundos) │ "O que fazemos + Ação" │ ← acima da dobra (above the fold) ├─────────────────────────────────┤ │ DESTAQUES — Prova social │ Mostre os melhores produtos primeiro │ "Veja nossos favoritos" │ ← gera interesse e desejo ├─────────────────────────────────┤ │ CARDÁPIO — Catálogo completo │ Para quem quer explorar mais │ "Veja tudo que oferecemos" │ ← filtros facilitam a busca ├─────────────────────────────────┤ │ SOBRE — Confiança │ História + números = credibilidade │ "Quem somos + Estatísticas" │ ← constrói confiança ├─────────────────────────────────┤ │ CONTATO — Ação final │ Formulário + informações │ "Fale conosco" │ ← converte visitante em lead ├─────────────────────────────────┤ │ FOOTER — Fechamento │ Copyright + redes sociais └─────────────────────────────────┘
Por que essa ordem?
Above the foldO Hero é a primeira coisa que o visitante vê. Você tem 5 segundos para prender a atenção — por isso título grande + CTA claro
Destaques antesMostre o melhor primeiro. Se o visitante não rolar, pelo menos viu os produtos principais
Sobre = confiançaEstatísticas (5+ anos, 10k clientes) são prova social — convencem o visitante de que o negócio é sério
Contato por últimoO visitante já conhece os produtos, a história e confia no negócio — agora está pronto para agir (enviar mensagem)
CTA em todo lugarO botão "Ver Cardápio" no Hero e o formulário no final são chamadas para ação — sem CTA, o visitante só olha e vai embora

Imagens — Unsplash e Otimização

Usando imagens gratuitas do Unsplash
O que é Unsplash?Banco de imagens gratuitas de alta qualidade — pode usar comercialmente sem pagar. Acesse: unsplash.com
Como buscarPesquise em inglês: "coffee shop", "cappuccino", "bakery". Clique na imagem → copie a URL
?w=400Adicione ?w=400 ao final da URL para redimensionar para 400px. Economiza banda — não carregue fotos de 5000px num card de 200px
object-coverFaz a imagem preencher o espaço sem distorcer — corta as bordas se necessário (como background-size: cover)
Em produçãoIdealmente, hospede as imagens no seu próprio servidor ou use um CDN (CloudFlare, etc.) — não dependa do Unsplash para sempre
Checkpoint S6.2 — Você agora sabe: ✅ Escolher uma paleta de cores com propósito (regra 60-30-10)
✅ Criar hierarquia visual com tipografia (tamanhos, pesos, espaçamento)
✅ Ler e entender qualquer combinação de classes Tailwind
✅ Usar responsividade com prefixos mobile-first (md:, lg:)
✅ Aplicar hover, transições e animações com propósito
✅ Estruturar uma LP seguindo a jornada do visitante
✅ Encontrar e otimizar imagens profissionais gratuitas

S6.3 Estrutura do App.tsx — Imports e Estado

Apague todo o conteúdo de src/App.tsx. Vamos construir a landing page parte por parte. Comece com os imports e o estado:

// src/App.tsx — Parte 1: Imports e Estado
import { useState, useEffect } from 'react';
import { Coffee, Phone, Mail, MapPin, Clock, Send, ChevronDown, Instagram, Facebook } from 'lucide-react';

// Interface TypeScript — define o formato de um item do cardápio
interface MenuItem {
  id: number;
  nome: string;
  descricao: string;
  preco: number;
  categoria: string;
  imagem_url: string;
  destaque: number;
}

export default function App() {
  // Estado — dados que mudam e fazem a tela atualizar
  const [menu, setMenu] = useState<MenuItem[]>([]);        // todos os itens
  const [destaques, setDestaques] = useState<MenuItem[]>([]); // itens em destaque
  const [nome, setNome] = useState('');        // campos do formulário
  const [email, setEmail] = useState('');
  const [telefone, setTelefone] = useState('');
  const [mensagem, setMensagem] = useState('');
  const [enviado, setEnviado] = useState(false);       // feedback de envio
  const [filtro, setFiltro] = useState('todos');     // filtro de categoria

  // useEffect — executa quando o componente carrega (1 vez)
  useEffect(() => {
    fetch('/api/menu').then(r => r.json()).then(d => setMenu(d.data));
    fetch('/api/menu/destaques').then(r => r.json()).then(d => setDestaques(d.data));
  }, []);

  // Função para enviar o formulário de contato
  const handleContato = async (e: React.FormEvent) => {
    e.preventDefault();
    await fetch('/api/contato', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ nome, email, telefone, mensagem }),
    });
    setEnviado(true);
    setNome(''); setEmail(''); setTelefone(''); setMensagem('');
    setTimeout(() => setEnviado(false), 4000);
  };

  // Filtro de categorias — se 'todos', mostra tudo
  const menuFiltrado = filtro === 'todos' ? menu : menu.filter(i => i.categoria === filtro);

  return (
    <div className="min-h-screen bg-stone-50">
Entendendo os conceitos
useStateCria variáveis reativas — quando o valor muda, a tela atualiza automaticamente
useEffectExecuta código após o componente aparecer na tela — ideal para buscar dados da API
interfaceDefine o formato dos dados — o TypeScript avisa se você tentar acessar um campo que não existe
fetchFaz requisições HTTP — busca dados do backend ou envia dados de formulário
preventDefaultImpede o formulário de recarregar a página — queremos enviar via JavaScript, não via HTML

S6.4 Navbar — Barra de Navegação Fixa

A navbar fica fixa no topo com backdrop blur (efeito de vidro fosco). Os links são âncoras que rolam suavemente até cada seção:

      {/* ===== NAVBAR — fixa no topo com efeito glassmorphism ===== */}
      <nav className="fixed top-0 w-full bg-stone-900/95 backdrop-blur-sm z-50 border-b border-stone-800">
        <div className="max-w-6xl mx-auto px-6 py-4 flex justify-between items-center">
          <div className="flex items-center gap-2">
            <Coffee className="text-amber-400" size={28} />
            <span className="text-xl font-bold text-white">Café Aroma</span>
          </div>
          <div className="hidden md:flex gap-8 text-stone-300 text-sm">
            <a href="#inicio" className="hover:text-amber-400 transition">Início</a>
            <a href="#cardapio" className="hover:text-amber-400 transition">Cardápio</a>
            <a href="#sobre" className="hover:text-amber-400 transition">Sobre</a>
            <a href="#contato" className="hover:text-amber-400 transition">Contato</a>
          </div>
        </div>
      </nav>
Explicando as classes Tailwind usadas
fixed top-0 w-fullFixa a navbar no topo e ocupa toda a largura
bg-stone-900/95Fundo escuro com 95% de opacidade — permite ver o conteúdo atrás
backdrop-blur-smEfeito glassmorphism — desfoque no fundo, visual moderno
z-50Z-index alto para a navbar ficar acima de todo o conteúdo
hidden md:flexEsconde no celular, mostra no desktop — responsividade
hover:text-amber-400Muda a cor do link ao passar o mouse — feedback visual
<Coffee /> é um ícone Lucide A biblioteca lucide-react fornece ícones como componentes React. Basta importar e usar: <Coffee size={28} />. O size define o tamanho em pixels. O className aplica cores via Tailwind.

S6.5 Hero — Seção de Abertura Impactante

O hero ocupa a tela inteira com uma foto de fundo escurecida (overlay) e um CTA (Call to Action):

      {/* ===== HERO — tela inteira com imagem de fundo e overlay escuro ===== */}
      <section id="inicio" className="relative h-screen flex items-center justify-center text-center text-white">
        <div className="absolute inset-0 bg-[url('https://images.unsplash.com/photo-1447933601403-0c6688de566e?w=1920')] bg-cover bg-center">
          <div className="absolute inset-0 bg-stone-900/70" />
        </div>
        <div className="relative z-10 max-w-3xl px-6">
          <p className="text-amber-400 text-sm tracking-[4px] uppercase mb-4">Bem-vindo ao</p>
          <h1 className="text-5xl md:text-7xl font-bold mb-6 leading-tight">Café Aroma</h1>
          <p className="text-lg md:text-xl text-stone-300 mb-10 max-w-xl mx-auto">
            Cada xícara conta uma história. Grãos selecionados, torrados com carinho, servidos com paixão desde 2020.
          </p>
          <a href="#cardapio" className="inline-flex items-center gap-2 bg-amber-600 hover:bg-amber-700 text-white px-8 py-3.5 rounded-full text-sm font-semibold transition shadow-lg">
            Ver Cardápio <ChevronDown size={18} />
          </a>
        </div>
      </section>
Técnicas usadas no Hero
Imagem de fundo UnsplashUsamos uma URL do Unsplash (banco de imagens gratuito) direto no Tailwind: bg-[url('...')]
Overlay escurobg-stone-900/70 — uma div escura com 70% de opacidade sobre a imagem, para o texto ficar legível
Posicionamentoabsolute inset-0 estica a div para cobrir todo o pai. relative z-10 coloca o texto acima
Responsividadetext-5xl md:text-7xl — título menor no celular, maior no desktop
CTA (botão)Botão arredondado (rounded-full) com sombra (shadow-lg) e efeito hover

S6.6 Destaques — Cards com Hover Animado

Mostra os 4 produtos marcados como destaque no banco de dados. Cada card tem imagem com zoom no hover:

      {/* ===== DESTAQUES — cards com imagem, zoom hover e sombra ===== */}
      <section className="py-20 bg-white">
        <div className="max-w-6xl mx-auto px-6">
          <p className="text-amber-600 text-sm tracking-[3px] uppercase text-center mb-2">Favoritos da Casa</p>
          <h2 className="text-3xl font-bold text-stone-800 text-center mb-12">Nossos Destaques</h2>
          <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
            {destaques.map(item => (
              <div key={item.id} className="group bg-stone-50 rounded-2xl overflow-hidden shadow-sm hover:shadow-xl transition-all duration-300">
                <div className="h-48 overflow-hidden">
                  <img src={item.imagem_url} alt={item.nome}
                    className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500" />
                </div>
                <div className="p-5">
                  <h3 className="font-bold text-stone-800 text-lg">{item.nome}</h3>
                  <p className="text-stone-500 text-sm mt-1 mb-3">{item.descricao}</p>
                  <p className="text-amber-600 font-bold text-lg">R$ {item.preco.toFixed(2)}</p>
                </div>
              </div>
            ))}
          </div>
        </div>
      </section>
Anatomia do Card de Destaque
Grid responsivogrid-cols-1 md:grid-cols-2 lg:grid-cols-4 — 1 coluna no celular, 2 no tablet, 4 no desktop
group + group-hoverO Tailwind permite que, ao passar o mouse no card inteiro (group), a imagem dentro dele faça zoom (group-hover:scale-110)
overflow-hiddenA imagem faz zoom mas não "vaza" do card — o overflow-hidden corta o que sai
rounded-2xlBordas bem arredondadas — visual moderno tipo card do Instagram
shadow-sm → shadow-xlSombra sutil que aumenta no hover — dá sensação de "elevação"
object-coverA imagem preenche todo o espaço sem distorcer — corta as bordas se necessário
toFixed(2)Formata o preço com 2 casas decimais: 12.5 vira 12.50
.map() — renderiza uma lista dinâmica O destaques.map(item => ...) percorre o array e cria um card para cada item. O key={item.id} é obrigatório no React — ele usa a key para saber qual card atualizar se os dados mudarem.

S6.7 Cardápio Completo — Filtro por Categoria

Seção com botões de filtro (todos, cafés, doces, salgados, bebidas) e cards horizontais com imagem lateral:

      {/* ===== CARDÁPIO — filtro por categoria + cards horizontais ===== */}
      <section id="cardapio" className="py-20 bg-stone-100">
        <div className="max-w-6xl mx-auto px-6">
          <p className="text-amber-600 text-sm tracking-[3px] uppercase text-center mb-2">Explore</p>
          <h2 className="text-3xl font-bold text-stone-800 text-center mb-8">Cardápio Completo</h2>

          {/* Botões de filtro */}
          <div className="flex justify-center gap-3 mb-10 flex-wrap">
            {['todos', 'cafes', 'doces', 'salgados', 'bebidas'].map(cat => (
              <button key={cat} onClick={() => setFiltro(cat)}
                className={`px-5 py-2 rounded-full text-sm font-medium transition ${
                  filtro === cat
                    ? 'bg-amber-600 text-white'
                    : 'bg-white text-stone-600 hover:bg-stone-200'
                }`}>
                {cat === 'todos' ? 'Todos' : cat.charAt(0).toUpperCase() + cat.slice(1)}
              </button>
            ))}
          </div>

          {/* Grid de cards do cardápio */}
          <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
            {menuFiltrado.map(item => (
              <div key={item.id} className="flex gap-4 bg-white rounded-xl p-4 shadow-sm hover:shadow-md transition">
                <img src={item.imagem_url} alt={item.nome}
                  className="w-24 h-24 rounded-lg object-cover flex-shrink-0" />
                <div>
                  <h3 className="font-bold text-stone-800">{item.nome}</h3>
                  <p className="text-stone-500 text-sm mt-0.5">{item.descricao}</p>
                  <p className="text-amber-600 font-bold mt-2">R$ {item.preco.toFixed(2)}</p>
                </div>
              </div>
            ))}
          </div>
        </div>
      </section>
Como funciona o filtro?
Estado filtroComeça como 'todos'. Quando clica em "Doces", muda para 'doces'
menuFiltradoSe filtro === 'todos', mostra tudo. Senão, .filter() mantém só os itens daquela categoria
Classe condicionalO botão ativo fica laranja (bg-amber-600), os outros ficam brancos — usando template literal com ${...}
Atualização automáticaAo clicar no filtro, o React re-renderiza automaticamente — mostra só os cards filtrados, sem recarregar a página
Layout do card horizontal flex gap-4 coloca imagem e texto lado a lado. flex-shrink-0 na imagem impede que ela encolha quando o texto é grande. O resultado é um card compacto e elegante.

S6.8 Sobre — Layout Dois Colunas com Estatísticas

Seção com foto à esquerda e texto + números à direita. Layout de duas colunas usando grid:

      {/* ===== SOBRE — imagem + texto lado a lado com estatísticas ===== */}
      <section id="sobre" className="py-20 bg-white">
        <div className="max-w-6xl mx-auto px-6 grid grid-cols-1 md:grid-cols-2 gap-12 items-center">
          <div>
            <img src="https://images.unsplash.com/photo-1495474472287-4d71bcdd2085?w=600" alt="Interior"
              className="rounded-2xl shadow-lg w-full h-80 object-cover" />
          </div>
          <div>
            <p className="text-amber-600 text-sm tracking-[3px] uppercase mb-2">Nossa História</p>
            <h2 className="text-3xl font-bold text-stone-800 mb-6">Paixão em cada grão</h2>
            <p className="text-stone-600 mb-4 leading-relaxed">
              O Café Aroma nasceu em 2020, no coração da cidade, com um sonho simples: servir o melhor café
              que você já provou. Selecionamos grãos de pequenos produtores brasileiros, torramos artesanalmente
              e preparamos cada xícara com o cuidado que ela merece.
            </p>
            <p className="text-stone-600 mb-6 leading-relaxed">
              Nosso espaço foi pensado para ser seu refúgio — venha trabalhar, estudar, encontrar amigos ou
              simplesmente apreciar um bom café em paz.
            </p>
            <div className="flex gap-8">
              <div>
                <p className="text-3xl font-bold text-amber-600">5+</p>
                <p className="text-stone-500 text-sm">Anos de história</p>
              </div>
              <div>
                <p className="text-3xl font-bold text-amber-600">10k+</p>
                <p className="text-stone-500 text-sm">Clientes felizes</p>
              </div>
              <div>
                <p className="text-3xl font-bold text-amber-600">50k+</p>
                <p className="text-stone-500 text-sm">Cafés servidos</p>
              </div>
            </div>
          </div>
        </div>
      </section>
Técnicas desta seção
Grid 2 colunasgrid-cols-1 md:grid-cols-2 — empilha no celular, lado a lado no desktop
items-centerAlinha verticalmente imagem e texto — a imagem pode ter altura diferente do texto
tracking-[3px]Espaçamento entre letras — usado no subtítulo "Nossa História" para um visual elegante
Estatísticas3 blocos com números grandes em amber + descrição pequena — padrão de landing pages profissionais

S6.9 Contato — Formulário Funcional + Cards de Info

Esta seção é dividida em duas colunas: informações de contato à esquerda e formulário à direita. Vamos construir cada parte:

Parte 1 — Abertura da seção e layout em grid

      {/* ===== CONTATO — fundo escuro, 2 colunas ===== */}
      <section id="contato" className="py-20 bg-stone-900 text-white">
        <div className="max-w-6xl mx-auto px-6 grid grid-cols-1 md:grid-cols-2 gap-12">
Estrutura de 2 colunas grid grid-cols-1 md:grid-cols-2 — no celular fica uma coluna (info em cima, formulário embaixo). No desktop fica lado a lado. O gap-12 dá espaçamento entre as colunas.

Parte 2 — Cards de informações de contato (coluna esquerda)

          {/* Lado esquerdo — informações de contato */}
          <div>
            <p className="text-amber-400 text-sm tracking-[3px] uppercase mb-2">Fale Conosco</p>
            <h2 className="text-3xl font-bold mb-8">Entre em Contato</h2>
            <div className="space-y-6">

              {/* Card de informação — cada um segue o mesmo padrão */}
              <div className="flex items-center gap-4">
                <div className="bg-amber-600/20 p-3 rounded-full">
                  <MapPin className="text-amber-400" size={22} />
                </div>
                <div>
                  <p className="font-medium">Endereço</p>
                  <p className="text-stone-400 text-sm">Rua do Café, 123 — Centro</p>
                </div>
              </div>

              {/* Repita o mesmo padrão para os outros 3: */}
              <div className="flex items-center gap-4">
                <div className="bg-amber-600/20 p-3 rounded-full"><Phone className="text-amber-400" size={22} /></div>
                <div><p className="font-medium">Telefone</p><p className="text-stone-400 text-sm">(22) 99999-0000</p></div>
              </div>
              <div className="flex items-center gap-4">
                <div className="bg-amber-600/20 p-3 rounded-full"><Clock className="text-amber-400" size={22} /></div>
                <div><p className="font-medium">Horário</p><p className="text-stone-400 text-sm">Seg-Sáb: 7h–20h | Dom: 8h–14h</p></div>
              </div>
              <div className="flex items-center gap-4">
                <div className="bg-amber-600/20 p-3 rounded-full"><Mail className="text-amber-400" size={22} /></div>
                <div><p className="font-medium">Email</p><p className="text-stone-400 text-sm">contato@cafearoma.com.br</p></div>
              </div>

            </div>
          </div>
Anatomia de um card de informação
flex items-center gap-4Coloca ícone e texto lado a lado, centralizados verticalmente
bg-amber-600/20Fundo amber com 20% de opacidade — cria um círculo translúcido atrás do ícone
p-3 rounded-fullPadding + bordas 100% arredondadas = círculo perfeito em volta do ícone
space-y-6Espaçamento vertical entre cada card — sem precisar de margin em cada um
Padrão repetívelOs 4 cards seguem a mesma estrutura — só muda o ícone (MapPin, Phone, Clock, Mail) e o texto

Parte 3 — Formulário funcional (coluna direita)

          {/* Lado direito — formulário */}
          <div>
            {enviado ? (
              {/* Mensagem de sucesso (aparece por 4 segundos) */}
              <div className="bg-green-600/20 border border-green-500/30 rounded-2xl p-8 text-center">
                <p className="text-green-400 text-xl font-bold mb-2">Mensagem enviada!</p>
                <p className="text-stone-400">Retornaremos em breve.</p>
              </div>
            ) : (
              {/* Formulário — cada input é um controlled component */}
              <form onSubmit={handleContato} className="space-y-4">
                <input value={nome} onChange={e => setNome(e.target.value)} required
                  placeholder="Seu nome"
                  className="w-full bg-stone-800 border border-stone-700 rounded-xl px-5 py-3 text-white placeholder-stone-500 focus:border-amber-500 focus:outline-none" />
                <input value={email} onChange={e => setEmail(e.target.value)} required type="email"
                  placeholder="Seu email"
                  className="w-full bg-stone-800 border border-stone-700 rounded-xl px-5 py-3 text-white placeholder-stone-500 focus:border-amber-500 focus:outline-none" />
                <input value={telefone} onChange={e => setTelefone(e.target.value)}
                  placeholder="Telefone (opcional)"
                  className="w-full bg-stone-800 border border-stone-700 rounded-xl px-5 py-3 text-white placeholder-stone-500 focus:border-amber-500 focus:outline-none" />
                <textarea value={mensagem} onChange={e => setMensagem(e.target.value)} required rows={4}
                  placeholder="Sua mensagem..."
                  className="w-full bg-stone-800 border border-stone-700 rounded-xl px-5 py-3 text-white placeholder-stone-500 focus:border-amber-500 focus:outline-none resize-none" />
                <button type="submit"
                  className="w-full bg-amber-600 hover:bg-amber-700 text-white py-3.5 rounded-xl font-semibold flex items-center justify-center gap-2 transition">
                  Enviar Mensagem <Send size={18} />
                </button>
              </form>
            )}
          </div>
        </div>
      </section>
Como o formulário funciona (passo a passo)
{enviado ? (...) : (...)}Renderização condicional: se enviado é true, mostra a mensagem de sucesso. Se false, mostra o formulário
value={nome}O valor do input vem do estado — é o que chamamos de controlled component
onChange={e => setNome(...)}Cada tecla digitada atualiza o estado — o React re-renderiza o input com o novo valor
requiredValidação HTML nativa — o navegador bloqueia o envio se o campo estiver vazio
onSubmit={handleContato}Quando clica "Enviar" (ou aperta Enter), chama a função que faz fetch POST /api/contato
focus:border-amber-500Quando clica no input, a borda fica amber — feedback visual de qual campo está ativo
resize-noneNo textarea, impede que o usuário redimensione a caixa de texto — mantém o layout limpo
Fluxo completo do envio 1) Usuário preenche e clica "Enviar" → 2) handleContato faz fetch POST /api/contato3) Backend valida e faz INSERT INTO contatos4) Frontend mostra "Mensagem enviada!" por 4s → 5) Formulário reaparece limpo (campos resetados)

S6.10 Footer — Rodapé com Redes Sociais

Rodapé simples e elegante com logo, copyright e ícones de redes sociais:

      {/* ===== FOOTER — logo, copyright e redes sociais ===== */}
      <footer className="bg-stone-950 text-stone-500 py-10">
        <div className="max-w-6xl mx-auto px-6 flex flex-col md:flex-row justify-between items-center gap-4">
          <div className="flex items-center gap-2">
            <Coffee className="text-amber-400" size={20} />
            <span className="text-white font-bold">Café Aroma</span>
          </div>
          <p className="text-sm">&copy; 2026 Café Aroma. Todos os direitos reservados.</p>
          <div className="flex gap-4">
            <a href="#" className="hover:text-amber-400 transition"><Instagram size={20} /></a>
            <a href="#" className="hover:text-amber-400 transition"><Facebook size={20} /></a>
          </div>
        </div>
      </footer>

    </div>  {/* fecha min-h-screen */}
  );
}
Resumo — Estrutura Completa da Landing Page
1NavbarFixa no topo, glassmorphism, links âncora, ícone Coffee
2HeroFullscreen, foto Unsplash com overlay, CTA "Ver Cardápio"
3Destaques4 cards com zoom hover, dados do banco (destaque=1)
4CardápioFiltro por categoria + cards horizontais com imagem lateral
5Sobre2 colunas: foto + texto + estatísticas (anos, clientes, cafés)
6ContatoCards com ícones + formulário funcional (salva no MySQL)
7FooterLogo, copyright, ícones de redes sociais
Monte o arquivo completo! Os blocos de código acima formam um único arquivo App.tsx. Cole tudo na ordem dentro de um só arquivo. Comece pelos imports (S6.3), depois o return com Navbar (S6.4), Hero (S6.5), Destaques (S6.6), Cardápio (S6.7), Sobre (S6.8), Contato (S6.9) e Footer (S6.10). Feche o componente com }.

S6.11 Testando Localmente

# Terminal 1 — Backend
cd /root/cafeteria/backend
node server.js

# Terminal 2 — Frontend
cd /root/cafeteria/frontend
npm run dev -- --host 0.0.0.0

Acesse http://SEU_IP:5173 no navegador. Você deve ver a landing page com o cardápio carregado do banco.

O --host 0.0.0.0 é necessário na VPS Por padrão, o Vite só aceita conexões de localhost. Na VPS, você está acessando de fora — o --host 0.0.0.0 libera acesso externo. Em produção isso não importa porque usamos npm run build.
Lembre de abrir a porta 5173 temporariamente para testar: ufw allow 5173. Depois do teste, remova: ufw delete allow 5173.
Checkpoint S6 ✅ Projeto React criado com Vite + TypeScript + Tailwind (S6.1)
✅ Entendeu paleta de cores, tipografia, responsividade e estrutura de LP (S6.2)
✅ Aprendeu a ler classes Tailwind e escala de espaçamento (S6.2)
✅ Entendeu useState, useEffect, interface e fetch (S6.3)
✅ Navbar fixa com glassmorphism e links âncora (S6.4)
✅ Hero fullscreen com imagem de fundo e CTA (S6.5)
✅ Cards de destaque com zoom hover e dados do banco (S6.6)
✅ Cardápio com filtro por categoria — atualização automática (S6.7)
✅ Seção Sobre com 2 colunas e estatísticas (S6.8)
✅ Formulário de contato funcional salvando no MySQL (S6.9)
✅ Footer com copyright e redes sociais (S6.10)
✅ Testou localmente com backend + frontend rodando (S6.11)

S6.12 Personalizando para Seu Negócio

Essa landing page foi construída para uma cafeteria, mas a mesma estrutura serve para qualquer negócio. Aqui está como adaptar:

Trocando as Cores (2 minutos)

Basta trocar amber e stone por outras paletas do Tailwind em todo o App.tsx:

NegócioCor principalCor de fundoVisual
Clínica / DentistablueslateProfissional, confiança
Loja de PlantasgreenstoneNatural, orgânico
BarbeariaorangezincMasculino, urbano
Tech / SaaSvioletzincModerno, inovador
RestauranteredneutralApetitoso, quente
Pet ShoptealslateCarinhoso, limpo
AcademialimezincEnergia, saúde
Como trocar na prática No VS Code, use Ctrl+H (Find and Replace): 1) Troque amber por blue (ou a cor desejada) 2) Troque stone por slate (ou a cor desejada) Clique "Replace All" — pronto, toda a paleta mudou em segundos.

Adaptando o Conteúdo

SeçãoO que mudar
NavbarTroque o ícone <Coffee /> por outro do Lucide (ex: <Scissors /> para barbearia, <Heart /> para clínica)
HeroTroque a imagem de fundo (Unsplash), o título e o texto. Mude o CTA para sua ação principal
DestaquesSeus 3-4 produtos/serviços mais vendidos. Troque nomes, descrições e imagens
CardápioRenomeie para "Serviços", "Produtos" ou "Planos". Mude as categorias no ENUM do banco
SobreSua história real. Troque números e a foto do interior do negócio
ContatoSeu endereço, telefone, email e horário reais

Adaptando o Banco de Dados

Para outro tipo de negócio, renomeie a tabela e mude as categorias:

-- Exemplo: Barbearia
CREATE TABLE servicos (
  id INT AUTO_INCREMENT PRIMARY KEY,
  nome VARCHAR(100) NOT NULL,
  descricao VARCHAR(255),
  preco DECIMAL(10,2) NOT NULL,
  categoria ENUM('cortes', 'barba', 'combos', 'tratamentos') NOT NULL,
  imagem_url VARCHAR(500),
  destaque TINYINT(1) DEFAULT 0,
  ativo TINYINT(1) DEFAULT 1,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Lembre de atualizar também: 1) O nome da tabela nas queries do server.js (menu_itemsservicos) 2) A interface MenuItem no App.tsx (renomeie para Servico) 3) As categorias no array de filtro: ['todos', 'cortes', 'barba', ...] 4) Os textos de cada seção para refletir o novo negócio
Resumo: para criar uma LP para QUALQUER negócio ✅ Troque as cores (Ctrl+H: amber → sua cor)
✅ Troque o ícone da Navbar (Lucide tem 1.500+ ícones)
✅ Troque imagens (Unsplash — pesquise o nicho)
✅ Troque textos (título, descrição, história, contato)
✅ Adapte o banco (categorias, nome da tabela)
✅ A estrutura da LP (Hero → Destaques → Catálogo → Sobre → Contato) funciona para qualquer tipo de negócio
S7 Build e Deploy — Colocando no Ar

O site está funcionando com npm run dev, mas isso é só para desenvolvimento. Para produção, precisamos fazer o build (gerar arquivos estáticos) e configurar o Nginx para servir o site.

S7.1 Build do Frontend

cd /root/cafeteria/frontend
npm run build

Isso cria a pasta dist/ com arquivos HTML, CSS e JS minificados — prontos para servir.

O que é "build"? O React usa JSX, TypeScript e imports — o navegador não entende isso diretamente. O npm run build transforma tudo em HTML/CSS/JS puros, minificados (menores) e otimizados. O resultado fica na pasta dist/.

S7.2 Subindo o Backend com PM2

cd /root/cafeteria/backend
pm2 start server.js --name cafeteria-api
pm2 save
pm2 startup
Comando O que faz
pm2 start server.js --name cafeteria-api Inicia o backend com o nome "cafeteria-api"
pm2 save Salva a lista de processos — será restaurada após reboot
pm2 startup Configura o PM2 para iniciar automaticamente quando o servidor ligar
Comandos úteis do PM2 pm2 list — ver processos rodando
pm2 logs cafeteria-api — ver logs em tempo real
pm2 restart cafeteria-api — reiniciar o backend
pm2 stop cafeteria-api — parar temporariamente

S7.3 Configurando o Nginx

Agora vamos dizer ao Nginx: "sirva os arquivos do React e redirecione as chamadas /api para o Node.js".

Crie o arquivo de configuração:

nano /etc/nginx/sites-available/cafeteria
Dica: Se estiver usando o VS Code com Remote SSH, você pode simplesmente navegar até /etc/nginx/sites-available/ na barra lateral e criar o arquivo clicando com o botão direito → New File.

Vamos construir o arquivo bloco por bloco, explicando cada diretiva:

Bloco 1 — Abertura do server e configuração básica

server {
    listen 80;
    server_name SEU_IP;   # Depois troque pelo domínio

    root /root/cafeteria/frontend/dist;
    index index.html;
}
O que cada linha faz?
server { ... }Bloco que define UM site. Você pode ter vários server {} no Nginx (um para cada domínio)
listen 80Escuta na porta 80 (HTTP). Quando configurarmos SSL, o Certbot vai adicionar a porta 443 (HTTPS) automaticamente
server_name SEU_IPO IP ou domínio que esse bloco atende. Troque por seu IP real agora — depois, pelo domínio
root /root/.../distPasta raiz do site — é onde estão os arquivos gerados pelo npm run build do React
index index.htmlQuando acessar /, o Nginx serve o index.html — que é a entrada do React

Bloco 2 — Proxy reverso para a API (Node.js)

    # Quando acessar /api/* → redireciona para o Node.js
    location /api/ {
        proxy_pass http://127.0.0.1:3737/api/;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
Entendendo o proxy reverso
location /api/ { ... }Captura TODAS as URLs que começam com /api/ (ex: /api/menu, /api/contato)
proxy_pass http://127.0.0.1:3737Proxy reverso — o Nginx recebe a requisição e repassa para o Node.js na porta 3737. O usuário nunca acessa a 3737 diretamente
proxy_set_header HostEnvia o nome do domínio original para o Node.js (útil se tiver vários sites no mesmo servidor)
X-Real-IPEnvia o IP real do visitante. Sem isso, o Node.js veria sempre 127.0.0.1 (o IP do Nginx)
X-Forwarded-ProtoInforma se a requisição original era HTTP ou HTTPS — importante para redirecionamentos

Bloco 3 — SPA fallback (obrigatório para React)

    # Qualquer outra URL → devolve o index.html do React
    location / {
        try_files $uri $uri/ /index.html;
    }
ESSENCIAL para Single Page Applications! Quando o usuário acessa /cardapio, o Nginx procura um arquivo chamado cardapio no disco — que não existe! (é uma rota do React, não um arquivo real). O try_files resolve assim: 1) Tenta $uri — existe o arquivo /cardapio? Não. 2) Tenta $uri/ — existe a pasta /cardapio/? Não. 3) Fallback: serve /index.html — o React carrega e o React Router cuida da rota. Sem essa linha, qualquer URL que não seja / daria erro 404!

Bloco 4 — Cache para arquivos estáticos

    # Imagens, CSS e JS ficam em cache por 30 dias
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
    }
O que é cache de assets?
location ~* \.(js|css|...)$O ~* é regex (case-insensitive). Captura qualquer arquivo terminado em .js, .css, .png, etc.
expires 30dO navegador guarda esses arquivos por 30 dias — na próxima visita, não precisa baixar de novo
immutableDiz ao navegador: "este arquivo nunca vai mudar". O Vite gera nomes com hash (app-abc123.js), então se o código mudar, o nome muda
Monte o arquivo completo! Junte os 4 blocos acima em um único arquivo /etc/nginx/sites-available/cafeteria. Lembre de trocar SEU_IP pelo IP real da sua VPS. O resultado final deve ter: server { ... } com os 4 blocos location dentro.

Ativando o site

# Cria o link simbólico (ativa o site)
ln -s /etc/nginx/sites-available/cafeteria /etc/nginx/sites-enabled/

# Remove o site padrão do Nginx (aquele "Welcome to nginx")
rm /etc/nginx/sites-enabled/default

# Testa a configuração (deve mostrar "syntax is ok")
nginx -t

# Recarrega o Nginx
systemctl reload nginx
O que é ln -s? Cria um atalho (link simbólico). O arquivo real fica em sites-available/, e o link em sites-enabled/ aponta para ele. O Nginx só lê o que está em sites-enabled/. Para desativar um site, basta deletar o link.

Agora acesse http://SEU_IP no navegador. O site da cafeteria deve aparecer!

Navegador (usuário) │ ▼ Nginx (:80) │ ├── /api/* ──► Node.js (:3737) ──► MySQL │ └── /* ──► /dist/index.html (React)
Por que não acessamos o Node.js diretamente? O Nginx é mais eficiente para servir arquivos estáticos (HTML, CSS, JS, imagens). Ele também faz: compressão gzip, cache, HTTPS, load balancing, proteção contra ataques. O Node.js fica só com a lógica de negócio (API).
Checkpoint S7 ✅ Build do frontend gerado (npm run build → pasta dist/)
✅ Backend rodando com PM2 (24/7 + reinício automático)
✅ Nginx configurado — proxy reverso + arquivos estáticos
✅ Site acessível via http://SEU_IP no navegador
✅ Entendeu o fluxo: Navegador → Nginx → React (estático) OU Node.js (API) → MySQL
S8 Domínio e SSL — HTTPS Gratuito

O site está no ar, mas só com IP. Vamos colocar um domínio bonito e HTTPS (cadeado verde).

S8.1 Registrando um Domínio

Onde comprar? Registro.br — domínios .com.br (R$ 40/ano)
Hostinger — domínios .com (às vezes vem grátis com a VPS)
Namecheap — domínios internacionais baratos

Exemplo: cafearoma.com.br ou cafearoma.com

S8.2 Configurando o DNS

No painel do provedor de domínio, crie um registro tipo A:

Campo Valor
Tipo A
Nome/Host @ (ou vazio — significa o domínio principal)
Valor/Aponta para 154.62.109.73 (o IP da sua VPS)
TTL 300 (ou o menor disponível)
DNS demora para propagar! Pode levar de 5 minutos a 48 horas para o domínio apontar para o IP. Geralmente funciona em 15 minutos.

Atualizando o Nginx com o domínio

Edite o arquivo /etc/nginx/sites-available/cafeteria e troque server_name:

server_name cafearoma.com.br;
nginx -t && systemctl reload nginx

S8.3 Instalando SSL (HTTPS) com Let's Encrypt

# Instala o Certbot
apt install -y certbot python3-certbot-nginx

# Gera o certificado SSL (gratuito!)
certbot --nginx -d cafearoma.com.br

O Certbot vai:

  1. Verificar que o domínio aponta para seu IP
  2. Gerar o certificado SSL
  3. Configurar o Nginx automaticamente para usar HTTPS
  4. Redirecionar HTTP → HTTPS
O que é SSL/HTTPS? SSL criptografa a comunicação entre o navegador e o servidor. Sem ele, dados (incluindo senhas do formulário) trafegam em texto puro. Com HTTPS, aparece o cadeado verde no navegador e o Google favorece no ranking de busca.
Renovação automática O certificado Let's Encrypt vale 90 dias. O Certbot cria automaticamente um cron job que renova antes de expirar. Para verificar: certbot renew --dry-run

Teste acessando https://cafearoma.com.br — deve aparecer o cadeado verde e o site completo.

Checkpoint Final — O que você construiu ✅ Configurou o VS Code com extensões essenciais
✅ Contratou e acessou uma VPS na Hostinger
✅ Usou o Remote SSH para editar arquivos do servidor direto no VS Code
✅ Instalou Node.js, MySQL, Nginx, PM2, UFW no servidor
✅ Criou um banco de dados com cardápio e formulário de contato
✅ Construiu uma API Express com 3 rotas funcionais
✅ Criou uma landing page profissional com React + Tailwind (6 seções)
✅ Fez o build e configurou o PM2 para manter o backend rodando 24/7
✅ Configurou o Nginx como proxy reverso — entendeu cada diretiva
✅ Registrou domínio e configurou DNS
✅ Instalou SSL/HTTPS gratuito com Let's Encrypt
✅ Entendeu o fluxo completo: domínio → DNS → Nginx → React/Node.js → MySQL

Parabéns! Você saiu do zero e colocou um site profissional no ar — com banco de dados, HTTPS e domínio próprio. A mesma base serve para qualquer projeto: mude o conteúdo, adapte o design, e lance seu próximo site.

PiTech Sistemas

pitech.com.br @pitechsistemas

Desenvolvedor: Paulo Inacio