Skip to content

Kapitel 11: Praxisprojekt - Persönlicher Blog & TodoList

In diesem Kapitel bauen wir zwei kleine Praxisprojekte: einen persönlichen Blog und eine TodoList-App.


Projekt 1: Persönlicher Blog

11.1 Bedarfsanalyse & Projektinitialisierung

Funktionen:

  • ✅ Startseite mit Blog-Beiträgen
  • ✅ Einzelne Blog-Beiträge anzeigen
  • ✅ Über-uns-Seite
  • ✅ Suche & Filterung
  • ✅ Responsive Design

Projekt erstellen:

bash
npx nuxi@latest init mein-blog
cd mein-blog
pnpm install
pnpm dev

11.2 Routing & Seiten aufbauen

Verzeichnisstruktur:

pages/
├── index.vue           →  Startseite (Blog-Liste)
├── blog/
│   ├── index.vue      →  Blog-Übersicht
│   └── [slug].vue    →  Einzelner Beitrag
├── about.vue          →  Über uns
└── contact.vue        →  Kontakt

pages/index.vue (Startseite):

vue
<script setup>
// SEO-Meta
useSeoMeta({
  title: 'Mein Blog - Startseite',
  description: 'Willkommen auf meinem persönlichen Blog'
})

// Beiträge abrufen
const { data: posts } = await useFetch('/api/posts')
</script>

<template>
  <div class="home-page">
    <header class="hero">
      <h1>Willkommen auf meinem Blog</h1>
      <p>Entdecken Sie interessante Artikel über Webentwicklung</p>
    </header>

    <section class="posts-section">
      <h2>Neueste Beiträge</h2>
      <div class="posts-grid">
        <article v-for="post in posts" :key="post.id" class="post-card">
          <img :src="post.image" :alt="post.title" class="post-image" />
          <div class="post-content">
            <h3>{{ post.title }}</h3>
            <p>{{ post.excerpt }}</p>
            <NuxtLink :to="`/blog/${post.slug}`">Weiterlesen →</NuxtLink>
          </div>
        </article>
      </div>
    </section>
  </div>
</template>

<style scoped>
.hero {
  background: linear-gradient(135deg, #00dc82, #007aff);
  color: white;
  padding: 60px 20px;
  text-align: center;
  border-radius: 0 0 20px 20px;
}

.posts-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 30px;
  margin-top: 30px;
}

.post-card {
  border: 1px solid #ddd;
  border-radius: 8px;
  overflow: hidden;
  transition: transform 0.3s;
}

.post-card:hover {
  transform: translateY(-5px);
}

.post-image {
  width: 100%;
  height: 200px;
  object-fit: cover;
}

.post-content {
  padding: 15px;
}
</style>

11.3 Datenabfrage & Anzeige

API-Route erstellen (server/api/posts.ts):

typescript
// Server API (Mock-Daten)
export default defineEventHandler(() => {
  return [
    {
      id: 1,
      title: 'Einstieg in Nuxt 3',
      slug: 'einstieg-nuxt3',
      excerpt: 'Lernen Sie die Grundlagen von Nuxt 3...',
      image: '/images/nuxt3.jpg',
      content: 'Hier ist der vollständige Inhalt...',
      date: '2024-01-15'
    },
    {
      id: 2,
      title: 'Vue 3 Composition API',
      slug: 'vue3-composition-api',
      excerpt: 'Alles über die Composition API...',
      image: '/images/vue3.jpg',
      content: 'Hier ist der vollständige Inhalt...',
      date: '2024-01-20'
    }
  ]
})

pages/blog/[slug].vue (Einzelner Beitrag):

vue
<script setup>
const route = useRoute()
const { data: post } = await useFetch(`/api/posts/${route.params.slug}`)

// SEO für diesen Beitrag
useSeoMeta({
  title: post.value.title,
  description: post.value.excerpt,
  ogImage: post.value.image
})
</script>

<template>
  <div class="post-page">
    <img :src="post.image" :alt="post.title" class="post-header-image" />
    <article class="post-body">
      <h1>{{ post.title }}</h1>
      <time>{{ post.date }}</time>
      <div v-html="post.content"></div>
    </article>
  </div>
</template>

<style scoped>
.post-header-image {
  width: 100%;
  height: 400px;
  object-fit: cover;
}

.post-body {
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
}

time {
  color: #666;
  font-size: 0.9em;
}
</style>

11.4 Suchfunktion implementieren

components/SearchBar.vue:

vue
<script setup>
const searchQuery = ref('')
const router = useRouter()

const search = () => {
  if (searchQuery.value.trim()) {
    router.push({
      path: '/blog',
      query: { q: searchQuery.value }
    })
  }
}
</script>

<template>
  <div class="search-bar">
    <input
      v-model="searchQuery"
      type="text"
      placeholder="Suchen..."
      @keyup.enter="search"
    />
    <button @click="search">Suchen</button>
  </div>
</template>

<style scoped>
.search-bar {
  display: flex;
  gap: 10px;
  max-width: 500px;
  margin: 20px auto;
}

input {
  flex: 1;
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 4px;
}

button {
  padding: 10px 20px;
  background: #00dc82;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
</style>

11.5 SEO-Optimierung

Globale Meta-Konfiguration (app.vue):

vue
<script setup>
useHead({
  titleTemplate: '%s - Mein Blog',
  meta: [
    { name: 'description', content: 'Mein persönlicher Blog über Webentwicklung' },
    { name: 'viewport', content: 'width=device-width, initial-scale=1' }
  ],
  link: [
    { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
  ]
})
</script>

<template>
  <NuxtPage />
</template>

Projekt 2: TodoList App

11.6 Projekt aufbauen & Komponenten aufteilen

Funktionen:

  • ✅ Aufgaben hinzufügen
  • ✅ Aufgaben löschen
  • ✅ Aufgaben als erledigt markieren
  • ✅ LocalStorage für Persistenz

composables/useTodos.ts (Zustandsverwaltung):

typescript
export function useTodos() {
  // Zustand (persistent mit localStorage)
  const todos = useState('todos', () => {
    if (process.client) {
      const saved = localStorage.getItem('todos')
      return saved ? JSON.parse(saved) : []
    }
    return []
  })

  // Aufgabe hinzufügen
  const addTodo = (text: string) => {
    todos.value.push({
      id: Date.now(),
      text,
      completed: false
    })
    saveTodos()
  }

  // Aufgabe löschen
  const removeTodo = (id: number) => {
    todos.value = todos.value.filter((todo: any) => todo.id !== id)
    saveTodos()
  }

  // Aufgabe umschalten (erledigt/offen)
  const toggleTodo = (id: number) => {
    const todo = todos.value.find((t: any) => t.id === id)
    if (todo) {
      todo.completed = !todo.completed
      saveTodos()
    }
  }

  // Speichern in localStorage
  const saveTodos = () => {
    if (process.client) {
      localStorage.setItem('todos', JSON.stringify(todos.value))
    }
  }

  return {
    todos: readonly(todos),
    addTodo,
    removeTodo,
    toggleTodo
  }
}

pages/todos.vue (Hauptseite):

vue
<script setup>
definePageMeta({
  title: 'Meine Todo-Liste'
})

const { todos, addTodo, removeTodo, toggleTodo } = useTodos()
const newTodo = ref('')

const handleAdd = () => {
  if (newTodo.value.trim()) {
    addTodo(newTodo.value)
    newTodo.value = ''
  }
}
</script>

<template>
  <div class="todo-page">
    <h1>Meine Aufgaben</h1>

    <!-- Eingabe -->
    <div class="todo-input">
      <input
        v-model="newTodo"
        type="text"
        placeholder="Neue Aufgabe..."
        @keyup.enter="handleAdd"
      />
      <button @click="handleAdd">Hinzufügen</button>
    </div>

    <!-- Liste -->
    <ul class="todo-list">
      <li
        v-for="todo in todos"
        :key="todo.id"
        :class="{ completed: todo.completed }"
      >
        <input
          type="checkbox"
          :checked="todo.completed"
          @change="toggleTodo(todo.id)"
        />
        <span>{{ todo.text }}</span>
        <button @click="removeTodo(todo.id)">Löschen</button>
      </li>
    </ul>

    <!-- Statistik -->
    <div class="todo-stats">
      <p>{{ todos.filter(t => t.completed).length }} von {{ todos.length }} erledigt</p>
    </div>
  </div>
</template>

<style scoped>
.todo-page {
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
}

.todo-input {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
}

.todo-input input {
  flex: 1;
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 4px;
}

.todo-list {
  list-style: none;
  padding: 0;
}

.todo-list li {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px;
  border-bottom: 1px solid #eee;
}

.todo-list li.completed span {
  text-decoration: line-through;
  color: #999;
}

.todo-stats {
  margin-top: 20px;
  padding-top: 20px;
  border-top: 1px solid #eee;
  color: #666;
}
</style>

11.7 Zusammenfassung

In diesem Kapitel haben Sie gelernt:

  • ✅ Einen persönlichen Blog mit Nuxt 3 zu bauen
  • ✅ Routing & Seitenstruktur zu planen
  • ✅ Daten von API-Routen abzurufen
  • ✅ SEO mit useSeoMeta() zu optimieren
  • ✅ Eine TodoList-App mit useState() zu erstellen
  • ✅ LocalStorage für Persistenz zu verwenden

Nächste Schritte: Im nächsten Kapitel bauen wir ein fortgeschrittenes Praxisprojekt – eine Unternehmenswebsite!


📚 Weiterführende Ressourcen


Nächstes Kapitel: Kapitel 12: Praxisprojekt - Unternehmenswebsite →

Frei für alle Anfänger