Cheatsheet Next.js
Framework React full-stack com renderização no servidor e App Router
Next.js
Setup e CLI
Criar projecto
npx create-next-app@latest meu-app cd meu-app npm run dev
create-next-app gera um projecto completo com App Router, TypeScript e ESLint. npm run dev inicia o servidor de desenvolvimento em localhost:3000 com hot reload.
TypeScript
// app/page.tsx
export default function Home() {
return <h1>Olá</h1>;
}
// Tipos para params:
export default function Post({
params,
}: { params: { slug: string } }) {}Next.js tem suporte nativo a TypeScript. Ficheiros .tsx para componentes com JSX. O tsconfig.json é gerado automaticamente. Types para params e searchParams são inferidos.
ESLint e Prettier
// .eslintrc.json
{
"extends": "next/core-web-vitals"
}
// next lint detecta:
// - hooks rules
// - image optimization
// - link issuesnext/core-web-vitals inclui regras React + Next.js. Detecta uso incorrecto de Image, Link e hooks. npm run lint verifica o projecto inteiro.
Comandos CLI
npm run dev # desenvolvimento npm run build # build de produção npm run start # servir build npm run lint # verificar código
dev para desenvolvimento com HMR. build compila e optimiza para produção. start serve o build. lint corre o ESLint configurado.
Variáveis de ambiente
// .env.local DATABASE_URL=postgres://... NEXT_PUBLIC_API_URL=https://api.exemplo.com // No código: process.env.DATABASE_URL // servidor process.env.NEXT_PUBLIC_API_URL // cliente
.env.local guarda segredos (não vai para git). Variáveis com prefixo NEXT_PUBLIC_ ficam disponíveis no cliente. Sem o prefixo, só acessíveis no servidor.
Estrutura App Router
app/ layout.tsx # layout raiz page.tsx # página inicial (/) globals.css # estilos globais public/ # ficheiros estáticos next.config.js # configuração
A pasta app/ define as rotas por ficheiros. layout.tsx envolve todas as páginas. public/ serve ficheiros estáticos na raiz. next.config.js personaliza o build.
Path aliases
// tsconfig.json
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}
// Uso:
import { Card } from "@/components/Card";@/ é o alias padrão para src/ (ou raiz). Configurado em tsconfig.json com paths. Evita imports relativos longos como ../../../components.
next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
images: {
domains: ["exemplo.com"],
},
};
module.exports = nextConfig;next.config.js configura o framework. reactStrictMode activa verificações extra. images.domains autoriza domínios externos para o componente Image.
src directory
src/
app/
layout.tsx
page.tsx
components/
Card.tsx
lib/
utils.tsA pasta src/ é opcional — organiza o código fonte separado de configs. Se existir, o App Router procura src/app/ em vez de app/. Recomendado para projectos grandes.
App Router e Páginas
Página básica
// app/sobre/page.tsx
export default function Sobre() {
return <h1>Sobre nós</h1>;
}Cada pasta com page.tsx torna-se uma rota. app/sobre/page.tsx → /sobre. O componente é exportado como default. Por defeito, é um Server Component.
loading.tsx
// app/blog/loading.tsx
export default function Loading() {
return <p>A carregar posts...</p>;
}loading.tsx mostra um fallback enquanto a página carrega. Envolve a página em Suspense automaticamente. Aparece instantaneamente na navegação — melhora a UX percebida.
Route groups
app/
(marketing)/
layout.tsx
sobre/page.tsx
(app)/
layout.tsx
dashboard/page.tsxParênteses (nome) criam grupos de rotas sem afectar o URL. Cada grupo pode ter o seu próprio layout. /sobre e /dashboard ficam em layouts diferentes sem prefixo na URL.
Rota raiz
// app/page.tsx
export default function Home() {
return (
<main>
<h1>Bem-vindo</h1>
</main>
);
}app/page.tsx é a página inicial (/). É o ponto de entrada da aplicação. Herda o layout raiz de app/layout.tsx automaticamente.
error.tsx
"use client";
// app/blog/error.tsx
export default function Error({
error,
reset,
}: { error: Error; reset: () => void }) {
return (
<div>
<p>Erro: {error.message}</p>
<button onClick={reset}>Tentar</button>
</div>
);
}error.tsx captura erros de runtime na secção. Deve ser Client Component ("use client"). reset() tenta re-renderizar. Funciona como error boundary automático.
Parallel e intercepting routes
app/
@modal/
(.)photo/[id]/page.tsx
layout.tsx # recebe {modal, children}
page.tsx
// layout.tsx:
export default function Layout({
children, modal
}) { return <>{children}{modal}</> }@slot define parallel routes (múltiplas páginas no mesmo layout). (.) intercepta rotas (modal sobre a página actual). Padrão para modais tipo Instagram/Twitter.
Layout raiz
// app/layout.tsx
export default function RootLayout({
children,
}: { children: React.ReactNode }) {
return (
<html lang="pt">
<body>{children}</body>
</html>
);
}O layout raiz é obrigatório e envolve todas as páginas. Deve conter <html> e <body>. children é a página ou layout filho. Persiste entre navegações (sem re-mount).
not-found.tsx
// app/not-found.tsx
export default function NotFound() {
return (
<div>
<h1>404</h1>
<p>Página não encontrada</p>
</div>
);
}not-found.tsx renderiza quando uma rota não existe ou quando notFound() é chamado manualmente. Pode existir global ou por secção. Retorna status HTTP 404.
Layout aninhado
// app/blog/layout.tsx
export default function BlogLayout({
children,
}: { children: React.ReactNode }) {
return (
<div>
<nav>Menu do blog</nav>
{children}
</div>
);
}Layouts em sub-pastas aplicam-se a todas as rotas dessa secção. app/blog/layout.tsx envolve /blog e /blog/[slug]. Layouts aninham hierarquicamente sem re-render na navegação.
template.tsx
// app/template.tsx
export default function Template({
children,
}: { children: React.ReactNode }) {
return <div className="animar">{children}</div>;
}template.tsx é como layout mas recria em cada navegação (novo mount). Não preserva estado. Útil para animações de entrada ou efeitos que devem reiniciar a cada rota.
Componentes e Layout
Metadata estático
// app/layout.tsx ou page.tsx
export const metadata = {
title: "Meu Site",
description: "Descrição do site",
openGraph: {
title: "Meu Site",
images: ["/og.png"],
},
};metadata define SEO e Open Graph. Exportado como constante em Server Components. Gera tags <title>, <meta> e og: automaticamente no <head>.
next/image
import Image from "next/image";
<Image
src="/foto.jpg"
width={800}
height={600}
alt="Descrição"
priority
/>next/image optimiza imagens: resize, lazy load, formatos modernos (WebP/AVIF). width/height evitam layout shift. priority desactiva lazy load para imagens above-the-fold.
global-error e sitemap
// app/global-error.tsx ("use client")
export default function GlobalError() {
return <html><body><h1>Erro</h1></body></html>;
}
// app/sitemap.ts
export default function sitemap() {
return [{ url: "https://site.com", lastModified: new Date() }];
}global-error.tsx substitui o layout raiz em erros fatais. sitemap.ts gera /sitemap.xml dinamicamente. Ambos são ficheiros de convenção do App Router.
Metadata dinâmico
// app/blog/[slug]/page.tsx
export async function generateMetadata({
params,
}: { params: { slug: string } }) {
const post = await getPost(params.slug);
return { title: post.titulo };
}generateMetadata() gera metadados dinâmicos por página. Recebe params e searchParams. Permite buscar dados e definir title, description, etc. por rota.
next/link
import Link from "next/link";
<Link href="/sobre">Sobre</Link>
<Link href="/blog" prefetch={true}>Blog</Link>
<Link href={{ pathname: "/post", query: { id: 1 } }}>
Post
</Link>next/link faz navegação client-side sem reload. Prefetch automático de páginas visíveis. Aceita string ou objecto com pathname e query. Substitui <a> para rotas internas.
Componente partilhado
// components/Card.tsx
export function Card({ titulo, texto }: {
titulo: string;
texto: string;
}) {
return (
<div className="card">
<h3>{titulo}</h3>
<p>{texto}</p>
</div>
);
}Componentes em components/ são reutilizáveis. Sem directiva, são Server Components por defeito. Adicione "use client" se precisar de interactividade (hooks, eventos).
next/script
import Script from "next/script"; <Script src="https://analytics.exemplo.com/script.js" strategy="afterInteractive" />
next/script optimiza carregamento de scripts externos. strategy: beforeInteractive, afterInteractive (default), lazyOnload. Evita bloquear a renderização.
next/font
import { Inter } from "next/font/google";
const inter = Inter({
subsets: ["latin"],
variable: "--font-inter",
});
// layout.tsx:
<body className={inter.variable}>next/font optimiza fontes automaticamente (zero layout shift). Faz download no build e serve localmente. variable cria CSS variable para usar em Tailwind ou CSS custom.
Icon e manifest
app/
icon.png # favicon automático
apple-icon.png # iOS home screen
manifest.ts # PWA manifest
// manifest.ts:
export default {
name: "Minha App",
theme_color: "#000",
};Ficheiros de convenção: icon.png gera favicon automaticamente. manifest.ts configura PWA. Sem necessidade de tags manuais no <head> — Next.js injecta tudo.
Data Fetching
Fetch no servidor
// app/page.tsx (Server Component)
export default async function Page() {
const res = await fetch("https://api.exemplo.com/posts");
const posts = await res.json();
return <ul>{posts.map(p => <li key={p.id}>{p.titulo}</li>)}</ul>;
}Server Components podem ser async e usar fetch directamente. Os dados são buscados no servidor — nunca expostos ao cliente. Sem necessidade de useEffect ou estado.
generateStaticParams
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map(post => ({
slug: post.slug,
}));
}generateStaticParams() pré-gera rotas dinâmicas no build. Retorna array de params. Cada objecto gera uma página estática. Sem isto, rotas dinâmicas são SSR por defeito.
loading com Skeleton
// app/blog/loading.tsx
export default function Loading() {
return (
<div className="skeleton">
<div className="h-4 w-3/4 animate-pulse" />
<div className="h-4 w-1/2 animate-pulse" />
</div>
);
}loading.tsx é automaticamente embrulhado em Suspense. Mostra skeleton/placeholder durante data fetching. Cada secção pode ter o seu próprio loading para granularidade.
SSG (estático)
// Gerado no build (estático)
export const dynamic = "force-static";
export default async function Page() {
const dados = await fetch(url);
return <p>{dados.titulo}</p>;
}force-static gera a página no build (SSG). O HTML é servido de CDN sem computação. Ideal para conteúdo que não muda. Por defeito, páginas sem dados dinâmicos já são estáticas.
revalidatePath
"use server";
import { revalidatePath } from "next/cache";
export async function publicar() {
await db.save(post);
revalidatePath("/blog");
revalidatePath("/");
}revalidatePath() invalida a cache de um caminho manualmente. A próxima visita regenera a página. Use em Server Actions após mutações para garantir dados frescos.
unstable_cache
import { unstable_cache } from "next/cache";
const getCachedPosts = unstable_cache(
async () => db.posts.findMany(),
["posts"],
{ revalidate: 3600, tags: ["posts"] }
);
const posts = await getCachedPosts();unstable_cache faz cache de qualquer função async (não só fetch). Aceita chave, revalidate e tags. Útil para queries de BD ou cálculos pesados que não precisam de fetch HTTP.
ISR (revalidação)
// Revalidar a cada 60 segundos
export const revalidate = 60;
// Ou por fetch:
const res = await fetch(url, {
next: { revalidate: 3600 }
});revalidate activa ISR (Incremental Static Regeneration). A página é estática mas regenera em background a cada N segundos. Combina performance de SSG com dados actualizados.
revalidateTag
// Fetch com tag:
fetch(url, { next: { tags: ["posts"] } })
// Invalidar por tag:
import { revalidateTag } from "next/cache";
revalidateTag("posts");tags agrupam fetches relacionados. revalidateTag() invalida todos os fetches com essa tag de uma vez. Mais granular que revalidatePath quando múltiplas páginas usam os mesmos dados.
Cache do fetch
// Cache indefinida (default em produção)
fetch(url, { cache: "force-cache" })
// Sem cache (sempre fresco)
fetch(url, { cache: "no-store" })
// Revalidar por tempo
fetch(url, { next: { revalidate: 60 } })force-cache guarda a resposta indefinidamente. no-store busca sempre dados frescos (equivalente a SSR). revalidate faz cache com expiração temporal.
Streaming com Suspense
import { Suspense } from "react";
export default function Page() {
return (
<div>
<h1>Blog</h1>
<Suspense fallback={<p>Carregando...</p>}>
<ListaPosts />
</Suspense>
</div>
);
}Suspense permite streaming: envia o HTML shell imediatamente e o conteúdo quando pronto. O fallback mostra enquanto carrega. Melhora TTFB e UX percebida.
Routing e Navegação
Rota dinâmica
// app/blog/[slug]/page.tsx
export default function Post({
params,
}: { params: { slug: string } }) {
return <h1>{params.slug}</h1>;
}Parênteses rectos [slug] criam segmentos dinâmicos. O valor fica em params.slug. /blog/ola-mundo → params.slug = "ola-mundo".
redirect
import { redirect } from "next/navigation";
export default async function Page() {
const user = await getUser();
if (!user) {
redirect("/login");
}
return <p>Olá, {user.nome}</p>;
}redirect() redirecciona no servidor (HTTP 307). Lança uma excepção internamente — não precisa de return depois. Use para proteger rotas ou redireccionar após acções.
Rewrites e redirects
// next.config.js
module.exports = {
async rewrites() {
return [{ source: "/api/:path*",
destination: "https://api.externa.com/:path*" }];
},
async redirects() {
return [{ source: "/velho",
destination: "/novo", permanent: true }];
},
};rewrites proxy requests sem mudar o URL (transparente). redirects envia HTTP 301/308. Configurados em next.config.js. Úteis para migrações e APIs externas.
Catch-all routes
// app/docs/[...slug]/page.tsx // /docs/a/b/c → params.slug = ["a", "b", "c"] // Optional catch-all: // app/docs/[[...slug]]/page.tsx // /docs → params.slug = undefined
[...slug] captura múltiplos segmentos como array. [[...slug]] (duplos colchetes) é opcional — também corresponde à rota sem segmentos.
notFound()
import { notFound } from "next/navigation";
export default async function Post({ params }) {
const post = await getPost(params.slug);
if (!post) {
notFound();
}
return <h1>{post.titulo}</h1>;
}notFound() renderiza o not-found.tsx mais próximo. Lança excepção interna (como redirect). Use quando um recurso não existe na BD para retornar 404 correctamente.
useRouter
"use client";
import { useRouter } from "next/navigation";
export function Botao() {
const router = useRouter();
return (
<button onClick={() => router.push("/login")}>
Entrar
</button>
);
}useRouter() permite navegação programática em Client Components. push() adiciona ao histórico. replace() substitui. back() volta. refresh() re-executa o load.
Link com prefetch
import Link from "next/link";
<Link href="/dashboard" prefetch={true}>
Dashboard
</Link>
<Link href="/pesado" prefetch={false}>
Página pesada
</Link>prefetch carrega a página em background quando o link é visível. Por defeito, é automático para links estáticos. prefetch={false} desactiva para páginas pesadas ou autenticadas.
usePathname e useSearchParams
"use client";
import { usePathname, useSearchParams } from "next/navigation";
export function Filtro() {
const pathname = usePathname(); // "/blog"
const searchParams = useSearchParams();
const pagina = searchParams.get("pagina"); // "2"
}usePathname() retorna o caminho actual. useSearchParams() dá acesso aos query params. Ambos reactivos — actualizam quando a URL muda. Só em Client Components.
Route handlers dinâmicos
// app/api/posts/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const post = await getPost(params.id);
return Response.json(post);
}Route handlers em pastas dinâmicas recebem params como segundo argumento. request é o objecto Request nativo. Suporta todos os verbos HTTP exportados.
API e Server Actions
Route handler GET
// app/api/users/route.ts
export async function GET() {
const users = await db.users.findMany();
return Response.json(users);
}route.ts cria um endpoint API. Exporte funções com nomes de verbos: GET, POST, PUT, DELETE. Response.json() retorna JSON com headers correctos.
Usar Server Action em form
import { criarPost } from "./actions";
export default function Form() {
return (
<form action={criarPost}>
<input name="titulo" required />
<button type="submit">Criar</button>
</form>
);
}action={serverAction} liga o form directamente. Funciona sem JavaScript (progressive enhancement). O form faz POST automático e a action processa no servidor.
Streaming responses
export async function GET() {
const stream = new ReadableStream({
start(controller) {
controller.enqueue("Olá ");
setTimeout(() => {
controller.enqueue("Mundo!");
controller.close();
}, 1000);
},
});
return new Response(stream);
}Route handlers suportam ReadableStream para respostas em streaming. Útil para SSE, AI responses ou dados progressivos. O cliente recebe chunks à medida que são gerados.
Route handler POST
// app/api/users/route.ts
export async function POST(request: Request) {
const body = await request.json();
const user = await db.users.create(body);
return Response.json(user, { status: 201 });
}request.json() lê o body JSON. request.formData() para forms. O segundo argumento de Response.json() define status e headers customizados.
useFormStatus e useActionState
"use client";
import { useFormStatus } from "react-dom";
import { useActionState } from "react";
function Submit() {
const { pending } = useFormStatus();
return <button disabled={pending}>
{pending ? "Enviando..." : "Enviar"}
</button>;
}useFormStatus() dá o estado do form (pending, data). useActionState() gere o retorno da action (sucesso/erro). Ambos melhoram UX durante submissões.
CORS e middleware API
export async function OPTIONS() {
return new Response(null, {
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST",
"Access-Control-Allow-Headers": "Content-Type",
},
});
}OPTIONS responde a preflight requests CORS. Defina headers Access-Control-* para permitir origens externas. Alternativa: configurar CORS no middleware.ts global.
Query params e headers
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const pagina = searchParams.get("pagina");
const auth = request.headers.get("Authorization");
return Response.json({ pagina, auth });
}new URL(request.url) dá acesso a searchParams. request.headers.get() lê headers. Ambos usam APIs web nativas (não Next.js específico).
cookies e headers
import { cookies, headers } from "next/headers";
// Ler cookie:
const token = cookies().get("token")?.value;
// Definir cookie:
cookies().set("tema", "escuro", { path: "/" });
// Ler header:
const ua = headers().get("user-agent");cookies() e headers() acedem ao request no servidor. Funcionam em Server Components, Route Handlers e Server Actions. cookies().set() define cookies na resposta.
Server Action básica
"use server";
// app/actions.ts
export async function criarPost(formData: FormData) {
const titulo = formData.get("titulo") as string;
await db.posts.create({ titulo });
}"use server" no topo marca o ficheiro como Server Actions. Funções correm apenas no servidor. Recebem FormData ou argumentos serializáveis. Substituem endpoints API para mutações.
revalidatePath em action
"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
export async function criar(formData: FormData) {
await db.posts.create({ titulo: formData.get("titulo") });
revalidatePath("/blog");
redirect("/blog");
}Após mutação, revalidatePath() invalida a cache e redirect() navega. Padrão completo: guardar → invalidar → redireccionar. O utilizador vê dados frescos imediatamente.
Renderização
Server Component (default)
// app/page.tsx (sem directiva = Server)
export default async function Page() {
const dados = await fetch(url);
return <div>{dados.titulo}</div>;
}Por defeito, todos os componentes são Server Components. Podem ser async, aceder a BD e usar segredos. Zero JavaScript enviado ao cliente — renderizados no servidor.
dynamic import
import dynamic from "next/dynamic";
const Mapa = dynamic(() => import("./Mapa"), {
ssr: false,
loading: () => <p>Carregando mapa...</p>,
});dynamic() faz code-splitting e lazy loading. ssr: false desactiva renderização no servidor (para libs que usam window/document). loading mostra fallback.
Hydration e erros
"use client";
import { useEffect, useState } from "react";
export function Hora() {
const [hora, setHora] = useState("");
useEffect(() => {
setHora(new Date().toLocaleTimeString());
}, []);
return <p>{hora}</p>;
}Hydration mismatch ocorre quando server HTML difere do client render. Valores como Date.now() ou Math.random() causam erros. Use useEffect para valores só do cliente.
Client Component
"use client";
import { useState } from "react";
export default function Contador() {
const [n, setN] = useState(0);
return <button onClick={() => setN(n + 1)}>{n}</button>;
}"use client" no topo marca como Client Component. Necessário para hooks (useState, useEffect), eventos (onClick) e APIs do browser. Executa no cliente e servidor (hydration).
Suspense para streaming
import { Suspense } from "react";
export default function Page() {
return (
<>
<h1>Dashboard</h1>
<Suspense fallback={<Skeleton />}>
<GraficoVendas />
</Suspense>
</>
);
}Suspense envia o HTML disponível imediatamente e faz stream do restante. Cada Suspense é independente — múltiplos podem resolver em paralelo. Melhora TTFB significativamente.
Compor Server + Client
// app/page.tsx (Server)
import Contador from "./Contador";
export default async function Page() {
const dados = await fetch(url);
return (
<div>
<h1>{dados.titulo}</h1>
<Contador inicial={dados.valor} />
</div>
);
}Padrão recomendado: Server busca dados e passa como props para Client Components. Mantém a lógica pesada no servidor. Client só recebe dados serializáveis e gere interactividade.
Renderização estática vs dinâmica
// Forçar estático: export const dynamic = "force-static"; // Forçar dinâmico (SSR): export const dynamic = "force-dynamic"; // Runtime: export const runtime = "edge";
dynamic controla quando a página é gerada. force-static = build time. force-dynamic = cada request. runtime = "edge" executa em edge (Vercel Edge, Cloudflare Workers).
children como composição
"use client";
// Modal.tsx
export function Modal({ children }: {
children: React.ReactNode
}) {
const [aberto, setAberto] = useState(true);
return aberto ? <div>{children}</div> : null;
}
// Server pode passar children:
<Modal><ServerContent /></Modal>children permite Server Components dentro de Client Components. O conteúdo é renderizado no servidor e "projectado" no cliente. Mantém a árvore Server o mais acima possível.
Partial prerendering (PPR)
// next.config.js
experimental: { ppr: true }
// app/page.tsx
import { Suspense } from "react";
export default function Page() {
return (
<>
<Header /> {/* estático */}
<Suspense fallback={<Skeleton />}>
<Perfil /> {/* dinâmico */}
</Suspense>
</>
);
}PPR (experimental) combina estático e dinâmico na mesma página. O shell é servido de CDN instantaneamente. Partes dinâmicas em Suspense são streamed do servidor.
Avançado e Deploy
Middleware
// middleware.ts (raiz ou src/)
import { NextResponse } from "next/server";
export function middleware(request: Request) {
const token = request.cookies.get("token");
if (!token) {
return NextResponse.redirect(new URL("/login", request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*"],
};middleware.ts intercepta requests antes de chegar à rota. matcher limita onde corre. Use para auth, redirects, A/B testing, geolocalização. Corre no edge (rápido).
Deploy na Vercel
npm i -g vercel vercel # deploy preview vercel --prod # deploy produção # Ou: push para GitHub # Vercel detecta e faz deploy automático
Vercel é a plataforma nativa do Next.js. Deploy automático a cada push. Preview deployments por PR. Edge functions, ISR e image optimization incluídos. Zero configuração.
Testing (Jest + RTL)
// __tests__/page.test.tsx
import { render, screen } from "@testing-library/react";
import Home from "@/app/page";
test("mostra título", () => {
render(<Home />);
expect(screen.getByText("Bem-vindo")).toBeTruthy();
});@testing-library/react renderiza componentes em testes. render() monta no DOM virtual. screen.getByText() procura elementos. Para Server Components, use jest-environment-jsdom.
Edge runtime
// app/api/geo/route.ts
export const runtime = "edge";
export async function GET(request: Request) {
const geo = request.geo;
return Response.json({
pais: geo?.country,
cidade: geo?.city,
});
}runtime = "edge" executa em edge locations (mais perto do utilizador). Sem acesso a Node.js APIs (fs, net). Ideal para respostas rápidas: geo, auth, redirects, A/B tests.
Docker (standalone)
# Dockerfile FROM node:20-alpine AS builder WORKDIR /app COPY . . RUN npm ci && npm run build FROM node:20-alpine AS runner WORKDIR /app COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/public ./public CMD ["node", "server.js"]
output: "standalone" gera um servidor Node mínimo. O Dockerfile multi-stage copia apenas o necessário. Imagem final ~50MB. Ideal para VPS, Kubernetes, AWS ECS.
Performance e Core Web Vitals
// app/layout.tsx
import { Inter } from "next/font/google";
const inter = Inter({ subsets: ["latin"] });
// Monitorizar:
export function reportWebVitals(metric) {
console.log(metric.name, metric.value);
}
// next build mostra:
// Route Size First Load JSNext.js optimiza automaticamente: code-splitting, image optimization, font optimization. next build mostra tamanho por rota. reportWebVitals monitoriza LCP, FID, CLS em produção.
Dynamic imports e lazy
import dynamic from "next/dynamic";
// Sem SSR (só cliente):
const Editor = dynamic(() => import("./Editor"), {
ssr: false,
});
// Com loading:
const Grafico = dynamic(() => import("./Grafico"), {
loading: () => <p>A carregar...</p>,
});dynamic() divide o bundle e carrega sob demanda. ssr: false para libs que precisam de window/document. Reduz JavaScript inicial — melhora First Contentful Paint.
Static export
// next.config.js
module.exports = {
output: "export",
};
// Build:
npm run build
// Gera pasta out/ com HTML estáticooutput: "export" gera um site 100% estático (sem servidor Node). HTML/CSS/JS na pasta out/. Sem SSR, API routes ou middleware. Ideal para GitHub Pages, S3, Netlify.
next.config avançado
module.exports = {
output: "standalone",
experimental: {
serverActions: { bodySizeLimit: "2mb" },
},
headers: async () => [{
source: "/(.*)",
headers: [{ key: "X-Frame-Options", value: "DENY" }],
}],
};output: "standalone" gera build mínimo para Docker. headers adiciona headers de segurança globais. experimental activa features experimentais. Configure conforme necessidades de deploy.
Instrumentation e logging
// instrumentation.ts
export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
await import("./sentry.server.config");
}
if (process.env.NEXT_RUNTIME === "edge") {
await import("./sentry.edge.config");
}
}instrumentation.ts corre uma vez no arranque do servidor. Ideal para inicializar Sentry, OpenTelemetry, ou conexões a BD. NEXT_RUNTIME distingue Node.js de Edge.
Estilização e Assets
CSS Modules
// Card.module.css
.cartao { padding: 1rem; }
.titulo { font-size: 1.5rem; }
// Card.tsx
import styles from "./Card.module.css";
<div className={styles.cartao}>
<h2 className={styles.titulo}>{titulo}</h2>
</div>CSS Modules fazem scope automático por componente. Nomes de classe são únicos (hash). Sem conflitos globais. Ficheiros .module.css são suportados nativamente.
next/image avançado
import Image from "next/image";
<Image
src="https://exemplo.com/foto.jpg"
fill
sizes="(max-width: 768px) 100vw, 50vw"
style={{ objectFit: "cover" }}
placeholder="blur"
blurDataURL="data:image/..."
/>fill para imagens responsivas (pai precisa position: relative). sizes optimiza o srcset. placeholder="blur" mostra blur-up enquanto carrega. Domínios externos devem estar em images.domains.
CSS-in-JS (styled-components)
// lib/registry.tsx
"use client";
import { ServerStyleSheet } from "styled-components";
// next.config.js:
compiler: { styledComponents: true }
// Componente:
const Titulo = styled.h1`
color: tomato;
font-size: 2rem;
`;Next.js suporta styled-components com compiler.styledComponents: true. Requer registry para SSR. Alternativas: Emotion, Vanilla Extract. Tailwind é a opção mais popular.
Tailwind CSS
// globals.css @tailwind base; @tailwind components; @tailwind utilities; // Componente: <div className="p-4 rounded-lg shadow-md"> <h2 className="text-xl font-bold">Título</h2> </div>
Tailwind CSS vem como opção no create-next-app. Classes utilitárias inline. tailwind.config.ts personaliza tema. Purge automático no build — só CSS usado é incluído.
Ficheiros estáticos
public/ logo.png favicon.ico robots.txt // Uso: <img src="/logo.png" /> // Servido em: https://site.com/logo.png
A pasta public/ serve ficheiros na raiz do site. Sem processamento — copiados como estão. Para imagens, prefira next/image (optimização). Ideal para robots.txt, favicons, SVGs.
Global CSS
// app/globals.css
* { box-sizing: border-box; }
body { margin: 0; font-family: sans-serif; }
// Importar no layout raiz:
// app/layout.tsx
import "./globals.css";CSS global só pode ser importado em app/layout.tsx (raiz). Aplica-se a todas as páginas. Para estilos por componente, use CSS Modules ou Tailwind. Não importe CSS global em sub-componentes.
SVG como componente
// next.config.js
module.exports = {
webpack(config) {
config.module.rules.push({
test: /\.svg$/,
use: ["@svgr/webpack"],
});
return config;
},
};
// Uso:
import Logo from "./logo.svg";
<Logo width={48} />@svgr/webpack permite importar SVGs como componentes React. Aceita props como width, fill. Alternativa: importar como URL com next/image.
Styled JSX
export function Card() {
return (
<div>
<p>Olá</p>
<style jsx>{`
p { color: blue; font-size: 16px; }
`}</style>
</div>
);
}styled-jsx vem incluído no Next.js. CSS dentro de <style jsx> tem scope ao componente. Suporta global para estilos não-escopados. Alternativa leve a CSS Modules.
Dark mode com Tailwind
// tailwind.config.ts darkMode: "class", // layout.tsx: <html className="dark"> // Componente: <div className="bg-white dark:bg-gray-900"> <p className="text-black dark:text-white">Olá</p> </div>
darkMode: "class" activa dark mode por classe no <html>. Prefixo dark: aplica estilos no modo escuro. Combine com next-themes para toggle com persistência.