#lessonlearned – Server-Middleware-Pipeline: Storyblok-Daten für SSR und SEO vorladen

Bei serverseitig gerenderten Websites entscheidet der Zeitpunkt der Datenbeschaffung oft direkt über Performance und Sichtbarkeit in Suchmaschinen. Werden Inhalte oder Relationen zu spät geladen, kann das sowohl die Ladezeit als auch die Qualität der ausgelieferten SEO-Daten verschlechtern. In diesem Lesson Learned zeigen wir, wie wir Storyblok-Daten in einer priorisierten Middleware-Pipeline bereits vor dem eigentlichen Rendering laden, damit Seiten vollständig und suchmaschinenfreundlich ausgeliefert werden können.

Textbegleitende Grafik zum Thema "Server-Middleware-Pipeline für SSR und SEO"

Warum der Zeitpunkt der Datenbeschaffung so wichtig ist

In unserem Projekt bestanden viele Seiten nicht nur aus einer einzelnen Story, sondern aus verschachtelten und verknüpften Inhalten. Ein News-Artikel konnte zum Beispiel zusätzlich Kategorien, Technologien, Kontaktpersonen oder verwandte Beiträge referenzieren.

Würden solche Informationen erst im Client nachgeladen, hätte das mehrere Nachteile:

  • Die Seite wäre beim ersten Rendern unvollständig

  • Suchmaschinen würden nicht alle Inhalte direkt sehen

  • es entstünde leicht ein Wasserfall aus aufeinanderfolgenden Anfragen

Unser Ziel war deshalb, alle relevanten Inhalte bereits auf dem Server zu laden, bevor die Seite gerendert wird.

Unser Ansatz: eine priorisierte Middleware-Kette

Dafür haben wir die Datenbeschaffung in mehrere Server-Middlewares aufgeteilt und ihre Reihenfolge bewusst über Dateinamen mit numerischen Präfixen gesteuert.

Die Struktur sah vereinfacht so aus:

server/middleware/
  00.pageData.ts
  01.newsData.ts
  01.jobData.ts
  10.checkDomainAuth.ts
  20.basicAuth.ts

Die früh ausgeführten Middlewares kümmern sich um die inhaltlichen Daten. Später folgende Middlewares übernehmen zusätzliche Aufgaben wie Domain-Prüfungen oder Zugriffsschutz.

Dieser Aufbau hat für uns den Vorteil, dass die Zuständigkeiten sauber getrennt bleiben und die Reihenfolge der Ausführung klar nachvollziehbar ist.

Seiten-Storys bereits früh laden

Die erste Middleware lädt in unserem Fall grundlegende Storyblok-Inhalte für reguläre Seiten. Dabei werden bestimmte dynamische Pfade bewusst ausgeschlossen, weil sie von spezialisierten Middlewares verarbeitet werden.

Ein vereinfachter Ausschnitt:

// server/middleware/00.pageData.ts
export default defineEventHandler(async (event) => {
  const urlPath = new URL(fullUrl).pathname

  const dynamicPattern = /^\/[^/]+\/(news|events|find-your-job|stellenangebote)\/[^/]+$/
  if (dynamicPattern.test(urlPath)) return

  const rootSlug = getRootFolderSlugByDomain(hostname)

  const response = await storyblokApi.get('cdn/stories/' + rootSlug + '/' + urlPath, {
    version: config.storyblokVersion,
    resolve_links: 'url'
  })

  event.context.currentPageStory = response?.data?.story
})

ts

Diese Middleware sorgt dafür, dass die aktuelle Seiten-Story bereits im Request-Kontext verfügbar ist, bevor die eigentliche Seitenausgabe beginnt.

Spezialisierte Middlewares für komplexere Inhalte

Für bestimmte Inhaltstypen wie News oder Jobs haben wir zusätzliche Middlewares aufgebaut, die gezielt weitere Relationen auflösen.

Ein Beispiel aus der News-Verarbeitung:

const resolveRelations = [
  'News.websiteRestriction',
  'News.technologies',
  'News.contact',
  'News.category',
  'News.areas',
  'News.relatedNews',
  'ContactPosition.contact'
]

const response = await storyblokApi.get('cdn/stories/' + recordUrl, {
  version: config.storyblokVersion,
  resolve_relations: resolveRelations,
  language: currentLocale,
  resolve_links: 'url'
})

event.context.currentStory = response?.data?.story

ts

Gerade bei redaktionellen Inhaltstypen mit vielen Beziehungen war das für uns wichtig. So konnten alle benötigten Daten gesammelt auf dem Server bereitgestellt werden, statt sie später stückweise nachzuladen.

Die Daten im Rendering direkt nutzen

Sobald die Middlewares ihre Daten geladen haben, stehen diese im Request-Kontext zur Verfügung und können in der eigentlichen Seite direkt verwendet werden.

Ein vereinfachtes Beispiel:

const event = useRequestEvent()
const story = event?.context?.currentPageStory ?? null

useHead({
  title: story?.content?.seoTitle || story?.name,
  meta: [
    { property: 'og:title', content: story?.content?.seoTitle },
    { property: 'og:description', content: story?.content?.seoDescription },
    { property: 'og:image', content: story?.content?.seoImage?.filename }
  ]
})

ts

Dadurch stehen Meta-Titel, Beschreibungen oder Open-Graph-Bilder bereits beim ersten Rendern zur Verfügung. Das ist sowohl für die Benutzerwahrnehmung als auch für die Suchmaschinenoptimierung ein großer Vorteil.

Fehler pro Middleware isoliert behandeln

Ein weiterer wichtiger Punkt in unserem Ansatz war die Fehlerbehandlung. Nicht jede fehlgeschlagene Datenabfrage soll den gesamten Request unbrauchbar machen. Deshalb fängt jede Middleware ihre Fehler selbst ab und protokolliert sie gezielt.

Ein vereinfachter Ausschnitt:

try {
  const response = await storyblokApi.get(...)
  event.context.currentPageStory = response?.data?.story
} catch (error) {
  event.context.currentPageStory = null
  Sentry.captureException(error, {
    tags: { source: 'page-data-middleware-story-fetch' }
  })
}

ts

So bleiben Probleme besser eingrenzbar, und gleichzeitig kann die Anwendung kontrollierter auf Teilfehler reagieren.

Warum die Trennung nach Inhaltstypen hilfreich war

Ein wichtiger Punkt in unserem Projekt war, dass nicht jede Story dieselben Anforderungen mitbringt. Eine statische Inhaltsseite braucht andere Relationen als ein News-Artikel oder ein Jobangebot.

Deshalb war es für uns sinnvoll, die Datenlogik nicht in einer zentralen „Alles-Middleware“ zu bündeln, sondern pro Inhaltstyp gezielt aufzuteilen. Das hält die Middlewares verständlicher und verhindert unnötige Abfragen.

Was sich für uns bewährt hat

Im Projekt hat sich vor allem gezeigt, dass SSR und SEO eng mit der Architektur der Datenbeschaffung zusammenhängen.

Früh laden statt später nachholen

Je früher relevante Inhalte und Relationen verfügbar sind, desto vollständiger und stabiler kann die Seite gerendert werden.

Reihenfolge explizit festlegen

Die Priorisierung über Dateinamen war für uns ein einfacher und nachvollziehbarer Weg, die Middleware-Ausführung klar zu steuern.

Inhaltstypen getrennt behandeln

Spezialisierte Middlewares haben sich bewährt, weil sie pro Seitentyp nur genau die Daten laden, die dort auch wirklich gebraucht werden.

Was wir daraus gelernt haben

Unser wichtigstes Learning war, dass eine gute SSR- und SEO-Qualität nicht erst in der Seitenausgabe entsteht, sondern bereits bei der Architektur der serverseitigen Datenbeschaffung.

Mit einer priorisierten Middleware-Pipeline konnten wir Storyblok-Inhalte und Relationen frühzeitig laden und so sicherstellen, dass Seiten bereits beim ersten Request vollständig gerendert werden. Das verbessert die technische Performance, stärkt die Sichtbarkeit in Suchmaschinen und macht die Datenflüsse innerhalb der Anwendung klarer nachvollziehbar.