Next.js 14
O framework React que você vai usar em produção. Domine SSR, SSG e App Router pra construir apps web rápidas, escaláveis e com SEO impecável.
⚡Por que Next.js Domina o Mercado
Adoção Empresarial
- Usado por Netflix, Uber, Hulu, TikTok
- +500.000 sites em produção
- Crescimento de 35% ao ano
- Vagas: R$ 12.000 - R$ 25.000
Vantagens Técnicas
- Performance automática com optimização
- SEO nativo com SSR/SSG
- Deploy simples na Vercel
- API Routes integradas
1Fundamentos do Next.js 14
1.1 Setup e Primeira Aplicação
Configure seu primeiro projeto Next.js e entenda como funciona o novo App Router.
# Criando projeto Next.js 14
npx create-next-app@latest meu-app-nextjs
cd meu-app-nextjs
# Configurações recomendadas:
# ✓ TypeScript
# ✓ ESLint
# ✓ Tailwind CSS
# ✓ App Router (novo)
# ✓ Import alias (@/*)
# Iniciando desenvolvimento
npm run dev📁 Estrutura do App Router: O novo sistema de roteamento do Next.js 13+ é baseado no filesystem dentro da pasta app/.
1.2 App Router vs Pages Router
Veja as diferenças entre o novo App Router e o Pages Router tradicional.
app/
├── page.tsx # Homepage (/)
├── about/
│ └── page.tsx # About page (/about)
├── blog/
│ ├── page.tsx # Blog listing (/blog)
│ └── [slug]/
│ └── page.tsx # Blog post (/blog/[slug])
├── api/
│ └── users/
│ └── route.ts # API route (/api/users)
├── globals.css
└── layout.tsx # Root layoutimport './globals.css'
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Meu App Next.js',
description: 'Aplicação construída com Next.js 14',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="pt-BR">
<body>
<header>
<nav>Navegação Global</nav>
</header>
<main>{children}</main>
<footer>Rodapé Global</footer>
</body>
</html>
)
}// Homepage - Server Component por padrão
export default function HomePage() {
return (
<div>
<h1>Bem-vindo ao Next.js 14!</h1>
<p>Esta é a homepage usando App Router</p>
</div>
)
}
// Metadata específica da página
export const metadata = {
title: 'Home | Meu App',
description: 'Página inicial do meu app Next.js'
}1.3 Server Components vs Client Components
Aprenda quando usar Server Components (padrão) ou Client Components (com "use client").
// Server Component (padrão no App Router)
// Roda no servidor, sem JavaScript no cliente
async function UserProfile({ userId }: { userId: string }) {
// Fetch direto no servidor
const user = await fetch(`/api/users/${userId}`)
.then(res => res.json())
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
{/* Sem event handlers aqui */}
</div>
)
}
export default UserProfile'use client' // Diretiva obrigatória para Client Components
import { useState, useEffect } from 'react'
// Client Component - roda no browser
function InteractiveCounter() {
const [count, setCount] = useState(0)
// Hooks funcionam normalmente
useEffect(() => {
console.log('Count changed:', count)
}, [count])
// Event handlers funcionam
const increment = () => setCount(count + 1)
return (
<div>
<p>Contador: {count}</p>
<button onClick={increment}>+1</button>
</div>
)
}
export default InteractiveCounter🎯 Regra: Use Server Components por padrão. Só use "use client" quando precisar de interatividade, hooks ou APIs do browser.
1.4 Roteamento Dinâmico
Crie rotas dinâmicas usando colchetes nos nomes das pastas.
app/
├── blog/
│ ├── page.tsx # /blog
│ ├── [slug]/
│ │ └── page.tsx # /blog/meu-post
│ └── [category]/
│ └── [slug]/
│ └── page.tsx # /blog/react/hooks-avancados
└── user/
└── [id]/
├── page.tsx # /user/123
└── profile/
└── page.tsx # /user/123/profile// Página de blog post dinâmica
interface PageProps {
params: { slug: string }
searchParams: { [key: string]: string | string[] | undefined }
}
export default async function BlogPost({ params }: PageProps) {
const { slug } = params
// Fetch do post baseado no slug
const post = await fetch(`${process.env.API_URL}/posts/${slug}`)
.then(res => res.json())
if (!post) {
return <div>Post não encontrado</div>
}
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
)
}
// Gerar metadata dinâmica
export async function generateMetadata({ params }: PageProps) {
const { slug } = params
const post = await fetch(`${process.env.API_URL}/posts/${slug}`)
.then(res => res.json())
return {
title: post.title,
description: post.excerpt,
}
}
// Gerar rotas estáticas em build time (SSG)
export async function generateStaticParams() {
const posts = await fetch(`${process.env.API_URL}/posts`)
.then(res => res.json())
return posts.map((post: any) => ({
slug: post.slug,
}))
}2Renderização: SSR, SSG e ISR
2.1 Static Site Generation (SSG)
Páginas geradas em build time pra máxima velocidade. Perfeito pra conteúdo que muda pouco.
// SSG - Página gerada em build time
interface Produto {
id: string
nome: string
preco: number
descricao: string
}
interface PageProps {
params: { id: string }
}
export default async function ProdutoPage({ params }: PageProps) {
const produto = await buscarProduto(params.id)
return (
<div>
<h1>{produto.nome}</h1>
<p>Preço: R$ {produto.preco}</p>
<p>{produto.descricao}</p>
</div>
)
}
// Função para buscar produto (roda em build time)
async function buscarProduto(id: string): Promise<Produto> {
const res = await fetch(`${process.env.API_URL}/produtos/${id}`)
if (!res.ok) throw new Error('Produto não encontrado')
return res.json()
}
// Definir quais páginas gerar estaticamente
export async function generateStaticParams() {
// Buscar todos os produtos em build time
const produtos = await fetch(`${process.env.API_URL}/produtos`)
.then(res => res.json())
// Retornar array com os parâmetros para cada página
return produtos.map((produto: Produto) => ({
id: produto.id,
}))
}
// Metadata estática
export async function generateMetadata({ params }: PageProps) {
const produto = await buscarProduto(params.id)
return {
title: `${produto.nome} | Loja`,
description: produto.descricao,
openGraph: {
title: produto.nome,
description: produto.descricao,
},
}
}🚀 Vantagem SSG: Páginas servidas diretamente do CDN. Tempo de carregamento < 100ms.
2.2 Server-Side Rendering (SSR)
Páginas renderizadas no servidor a cada request. Bom pra conteúdo dinâmico personalizado.
import { cookies, headers } from 'next/headers'
// SSR - Renderizada a cada request
export default async function Dashboard() {
// Acessar cookies e headers do request
const cookieStore = cookies()
const headersList = headers()
const token = cookieStore.get('auth-token')?.value
const userAgent = headersList.get('user-agent')
if (!token) {
redirect('/login')
}
// Fetch de dados personalizados baseado no usuário
const userData = await fetch(`${process.env.API_URL}/user/profile`, {
headers: {
'Authorization': `Bearer ${token}`,
'User-Agent': userAgent || '',
},
}).then(res => res.json())
const notifications = await fetch(`${process.env.API_URL}/notifications`, {
headers: { 'Authorization': `Bearer ${token}` },
}).then(res => res.json())
return (
<div>
<h1>Dashboard de {userData.name}</h1>
<section>
<h2>Notificações ({notifications.length})</h2>
{notifications.map(notification => (
<div key={notification.id}>
<p>{notification.message}</p>
<small>{new Date(notification.createdAt).toLocaleString()}</small>
</div>
))}
</section>
<section>
<h2>Informações do Dispositivo</h2>
<p>User Agent: {userAgent}</p>
<p>Carregado em: {new Date().toLocaleString()}</p>
</section>
</div>
)
}
// Forçar SSR (não cachear)
export const dynamic = 'force-dynamic'⚠️ Atenção: SSR é mais lento que SSG pois renderiza a cada request. Use apenas quando necessário.
2.3 Incremental Static Regeneration (ISR)
O melhor dos dois mundos: velocidade do SSG com atualizações automáticas.
// ISR - Regeneração incremental a cada 60 segundos
export default async function NoticiaPage({ params }: { params: { slug: string } }) {
const noticia = await fetch(`${process.env.API_URL}/noticias/${params.slug}`, {
// Revalidar a cada 60 segundos
next: { revalidate: 60 }
}).then(res => res.json())
return (
<article>
<h1>{noticia.titulo}</h1>
<p>Publicado em: {new Date(noticia.publicadoEm).toLocaleDateString()}</p>
<p>Visualizações: {noticia.visualizacoes}</p>
<div dangerouslySetInnerHTML={{ __html: noticia.conteudo }} />
</article>
)
}
// Gerar páginas iniciais
export async function generateStaticParams() {
const noticias = await fetch(`${process.env.API_URL}/noticias?limit=100`)
.then(res => res.json())
return noticias.map((noticia: any) => ({
slug: noticia.slug,
}))
}
// Configuração de revalidação
export const revalidate = 60 // 60 segundosimport { revalidatePath, revalidateTag } from 'next/cache'
import { NextRequest, NextResponse } from 'next/server'
// API Route para revalidação manual
export async function POST(request: NextRequest) {
const { searchParams } = new URL(request.url)
const path = searchParams.get('path')
const tag = searchParams.get('tag')
const secret = searchParams.get('secret')
// Verificar token de segurança
if (secret !== process.env.REVALIDATE_SECRET) {
return NextResponse.json({ error: 'Invalid secret' }, { status: 401 })
}
try {
if (path) {
// Revalidar path específico
revalidatePath(path)
return NextResponse.json({ revalidated: true, path })
}
if (tag) {
// Revalidar por tag
revalidateTag(tag)
return NextResponse.json({ revalidated: true, tag })
}
return NextResponse.json({ error: 'No path or tag provided' }, { status: 400 })
} catch (err) {
return NextResponse.json({ error: 'Error revalidating' }, { status: 500 })
}
}
// Uso: POST /api/revalidate?path=/noticias/slug&secret=TOKEN🎯 Estratégia ISR: Use revalidate baixo (60s) para conteúdo que muda frequentemente, alto (3600s) para conteúdo estável.
3API Routes e Middleware
3.1 API Routes com App Router
Crie APIs REST completas diretamente no Next.js usando Route Handlers.
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
// Schema de validação
const CreateUserSchema = z.object({
name: z.string().min(1, 'Nome é obrigatório'),
email: z.string().email('Email inválido'),
age: z.number().min(18, 'Idade mínima 18 anos'),
})
// GET /api/users
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const page = searchParams.get('page') || '1'
const limit = searchParams.get('limit') || '10'
try {
// Simulate database query
const users = await fetch(`${process.env.DATABASE_URL}/users?page=${page}&limit=${limit}`)
.then(res => res.json())
return NextResponse.json({
users,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total: users.length,
},
})
} catch (error) {
return NextResponse.json(
{ error: 'Erro ao buscar usuários' },
{ status: 500 }
)
}
}
// POST /api/users
export async function POST(request: NextRequest) {
try {
const body = await request.json()
// Validar dados de entrada
const validatedData = CreateUserSchema.parse(body)
// Simular criação no banco
const newUser = {
id: Date.now().toString(),
...validatedData,
createdAt: new Date().toISOString(),
}
// Aqui você salvaria no banco de dados real
console.log('Creating user:', newUser)
return NextResponse.json(newUser, { status: 201 })
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: 'Dados inválidos', details: error.errors },
{ status: 400 }
)
}
return NextResponse.json(
{ error: 'Erro interno do servidor' },
{ status: 500 }
)
}
}import { NextRequest, NextResponse } from 'next/server'
interface RouteParams {
params: { id: string }
}
// GET /api/users/[id]
export async function GET(request: NextRequest, { params }: RouteParams) {
const { id } = params
try {
// Buscar usuário específico
const user = await fetch(`${process.env.DATABASE_URL}/users/${id}`)
.then(res => res.json())
if (!user) {
return NextResponse.json(
{ error: 'Usuário não encontrado' },
{ status: 404 }
)
}
return NextResponse.json(user)
} catch (error) {
return NextResponse.json(
{ error: 'Erro ao buscar usuário' },
{ status: 500 }
)
}
}
// PUT /api/users/[id]
export async function PUT(request: NextRequest, { params }: RouteParams) {
const { id } = params
try {
const body = await request.json()
// Atualizar usuário
const updatedUser = {
id,
...body,
updatedAt: new Date().toISOString(),
}
return NextResponse.json(updatedUser)
} catch (error) {
return NextResponse.json(
{ error: 'Erro ao atualizar usuário' },
{ status: 500 }
)
}
}
// DELETE /api/users/[id]
export async function DELETE(request: NextRequest, { params }: RouteParams) {
const { id } = params
try {
// Deletar usuário
// await deleteUserFromDatabase(id)
return NextResponse.json(
{ message: 'Usuário deletado com sucesso' },
{ status: 200 }
)
} catch (error) {
return NextResponse.json(
{ error: 'Erro ao deletar usuário' },
{ status: 500 }
)
}
}3.2 Middleware para Autenticação e CORS
Use middleware pra interceptar requests e adicionar autenticação, CORS, redirects.
import { NextRequest, NextResponse } from 'next/server'
import { jwtVerify } from 'jose'
export async function middleware(request: NextRequest) {
const { pathname, origin } = request.nextUrl
// CORS Headers para API routes
if (pathname.startsWith('/api/')) {
const response = NextResponse.next()
response.headers.set('Access-Control-Allow-Origin', '*')
response.headers.set(
'Access-Control-Allow-Methods',
'GET, POST, PUT, DELETE, OPTIONS'
)
response.headers.set(
'Access-Control-Allow-Headers',
'Content-Type, Authorization'
)
// Handle preflight requests
if (request.method === 'OPTIONS') {
return new Response(null, { status: 200, headers: response.headers })
}
return response
}
// Autenticação para rotas protegidas
if (pathname.startsWith('/dashboard') || pathname.startsWith('/admin')) {
const token = request.cookies.get('auth-token')?.value
if (!token) {
return NextResponse.redirect(new URL('/login', origin))
}
try {
// Verificar JWT
const { payload } = await jwtVerify(
token,
new TextEncoder().encode(process.env.JWT_SECRET!)
)
// Adicionar user info no header para usar nas páginas
const response = NextResponse.next()
response.headers.set('x-user-id', payload.sub as string)
response.headers.set('x-user-role', payload.role as string)
// Verificar permissões de admin
if (pathname.startsWith('/admin') && payload.role !== 'admin') {
return NextResponse.redirect(new URL('/dashboard', origin))
}
return response
} catch (error) {
// Token inválido - redirecionar para login
const response = NextResponse.redirect(new URL('/login', origin))
response.cookies.delete('auth-token')
return response
}
}
// Redirect de rotas antigas
if (pathname === '/old-page') {
return NextResponse.redirect(new URL('/new-page', origin))
}
// Geolocation redirect
const country = request.geo?.country
if (pathname === '/special-offer' && country !== 'BR') {
return NextResponse.redirect(new URL('/global-offer', origin))
}
return NextResponse.next()
}
// Configurar em quais rotas o middleware deve rodar
export const config = {
matcher: [
'/api/:path*',
'/dashboard/:path*',
'/admin/:path*',
'/old-page',
'/special-offer'
],
}🔒 Segurança: Sempre valide tokens no middleware. Nunca confie apenas na validação do frontend.
3.3 Tratamento de Erros e Loading States
Adicione error boundaries e loading states com arquivos especiais do App Router.
'use client' // Error boundaries devem ser Client Components
import { useEffect } from 'react'
import Link from 'next/link';
interface ErrorBoundaryProps {
error: Error & { digest?: string }
reset: () => void
}
export default function ErrorBoundary({ error, reset }: ErrorBoundaryProps) {
useEffect(() => {
// Log do erro para serviços de monitoramento
console.error('Blog error:', error)
// Enviar para Sentry, Bugsnag, etc.
// sendToErrorReporting(error)
}, [error])
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<h1 className="text-4xl font-bold text-red-500 mb-4">
Ops! Algo deu errado
</h1>
<p className="text-fg-dim mb-8">
Ocorreu um erro inesperado ao carregar esta página.
</p>
<div className="space-x-4">
<button
onClick={reset}
className="px-6 py-2 bg-blue-500 text-foreground rounded-lg hover:bg-blue-600"
>
Tentar Novamente
</button>
<Link href="/blog"
className="px-6 py-2 bg-bg-3 text-foreground rounded-lg hover:bg-bg-3"
>
Voltar ao Blog
</Link>
</div>
{process.env.NODE_ENV === 'development' && (
<details className="mt-8 text-left">
<summary className="cursor-pointer text-sm text-fg-dim">
Detalhes do erro (desenvolvimento)
</summary>
<pre className="mt-4 p-4 bg-bg-2 rounded text-xs overflow-auto">
{error.message}
{error.stack}
</pre>
</details>
)}
</div>
</div>
)
}// Loading UI mostrado durante navegação
export default function Loading() {
return (
<div className="container mx-auto px-4 py-8">
{/* Skeleton para header */}
<div className="animate-pulse mb-8">
<div className="h-8 bg-bg-3 rounded w-64 mb-4"></div>
<div className="h-4 bg-bg-3 rounded w-96"></div>
</div>
{/* Skeleton para posts grid */}
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{[...Array(6)].map((_, i) => (
<div key={i} className="animate-pulse">
<div className="bg-bg-3 h-48 rounded-lg mb-4"></div>
<div className="space-y-2">
<div className="h-4 bg-bg-3 rounded w-3/4"></div>
<div className="h-4 bg-bg-3 rounded w-1/2"></div>
<div className="h-3 bg-bg-3 rounded w-1/4"></div>
</div>
</div>
))}
</div>
</div>
)
}// Página 404 customizada
export default function NotFound() {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<h1 className="text-6xl font-bold text-foreground mb-4">404</h1>
<h2 className="text-2xl font-semibold text-fg-dim mb-8">
Página não encontrada
</h2>
<p className="text-fg-dim mb-8 max-w-md">
A página que você está procurando não existe ou foi movida.
</p>
<div className="space-x-4">
<Link href="/blog"
className="px-6 py-2 bg-blue-500 text-foreground rounded-lg hover:bg-blue-600"
>
Ver Todos os Posts
</Link>
<a
href="/"
className="px-6 py-2 border border-border rounded-lg hover:bg-bg-2"
>
Voltar ao Início
</Link>
</div>
</div>
</div>
)
}4Performance e SEO Avançado
4.1 Otimização de Imagens com next/image
O componente Image do Next.js otimiza automaticamente imagens para performance.
import Image from 'next/image'
// Imagem com otimização automática
function ProductCard({ product }) {
return (
<div className="product-card">
<Image
src={product.imageUrl}
alt={product.name}
width={400}
height={300}
// Priorizar imagens above the fold
priority={product.featured}
// Placeholder blur base64
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k="
// Lazy loading por padrão (exceto priority)
loading="lazy"
// Qualidade da imagem (1-100)
quality={85}
// Sizes para responsividade
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
// Classes CSS
className="rounded-lg object-cover"
// Event handlers
onLoad={() => console.log('Image loaded')}
onError={() => console.log('Image failed to load')}
/>
<h3>{product.name}</h3>
<p>R$ {product.price}</p>
</div>
)
}
// Hero image com fill
function HeroBanner() {
return (
<div className="relative h-96 w-full">
<Image
src="/hero-banner.jpg"
alt="Banner principal"
fill // Preenche o container pai
className="object-cover"
priority // Carregar imediatamente
/>
<div className="absolute inset-0 bg-background bg-opacity-50 flex items-center justify-center">
<h1 className="text-foreground text-4xl font-bold">Bem-vindo!</h1>
</div>
</div>
)
}/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
// Domínios permitidos para imagens externas
remotePatterns: [
{
protocol: 'https',
hostname: 'images.unsplash.com',
},
{
protocol: 'https',
hostname: 'cdn.example.com',
port: '',
pathname: '/images/**',
},
],
// Formatos de imagem suportados
formats: ['image/webp', 'image/avif'],
// Tamanhos de device para otimização
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
// Tamanhos de imagem para diferentes breakpoints
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
// Qualidade padrão
quality: 85,
// Cache de imagens otimizadas (em segundos)
minimumCacheTTL: 60 * 60 * 24 * 365, // 1 ano
},
}
module.exports = nextConfig4.2 Meta Tags e SEO Dinâmico
Configure meta tags dinâmicas pra cada página, incluindo Open Graph e Twitter Cards.
import { Metadata } from 'next'
interface Produto {
id: string
nome: string
descricao: string
preco: number
imagem: string
categoria: string
slug: string
}
interface PageProps {
params: { slug: string }
}
export default async function ProdutoPage({ params }: PageProps) {
const produto = await buscarProduto(params.slug)
if (!produto) {
notFound()
}
// Schema.org JSON-LD para rich snippets
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'Product',
name: produto.nome,
description: produto.descricao,
image: produto.imagem,
offers: {
'@type': 'Offer',
price: produto.preco,
priceCurrency: 'BRL',
availability: 'https://schema.org/InStock',
},
}
return (
<>
{/* JSON-LD para SEO */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<div>
<h1>{produto.nome}</h1>
<p>{produto.descricao}</p>
<p>Preço: R$ {produto.preco}</p>
</div>
</>
)
}
// Metadata dinâmica
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const produto = await buscarProduto(params.slug)
if (!produto) {
return {
title: 'Produto não encontrado',
}
}
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'https://loja.com'
return {
title: `${produto.nome} - Comprar Online | Loja`,
description: `${produto.descricao}. Preço: R$ ${produto.preco}. Compre agora com frete grátis!`,
keywords: [
produto.nome,
produto.categoria,
'comprar online',
'frete grátis',
'promoção'
],
// Open Graph
openGraph: {
type: 'product',
title: produto.nome,
description: produto.descricao,
url: `${baseUrl}/produtos/${produto.slug}`,
images: [
{
url: produto.imagem,
width: 800,
height: 600,
alt: produto.nome,
},
],
siteName: 'CrazyStack',
},
// Twitter Cards
twitter: {
card: 'summary_large_image',
title: produto.nome,
description: produto.descricao,
images: [produto.imagem],
creator: '@minhaloja',
},
// Canonical URL
alternates: {
canonical: `${baseUrl}/produtos/${produto.slug}`,
},
// Robots
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
'max-video-preview': -1,
'max-image-preview': 'large',
'max-snippet': -1,
},
},
}
}4.3 Sitemap e Robots.txt Automáticos
Gere sitemap.xml e robots.txt automaticamente baseado nas suas páginas.
import { MetadataRoute } from 'next'
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'https://loja.com'
// Páginas estáticas
const staticPages = [
{
url: baseUrl,
lastModified: new Date(),
changeFrequency: 'daily' as const,
priority: 1,
},
{
url: `${baseUrl}/sobre`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.8,
},
{
url: `${baseUrl}/contato`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.5,
},
]
// Buscar produtos do banco/API
const produtos = await fetch(`${process.env.API_URL}/produtos`)
.then(res => res.json())
const produtoPages = produtos.map((produto: any) => ({
url: `${baseUrl}/produtos/${produto.slug}`,
lastModified: new Date(produto.updatedAt),
changeFrequency: 'weekly' as const,
priority: 0.7,
}))
// Buscar posts do blog
const posts = await fetch(`${process.env.API_URL}/blog/posts`)
.then(res => res.json())
const blogPages = posts.map((post: any) => ({
url: `${baseUrl}/blog/${post.slug}`,
lastModified: new Date(post.publishedAt),
changeFrequency: 'monthly' as const,
priority: 0.6,
}))
return [...staticPages, ...produtoPages, ...blogPages]
}import { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'https://loja.com'
return {
rules: [
{
userAgent: '*',
allow: '/',
disallow: [
'/api/*',
'/admin/*',
'/private/*',
'/checkout/success', // Evitar indexação de páginas de sucesso
'/search?*', // Evitar indexação de páginas de busca com parâmetros
],
},
{
userAgent: 'Googlebot',
allow: '/',
disallow: ['/admin/*'],
},
{
userAgent: 'Bingbot',
allow: '/',
disallow: ['/admin/*', '/api/*'],
}
],
sitemap: `${baseUrl}/sitemap.xml`,
host: baseUrl,
}
}🚀Projeto Final: E-commerce Completo
Construa um e-commerce full-stack com Next.js 14, aplicando todos os conceitos aprendidos.
Funcionalidades
- Catálogo de produtos com SSG
- Busca com filtros dinâmicos
- Carrinho de compras persistente
- Dashboard do usuário com SSR
- Admin panel com autenticação
- API completa para produtos/pedidos
Stack Técnica
- Next.js 14 com App Router
- TypeScript para type safety
- Prisma ORM + PostgreSQL
- NextAuth.js para autenticação
- Tailwind CSS + shadcn/ui
- Deploy na Vercel
📁 Estrutura do Projeto
Domine Next.js 14 e Se Torne um Dev Full-Stack
Com este conhecimento avançado em Next.js, você está preparado para construir aplicações web modernas, escaláveis e otimizadas para produção.
Próximos Passos
Continue evoluindo com React Native pra mobile, testes automatizados e arquiteturas serverless.