Appearance
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 dev11.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 → Kontaktpages/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 →
