Skip to content

Kapitel 11: Praxisprojekt - Persönlicher Blog

📖 Lernziele

In diesem Kapitel lernen Sie:

  • ✅ Einen persönlichen Blog mit Next.js 14 erstellen
  • ✅ App Router für Routing verwenden
  • ✅ Daten mit Server Components abfragen
  • ✅ Suche und Filterung implementieren
  • ✅ Metadaten für SEO konfigurieren
  • ✅ Responsive Design mit Tailwind CSS

11.1 Anforderungsanalyse & Projektinitialisierung

🎯 Projektziel

Einen funktionalen persönlichen Blog mit folgenden Funktionen:

  • ✅ Startseite mit Blog-Liste
  • ✅ Detailseite für jeden Blog-Artikel
  • ✅ Suche nach Artikeln
  • ✅ Kategorien-Filter
  • ✅ SEO-optimierte Metadaten
  • ✅ Responsive Design

📦 Projekt erstellen

Schritt 1: Projekt initialisieren

bash
npx create-next-app@latest mein-blog

Konfiguration:

✔ Would you like to use TypeScript? … Yes
✔ Would you like to use ESLint? … Yes
✔ Would you like to use Tailwind CSS? … Yes
✔ Would you like to use `src/` directory? … No
✔ Would you like to use App Router? (recommended) … Yes
✔ Would you like to customize the default import alias? … No

Schritt 2: Projekt starten

bash
cd mein-blog
pnpm dev

11.2 Routing & Layout aufbauen

📂 Verzeichnisstruktur

app/
├── layout.js               # Globales Layout
├── page.js                 # Startseite (/)
├── blog/
│   ├── page.js            # Blog-Liste (/blog)
│   └── [slug]/
│       └── page.js        # Blog-Detail (/blog/[slug])
├── über/
│   └── page.js            # Über mich Seite (/über)
└── globals.css            # Globales CSS

🎨 Globales Layout (app/layout.js)

jsx
import './globals.css';
import Link from 'next/link';

export const metadata = {
  title: 'Mein Blog | Persönliche Webseite',
  description: 'Ein Blog über Webentwicklung und Technologie',
};

export default function RootLayout({ children }) {
  return (
    <html lang="de">
        <body>
          <header className="bg-white shadow">
            <nav className="container mx-auto px-4 py-6">
              <div className="flex justify-between items-center">
                <Link href="/" className="text-2xl font-bold text-gray-800">
                  Mein Blog
                </Link>
                <div className="space-x-4">
                  <Link href="/" className="text-gray-600 hover:text-gray-900">
                    Startseite
                  </Link>
                  <Link href="/blog" className="text-gray-600 hover:text-gray-900">
                    Blog
                  </Link>
                  <Link href="/über" className="text-gray-600 hover:text-gray-900">
                    Über mich
                  </Link>
                </div>
              </div>
            </nav>
          </header>
          
          <main className="container mx-auto px-4 py-8">
            {children}
          </main>
          
          <footer className="bg-gray-800 text-white py-8">
            <div className="container mx-auto px-4 text-center">
              <p>&copy; 2024 Mein Blog. Alle Rechte vorbehalten.</p>
            </div>
          </footer>
        </body>
      </html>
  );
}

11.3 Datenabfrage & Anzeige (Server Components)

📝 Blog-Daten (Mock-Daten)

Datei: lib/posts.js

javascript
export const posts = [
  {
    slug: 'erster-blog-artikel',
    title: 'Mein erster Blog-Artikel',
    excerpt: 'Dies ist eine Zusammenfassung meines ersten Blog-Artikels...',
    content: 'Hier ist der vollständige Inhalt des Artikels...',
    date: '2024-01-15',
    category: 'Webentwicklung',
    image: '/images/blog1.jpg',
  },
  {
    slug: 'nextjs-tutorial',
    title: 'Next.js 14 Tutorial für Anfänger',
    excerpt: 'Lernen Sie Next.js 14 von Grund auf...',
    content: 'Next.js 14 ist ein fantastisches Framework...',
    date: '2024-02-20',
    category: 'Tutorials',
    image: '/images/blog2.jpg',
  },
  {
    slug: 'tailwind-css-tipps',
    title: 'Tailwind CSS Tipps & Tricks',
    excerpt: 'Verbessern Sie Ihren Workflow mit Tailwind CSS...',
    content: 'Tailwind CSS ist ein utility-first CSS-Framework...',
    date: '2024-03-10',
    category: 'Webentwicklung',
    image: '/images/blog3.jpg',
  },
];

export function getPosts() {
  return posts.sort((a, b) => new Date(b.date) - new Date(a.date));
}

export function getPostBySlug(slug) {
  return posts.find((post) => post.slug === slug);
}

export function getCategories() {
  return [...new Set(posts.map((post) => post.category))];
}

🏠 Startseite (app/page.js)

jsx
import Link from 'next/link';
import { getPosts } from '@/lib/posts';

export default function HomePage() {
  const posts = getPosts();
  const latestPosts = posts.slice(0, 3);
  
  return (
    <div>
      {/* Hero-Sektion */}
      <section className="bg-gradient-to-r from-blue-500 to-purple-600 text-white py-20 mb-12 rounded-lg">
        <div className="text-center">
          <h1 className="text-5xl font-bold mb-4">Willkommen auf meinem Blog</h1>
          <p className="text-xl mb-8">Entdecken Sie Artikel über Webentwicklung und Technologie</p>
          <Link
            href="/blog"
            className="bg-white text-blue-600 px-6 py-3 rounded-lg font-semibold hover:bg-gray-100"
          >
            Artikel ansehen
          </Link>
        </div>
      </section>
      
      {/* Neueste Artikel */}
      <section>
        <h2 className="text-3xl font-bold mb-8">Neueste Artikel</h2>
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
          {latestPosts.map((post) => (
            <article key={post.slug} className="border rounded-lg overflow-hidden shadow-lg hover:shadow-xl transition">
              <img src={post.image} alt={post.title} className="w-full h-48 object-cover" />
              <div className="p-6">
                <div className="text-sm text-gray-500 mb-2">{post.date}</div>
                <h3 className="text-xl font-semibold mb-2">
                  <Link href={`/blog/${post.slug}`} className="hover:text-blue-600">
                    {post.title}
                  </Link>
                </h3>
                <p className="text-gray-600">{post.excerpt}</p>
              </div>
            </article>
          ))}
        </div>
      </section>
    </div>
  );
}

11.4 Blog-Liste & Suche

📄 Blog-Liste (app/blog/page.js)

jsx
import Link from 'next/link';
import { getPosts, getCategories } from '@/lib/posts';

export default function BlogPage({ searchParams }) {
  const posts = getPosts();
  const categories = getCategories();
  
  const selectedCategory = searchParams?.category;
  const searchQuery = searchParams?.q?.toLowerCase() || '';
  
  // Filterung
  let filteredPosts = posts;
  
  if (selectedCategory) {
    filteredPosts = filteredPosts.filter((post) => post.category === selectedCategory);
  }
  
  if (searchQuery) {
    filteredPosts = filteredPosts.filter(
      (post) =>
        post.title.toLowerCase().includes(searchQuery) ||
        post.excerpt.toLowerCase().includes(searchQuery)
    );
  }
  
  return (
    <div>
      <h1 className="text-4xl font-bold mb-8">Blog-Artikel</h1>
      
      {/* Such- und Filterformular */}
      <form className="mb-8 space-y-4 md:space-y-0 md:flex md:space-x-4">
        <input
          type="text"
          name="q"
          placeholder="Suchen..."
          defaultValue={searchQuery}
          className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
        />
        <select
          name="category"
          defaultValue={selectedCategory || ''}
          className="px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
        >
          <option value="">Alle Kategorien</option>
          {categories.map((cat) => (
            <option key={cat} value={cat}>
              {cat}
            </option>
          ))}
        </select>
        <button
          type="submit"
          className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
        >
          Filtern
        </button>
      </form>
      
      {/* Artikelliste */}
      <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
        {filteredPosts.map((post) => (
          <article key={post.slug} className="border rounded-lg overflow-hidden shadow hover:shadow-lg transition">
            <img src={post.image} alt={post.title} className="w-full h-48 object-cover" />
            <div className="p-6">
              <div className="flex items-center justify-between mb-2">
                <span className="text-sm text-gray-500">{post.date}</span>
                <span className="text-sm bg-blue-100 text-blue-800 px-2 py-1 rounded">
                  {post.category}
                </span>
              </div>
              <h2 className="text-2xl font-semibold mb-2">
                <Link href={`/blog/${post.slug}`} className="hover:text-blue-600">
                  {post.title}
                </Link>
              </h2>
              <p className="text-gray-600">{post.excerpt}</p>
            </div>
          </article>
        ))}
      </div>
      
      {filteredPosts.length === 0 && (
        <p className="text-center text-gray-500 py-12">Keine Artikel gefunden.</p>
      )}
    </div>
  );
}

11.5 Blog-Detailseite

📄 Detailseite (app/blog/[slug]/page.js)

jsx
import { getPostBySlug, getPosts } from '@/lib/posts';
import { notFound } from 'next/navigation';

export default function BlogPost({ params }) {
  const post = getPostBySlug(params.slug);
  
  if (!post) {
    notFound();
  }
  
  return (
    <article className="max-w-4xl mx-auto">
      <img src={post.image} alt={post.title} className="w-full h-64 object-cover rounded-lg mb-8" />
      <div className="mb-4">
        <span className="text-sm text-gray-500">{post.date}</span>
        <span className="mx-2">•</span>
        <span className="text-sm bg-blue-100 text-blue-800 px-2 py-1 rounded">
          {post.category}
        </span>
      </div>
      <h1 className="text-4xl font-bold mb-6">{post.title}</h1>
      <div className="prose max-w-none">
        <p className="text-lg leading-relaxed">{post.content}</p>
      </div>
    </article>
  );
}

// Metadaten für SEO
export async function generateMetadata({ params }) {
  const post = getPostBySlug(params.slug);
  
  if (!post) {
    return {};
  }
  
  return {
    title: `${post.title} | Mein Blog`,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [post.image],
    },
  };
}

// Statische Generierung für bessere Performance
export async function generateStaticParams() {
  const posts = getPosts();
  
  return posts.map((post) => ({
    slug: post.slug,
  }));
}

11.6 SEO-Optimierung

🔍 Metadaten-Konfiguration

In app/layout.js (global):

jsx
export const metadata = {
  title: {
    default: 'Mein Blog',
    template: '%s | Mein Blog',
  },
  description: 'Ein Blog über Webentwicklung und Technologie',
  robots: {
    index: true,
    follow: true,
  },
};

In app/blog/[slug]/page.js (pro Artikel):

jsx
export async function generateMetadata({ params }) {
  const post = getPostBySlug(params.slug);
  
  if (!post) {
    return {};
  }
  
  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      type: 'article',
      publishedTime: post.date,
    },
  };
}

11.7 Responsive Design mit Tailwind CSS

📱 Responsive Anpassungen

Grid-Layout (responsive):

jsx
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
  {/* Artikel-Karten */}
</div>

Navigation (mobile-optimiert):

jsx
{/* In layout.js */}
<header className="bg-white shadow">
  <nav className="container mx-auto px-4 py-6">
    <div className="flex justify-between items-center">
      <Link href="/" className="text-2xl font-bold">
        Mein Blog
      </Link>
      {/* Mobile Menu Button (optional mit useState) */}
      <div className="hidden md:flex space-x-4">
        <Link href="/">Startseite</Link>
        <Link href="/blog">Blog</Link>
        <Link href="/über">Über mich</Link>
      </div>
    </div>
  </nav>
</header>

11.8 Code-Erklärung & Optimierung

✅ Best Practices

AspektEmpfehlung
Server ComponentsStandardmäßig verwenden (bessere Performance)
MetadatengenerateMetadata() für SEO verwenden
Bildernext/image Komponente für Optimierung
Statische GenerierunggenerateStaticParams() für bessere Performance
Tailwind CSSUtility-Classes für schnelles Styling

🚀 Performance-Optimierung

Bildoptimierung:

jsx
import Image from 'next/image';

<Image
  src={post.image}
  alt={post.title}
  width={800}
  height={400}
  className="w-full h-48 object-cover"
/>

Lazy Loading:

jsx
// Next.js lädt Komponenten automatisch lazy mit dynamic imports
import dynamic from 'next/dynamic';

const HeavyComponent = dynamic(() => import('@/components/HeavyComponent'));

📝 Zusammenfassung

In diesem Kapitel haben Sie gelernt:

KonzeptErklärung
ProjektsetupNext.js 14 mit App Router und Tailwind CSS
RoutingDateisystem-basiertes Routing (app/, app/blog/[slug])
Server ComponentsDatenabfrage in Server Components
Suche & FilterungSuch- und Filterfunktionen implementieren
SEOMetadaten mit generateMetadata() optimieren
Responsive DesignTailwind CSS für mobile Optimierung

✅ Nächste Schritte

  1. Übung: Fügen Sie Kommentare zu Blog-Artikeln hinzu
  2. Übung: Implementieren Sie Paginierung für die Blog-Liste
  3. Weiter geht's: Kapitel 12 - Praxisprojekt: E-Commerce (Fortgeschritten)

🎯 Selbsttest

Frage 1: Wie erstellt man eine dynamische Route im App Router?

Antwort anzeigen Durch Erstellen eines Ordners mit eckigen Klammern: `app/blog/[slug]/page.js` erstellt die Route `/blog/[slug]`.

Frage 2: Was ist der Vorteil von Server Components bei der Datenabfrage?

Antwort anzeigen Daten werden auf dem Server abgerufen, was bessere SEO und schnellere Ladezeiten ermöglicht. Kein Client-seitiges JavaScript nötig.

Frage 3: Wie optimiert man Bilder in Next.js?

Antwort anzeigen Verwenden Sie die `next/image` Komponente (`import Image from 'next/image'`). Sie bietet automatische Optimierung, Lazy Loading und WebP-Konvertierung.

🚀 Weiter zu Kapitel 12: Praxisprojekt - E-Commerce (Fortgeschritten)

Frei für alle Anfänger