Mein erster Blog

Problemstellung:
Der Plattform fehlt ein Content-Marketing-Kanal. Ein Blog ermöglicht es jedem Tenant, SEO-optimierte, mehrsprachige Artikel in Strapi CMS zu verfassen und in der SvelteKit-App auszuliefern – um organischen Traffic zu steigern, Autorität aufzubauen und die bestehende i18n-Pipeline mit 13 Sprachvarianten zu unterstützen.
Erfolgskriterien
- In Strapi verfasste Blogposts werden im Frontend im Branding des Tenants gerendert
- Posts werden über die bestehende Webhook-Pipeline automatisch in alle 13 Sprachen übersetzt (keine Änderungen an der Übersetzungs-Infrastruktur)
- Blogseiten ranken in Suchmaschinen (Structured Data, Sitemap, hreflang, og:article)
- Tenants ohne das Feature-Flag
blogsehen keine Blog-Routen (404)
User Stories
- Als Content-Autor möchte ich Blogbeiträge in Strapi mit umfangreichem Markdown, Bildern und Video-Einbettungen schreiben, damit ich Inhalte veröffentlichen kann, ohne Code anzufassen.
- Als Website-Besucher möchte ich Blogbeiträge nach Kategorien filtern können, damit ich relevante Inhalte finde.
- Als Website-Besucher möchte ich ein Inhaltsverzeichnis bei langen Beiträgen haben, damit ich schnell navigieren kann.
- Als Website-Besucher möchte ich Beiträge in meiner Sprache lesen, mit automatischer Übersetzung aus dem Englischen.
- Als Website-Besucher möchte ich Beiträge in sozialen Netzwerken teilen können.
- Als Website-Besucher möchte ich verwandte Beiträge entdecken und mehr über den Autor erfahren.
- Als SEO-Manager möchte ich, dass jeder Beitrag passende Meta-Tags, strukturierte Daten und Sitemap-Einträge hat.
Funktionale Anforderungen
Must Have (MVP)
- Strapi-Content-Types:
blog-posts,blog-authors,blog-categories— alle mit tenant-, locale- und inherit-Feldern - Blog-Übersichtsseite (
/blog) — paginierte Grid-Ansicht der Beiträge, Kategoriefilter über Query-Parameter - Beitragsdetailseite (
/blog/[slug]) — vollständiges Markdown-Rendering mit Inhaltsverzeichnis, Autorenkarte, verwandten Beiträgen, Social-Sharing - Kategorie-Seite (
/blog/category/[slug]) — gefilterte Liste, die dieselben Listing-Komponenten wiederverwendet - Autorenseite (
/blog/author/[slug]) — Autorenbio + deren Beiträge - Inhaltsverzeichnis — automatisch generiert aus h2/h3-Überschriften, sticky Sidebar auf Desktop, einklappbar auf Mobile, aktives Überschriften-Highlighting via Intersection Observer
- YouTube/Vimeo-Einbettungen — nackte URLs im Markdown werden automatisch in responsive iframes konvertiert (youtube-nocookie.com für Datenschutz)
- Lesezeit — serverseitig aus der Wortanzahl berechnet (Math.ceil(words / 200))
- Social-Share-Buttons — X, Facebook, LinkedIn, E-Mail, Link kopieren (Web Share API, wo verfügbar)
- Verwandte Beiträge — gleiche Kategorie, aktueller Beitrag ausgeschlossen, Limit 3, sortiert nach publishedAt absteigend
- RSS-Feed (
/blog/rss.xml) — RSS 2.0, Locale über Query-Parameter - SEO — SeoComponent pro Beitrag, og:article-Tags, BlogPosting JSON-LD-Schema, kanonische URLs
- Sitemap — dynamische Blog-Post-/Kategorie-/Autoren-URLs werden der bestehenden Sitemap mit hreflang hinzugefügt
- Feature Flag —
blog: booleanin TenantConfig.features, Layout-Guard gibt 404 zurück, wenn deaktiviert - i18n — Paraglide-Message-Keys für alle UI-Strings (Navigation, Labels, Pagination), Strapi-Content wird über bestehenden Webhook automatisch übersetzt
- Pagination — seitenbasiert mit Strapis nativer Pagination (9 Beiträge pro Seite)
- Featured Post — boolesches
featured-Flag auf blog-posts; Featured Post wird als Hero-Karte oben auf der/blog-Übersichtsseite angezeigt (größere Karte, volle Breite, über dem Grid)
Nice to Have (Post-MVP)
- Volltextsuche über alle Blogbeiträge
- Beitragsserien / mehrteilige Artikel
- Lesefortschrittsbalken
- Kommentare (vermutlich via Third-Party-Embed)
- Newsletter-Signup-CTA innerhalb der Beiträge
- View-Count- / Analytics-Integration
Technische Anforderungen
Datenmodell (Strapi Content Types)
blog-posts (api::blog-post.blog-post):
| Feld | Typ | Lokalisiert | Hinweise |
|---|---|---|---|
| title | Text (erforderlich) | Ja | Durch Übersetzungskonfiguration abgedeckt |
| slug | UID (aus title) | Nein | Von Übersetzung ausgeschlossen |
| excerpt | Text | Ja | Für Karten + Meta-Description |
| body | Rich Text (Markdown) | Ja | Gerendert über marked |
| featuredImage | Media (einzeln) | Nein | URL + alternativeText |
| category | Relation (manyToOne -> blog-category) | Nein | |
| author | Relation (manyToOne -> blog-author) | Nein | |
| featured | Boolean (Standard false) | Nein | Featured-Post wird als Hero-Karte im Listing angezeigt |
| seo | Component (shared.seo) | Ja | Vorhandene SeoComponent wiederverwenden |
| tenant | Text | Nein | Standard-Tenant-Filter |
| inherit | Boolean (Standard true) | Nein | Auto-Übersetzungs-Flag |
blog-authors (api::blog-author.blog-author):
| Feld | Typ | Lokalisiert | Hinweise |
|---|---|---|---|
| name | Text (erforderlich) | Ja | |
| slug | UID (aus name) | Nein | |
| bio | Rich Text (Markdown) | Ja | |
| avatar | Media (einzeln) | Nein | |
| socialLinks | JSON | Nein | { twitter?, linkedin?, website? } |
| tenant | Text | Nein | |
| inherit | Boolean (Standard true) | Nein |
blog-categories (api::blog-category.blog-category):
| Feld | Typ | Lokalisiert | Hinweise |
|---|---|---|---|
| name | Text (erforderlich) | Ja | |
| slug | UID (aus name) | Nein | |
| description | Text | Ja | |
| color | Text | Nein | Hex-Farbe für UI-Badges |
| tenant | Text | Nein | |
| inherit | Boolean (Standard true) | Nein |
TypeScript-Interfaces
Siehe src/lib/services/strapi-types.ts für BlogPost, BlogAuthor, BlogCategory.
Routen
Alle unter src/routes/(marketing)/blog/:
| Route | Server Load | Zweck |
|---|---|---|
blog/+layout.server.ts |
Feature-Guard + Laden der Kategorien | Gate + Navigationsdaten |
blog/+page.server.ts |
Paginierte Posts, optionaler ?category=-Filter |
Listing |
blog/[slug]/+page.server.ts |
Post nach slug + verwandte Posts + Berechnung der Lesezeit | Detail |
blog/category/[slug]/+page.server.ts |
Posts gefiltert nach Kategorie | Kategorie-Listing |
blog/author/[slug]/+page.server.ts |
Autor + seine/ihre Posts | Autorenseite |
blog/rss.xml/+server.ts |
RSS-2.0-XML, ?locale=-Parameter |
Feed |
UI-Komponenten
Alle in src/lib/components/blog/:
| Komponente | Props | Hinweise |
|---|---|---|
BlogPostCard.svelte |
post: BlogPost |
Karte mit Bild, Kategorie-Badge, Titel, Auszug, Autor, Datum, Lesezeit |
BlogPostContent.svelte |
body: string |
Markdown zu HTML über marked mit Video-Embed-Erweiterung + Heading-IDs |
TableOfContents.svelte |
entries: TocEntry[] |
Sticky Sidebar, Intersection Observer für aktiven Zustand, mobil einklappbar |
AuthorCard.svelte |
author: BlogAuthor |
Avatar, Name, Bio, Social Links |
SocialShare.svelte |
url: string, title: string |
Share-Buttons mit Web-Share-API-Fallback |
RelatedPosts.svelte |
posts: BlogPost[] |
Grid aus BlogPostCard |
BlogPagination.svelte |
pagination: StrapiPagination, basePath: string |
Vor/Zurück + Seitenzahlen |
Sicherheit & Authentifizierung
- Blog ist öffentlich (keine Authentifizierung erforderlich) — Marketing-Inhalte
- Strapi-API-Lesezugriff über vorhandenes
STRAPI_JWT-Token - Video-Einbettungen nutzen
youtube-nocookie.comfür DSGVO-Konformität
Randfälle & Fehlerbehandlung
- Beitrag nicht gefunden: 404 über SvelteKit
error() - Keine Beiträge in Kategorie: Leerzustand mit
blog_no_posts-Hinweis - Fehlendes Beitragsbild: Karte wird ohne Bild gerendert
- Übersetzung nicht verfügbar:
loadContentWithFallback()fällt auf Englisch zurück - RTL-Sprachen: Layout respektiert
dir="rtl"(ar, he, ur)
E2E-Testplan
Testdateien liegen in tests/blog/. Folge den bestehenden Mustern: parameterisiert über Viewports, Axe für Barrierefreiheit (a11y), networkidle wartet auf Strapi-Content.
Critical-Path-Tests
- Blog-Listing lädt —
/blogrendert ein Grid aus Post-Karten mit Titel, Auszug, Kategorie-Badge, Autor, Datum und Lesezeit. - Pagination funktioniert —
/blog?page=2zeigt die zweite Ergebnisseite; Zurück-/Weiter-Buttons navigieren korrekt; Seite 1 hat keinen „Zurück“-Button. - Kategorie-Filter — Klick auf einen Kategorie-Chip filtert Posts; URL aktualisiert sich zu
?category=<slug>; „Alle“-Chip entfernt den Filter. - Post-Detailansicht rendert —
/blog/[slug]zeigt Titel, Feature-Bild, formatierten Body, Autor-Card, Lesezeit und Veröffentlichungsdatum. - Table of Contents — Detailseite erzeugt ein Inhaltsverzeichnis (ToC) aus h2/h3-Überschriften; Klick auf einen ToC-Eintrag scrollt zur Überschrift.
- Verwandte Posts — Detailseite zeigt bis zu 3 verwandte Posts aus derselben Kategorie (ohne den aktuellen).
- Kategorie-Seite —
/blog/category/[slug]zeigt Kategorienamen, Beschreibung und ausschließlich Posts aus dieser Kategorie. - Autor-Seite —
/blog/author/[slug]zeigt Autorenbio, Avatar, Social Links und deren Posts. - Social-Share-Buttons — Share-Buttons sind in der Detailansicht sichtbar; „Link kopieren“ kopiert die URL und zeigt einen Bestätigungstext.
- RSS-Feed —
GET /blog/rss.xmlliefert gültiges XML mitContent-Type: application/rss+xmlund enthält Post-Einträge. - Navigation — Blog-Link im Site-Menü navigiert zu
/blog; Breadcrumbs zeigen die korrekte Hierarchie. - Featured Post —
/blogzeigt den hervorgehobenen Post als größere Hero-Karte oberhalb des regulären Grids.
Edge-Case- & Fehler-Tests
- Post nicht gefunden —
/blog/nonexistent-slugliefert eine 404-Seite. - Leere Kategorie — Eine Kategorie ohne Posts zeigt die leere Zustandsmeldung
blog_no_posts. - Fehlendes Feature-Bild — Post-Karte rendert sauber ohne Bild (kein kaputtes
<img>). - Fehlender Autor/Kategorie — Post ohne Autor oder Kategorie rendert ohne diese Elemente (kein Crash).
- Feature Flag deaktiviert — Wenn
blog: false, liefert/blogeinen 404.
Querschnitts-Tests
- i18n: Locale-Switch — Zu
/fr/blognavigieren, prüfen, dass UI-Texte (Seitentitel, „Weiterlesen“, Pagination-Labels) auf Französisch sind; Post-Content fällt auf Englisch zurück, wenn nicht übersetzt. - i18n: RTL-Layout — Zu
/ar/blogund/ar/blog/[slug]navigieren, prüfen, dassdir="rtl"gesetzt ist und das Layout korrekt rendert (ToC auf der richtigen Seite). - SEO: Meta-Tags — Detailseite eines Posts hat
og:type=article,og:title,og:description,article:published_timeund einen Canonical-Link. - SEO: JSON-LD — Detailseite eines Posts enthält ein
<script type="application/ld+json">mit@type: BlogPosting, Headline, Autor, datePublished. - SEO: Sitemap —
/sitemap.xmlenthält Blog-Post-URLs mit hreflang-Alternativen. - Accessibility — Axe-Audits auf
/blog,/blog/[slug],/blog/author/[slug]bei mobilen und Desktop-Viewports ausführen (folgt dem bestehenden a11y-Testmuster). - Tastaturnavigation — ToC-Einträge und Pagination-Buttons sind per Tastatur fokussier- und aktivierbar; Fokus-Indikatoren sind sichtbar.
Nicht im Umfang
- Blog-Kommentare
- Volltextsuche
- Newsletter-Integration innerhalb von Beiträgen
- Analytics/Ansichtszahlen
- Benutzerdefinierte Beitragstemplates pro Tenant
Ludo ist Lehrer und Unternehmer im Technologiebereich, angetrieben von einem einfachen Glauben: Hochwertige Bildung sollte für alle zugänglich sein.