Pular para o conteúdo
Framework Full-Stack

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.

Server-Side Rendering
Static Site Generation
App Router

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

1
Fundamentos 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.

terminal
# 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.

Estrutura App Router
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 layout
app/layout.tsx
import './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>
  )
}
app/page.tsx
// 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").

components/ServerComponent.tsx
// 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
components/ClientComponent.tsx
'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.

Estrutura de Rotas Dinâmicas
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
app/blog/[slug]/page.tsx
// 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,
  }))
}

2
Renderizaçã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.

app/produtos/[id]/page.tsx
// 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.

app/dashboard/page.tsx
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.

app/noticias/[slug]/page.tsx
// 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 segundos
app/api/revalidate/route.ts
import { 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.

3
API Routes e Middleware

3.1 API Routes com App Router

Crie APIs REST completas diretamente no Next.js usando Route Handlers.

app/api/users/route.ts
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 }
    )
  }
}
app/api/users/[id]/route.ts
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.

middleware.ts
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.

app/blog/error.tsx
'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>
  )
}
app/blog/loading.tsx
// 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>
  )
}
app/blog/not-found.tsx
// 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>
  )
}

4
Performance e SEO Avançado

4.1 Otimização de Imagens com next/image

O componente Image do Next.js otimiza automaticamente imagens para performance.

components/OptimizedImage.tsx
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>
  )
}
next.config.js
/** @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 = nextConfig

4.2 Meta Tags e SEO Dinâmico

Configure meta tags dinâmicas pra cada página, incluindo Open Graph e Twitter Cards.

app/produtos/[slug]/page.tsx
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.

app/sitemap.ts
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]
}
app/robots.ts
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

app/
├── (shop)/
│ ├── page.tsx # Homepage
│ ├── produtos/
│ │ ├── page.tsx # Lista produtos
│ │ └── [slug]/page.tsx # Produto individual
│ └── carrinho/page.tsx # Carrinho
├── dashboard/
│ ├── layout.tsx # Layout autenticado
│ ├── page.tsx # Dashboard
│ └── pedidos/page.tsx # Histórico pedidos
├── admin/
│ └── produtos/page.tsx # Gestão produtos
└── api/
├── produtos/route.ts # CRUD produtos
└── auth/[...nextauth]/route.ts

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.

SSR/SSG/ISR dominados
API Routes e Middleware
Performance e SEO otimizados

Próximos Passos

Continue evoluindo com React Native pra mobile, testes automatizados e arquiteturas serverless.

Próximo: React Native Fundamentals95% concluído