Modul 8: Nuxt Grundlagen

Dateibasiertes Routing

In Nuxt definieren Dateien deine Routen — keine zentrale routes.rb nötig. Wie Rails Resourcen, aber noch einfacher.

Dateien = Routen

Das Routing in Nuxt funktioniert nach einem simplen Prinzip: Jede .vue-Datei im pages/-Verzeichnis wird automatisch zu einer Route. Kein config/routes.rb, kein manuelles Registrieren.

pages/ → Routen-Mapping
pages/
├── index.vue           → /
├── about.vue           → /about
├── contact.vue         → /contact
└── blog/
    ├── index.vue       → /blog
    └── [slug].vue      → /blog/:slug
# Rails-Äquivalent in routes.rb:
# root 'pages#home'
# get '/about', to: 'pages#about'
# get '/contact', to: 'pages#contact'
# resources :blog, only: [:index, :show]
🛤️

Rails-Vergleich: config/routes.rb

In Rails definierst du Routen zentral in config/routes.rb: resources :posts erzeugt 7 Routen. In Nuxt erstellst du stattdessen Dateien. Der Vorteil: Du siehst sofort im Dateibaum, welche Routen existieren — keine Abstraktion dazwischen.

Dynamische Routen mit [id]

Eckige Klammern im Dateinamen erzeugen dynamische Segmente — genau wie :id in Rails-Routen.

pages/posts/[id].vue
<template>
  <article>
    <h1>Post #{{ route.params.id }}</h1>
    <p>Dynamischer Parameter aus der URL</p>
  </article>
</template>
<script setup lang="ts">
// useRoute() ist auto-importiert
const route = useRoute()
// route.params.id enthält den Wert aus der URL
// /posts/42 → route.params.id === '42'
// /posts/hello → route.params.id === 'hello'
</script>

Mehrere dynamische Segmente sind auch möglich:

Dynamische Segmente
pages/
├── posts/[id].vue              → /posts/:id
├── users/[userId]/posts.vue    → /users/:userId/posts
└── [category]/[id].vue         → /:category/:id
# Beispiele:
# /posts/42          → { id: '42' }
# /users/5/posts     → { userId: '5' }
# /technik/123       → { category: 'technik', id: '123' }
💡

Typ-Sicherheit

Der Parameter ist immer ein string. Für numerische IDs musst du selbst konvertieren: Number(route.params.id). In Rails macht das der Type-Cast in params[:id].to_i — selbes Prinzip.

Catch-all Routen mit [...slug]

Drei Punkte vor dem Parameter fangen alle verbleibenden Pfadsegmente ein. Perfekt für CMS-Seiten, Dokumentation oder verschachtelte Kategorien.

pages/docs/[...slug].vue
<template>
  <div>
    <h1>Dokumentation</h1>
    <p>Pfad: {{ route.params.slug }}</p>
    <!-- slug ist ein Array! -->
  </div>
</template>
<script setup lang="ts">
const route = useRoute()
// /docs/getting-started
// → route.params.slug = ['getting-started']
// /docs/api/composables/useFetch
// → route.params.slug = ['api', 'composables', 'useFetch']
</script>
Catch-all Beispiele
pages/docs/[...slug].vue
# Matcht:
# /docs/intro                    → slug: ['intro']
# /docs/guide/installation       → slug: ['guide', 'installation']
# /docs/api/ref                  → slug: ['api', 'ref']
# Vergleich mit Rails:
# get 'docs/*path', to: 'docs#show'
ℹ️

404-Seiten

Eine Datei pages/[...slug].vue im Root fängt alle nicht gematchten Routen — perfekt als Custom-404-Seite. In Rails wäre das die match '*path' Route am Ende der routes.rb.

Verschachtelte Routen

Unterordner erzeugen verschachtelte URL-Pfade. Für echte Layout-Verschachtelung (Nested Routes mit <NuxtPage />) brauchst du eine gleichnamige Vue-Datei neben dem Ordner.

Verschachteltes Routing
# Für verschachtelte Layouts:
pages/
├── users.vue           → Parent (enthält <NuxtPage />)
└── users/
    ├── index.vue       → /users (in Parent eingebettet)
    └── [id].vue        → /users/:id (in Parent eingebettet)
# users.vue rendert den Rahmen,
# die Dateien in users/ werden darin angezeigt.
pages/users.vue (Parent-Route)
<template>
  <div>
    <h1>Benutzerverwaltung</h1>
    <nav>
      <NuxtLink to="/users">Alle Benutzer</NuxtLink>
    </nav>
    <!-- Hier wird die Child-Route gerendert -->
    <NuxtPage />
  </div>
</template>
<!-- Wie in Rails:
  layouts/users.html.erb mit <%= yield %>
  Das Layout bleibt, der Inhalt wechselt.
-->
🛤️

Rails-Vergleich: Nested Resources

In Rails nutzt du resources :users do; resources :posts; end für verschachtelte Routen. In Nuxt ergibt sich die Verschachtelung aus der Ordnerstruktur. Der Parent (users.vue) mit <NuxtPage /> ist wie ein Layout, das yield enthält.

Navigation mit NuxtLink

Für interne Links verwendest du <NuxtLink> statt <a>. Das aktiviert clientseitiges Routing ohne vollständigen Seitenreload — wie Turbolinks in Rails, aber nativ.

Navigation mit NuxtLink
<template>
  <nav>
    <!-- Einfacher Link -->
    <NuxtLink to="/">Startseite</NuxtLink>
    <!-- Dynamische Route -->
    <NuxtLink :to="'/posts/' + postId">
      Zum Post
    </NuxtLink>
    <!-- Mit Objekt-Syntax -->
    <NuxtLink :to="{ path: '/users', query: { page: 2 } }">
      Benutzer (Seite 2)
    </NuxtLink>
    <!-- Externer Link (normaler <a> Tag) -->
    <NuxtLink to="https://nuxt.com" external>
      Nuxt Docs
    </NuxtLink>
    <!-- Aktiver Link bekommt automatisch CSS-Klassen -->
    <!-- .router-link-active & .router-link-exact-active -->
  </nav>
</template>
<script setup lang="ts">
const postId = ref(42)
</script>
💡

Prefetching

NuxtLink lädt automatisch die Zielseite vor, sobald der Link im Viewport sichtbar wird. Das macht die Navigation blitzschnell. In Rails brauchst du dafür Turbo Prefetch — in Nuxt ist es eingebaut.

Programmatische Navigation

Manchmal musst du per Code navigieren — nach einem Login, einer Formular-Submission oder einem API-Call. Nuxt bietet dafür navigateTo().

Programmatische Navigation
<template>
  <form @submit.prevent="handleLogin">
    <input v-model="email" type="email" placeholder="E-Mail" />
    <input v-model="password" type="password" placeholder="Passwort" />
    <button type="submit">Einloggen</button>
  </form>
</template>
<script setup lang="ts">
const email = ref('')
const password = ref('')
async function handleLogin() {
  try {
    await $fetch('/api/auth/login', {
      method: 'POST',
      body: { email: email.value, password: password.value }
    })
    // Wie redirect_to in Rails
    navigateTo('/dashboard')
    // Oder mit Replace (kein Browser-History Eintrag):
    // navigateTo('/dashboard', { replace: true })
    // Oder externe URL:
    // navigateTo('https://example.com', { external: true })
  } catch (error) {
    console.error('Login fehlgeschlagen:', error)
  }
}
</script>
🛤️

Rails-Vergleich: redirect_to

navigateTo('/dashboard') ist wie redirect_to dashboard_path in Rails. Der Unterschied: In Nuxt passiert die Navigation clientseitig ohne Server-Roundtrip. Für serverseitige Redirects in API-Routen gibt es sendRedirect(event, '/ziel').

Route Middleware — Ein Ausblick

Middleware wird vor der Navigation ausgeführt — wie before_action in Rails-Controllern. Perfekt für Auth-Checks, Logging oder Redirects.

middleware/auth.ts
// middleware/auth.ts
// Wie before_action :authenticate_user! in Rails
export default defineNuxtRouteMiddleware((to, from) => {
  const { isLoggedIn } = useAuth()
  if (!isLoggedIn.value) {
    // Redirect zum Login, merke Ziel-URL
    return navigateTo({
      path: '/login',
      query: { redirect: to.fullPath }
    })
  }
})
pages/dashboard.vue (geschützt)
<template>
  <div>
    <h1>Dashboard</h1>
    <p>Nur für eingeloggte Benutzer sichtbar.</p>
  </div>
</template>
<script setup lang="ts">
// Middleware zuweisen — wie before_action in Rails
  layout: 'app',
  middleware: 'auth'  // → middleware/auth.ts
})
</script>
ℹ️

Drei Arten von Middleware

Inline: Direkt in der Seite definiert.
Named: In middleware/ als Datei — wiederverwendbar.
Global: Dateiname mit .global Suffix — läuft bei jeder Navigation.

Komplettes Beispiel: Blog-Routing

So sieht ein typisches Blog-Routing in Nuxt aus — ohne eine einzige Zeile Router-Konfiguration:

Blog-Routing Struktur
pages/
├── blog/
│   ├── index.vue           → /blog (Liste aller Posts)
│   ├── [slug].vue          → /blog/:slug (Einzelner Post)
│   ├── kategorie/
│   │   └── [name].vue      → /blog/kategorie/:name
│   └── archiv/
│       └── [...date].vue   → /blog/archiv/2024/01
# Rails-Äquivalent:
# resources :blog, only: [:index, :show]
# get 'blog/kategorie/:name', to: 'blog#category'
# get 'blog/archiv/*date', to: 'blog#archive'
💡

Nächster Schritt

Jetzt kennst du das Routing-System. Als Nächstes schauen wir uns Layouts an — wie du Seitenstrukturen definierst, die mehrere Seiten teilen.