Responsive Images mit TYPO3 7.6 und Fluid Styled Content (FSC) unter Berücksichtigung mehrspaltiger Layouts

Neben zahlreichen Maßnahmen zur Optimierung von Websites wird oft eine der wichtigsten vergessen: Responsive Images. Laut Can I use unterstützen mittlerweile, abgesehen vom Internet Explorer, alle Browser das Attribut srcset.

Auf die Funktionsweise von srcset möchte ich hier nicht weiter eingehen, kann aber diese beiden Seiten dazu sehr empfehlen:

Vielmehr möchte ich in diesem Artikel auf die Integration von Responsive Images in TYPO3 eingehen. Während es unter CSS Styled Content noch relativ einfach war, sie zu integrieren, gestaltet sich dieses Unterfangen mit Fluid Styled Content momentan (Stand: Januar 2017) etwas schwieriger, da sie noch nicht nativ unterstützt werden.

Die Ausgabe der Bilder möchte ich in Abhängigkeit zu folgenden Gegebenheiten anpassen:

  1. Spaltenbreite in der das Inhaltselement „Texte & Medien“ angelegt wurde
  2. Position und Ausrichtung des Bildes zum Text (im / neben dem Text links / rechts oder unter / über dem Text)
  3. Anzahl an Spalten für die Darstellung der Bilder

Mein Setup (Stand: Januar 2017)

  • TYPO3 7.6.15
  • Fluid Styled Content (System-Extension)
  • VHS: Fluid ViewHelpers 3.1.0

MediaGallery.html kopieren und Pfad bekannt machen

Als erstes empfiehlt es sich, die Template-Datei für die Ausgabe der Media Gallery typo3/sysext/fluid_styled_content/Resources/Private/Partials/MediaGallery.html in das eigene Projekt-Verzeichnis zu kopieren und die Pfade dorthin mit TypoScript bekannt zu machen.

lib.fluidContent {
	partialRootPaths {
		1000 = pfad-zum-eigenen-projekt-verzeichnis/fluid_styled_content/partials/
	}
}

Mit der 1000 sorgen wir dafür, dass der Pfad zu den Partials nicht komplett überschrieben, sondern lediglich erweitert wird. Wir brauchen also nur Dateien kopieren, die wir auch anpassen. Alle anderen bleiben im Original-Verzeichnis.

MediaGallery.html anpassen

Nun passen wir den Abschnitt <f:section name="imageType"> an und erweitern dort die ViewHelper <f:render /> um folgende Argumente, die später die Breite unserer Responsive Images beeinflussen sollen.

  1. isImage: 1
    Damit unterscheiden wir im Abschnitt <f:section name="media"> ob es sich um ein Bild oder ein anderes Medium handelt.
  2. columnCount: gallery.count.columns
    Hiermit übergeben wir die Anzahl an Spalten für die Bilder, welche man im Inhaltselement „Texte & Medien“ auswählen kann.
  3. verticalPosition: gallery.position.vertical
    Damit übergeben wir die Information ob die Bilder „neben dem Text links / rechts“ bzw. „im Text links / rechts“ dargestellt werden oder über / unter dem Text.
  4. col: data.colPos
    Zu guter Letzt übermitteln wir die Information in welcher Spalte das Inhaltselement angelegt wurde. Diese holen wir uns aus der Eigenschaft colPos im {data}-Objekt. Diese gibt mir in meinem Fall die Werte 0, 1 oder 2 aus, je nachdem in welcher Spalte sich das aktuelle Inhaltselement befindet.
<f:section name="imageType">
	<f:if condition="{column.media.link}">
		<f:then>
			<f:link.typolink parameter="{column.media.link}">
				<f:render section="media" arguments="{column: column, isImage: 1, columnCount: gallery.count.columns, verticalPosition: gallery.position.vertical, col: data.colPos }" />
			</f:link.typolink>
		</f:then>
		<f:else>
			<f:if condition="{data.image_zoom}">
				<f:then>
					<ce:link.clickEnlarge image="{column.media}" configuration="{settings.media.popup}">
						<f:render section="media" arguments="{column: column, isImage: 1, columnCount: gallery.count.columns, verticalPosition: gallery.position.vertical, col: data.colPos }" />
					</ce:link.clickEnlarge>
				</f:then>
				<f:else>
					<f:debug>{gallery.count.columns}</f:debug>
					<f:render section="media" arguments="{column: column, isImage: 1, columnCount: gallery.count.columns, verticalPosition: gallery.position.vertical, col: data.colPos }" />
				</f:else>
			</f:if>
		</f:else>
	</f:if>
</f:section>

Unterschiedliche colPos benutzen

Wenn man verschiedene Layouts mit unterschiedlich breiten Spalten hat, sollte man natürlich darauf achten, dass man unterschiedliche colPos benutzt.

Also z. B.:

  • 100 % breite Spalte > colPos 0
  • 75 % breite Spalte > colPos 1
  • 25 % breite Spalte > colPos 2
  • 50 % breite Spalte (links) > colPos 3
  • 50 % breite Spalte (rechts) > colPos 4

Einsatz von Grid Elements

Das Abfragen der aktuellen Spalte funktioniert auch mit der Extension Grid Elements. Anstelle von col: data.colPos verwendet man dann einfach col: data.tx_gridelements_columns. Wenn man Grid Elements verschachtelt hat, kann man auch die Eltern-Spalte abfragen und dann z. B. ein weiteres Argument parentCol: data.parentgrid_tx_gridelements_columns mit übergeben.

Wie man Backend-Layouts sowie Grid Elements einsetzt, kann man hier nachlesen:
https://www.braune-digital.com/blog/typo3-backend-layouts-ueber-page-tsconfig-in-dateien-auslagern/
https://jweiland.net/typo3/showcase/gridelements.html

Unterscheidung nach Spalten colPos

Als nächstes widmen wir uns dem Abschnitt <f:section name="media"> und erweitern diesen folgendermaßen.

<f:section name="media">
	<f:if condition="{isImage}">
		<f:then>
			<f:switch expression="{col}">
				<!-- 100 % Spalte -->
				<f:case value="0">
					<f:render partial="ResponsiveImages/ImagesInside100PercentCol" arguments="{_all}" />
				</f:case>

				<!-- 75 % Spalte -->
				<f:case value="1">
					<f:render partial="ResponsiveImages/ImagesInside75PercentCol" arguments="{_all}" />
				</f:case>

				<!-- 25 % Spalte -->
				<f:case value="2">
					<f:render partial="ResponsiveImages/ImagesInside25PercentCol" arguments="{_all}" />
				</f:case>
				
				<!-- Fallback -->
				<f:case default="TRUE">
					<f:render partial="ResponsiveImages/ImagesInside100PercentCol" arguments="{_all}" />
				</f:case>
			</f:switch>
		</f:then>
		<f:else>
			<f:media
				file="{column.media}"
				width="{column.dimensions.width}"
				height="{column.dimensions.height}"
				alt="{column.media.alternative}"
				title="{column.media.title}"
			/>
		</f:else>
	</f:if>
</f:section>

In dem Markup oben unterscheiden wir nun zwischen den verschiedenen Spalten und rendern Partials für:

  1. Bilder in einer 100 % breiten Spalte
  2. Bilder in einer 75 % breiten Spalte
  3. Bilder in einer 25 % breiten Spalte

Auslagerung weiterer Partials

Um die Datei übersichtlich zu halten, habe ich mich entschlossen die Partials in eigene Dateien auszulagern. Dafür habe ich mir einen Ordner ResponsiveImages angelegt und dort wiederum die Dateien ImagesInside100PercentCol.html, ImagesInside75PercentCol.html und ImagesInside25PercentCol.html.

Da alle Dateien grundsätzlich gleich aufgebaut sind, möchte ich hier lediglich eine zeigen.

{namespace v=FluidTYPO3\Vhs\ViewHelpers}

<f:if condition="{verticalPosition} == 'intext'">
	<!-- Bild / Bilder befinden sich in oder neben dem Text -->
	<f:then>
		<f:switch expression="{columnCount}">

			<!-- Anzahl an Spalten: 1 -->
			<f:case value="1">
				<v:media.image 
					src="{column.media}" 
					width="600" 
					alt="{column.media.alternative}" 
					title="{column.media.title}" 
					srcset="300,600,900,1200"
					treatIdAsReference="1" 
					additionalAttributes="{sizes: '
						(min-width: 90em) 900px, 
						(min-width: 64em) 600px, 
						(min-width: 30em) 450px, 
						100vw'}" />
			</f:case>
			
			<!-- Anzahl an Spalten: 2 -->
			<f:case value="2">
				<v:media.image 
					src="{column.media}" 
					width="300" 
					alt="{column.media.alternative}" 
					title="{column.media.title}" 
					srcset="300,600,900,1200"
					treatIdAsReference="1" 
					additionalAttributes="{sizes: '
						(min-width: 90em) 450px, 
						(min-width: 64em) 300px, 
						(min-width: 30em) 200px, 
						100vw'}" />
			</f:case>
			
			<!-- Anzahl an Spalten: 5 -->
			<f:case value="5">
				<v:media.image 
					src="{column.media}" 
					width="600" 
					alt="{column.media.alternative}" 
					title="{column.media.title}" 
					srcset="300,600,900"
					treatIdAsReference="1" 
					additionalAttributes="{sizes: '
						(min-width: 90em) 300px, 
						(min-width: 64em) 200px, 
						(min-width: 30em) 100px, 
						100vw'}" />
			</f:case>
			
			<!-- Fallback -->
			<f:case default="TRUE">
				<v:media.image 
					src="{column.media}" 
					width="600" 
					alt="{column.media.alternative}" 
					title="{column.media.title}" 
					srcset="300,600,900,1200"
					treatIdAsReference="1" 
					additionalAttributes="{sizes: '
						(min-width: 90em) 1200px, 
						(min-width: 64em) 900px, 
						(min-width: 30em) 600px, 
						100vw'}" />
			</f:case>
		</f:switch>
	</f:then>

	<!-- Bild / Bilder befinden sich unter oder über dem Text -->
	<f:else>
		<f:switch expression="{columnCount}">

			<!-- Anzahl an Spalten: 1 -->
			<f:case value="1">
				<v:media.image 
					src="{column.media}" 
					width="900" 
					alt="{column.media.alternative}" 
					title="{column.media.title}" 
					srcset="300,600,900,1200"
					treatIdAsReference="1" 
					additionalAttributes="{sizes: '
						(min-width: 90em) 1200px, 
						(min-width: 64em) 900px, 
						(min-width: 30em) 600px, 
						100vw'}" />
			</f:case>
			
			<!-- Anzahl an Spalten: 2 -->
			<f:case value="2">
				<v:media.image 
					src="{column.media}" 
					width="600" 
					alt="{column.media.alternative}" 
					title="{column.media.title}" 
					srcset="300,600,900"
					treatIdAsReference="1" 
					additionalAttributes="{sizes: '
						(min-width: 90em) 600px, 
						(min-width: 64em) 450px, 
						(min-width: 30em) 300px, 
						100vw'}" />
			</f:case>
			
			<!-- Anzahl an Spalten: 5 -->
			<f:case value="5">
				<v:media.image 
					src="{column.media}" 
					width="600" 
					alt="{column.media.alternative}" 
					title="{column.media.title}" 
					srcset="300,600,900"
					treatIdAsReference="1" 
					additionalAttributes="{sizes: '
						(min-width: 90em) 300px, 
						(min-width: 64em) 200px, 
						(min-width: 30em) 100px, 
						100vw'}" />
			</f:case>
			
			<!-- Fallback -->
			<f:case default="TRUE">
				<v:media.image 
					src="{column.media}" 
					width="900" 
					alt="{column.media.alternative}" 
					title="{column.media.title}" 
					srcset="300,600,900,1200"
					treatIdAsReference="1" 
					additionalAttributes="{sizes: '
						(min-width: 90em) 1200px, 
						(min-width: 64em) 900px, 
						(min-width: 30em) 600px, 
						100vw'}" />
			</f:case>
		</f:switch>	
	</f:else>
</f:if>

Unterscheidung nach Position des Bildes und Anzahl an Spalten

Zunächst unterscheiden wir nach der vertikalen Position des Bildes, also ob es im / neben dem Text dargestellt wird oder unter / über dem Text. Meinem Beispiel liegt die Annahme zugrunde, dass Bilder die „im Text links / rechts“ oder „neben dem Text links / rechts“ immer die gleiche Breite haben.

Nun unterscheiden wir noch in wie vielen Spalten die Bilder organisiert sind. Die Website aus meinem Beispiel erlaubt nur die 1-, 2- und 5-spaltige Anordnung von Bildern.

Achtung, unsere Abfrage nach der Anzahl der Spalten columnCount (gallery.count.columns) gibt uns nur die eingestellte Zahl zurück, wenn es mindestens genau so viele Bilder gibt.

Beispiel 1: Wir haben in einem Inhaltselement 4 Bilder ausgewählt und stellen die Option „Anzahl an Spalten“ auf 2. In diesem Fall gibt uns gallery.count.columns die Zahl 2 zurück.

Beispiel 2: Wieder haben wir 4 Bilder in unserem Inhaltselement, stellen die Anzahl an Spalten dieses Mal aber auf 5. Nun gibt uns gallery.count.columns die Zahl 4 zurück und nicht 5.

Wir müssen also dafür sorgen, dass auch Rückgabewerte die zwischen den möglichen Einstellungen (bei mir 1, 2 und 5) liegen, berücksichtigt werden. In diesem Beispiel mache ich das über einen Default-Wert. Man könnte es aber auch mit if-Anweisungen lösen.

Anpassung der Bildgrößen

Zu guter Letzt werden die Bilder selbst gerendert. Dies geschieht mit Hilfe des Media / ImageViewHelpers der VHS Extension.

Jetzt müssen „nur noch“ die Attribute für width, sizes und srcset definiert werden. Die Größen in meinem Beispiel sind nur als Platzhalter zu verstehen, teilweise nicht sinnvoll und müssen natürlich für euren expliziten Fall angepasst werden.

Wie sehr man hier ins Detail geht, muss sich jeder selbst überlegen. Hier kommt es natürlich auch darauf an, ob man tendenziell viele Bilder auf der Website darstellt und wie weit man die Performance-Optimierung treiben möchte.

Komplexeres Beispiel auf GitHub

Dieses Beispiel hier habe ich stark vereinfacht. Für die TYPO3-Website von MENZEL Elektromotoren habe ich das etwas ausführlicher gemacht. Die Dateien dafür findet ihr in diesem Gist auf GitHub. Dort kann man auch sehen, dass ich auf manche Kombinationen verzichte, weil sie nicht wirklich Sinn ergeben. In einer 25 % breiten Spalte wird es aus Platzgründen neben dem Text ganz sicher keine 5-spaltige Bildergalerie geben.

Quellen / Links

Sebastian Schrama

Sebastian fühlt sich als Frontend-Entwickler in großen Systemen Zuhause. Wohldosiert packt er zur rechten Zeit den Perfektionismus-Hammer aus.