Rotas no Nuxt: dinâmicas, aninhadas e catch-all
O Nuxt transforma os arquivos da pasta pages em rotas. Este artigo mostra como esse mapeamento funciona no Nuxt 4, com slugs dinâmicos, rotas aninhadas, opcionais e catch-all, parâmetros de consulta e as escolhas de roteamento que afetam o SEO de um site real.
Como funciona o roteamento baseado em arquivos do Nuxt
Todo arquivo .vue dentro da pasta pages vira uma página, e a URL vem do caminho do arquivo:
| Arquivo | Rota |
|---|---|
| app/pages/index.vue | / |
| app/pages/links.vue | /links |
| app/pages/tools/cpf-generator.vue | /tools/cpf-generator |
No Nuxt 4, projetos novos guardam o código da aplicação dentro de um diretório app/. As páginas ficam em app/pages/, os layouts em app/layouts/ e os middlewares de rota em app/middleware/. Se você migrou do Nuxt 3 e ainda tem uma pasta pages/ na raiz, ela continua funcionando: o Nuxt detecta a estrutura antiga e não força a nova. Os exemplos abaixo usam os caminhos com app/.
O sistema usa o Vue Router por baixo. O Nuxt gera a lista de rotas a partir dos seus arquivos, então raramente você precisa escrever uma configuração de roteador à mão.
Rotas estáticas
Uma rota estática é uma página cuja URL nunca muda, como uma página institucional ou uma ferramenta:
app/pages/
├── index.vue -> /
├── about.vue -> /about
├── privacy-policy.vue -> /privacy-policy
└── tools/
└── cpf-generator.vue -> /tools/cpf-generator
Se a URL base for https://lojaonline.com, o Nuxt serve https://lojaonline.com/about e https://lojaonline.com/privacy-policy a partir desses arquivos.
Rotas dinâmicas
Quando muitas URLs usam o mesmo modelo de página, como os posts de um blog, você cria um único arquivo com o nome do parâmetro entre colchetes. As duas estruturas abaixo geram a mesma rota:
| Local do parâmetro | Arquivo | Rota |
|---|---|---|
| Nome do arquivo | app/pages/blog/[slug].vue | /blog/meu-post |
| Nome do diretório | app/pages/blog/[slug]/index.vue | /blog/meu-post |
O nome entre colchetes é a chave que você lê em route.params. O slug é a parte legível da URL que identifica um recurso, geralmente derivada do título, como como-funciona-o-sistema-de-rotas-no-nuxt. Ele não é um id de banco de dados: nada garante que seja único nem que nunca mude, então quem decide como resolvê-lo é a sua aplicação.
Aqui está uma página de post que carrega o conteúdo do Nuxt Content usando o slug:
<script setup lang="ts">
const route = useRoute();
const slug = computed(() => String(route.params.slug));
const { data: post } = await useAsyncData(
() => `post-${slug.value}`,
() => queryCollection("blog").path(`/blog/${slug.value}`).first()
);
if (!post.value) {
throw createError({
status: 404,
statusText: "Post não encontrado",
fatal: true
});
}
</script>
<template>
<article v-if="post">
<h1>{{ post.title }}</h1>
</article>
</template>
O slug é um computed, e a chave do useAsyncData é uma função que depende dele. Quando o usuário vai de /blog/a para /blog/b, o Vue Router pode manter o mesmo componente de página montado, então o código do <script setup> não necessariamente roda de novo. Com uma chave reativa, o Nuxt busca o novo post quando o slug muda. Se você ler route.params.slug uma vez e guardar em uma variável comum, a URL muda, mas a página continua mostrando o post anterior.
Um slug que não existe também precisa de um 404. Sem a verificação com createError, ele renderiza uma página vazia com status 200, e os buscadores enxergam uma URL válida, porém vazia.
Validando o parâmetro antes de renderizar
O definePageMeta aceita uma função validate que decide se a rota pode ser renderizada por aquela página. Se ela retornar false e nenhuma outra rota combinar, o Nuxt responde com 404:
<script setup lang="ts">
definePageMeta({
validate: (route) => /^[a-z0-9-]+$/.test(String(route.params.slug))
});
</script>
O validate só confere o formato do parâmetro. Saber se o post existe continua exigindo a consulta aos dados mostrada acima.
Rotas aninhadas
Rotas aninhadas servem para seções de uma interface em que uma área pai permanece na tela enquanto o conteúdo filho muda. Um painel administrativo com Pedidos, Produtos e Configurações é o caso típico.
Para criar uma, você precisa de um arquivo de página pai e de uma pasta com o mesmo nome que guarda os filhos:
app/pages/
├── admin.vue
└── admin/
├── index.vue -> /admin
├── orders.vue -> /admin/orders
├── products.vue -> /admin/products
└── settings.vue -> /admin/settings
O arquivo pai renderiza a interface compartilhada e usa <NuxtPage /> onde a página filha deve aparecer:
<template>
<div>
<nav>
<NuxtLink to="/admin/orders">Pedidos</NuxtLink>
<NuxtLink to="/admin/products">Produtos</NuxtLink>
<NuxtLink to="/admin/settings">Configurações</NuxtLink>
</nav>
<NuxtPage />
</div>
</template>
Ao navegar entre os filhos, só o conteúdo renderizado pelo <NuxtPage /> troca, e o menu continua no lugar.
Rotas aninhadas não são layouts
O Nuxt não tem um arquivo layout.vue dentro de pages/admin. Página pai e layout são recursos separados:
- Uma página pai aninhada (
admin.vue) faz parte da árvore de rotas. Ela renderiza as rotas filhas por meio do<NuxtPage />. - Um layout fica em
app/layouts/e é uma estrutura reutilizável, como um cabeçalho com rodapé. A página escolhe um comdefinePageMeta({ layout: "dashboard" }), e o layout renderiza a página pelo slot.
Rotas opcionais e catch-all
Dois padrões com colchetes cobrem casos que um [slug] simples não resolve:
| Arquivo | Casa com |
|---|---|
| app/pages/[[slug]].vue | / e /qualquer-coisa |
| app/pages/[...slug].vue | qualquer caminho, inclusive com vários trechos |
Colchetes duplos tornam o trecho opcional. Os três pontos criam um catch-all que recebe o restante do caminho. Rotas catch-all servem para URLs de profundidade variável, como em documentações, e para tratar URLs antigas que você precisa redirecionar.
Caminhos personalizados e aliases
Às vezes a URL pública não deve seguir a estrutura de arquivos. O definePageMeta permite definir um path personalizado ou adicionar um alias para uma página:
<script setup lang="ts">
definePageMeta({
path: "/shirts/:slug",
alias: ["/t-shirts/:slug"]
});
</script>
O alias faz a mesma página responder em mais de uma URL. Isso cria URLs duplicadas, então use só quando precisar das duas e aponte o canonical para a que você quer nos resultados de busca.
Parâmetros de consulta (query)
Os parâmetros de consulta carregam um estado opcional que não muda qual recurso a página representa: um filtro, a página atual de uma lista ou um termo de busca. O formato é ?nome=valor, e parâmetros extras são unidos com &:
https://lojaonline.com/camisetas?cor=azul&tamanho=m&page=2
Você os lê com useRoute().query. O valor de um parâmetro nem sempre é uma única string. Pode ser uma string, null (para ?cor sem valor), um array (para ?tag=seo&tag=nuxt) ou undefined quando o parâmetro não existe. Um helper pequeno evita bugs quando o valor é usado em um filtro ou em uma chamada de API:
// app/utils/query.ts
export function firstQueryValue(
value: string | null | (string | null)[] | undefined
) {
const first = Array.isArray(value) ? value[0] : value;
return first ?? undefined;
}
<script setup lang="ts">
const route = useRoute();
const page = computed(() => Number(firstQueryValue(route.query.page)) || 1);
const cor = computed(() => firstQueryValue(route.query.cor));
</script>
O Nuxt importa o helper de app/utils automaticamente, e os dois valores se atualizam quando a query muda.
Query strings e SEO
Cada combinação de filtros cria uma URL diferente, e uma listagem com poucos filtros pode gerar uma quantidade enorme delas. Para URLs que só ordenam ou restringem a mesma lista, defina um canonical apontando para a listagem sem filtro:
<script setup lang="ts">
useHead({
link: [{ rel: "canonical", href: "https://lojaonline.com/camisetas" }]
});
</script>
Páginas de paginação são outro caso: cada página da lista tem conteúdo próprio, então ?page=2 mantém um canonical apontando para ela mesma, não para a página 1. Um conteúdo que deve ranquear sozinho, como uma categoria, funciona melhor como caminho (/camisetas/azul) do que como query string.
Navegando entre rotas
Use o NuxtLink nos links que o usuário vê. Ele renderiza um elemento âncora normal e navega no cliente, sem recarregar a página. Para redirecionamentos e navegação disparada por código, use o navigateTo:
<script setup lang="ts">
async function aplicarFiltro() {
await navigateTo({
path: "/camisetas",
query: { cor: "azul", tamanho: "m" }
});
}
</script>
<template>
<NuxtLink to="/blog/como-funciona-o-sistema-de-rotas-no-nuxt">
Ler o post
</NuxtLink>
<button @click="aplicarFiltro">Azul, tamanho M</button>
</template>
Quando uma regra vale para muitas rotas, como exigir login, use um middleware de rota em app/middleware/. Dentro do middleware, leia os argumentos to e from em vez de chamar useRoute(). A documentação do Nuxt desaconselha o useRoute() ali, porque o middleware ainda não tem uma rota atual.
Erros comuns de roteamento
- Usar um layout como pai de rotas aninhadas, em vez de uma página pai com
<NuxtPage />. - Ler
route.paramsuma vez em vez de derivar os dados dele de forma reativa. - Tratar os valores de
route.querycomo strings simples. - Devolver uma página vazia com status 200 para um slug que não existe.
- Criar URLs de filtro sem decidir qual versão os buscadores devem indexar.
Perguntas frequentes
O que é roteamento baseado em arquivos no Nuxt?
O Nuxt cria as rotas a partir dos arquivos e pastas dentro do diretório pages. Por exemplo, app/pages/about.vue vira /about.
Onde ficam as páginas no Nuxt 4?
Projetos novos do Nuxt 4 usam app/pages/. Projetos que ainda têm uma pasta pages/ na raiz continuam funcionando, porque o Nuxt detecta a estrutura antiga.
Como criar uma rota dinâmica no Nuxt?
Nomeie o arquivo com um parâmetro entre colchetes, como app/pages/blog/[slug].vue, e leia o valor com useRoute().params.slug. Derive tudo que depende dele de um computed, para atualizar quando o slug mudar.
Qual é a diferença entre rotas aninhadas e layouts?
Uma rota aninhada usa uma página pai com <NuxtPage /> para renderizar as rotas filhas. Um layout fica em app/layouts/ e envolve as páginas com uma estrutura reutilizável, escolhida com definePageMeta({ layout }).
Como mostrar um 404 para um slug inválido?
Lance um createError com status: 404 quando os dados não existirem, ou retorne false no validate do definePageMeta quando o parâmetro tiver um formato inválido.
Conclusão
Antes de criar os arquivos, liste as suas URLs públicas: quais páginas são estáticas, quais precisam de slug e qual estado pertence à query string. Depois decida quais URLs filtradas devem ser indexadas. Para a lista completa de opções, consulte a documentação de roteamento do Nuxt.
