#lessonlearned – Storyblok-Inhalte in Echtzeit mit Solr synchronisieren

Wenn Inhalte in einem CMS veröffentlicht werden, sollen sie idealerweise nicht erst mit großer Verzögerung in der Suche auftauchen. Gerade bei News, Events oder neuen Seiten ist es wichtig, dass veröffentlichte Inhalte zeitnah indexiert werden und in den Suchergebnissen erscheinen. In diesem Lesson Learned zeigen wir, wie wir Storyblok und Solr über Webhooks miteinander verbunden haben, um Inhalte automatisiert und in Echtzeit zu synchronisieren.

Begleitende Grafik zum Text "Storyblok-Inhalte mit Solr synchronisieren"

Warum eine automatische Synchronisierung wichtig ist

In unserem Projekt kam Hosted Solr als Suchbackend zum Einsatz – verteilt auf mehrere sprachspezifische Instanzen. Gleichzeitig wurden Inhalte in Storyblok redaktionell gepflegt und veröffentlicht.

Die Herausforderung dabei war, dass nicht jeder Inhaltstyp auf dieselbe Weise verarbeitet werden kann. Seiten, News, Events oder Blog-Beiträge unterscheiden sich in ihrer Struktur, in ihren Relationen und in der Art, wie sie in den Suchindex überführt werden müssen.

Eine einfache Einheitslogik wäre hier schnell unübersichtlich geworden. Deshalb brauchten wir einen Ansatz, der unterschiedliche Content-Typen gezielt behandeln kann, ohne dass für jede Erweiterung große Umbauten notwendig sind.

Unser Ansatz: Auswahl des passenden Indexers über eine zentrale Factory

Damit je nach Inhalt automatisch die passende Indexierungslogik verwendet wird, haben wir eine zentrale Factory aufgebaut. Sie lädt zunächst die betroffene Story aus Storyblok, erkennt den Content-Typ und wählt auf dieser Basis den richtigen Indexer und den passenden Data-Handler aus.

Ein vereinfachter Ausschnitt:

class StoryblokSolrIndexerFactory {
  async getIndexerByData(data) {
    const story = await this.getStoryById(data.story_id)
    const contentType = story?.content.component

    const config = Object.values(indexerConfiguration).find(
      (cfg) => cfg.storyblokContentType === contentType
    )
    if (!config) return null

    if (config.indexerConfiguration?.websites) {
      if (!this.slugInWebsites(story.full_slug, config.indexerConfiguration.websites)) {
        return null
      }
    }

    const IndexerClass = indexers[config.indexer]
    const DataHandlerClass = dataHandlers[config.dataHandler]

    return new IndexerClass({}, new DataHandlerClass(), config, this.languages, this.env)
  }
}

JavaScript

Für uns hatte dieser Ansatz einen großen Vorteil: Die Entscheidung, wie ein Inhalt behandelt werden soll, liegt nicht verteilt an vielen Stellen im Code, sondern an einer zentralen Stelle.

Content-Typen über Konfiguration statt Sonderlogik abbilden

Damit neue Inhaltstypen leicht ergänzt werden können, erfolgt die Zuordnung nicht direkt im Code über viele Fallunterscheidungen, sondern über eine Konfiguration.

Ein vereinfachtes Beispiel:

const configuration = {
  page: {
    storyblokContentType: 'Page',
    indexer: 'PageIndexer',
    dataHandler: 'PageDataHandler',
    indexerConfiguration: {
      websites: {
        'corporate-page-global': { languages: ['de', 'en'] },
        'country-page-fr': { languages: ['en', 'fr'] },
        'country-page-de': { languages: ['en', 'de'] }
      }
    }
  },
  news: {
    storyblokContentType: 'News',
    indexer: 'RecordsIndexer',
    dataHandler: 'RecordsDataHandler',
    dataHandlerConfiguration: {
      fields: {
        url: 'news/{slug}',
        title: { field: 'content.title', processor: 'extractText' },
        date_dateS: { field: 'content.date', processor: 'formatDate' }
      }
    }
  }
}

JavaScript

Das hat sich für uns besonders bewährt, weil neue Anforderungen dadurch wesentlich einfacher ergänzt werden können. Statt bestehende Logik immer weiter auszubauen, lässt sich das Verhalten neuer Inhaltstypen über die Konfiguration beschreiben.

Storyblok-Daten in Solr-Dokumente umwandeln

Ein zentraler Bestandteil der Verarbeitung ist die Transformation der Storyblok-Inhalte in ein Format, das Solr indexieren kann. Diese Aufgabe übernimmt jeweils der passende Data-Handler.

Ein vereinfachter Ausschnitt aus der Seitenverarbeitung:

prepareSolrDoc(story, language, configuration) {
  const textStrings = this.findAllTextValues(story.content, ['text'])
  const titleStrings = this.findAllTextValues(story.content, ['name', 'title'])

  const imageUuid =
    this.findAdmiralCloudImageUuid(story?.content?.stage) ||
    this.findAdmiralCloudImageUuid(story?.content?.searchResultImage)

  return {
    id: story.id,
    type: 'storyblokPage',
    url: story.full_slug,
    title: story.name,
    content: [...textStrings, ...titleStrings].join(' '),
    admiralCloudImageUuid_stringS: imageUuid ?? '',
    websites_stringM: [this.getWebsiteFromFullSlug(story.full_slug, language)],
    language_stringS: language
  }
}

JavaScript

Dabei wird Text auch aus verschachtelten Inhaltsblöcken rekursiv ausgelesen. Das war für uns besonders wichtig, weil viele Storyblok-Inhalte nicht flach aufgebaut sind, sondern aus mehreren Ebenen und Komponenten bestehen.

Zusätzlich wird berücksichtigt, ob bestimmte Blöcke als verborgen markiert sind. Solche Inhalte werden nicht indexiert.

Auch vollständige Neuaufbauten des Index unterstützen

Neben der Verarbeitung einzelner Veröffentlichungen sollte das System auch vollständige Neuindexierungen unterstützen. Dafür iteriert der Indexer über alle konfigurierten Websites und Sprachen, lädt die Inhalte seitenweise und übergibt sie gesammelt an Solr.

Ein vereinfachter Ausschnitt:

async indexAll() {
  for (const [website, config] of Object.entries(websites)) {
    for (const language of config.languages) {
      const firstPage = await this.fetchStories(1, `${website}/${language}`)
      const totalPages = Math.ceil(parseInt(firstPage.headers.total) / perPage)

      const responses = await Promise.all(
        Array.from({ length: totalPages }, (_, i) =>
          this.fetchStories(i + 1, `${website}/${language}`)
        )
      )

      const docs = responses.flatMap((r) =>
        r.data.stories.map((story) =>
          this.dataHandler.prepareSolrDoc(story, language, config)
        )
      )

      if (docs.length > 0) {
        await this.doSolrRequest(docs)
      }
    }
  }
}

JavaScript

Gerade für Initialimporte, Umstellungen oder größere Bereinigungen war diese Möglichkeit für uns sehr wertvoll.

Storyblok-Webhooks als Auslöser verwenden

Die Synchronisierung wird durch Webhooks aus Storyblok angestoßen. Je nach Ereignis wird dann eine andere Aktion im Suchindex ausgeführt.

Zum Beispiel:

  • veröffentlichte oder verschobene Inhalte werden neu indexiert oder aktualisiert

  • gelöschte oder zurückgezogene Inhalte werden aus dem Index entfernt

  • über technische Aktionen kann ein kompletter Inhaltstyp oder der gesamte Index neu aufgebaut werden

So bleibt die Suche eng mit den redaktionellen Prozessen gekoppelt.

Was sich für uns bewährt hat

Im Projekt hat sich vor allem gezeigt, dass Suchindexierung langfristig nur dann gut wartbar bleibt, wenn Unterschiede zwischen Inhaltstypen nicht hart im Code verteilt werden.

Konfiguration schlägt Sonderlogik

Die konfigurationsbasierte Zuordnung von Content-Typ, Indexer und Data-Handler hat die Lösung für uns wesentlich flexibler gemacht.

Rekursive Textauslesung ist entscheidend

Gerade bei komponentenbasierten CMS-Strukturen reicht es nicht, nur einige Standardfelder zu betrachten. Relevanter Text steckt oft tief in verschachtelten Blöcken.

Webhooks sorgen für Aktualität ohne manuelle Prozesse

Durch die direkte Anbindung an Veröffentlichungen in Storyblok bleibt der Suchindex ohne zusätzlichen Pflegeaufwand aktuell.

Unser Learning

Unser wichtigstes Learning war, dass eine zuverlässige Suchindexierung bei unterschiedlichen Content-Typen am besten über eine klare Trennung von Zuständigkeiten funktioniert: Webhooks stoßen Prozesse an, eine Factory wählt die passende Verarbeitungslogik aus, und Data-Handler übernehmen die eigentliche Transformation der Inhalte.

Mit diesem Aufbau konnten wir Storyblok-Inhalte automatisiert, zeitnah und strukturiert in Solr übernehmen. Das verbessert nicht nur die Aktualität der Suche, sondern macht die Lösung auch deutlich leichter erweiterbar, wenn neue Inhaltstypen oder Märkte hinzukommen.