Rotas no Nuxt: dinâmicas, aninhadas e catch-all

Thiago Nunes Batista
Escrito por: Thiago Nunes BatistaPublicado em: Última atualização:
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:

ArquivoRota
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âmetroArquivoRota
Nome do arquivoapp/pages/blog/[slug].vue/blog/meu-post
Nome do diretórioapp/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 com definePageMeta({ 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:

ArquivoCasa com
app/pages/[[slug]].vue/ e /qualquer-coisa
app/pages/[...slug].vuequalquer 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.

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.params uma vez em vez de derivar os dados dele de forma reativa.
  • Tratar os valores de route.query como 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.