Redirects e canonicals no Next.js: config ou middleware
Publicado em
next.config.ts e middleware fazem a mesma coisa por caminhos diferentes: o
redirects() do config é estático, avaliado antes do roteamento e sem custo
por request; o middleware roda em cada request e é o único que consegue
decidir com base em cookie, header ou geografia. A regra prática: se a
regra é conhecida no build, ela não tem o que fazer no middleware. Mas a
pergunta anterior a essa é mais importante — e é a que quase ninguém faz:
esse problema pede mesmo um redirect, ou pede um canonical, ou um noindex?
Quase todo status de "página duplicada" no Search Console nasce da escolha
do instrumento errado.
Três instrumentos, três problemas
A tabela de decisão que organiza o artigo inteiro:
| A situação | O instrumento | O que ele diz ao Google |
|---|---|---|
| A URL antiga deve deixar de existir | redirect permanente (301/308) | "isto mudou de endereço — transfira tudo para lá" |
| As duas URLs existem, mas uma consolida os sinais | canonical | "indexe aquela; esta é uma variante" |
| A página existe para o usuário, não para o índice | noindex | "rastreie se quiser, mas não indexe" |
| As duas URLs existem e são conteúdos distintos | nada | não há problema a resolver |
Errar essa escolha não dá erro — dá sintoma meses depois, no relatório de indexação. O resto do artigo percorre cada instrumento e termina no caso em que dois deles, combinados, se anulam — medido neste site.
redirects() no next.config: o caminho padrão
Quando a regra é conhecida no build — a migração de slug, a URL legada, o
caminho renomeado — o lugar dela é o array redirects() do config. O
formato (ilustrativo; a razão de não haver um exemplo de produção aqui vem
já na próxima frase):
// next.config.ts — exemplo ilustrativo, não código deste site
async redirects() {
return [
{ source: "/blog/slug-antigo", destination: "/blog/slug-novo", permanent: true },
];
}E a transparência que este site deve ao leitor: o array redirects() de
produção daqui está vazio — existe só o comentário de exemplo em
next.config.ts. Não por descuido: o site nasceu em 2026-07 já com a
estrutura atual de URLs, e redirect é dívida de histórico, não boa prática
por si. Quem não tem URL legada não tem o que redirecionar — e cada
redirect que você não precisa criar é uma cadeia que nunca vai existir.
permanent: true é 308, não 301 — e por que dá no mesmo
O permanent: true emite HTTP 308, não 301. A diferença técnica: o 308
obriga o cliente a preservar o método do request (POST segue POST), enquanto
o 301 permite rebaixar para GET. Para SEO, é indiferente — o Google trata
ambos como redirect permanente e consolida os sinais no destino. Se alguma
ferramenta de auditoria reclamar que "não é 301", ela está desatualizada,
não o seu site.
source, destination e o casamento de caminho
O source aceita parâmetros (/blog/:slug) e o destination os
reutiliza — o que permite migrar uma estrutura inteira numa regra. O ponto
de atenção é a ordem e a sobreposição: regras genéricas demais capturam
caminhos que não deviam, e a primeira regra que casa, vence. Toda regra nova
merece um teste de caminho real antes do deploy — a seção de verificação
mostra como.
Middleware: quando a regra só existe em runtime
O middleware intercepta toda request antes do roteamento e pode responder com um redirect. É a ferramenta certa para exatamente um tipo de caso: a decisão que depende de informação que só existe na request — um cookie de sessão, um header, um país de origem. Um exemplo hipotético (este site não tem middleware — nenhuma linha):
// middleware.ts — exemplo hipotético, este site não usa middleware
import { NextResponse, type NextRequest } from "next/server";
export function middleware(request: NextRequest) {
if (request.cookies.has("beta-optin")) {
return NextResponse.redirect(new URL("/beta", request.url));
}
}O custo: ele roda em toda request
Cada request do site — página, asset que passe pelo matcher, crawler — executa o middleware. É latência somada na frente de tudo, inclusive na frente do Googlebot. Esse custo se justifica quando a lógica é verdadeiramente dinâmica; para regra estática, é desperdício puro.
Redirect em middleware não é para URL que você já conhece
Vale repetir como regra de revisão de código: se o source e o
destination são conhecidos antes do deploy, a regra pertence ao config. Um
mapeamento fixo dentro do middleware entrega o mesmo resultado pagando
execução por request — e espalha a lógica de redirect por dois lugares, que
é como cadeias acidentais nascem.
redirect() e permanentRedirect() dentro do componente
A terceira via: as funções redirect() e permanentRedirect() de
next/navigation, chamadas dentro do próprio componente ou de um Server
Action. O caso de uso legítimo é a decisão que depende do dado da rota:
o slug consultado existe, mas o conteúdo mudou de endereço; o produto saiu
de linha e a página aponta para o sucessor. A decisão nasce onde o dado
está — no componente que acabou de carregá-lo — e não teria como viver num
config estático.
Canonical: uma dica forte, não uma ordem
O canonical inverte a hierarquia dos instrumentos anteriores: redirect é uma ordem que o Google não pode ignorar — o conteúdo da URL antiga literalmente não é mais servido; canonical é uma dica que o Google pondera junto com os outros sinais.
Auto-referente em toda página
A implementação deste site é o lado maduro deste artigo: toda rota indexável
emite canonical apontando para si mesma, com o og:url casado, via um
helper único de metadados:
// src/lib/metadata.ts
return {
title: absoluteTitle ? { absolute: title } : title,
description,
alternates: {
canonical: path,
// Autodiscovery do feed em toda página. Precisa estar aqui (e não no
// root layout) porque `alternates` da página substitui o herdado.
types: {
'application/rss+xml': [{ url: '/feed.xml', title: site.name }],
},
},
openGraph: {
url: absoluteUrl(path),
siteName: site.name,
locale: site.locale,
// …
},
...(noindex ? { robots: { index: false, follow: true } } : {}),
}A mecânica completa desse helper — e o incidente que o motivou — está no artigo da Metadata API. Aqui o canonical importa pelo ângulo do conflito: o que acontece quando ele discorda dos outros sinais.
O que acontece quando os sinais se contradizem
O Google lê o canonical ao lado do sitemap, dos links internos, dos redirects e da similaridade de conteúdo. Quando o canonical aponta para A, o sitemap lista B e os links internos vão para C, o Google desempata sozinho — e o resultado aparece no Search Console como "Página duplicada, o Google escolheu um canônico diferente". Esse status quase nunca é um bug do Google: é o site contando três histórias. Neste projeto, canonical e sitemap saem do mesmo pipeline de conteúdo justamente para que contar histórias diferentes seja impossível por construção.
Quando é noindex, e não redirect
O terceiro instrumento cobre a página que deve existir para o usuário e
não deve existir no índice: busca interna, páginas de confirmação, áreas
utilitárias. Redirecionar seria quebrar a função da página; canonical seria
mentira (ela não é variante de ninguém). No App Router, o noindex sai do
mesmo helper de metadados deste site, como opção explícita:
// src/lib/metadata.ts
export interface BuildMetadataInput {
// …
/** Para rotas utilitárias (ex.: /busca) que não devem ser indexadas. */
noindex?: boolean
}
// e, na montagem do objeto Metadata:
...(noindex ? { robots: { index: false, follow: true } } : {})Repare no follow: true: a página não deve ser indexada, mas os links dela
continuam valendo para descoberta. noindex não precisa vir acompanhado de
nofollow — são decisões separadas.
E é exatamente essa página — a busca interna — que protagoniza o caso real.
Instrumentos que se cancelam: um caso real deste site
No baseline de 2026-07-20, a auditoria deste site encontrou a página
/busca com noindex na meta e Disallow no robots.txt ao mesmo
tempo. Parece cinto e suspensório; é um anulando o outro.
A mecânica: o robots.txt é consultado antes do rastreamento. URL
bloqueada não é rastreada; URL não rastreada nunca tem a meta noindex
lida; e uma URL bloqueada-mas-conhecida (por links, por histórico) segue
elegível a aparecer como resultado só de URL, sem snippet. O
instrumento que removeria a página do índice estava trancado do lado de
dentro da porta que o outro instrumento fechou.
Em texto: com só noindex, o Googlebot rastreia a página, lê a meta e a
remove do índice. Com noindex mais Disallow, o Googlebot é barrado antes
de ler qualquer coisa — e a URL continua elegível a ser exibida.
A correção, feita no mesmo dia: remover o Disallow e deixar o
noindex trabalhar — quem remove do índice é ele, e para isso precisa ser
lido. O robots.ts de produção ficou assim, com o motivo registrado no
próprio código:
// src/app/robots.ts
export default function robots(): MetadataRoute.Robots {
if (!indexable) {
return { rules: [{ userAgent: '*', disallow: '/' }] }
}
return {
rules: [
{
userAgent: '*',
allow: '/',
// /busca NÃO entra aqui: a página já é noindex, e um Disallow impediria
// o Googlebot de rastreá-la — logo, de ler o próprio noindex. As duas
// diretivas se cancelam. Ver /blog/sitemap-dinamico-nextjs.
disallow: ['/api/'],
},
],
sitemap: `${site.url}/sitemap.xml`,
}
}E o fechamento com medição, registrado em docs/experiment-log.md: o
recrawl de produção confirmou 11 URLs, 0 erros, 0 avisos. Hipótese,
correção, verificação — o ciclo completo, com datas.
Trailing slash: escolha uma forma e seja consistente
/blog/meu-post e /blog/meu-post/ são URLs diferentes; servir as duas
como páginas é criar um duplicado por rota. A regra tem três partes:
escolha uma forma; garanta que a outra redireciona (não canonicaliza —
redirect, para nem existir como página); e mantenha sitemap, links internos
e canonicals unânimes na forma escolhida. No Next.js, a opção
trailingSlash do config controla o comportamento — este site fica no
default (sem barra final), e a consistência dos três sinais é o que o teste
de CI confere.
Como verificar: cadeia de redirect, status real e o canônico escolhido
- O status real, sem browser:
curl -sI https://exemplo.com.br/url-antigae leiaHTTP/2 308(ou 301) e o headerlocation. Browsers escondem cadeias; ocurlmostra um salto por vez. - A cadeia inteira:
curl -sILsegue os redirects — se aparecer mais de um salto até o 200 final, você tem uma cadeia para achatar: a regra deve apontar direto para o destino final. - O canônico segundo o Google: Search Console → Inspeção de URL → o campo do canônico declarado pelo usuário contra o escolhido pelo Google. Divergência ali é o sintoma de sinais contraditórios — confira canonical, sitemap e links internos até os três concordarem.
- O caso noindex: para uma página que deve sair do índice, confirme as
duas metades: a meta
noindexpresente no HTML e a URL rastreável (fora do robots.txt). Uma sem a outra não funciona — este site tem a cicatriz para provar. - O JSON-LD da página de destino: depois de consolidar URLs, o
@id/urldo schema precisa apontar para a URL canônica final — o gerador de JSON-LD emite o formato usado neste site. E como sempre no guia de SEO técnico para Next.js: a regra que importa vira teste no CI, porque instrumento de indexação sem verificação é promessa, não engenharia.