Vercel Logo

Pages & Routing

In Next.js, a route is a file called page.tsx inside a folder that matches the URL. Want /about? Create app/about/page.tsx. Want /springs/[id]? Create app/springs/[id]/page.tsx. The folder is the route, the file is always page.tsx.

Nuxt flips this. The file IS the route. Want /about? Create app/pages/about.vue. No wrapper folder, no mandatory filename. The trade-off is subtle but it matters: Next.js routes are verbose but explicit, Nuxt routes are compact but rely on you knowing the naming rules.

Let's build out the pages for Hot Springs Finder and see where those rules match up and where they diverge.

Outcome

Create the route structure for the Hot Springs Finder with home, browse, and detail pages.

Fast Track

  1. Create page files in app/pages/ for each route
  2. Add NuxtLink navigation between pages
  3. Verify routes resolve correctly in the browser

Hands-on exercise 1.2

Build the page structure for the app. Right now, the starter has placeholder pages. We'll update them with real structure and connect them with navigation.

Requirements:

  1. Verify these pages exist in app/pages/ and understand which URL each one maps to
  2. Update the home page with a link to the browse page using NuxtLink
  3. Navigate between pages and confirm the routes work

Implementation hints:

  • NuxtLink is the equivalent of Next.js's Link component. It's auto-imported, so you don't need to import it
  • In Nuxt, pages/springs/index.vue maps to /springs and pages/springs/[id].vue maps to /springs/:id. In Next.js, those would be app/springs/page.tsx and app/springs/[id]/page.tsx
  • Square brackets for dynamic segments work the same way conceptually, but the file structure is flatter

Here's the side-by-side for every route in our app:

URL              Nuxt file                      Next.js file
───              ─────────                      ────────────
/                pages/index.vue                app/page.tsx
/springs         pages/springs/index.vue        app/springs/page.tsx
/springs/:id     pages/springs/[id].vue         app/springs/[id]/page.tsx
/favorites       pages/favorites.vue            app/favorites/page.tsx
/visited         pages/visited.vue              app/visited/page.tsx
/login           pages/login.vue                app/login/page.tsx

Notice the pattern. Next.js always needs a folder. Nuxt lets you use either a folder with index.vue or a standalone file. /favorites doesn't need a favorites/ folder because there are no nested routes under it. /springs uses a folder because it has a child route ([id]).

Now let's look at how navigation works. In Next.js, you'd write:

import Link from "next/link";
 
<Link href="/springs">Browse</Link>

In Nuxt, NuxtLink is auto-imported. It accepts href, but to is the convention across Vue Router and most Vue UI libraries. We'll use to:

<NuxtLink to="/springs">Browse</NuxtLink>

The home page already uses this pattern. Let's look at the link that takes users from the home page to the browse page:

app/pages/index.vue
<template>
  <div>
    <section>
      <h1>
        Find your next soak
      </h1>
      <p>
        Discover hot springs around the world. Browse by region, temperature, or
        type. Save your favorites and track the ones you've visited.
      </p>
      <div>
        <NuxtLink to="/springs">
          Browse Hot Springs
        </NuxtLink>
      </div>
    </section>
  </div>
</template>

NuxtLink handles client-side navigation automatically. No full page reload, no manual prefetching. When a NuxtLink enters the viewport, Nuxt prefetches the target route's code in the background. You get the performance behavior for free.

One thing that might catch you off guard: useRoute() in Nuxt returns a reactive route object, similar to Next.js's useParams() and useSearchParams() combined into one. We'll use this heavily when we build the detail page in a later lesson.

<script setup lang="ts">
const route = useRoute();
// route.params.id — same as useParams() in Next.js
// route.query.search — same as useSearchParams().get("search")
</script>

No import needed. useRoute is auto-imported like everything else in Nuxt.

Navigating in Nuxt

useRoute() gives you the current route (read-only). For programmatic navigation — what Next.js's useRouter().push() does — Nuxt prefers navigateTo("/springs"), which works on the server and handles redirects cleanly. useRouter() exists if you need the raw router instance, but reach for navigateTo first.

Catch-all routes look different

In Next.js, a catch-all route is [...slug]/page.tsx. In Nuxt, it's [...slug].vue. Same concept, different syntax. We won't need one in this project, but it's a common gotcha when translating patterns.

Try It

Start the dev server if it's not already running:

pnpm dev
  1. Visit http://localhost:3000. Click "Browse Hot Springs." You should land on /springs
  2. Check the URL bar. Click the browser back button. You should return to / without a full page reload
  3. Visit http://localhost:3000/springs/breitenbush-hot-springs directly. You should see the detail page placeholder
  4. Visit http://localhost:3000/favorites and http://localhost:3000/visited. Both should render their placeholder content

All six routes should resolve without 404 errors.

Commit

git add -A && git commit -m "feat(routing): verify page routes and NuxtLink navigation"

Done-When

  • All six routes (/, /springs, /springs/:id, /favorites, /visited, /login) render without errors
  • Clicking NuxtLink navigates without full page reloads
  • You can explain when to use useRoute(), navigateTo(), and useRouter() in Nuxt
  • You can map any Next.js route file to its Nuxt equivalent

Solution

The starter already contains all the page files. The key files for routing are:

app/pages/
├── index.vue              → /
├── favorites.vue          → /favorites
├── visited.vue            → /visited
├── login.vue              → /login
└── springs/
    ├── index.vue          → /springs
    └── [id].vue           → /springs/:id
app/pages/index.vue
<template>
  <div>
    <section>
      <h1>
        Find your next soak
      </h1>
      <p>
        Discover hot springs around the world. Browse by region, temperature, or
        type. Save your favorites and track the ones you've visited.
      </p>
      <div>
        <NuxtLink to="/springs">
          Browse Hot Springs
        </NuxtLink>
      </div>
    </section>
  </div>
</template>
app/pages/springs/index.vue
<template>
  <div>
    <h1>
      Browse Hot Springs
    </h1>
    <p>
      Hot springs will be listed here once we wire up data fetching.
    </p>
  </div>
</template>
app/pages/springs/[id].vue
<template>
  <div>
    <h1>
      Spring Detail
    </h1>
    <p>
      Individual spring details will appear here once we build dynamic routes.
    </p>
  </div>
</template>