From 8f366710090ae322da447ffe01a50fe503307657 Mon Sep 17 00:00:00 2001
From: Kevin Ditscheid <kevin.ditscheid@sgalinski.de>
Date: Thu, 28 Apr 2022 09:41:57 +0200
Subject: [PATCH] [TASK] Introduce a Service for the new meta data

---
 Classes/Controller/AbstractController.php     | 145 ---------------
 Classes/Controller/LatestController.php       |  15 +-
 .../Controller/ListByCategoryController.php   |  15 +-
 Classes/Controller/NewsByAuthorController.php |  15 +-
 Classes/Controller/OverviewController.php     |  19 +-
 Classes/Controller/SingleViewController.php   |  15 +-
 Classes/Domain/Model/News.php                 |  73 --------
 Classes/Domain/Service/NewsService.php        | 167 ++++++++++++++++++
 Classes/ViewHelpers/RelatedViewHelper.php     |  42 ++++-
 .../Templates/SingleView/SingleView.html      |  29 ++-
 10 files changed, 290 insertions(+), 245 deletions(-)
 create mode 100644 Classes/Domain/Service/NewsService.php

diff --git a/Classes/Controller/AbstractController.php b/Classes/Controller/AbstractController.php
index d4d44b1..d7fc772 100644
--- a/Classes/Controller/AbstractController.php
+++ b/Classes/Controller/AbstractController.php
@@ -27,32 +27,17 @@ namespace SGalinski\SgNews\Controller;
  ***************************************************************/
 
 use RuntimeException;
-use SGalinski\SgNews\Domain\Model\Category;
-use SGalinski\SgNews\Domain\Model\News;
-use SGalinski\SgNews\Service\ImageService;
-use TYPO3\CMS\Core\Utility\GeneralUtility;
-use TYPO3\CMS\Extbase\Domain\Model\FileReference;
 use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
 
 /**
  * Abstract Controller
  */
 abstract class AbstractController extends ActionController {
-	/**
-	 * @var ImageService
-	 */
-	protected $imageService;
-
 	/**
 	 * @var array
 	 */
 	protected $extensionConfiguration = [];
 
-	/**
-	 * @var array
-	 */
-	protected $cachedSingleNews = [];
-
 	/**
 	 * Initializes any action
 	 *
@@ -65,13 +50,6 @@ abstract class AbstractController extends ActionController {
 		parent::initializeAction();
 	}
 
-	/**
-	 * @param ImageService $imageService
-	 */
-	public function injectImageService(ImageService $imageService) {
-		$this->imageService = $imageService;
-	}
-
 	/**
 	 * Error Handler
 	 *
@@ -82,129 +60,6 @@ abstract class AbstractController extends ActionController {
 		throw new RuntimeException(parent::errorAction());
 	}
 
-	/**
-	 * Returns the metadata of the given news.
-	 *
-	 * @param News $news
-	 * @param Category $category
-	 * @return array
-	 * @throws \InvalidArgumentException
-	 */
-	protected function getMetaDataForNews(News $news, Category $category): array {
-		$newsId = $news->getUid();
-		if (isset($this->cachedSingleNews[$newsId])) {
-			return $this->cachedSingleNews[$newsId];
-		}
-
-		$fileRepository = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Resource\FileRepository::class);
-		$fileObjects = $fileRepository->findByRelation('pages', 'media', $news->getUid());
-
-		$singleNewsImageData = $this->getDataForSingleViewImage($news, $category);
-		$teaserImageData = $this->getDataForTeaserImage($news, $category);
-
-		// Use single news image data for teaser image data, if the teaser imaga data are empty.
-		$teaserIsEmpty = $teaserImageData['teaserImage'] === NULL && $teaserImageData['teaserImageObject'] === NULL;
-		$singleNewsIsEmpty = $singleNewsImageData['image'] === NULL && $singleNewsImageData['imageObject'] === NULL;
-		if ($teaserIsEmpty && !$singleNewsIsEmpty) {
-			$teaserImageData = [
-				'teaserImage' => $singleNewsImageData['image'],
-				'teaserImageObject' => $singleNewsImageData['imageObject'],
-			];
-		}
-
-		$newsRecord = array_merge(
-			[
-				'category' => $category,
-				'news' => $news,
-			],
-			$singleNewsImageData,
-			$teaserImageData,
-			['media' => $fileObjects]
-		);
-
-		$this->cachedSingleNews[$newsId] = $newsRecord;
-
-		return $newsRecord;
-	}
-
-	/**
-	 * Returns the single view image data as an array for the given news and category as fallback.
-	 *
-	 * @param News $news
-	 * @param Category $category
-	 * @return array
-	 * @throws \InvalidArgumentException
-	 */
-	protected function getDataForSingleViewImage(News $news, Category $category): array {
-		/** @var FileReference $singleNewsImage */
-		$singleNewsImage = $singleNewsImageObject = NULL;
-		$singleNewsImages = $news->getTeaser2Image();
-		if (count($singleNewsImages)) {
-			$singleNewsImage = $singleNewsImages->current();
-		} else {
-			$categoryImages = $category->getTeaser2Image();
-			if (count($categoryImages)) {
-				$singleNewsImage = $categoryImages->current();
-			}
-		}
-
-		if ($singleNewsImage) {
-			$singleNewsImageObject = $singleNewsImage;
-			$originalResource = $singleNewsImage->getOriginalResource();
-			if ($originalResource) {
-				$singleNewsImage = $originalResource->getPublicUrl();
-			}
-
-			if ($singleNewsImage) {
-				$singleNewsImage = $GLOBALS['TSFE']->absRefPrefix . $singleNewsImage;
-			}
-		}
-
-		return [
-			'image' => $singleNewsImage,
-			'imageObject' => $singleNewsImageObject,
-		];
-	}
-
-	/**
-	 * Returns the teaser image data as an array for the given news and category as fallback.
-	 *
-	 * @param News $news
-	 * @param Category $category
-	 * @return array
-	 * @throws \InvalidArgumentException
-	 */
-	protected function getDataForTeaserImage(News $news, Category $category): array {
-		/** @var FileReference $teaserImage */
-		$teaserImage = $teaserImageObject = NULL;
-		$teaserImages = $news->getTeaser1Image();
-		if (count($teaserImages)) {
-			$teaserImage = $teaserImages->current();
-		} else {
-			$categoryImages = $category->getTeaser1Image();
-			if (count($categoryImages)) {
-				$teaserImage = $categoryImages->current();
-			}
-		}
-
-		if ($teaserImage) {
-			$teaserImageObject = $teaserImage;
-			$originalResource = $teaserImage->getOriginalResource();
-			if ($originalResource) {
-				$teaserImage = $originalResource->getPublicUrl();
-			}
-
-			if ($teaserImage) {
-				$teaserImage = $GLOBALS['TSFE']->absRefPrefix . $teaserImage;
-			}
-		}
-
-		return [
-			'teaserImage' => $teaserImage,
-			'teaserImageObject' => $teaserImageObject,
-		];
-	}
-
 	/**
 	 * Calculate the pagination offset
 	 *
diff --git a/Classes/Controller/LatestController.php b/Classes/Controller/LatestController.php
index 393c956..e5eb3ea 100644
--- a/Classes/Controller/LatestController.php
+++ b/Classes/Controller/LatestController.php
@@ -31,6 +31,7 @@ use SGalinski\SgNews\Domain\Model\News;
 use SGalinski\SgNews\Domain\Repository\CategoryRepository;
 use SGalinski\SgNews\Domain\Repository\NewsRepository;
 use SGalinski\SgNews\Domain\Repository\TagRepository;
+use SGalinski\SgNews\Domain\Service\NewsService;
 use SGalinski\SgNews\Service\ConfigurationService;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 
@@ -53,6 +54,11 @@ class LatestController extends AbstractController {
 	 */
 	protected $newsRepository;
 
+	/**
+	 * @var NewsService
+	 */
+	protected $newsService;
+
 	/**
 	 * Renders the news overview
 	 *
@@ -96,7 +102,7 @@ class LatestController extends AbstractController {
 				$categories[$categoryUid] = $category;
 			}
 
-			$newsMetaData[] = $this->getMetaDataForNews($latestNewsEntry, $category);
+			$newsMetaData[] = $this->newsService->getMetaDataForNews($latestNewsEntry, $category);
 			// Make sure, that the amount is the same as the configured limit.
 			if (count($newsMetaData) >= $limit) {
 				break;
@@ -127,4 +133,11 @@ class LatestController extends AbstractController {
 	public function injectTagRepository(TagRepository $tagRepository) {
 		$this->tagRepository = $tagRepository;
 	}
+
+	/**
+	 * @param NewsService $newsService
+	 */
+	public function injectNewsService(NewsService $newsService) {
+		$this->newsService = $newsService;
+	}
 }
diff --git a/Classes/Controller/ListByCategoryController.php b/Classes/Controller/ListByCategoryController.php
index 2f405c9..2a1c961 100644
--- a/Classes/Controller/ListByCategoryController.php
+++ b/Classes/Controller/ListByCategoryController.php
@@ -30,6 +30,7 @@ use SGalinski\SgNews\Domain\Model\News;
 use SGalinski\SgNews\Domain\Repository\CategoryRepository;
 use SGalinski\SgNews\Domain\Repository\NewsRepository;
 use SGalinski\SgNews\Domain\Repository\TagRepository;
+use SGalinski\SgNews\Domain\Service\NewsService;
 use SGalinski\SgNews\Service\ConfigurationService;
 use SGalinski\SgNews\Service\HeaderMetaDataService;
 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
@@ -57,6 +58,11 @@ class ListByCategoryController extends AbstractController {
 	 */
 	protected $newsRepository;
 
+	/**
+	 * @var NewsService
+	 */
+	protected $newsService;
+
 	/**
 	 * @param CategoryRepository $categoryRepository
 	 */
@@ -79,6 +85,13 @@ class ListByCategoryController extends AbstractController {
 		$this->tagRepository = $tagRepository;
 	}
 
+	/**
+	 * @param NewsService $newsService
+	 */
+	public function injectNewsService(NewsService $newsService) {
+		$this->newsService = $newsService;
+	}
+
 	/**
 	 * Initialize the indexAction to set the currentPageBrowserPage parameter
 	 *
@@ -164,7 +177,7 @@ class ListByCategoryController extends AbstractController {
 
 		foreach ($news as $newsEntry) {
 			/** @var News $newsEntry */
-			$data = $this->getMetaDataForNews($newsEntry, $categories[$newsEntry->getPid()]);
+			$data = $this->newsService->getMetaDataForNews($newsEntry, $categories[$newsEntry->getPid()]);
 			$newsMetaData[] = $data;
 
 			if (!$headerSet) {
diff --git a/Classes/Controller/NewsByAuthorController.php b/Classes/Controller/NewsByAuthorController.php
index 4ace64d..217140f 100644
--- a/Classes/Controller/NewsByAuthorController.php
+++ b/Classes/Controller/NewsByAuthorController.php
@@ -32,6 +32,7 @@ use SGalinski\SgNews\Domain\Model\News;
 use SGalinski\SgNews\Domain\Repository\AuthorRepository;
 use SGalinski\SgNews\Domain\Repository\CategoryRepository;
 use SGalinski\SgNews\Domain\Repository\NewsRepository;
+use SGalinski\SgNews\Domain\Service\NewsService;
 use SGalinski\SgSeo\Service\HeadTagService;
 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
@@ -40,6 +41,18 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
  * News Author List Controller
  */
 class NewsByAuthorController extends AbstractController {
+	/**
+	 * @var NewsService
+	 */
+	protected $newsService;
+
+	/**
+	 * @param NewsService $newsService
+	 */
+	public function injectNewsService(NewsService $newsService) {
+		$this->newsService = $newsService;
+	}
+
 	/**
 	 * Renders the news author list.
 	 *
@@ -118,7 +131,7 @@ class NewsByAuthorController extends AbstractController {
 					$categories[$categoryId] = $category;
 				}
 
-				$newsMetaData[] = $this->getMetaDataForNews($newsEntry, $categories[$categoryId]);
+				$newsMetaData[] = $this->newsService->getMetaDataForNews($newsEntry, $categories[$categoryId]);
 			}
 		}
 
diff --git a/Classes/Controller/OverviewController.php b/Classes/Controller/OverviewController.php
index 0a524e7..5b00281 100644
--- a/Classes/Controller/OverviewController.php
+++ b/Classes/Controller/OverviewController.php
@@ -31,6 +31,7 @@ use SGalinski\SgNews\Domain\Model\News;
 use SGalinski\SgNews\Domain\Repository\CategoryRepository;
 use SGalinski\SgNews\Domain\Repository\NewsRepository;
 use SGalinski\SgNews\Domain\Repository\TagRepository;
+use SGalinski\SgNews\Domain\Service\NewsService;
 use SGalinski\SgNews\Service\ConfigurationService;
 use SGalinski\SgNews\Service\HeaderMetaDataService;
 use TYPO3\CMS\Core\Http\ImmediateResponseException;
@@ -60,6 +61,11 @@ class OverviewController extends AbstractController {
 	 */
 	protected $newsRepository;
 
+	/**
+	 * @var NewsService
+	 */
+	protected $newsService;
+
 	/**
 	 * @param CategoryRepository $categoryRepository
 	 */
@@ -82,6 +88,13 @@ class OverviewController extends AbstractController {
 		$this->tagRepository = $tagRepository;
 	}
 
+	/**
+	 * @param NewsService $newsService
+	 */
+	public function injectNewsService(NewsService $newsService) {
+		$this->newsService = $newsService;
+	}
+
 	/**
 	 * Initialize the overviewAction to set the currentPageBrowserPage parameter
 	 *
@@ -216,7 +229,7 @@ class OverviewController extends AbstractController {
 					$newsEntry->getPid()
 				) ?? $this->categoryRepository->findByUid($newsEntry->getPid());
 			$newsCategoryId = $newsCategory->getUid();
-			$newsMetaData = $this->getMetaDataForNews($newsEntry, $newsCategory);
+			$newsMetaData = $this->newsService->getMetaDataForNews($newsEntry, $newsCategory);
 
 			if ((int) $this->settings['groupBy'] === 1) {
 				if (!$areCategoriesCreated) {
@@ -353,7 +366,7 @@ class OverviewController extends AbstractController {
 		$category = $this->categoryRepository->findByUid($highlightedNews->getPid());
 		$highlightedNewsMetaData = NULL;
 		if ($category) {
-			$highlightedNewsMetaData = $this->getMetaDataForNews($highlightedNews, $category);
+			$highlightedNewsMetaData = $this->newsService->getMetaDataForNews($highlightedNews, $category);
 		}
 
 		if (!version_compare(ExtensionManagementUtility::getExtensionVersion('sg_seo'), '5.0.0', '>=')) {
@@ -470,7 +483,7 @@ class OverviewController extends AbstractController {
 		);
 		foreach ($news as $newsEntry) {
 			/** @var News $newsEntry */
-			$data = $this->getMetaDataForNews($newsEntry, $categoriesById[$newsEntry->getPid()]);
+			$data = $this->newsService->getMetaDataForNews($newsEntry, $categoriesById[$newsEntry->getPid()]);
 			$newsMetaData[] = $data;
 		}
 
diff --git a/Classes/Controller/SingleViewController.php b/Classes/Controller/SingleViewController.php
index 3890778..a7d929f 100644
--- a/Classes/Controller/SingleViewController.php
+++ b/Classes/Controller/SingleViewController.php
@@ -31,6 +31,7 @@ use SGalinski\SgNews\Domain\Model\News;
 use SGalinski\SgNews\Domain\Repository\CategoryRepository;
 use SGalinski\SgNews\Domain\Repository\NewsRepository;
 use SGalinski\SgNews\Domain\Repository\TagRepository;
+use SGalinski\SgNews\Domain\Service\NewsService;
 use SGalinski\SgNews\Service\HeaderMetaDataService;
 use TYPO3\CMS\Core\Charset\CharsetConverter;
 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
@@ -59,6 +60,11 @@ class SingleViewController extends AbstractController {
 	 */
 	protected $characterSetConverter;
 
+	/**
+	 * @var NewsService
+	 */
+	protected $newsService;
+
 	/**
 	 * Renders the news single view
 	 *
@@ -92,7 +98,7 @@ class SingleViewController extends AbstractController {
 //			$similarNewsMetaData[] = $this->getMetaDataForNews($similarNewsEntry, $category);
 //		}
 
-		$newsMetaData = $this->getMetaDataForNews($news, $newsCategory);
+		$newsMetaData = $this->newsService->getMetaDataForNews($news, $newsCategory);
 		if (!version_compare(ExtensionManagementUtility::getExtensionVersion('sg_seo'), '5.0.0', '>=')) {
 			if ($newsMetaData['image']) {
 				HeaderMetaDataService::addOgImageToHeader($newsMetaData['image']);
@@ -145,4 +151,11 @@ class SingleViewController extends AbstractController {
 	public function injectTagRepository(TagRepository $tagRepository) {
 		$this->tagRepository = $tagRepository;
 	}
+
+	/**
+	 * @param NewsService $newsService
+	 */
+	public function injectNewsService(NewsService $newsService) {
+		$this->newsService = $newsService;
+	}
 }
diff --git a/Classes/Domain/Model/News.php b/Classes/Domain/Model/News.php
index 607c4a7..d042f4b 100644
--- a/Classes/Domain/Model/News.php
+++ b/Classes/Domain/Model/News.php
@@ -26,9 +26,6 @@ namespace SGalinski\SgNews\Domain\Model;
  *  This copyright notice MUST APPEAR in all copies of the script!
  ***************************************************************/
 
-use SGalinski\SgNews\Domain\Repository\CategoryRepository;
-use TYPO3\CMS\Core\Utility\GeneralUtility;
-use TYPO3\CMS\Extbase\Domain\Model\FileReference;
 use TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy;
 use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
 
@@ -105,16 +102,6 @@ class News extends CategoryAndNews {
 	 */
 	protected $enableComments = TRUE;
 
-	/**
-	 * @var Category
-	 */
-	protected $categoryCache = NULL;
-
-	/**
-	 * @var FileReference
-	 */
-	protected $teaserImageCache = NULL;
-
 	/**
 	 * Constructor
 	 */
@@ -415,64 +402,4 @@ class News extends CategoryAndNews {
 	public function setEnableComments(bool $enableComments): void {
 		$this->enableComments = $enableComments;
 	}
-
-	/**
-	 * Get the assigned category
-	 *
-	 * @return Category
-	 */
-	public function getCategory(): Category {
-		if ($this->categoryCache instanceof Category) {
-			return $this->categoryCache;
-		}
-
-		return $this->categoryCache = GeneralUtility::makeInstance(CategoryRepository::class)
-			->findByUid($this->pid);
-	}
-
-	/**
-	 * Get the teaser image object
-	 *
-	 * @return FileReference|null
-	 */
-	public function getTeaserImageObject(): ?FileReference {
-		if ($this->teaserImageCache instanceof FileReference) {
-			return $this->teaserImageCache;
-		}
-		$teaserImage = $this->getTeaser1Image()->current();
-		if ($teaserImage instanceof FileReference) {
-			return $this->teaserImageCache = $teaserImage;
-		}
-
-		$teaserImage = $this->getCategory()->getTeaser1Image()->current();
-		if ($teaserImage instanceof FileReference) {
-			return $this->teaserImageCache = $teaserImage;
-		}
-
-		$teaserImage = $this->getTeaser2Image()->current();
-		if ($teaserImage instanceof FileReference) {
-			return $this->teaserImageCache = $teaserImage;
-		}
-
-		return NULL;
-	}
-
-	/**
-	 * Get the teaser image public url
-	 *
-	 * @return string
-	 */
-	public function getTeaserImage() {
-		$teaserImage = $this->getTeaserImageObject();
-		if ($teaserImage === NULL) {
-			return '';
-		}
-
-		$originalResource = $teaserImage->getOriginalResource();
-		if ($originalResource === NULL) {
-			return '';
-		}
-
-		return $originalResource->getPublicUrl();
-	}
 }
diff --git a/Classes/Domain/Service/NewsService.php b/Classes/Domain/Service/NewsService.php
new file mode 100644
index 0000000..7ba3ad6
--- /dev/null
+++ b/Classes/Domain/Service/NewsService.php
@@ -0,0 +1,167 @@
+<?php
+
+namespace SGalinski\SgNews\Domain\Service;
+
+/***************************************************************
+ *  Copyright notice
+ *
+ *  (c) sgalinski Internet Services (https://www.sgalinski.de)
+ *
+ *  All rights reserved
+ *
+ *  This script is part of the TYPO3 project. The TYPO3 project is
+ *  free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  The GNU General Public License can be found at
+ *  http://www.gnu.org/copyleft/gpl.html.
+ *
+ *  This script is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  This copyright notice MUST APPEAR in all copies of the script!
+ ***************************************************************/
+
+use SGalinski\SgNews\Domain\Model\Category;
+use SGalinski\SgNews\Domain\Model\News;
+use TYPO3\CMS\Core\SingletonInterface;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Extbase\Domain\Model\FileReference;
+use TYPO3\CMS\Core\Resource\FileRepository;
+
+/**
+ * This service takes care of meta data generation for the news objects
+ */
+class NewsService implements SingletonInterface {
+	/**
+	 * @var array
+	 */
+	protected $cachedSingleNews = [];
+
+	/**
+	 * Returns the metadata of the given news.
+	 *
+	 * @param News $news
+	 * @param Category $category
+	 * @return array
+	 * @throws \InvalidArgumentException
+	 */
+	public function getMetaDataForNews(News $news, Category $category): array {
+		$newsId = $news->getUid();
+		if (isset($this->cachedSingleNews[$newsId])) {
+			return $this->cachedSingleNews[$newsId];
+		}
+
+		$fileRepository = GeneralUtility::makeInstance(FileRepository::class);
+		$fileObjects = $fileRepository->findByRelation('pages', 'media', $news->getUid());
+
+		$singleNewsImageData = $this->getDataForSingleViewImage($news, $category);
+		$teaserImageData = $this->getDataForTeaserImage($news, $category);
+
+		// Use single news image data for teaser image data, if the teaser imaga data are empty.
+		$teaserIsEmpty = $teaserImageData['teaserImage'] === NULL && $teaserImageData['teaserImageObject'] === NULL;
+		$singleNewsIsEmpty = $singleNewsImageData['image'] === NULL && $singleNewsImageData['imageObject'] === NULL;
+		if ($teaserIsEmpty && !$singleNewsIsEmpty) {
+			$teaserImageData = [
+				'teaserImage' => $singleNewsImageData['image'],
+				'teaserImageObject' => $singleNewsImageData['imageObject'],
+			];
+		}
+
+		$newsRecord = array_merge(
+			[
+				'category' => $category,
+				'news' => $news,
+			],
+			$singleNewsImageData,
+			$teaserImageData,
+			['media' => $fileObjects]
+		);
+
+		$this->cachedSingleNews[$newsId] = $newsRecord;
+
+		return $newsRecord;
+	}
+
+	/**
+	 * Returns the single view image data as an array for the given news and category as fallback.
+	 *
+	 * @param News $news
+	 * @param Category $category
+	 * @return array
+	 * @throws \InvalidArgumentException
+	 */
+	public function getDataForSingleViewImage(News $news, Category $category): array {
+		/** @var FileReference $singleNewsImage */
+		$singleNewsImage = $singleNewsImageObject = NULL;
+		$singleNewsImages = $news->getTeaser2Image();
+		if (count($singleNewsImages)) {
+			$singleNewsImage = $singleNewsImages->current();
+		} else {
+			$categoryImages = $category->getTeaser2Image();
+			if (count($categoryImages)) {
+				$singleNewsImage = $categoryImages->current();
+			}
+		}
+
+		if ($singleNewsImage) {
+			$singleNewsImageObject = $singleNewsImage;
+			$originalResource = $singleNewsImage->getOriginalResource();
+			if ($originalResource) {
+				$singleNewsImage = $originalResource->getPublicUrl();
+			}
+
+			if ($singleNewsImage) {
+				$singleNewsImage = $GLOBALS['TSFE']->absRefPrefix . $singleNewsImage;
+			}
+		}
+
+		return [
+			'image' => $singleNewsImage,
+			'imageObject' => $singleNewsImageObject,
+		];
+	}
+
+	/**
+	 * Returns the teaser image data as an array for the given news and category as fallback.
+	 *
+	 * @param News $news
+	 * @param Category $category
+	 * @return array
+	 * @throws \InvalidArgumentException
+	 */
+	public function getDataForTeaserImage(News $news, Category $category): array {
+		/** @var FileReference $teaserImage */
+		$teaserImage = $teaserImageObject = NULL;
+		$teaserImages = $news->getTeaser1Image();
+		if (count($teaserImages)) {
+			$teaserImage = $teaserImages->current();
+		} else {
+			$categoryImages = $category->getTeaser1Image();
+			if (count($categoryImages)) {
+				$teaserImage = $categoryImages->current();
+			}
+		}
+
+		if ($teaserImage) {
+			$teaserImageObject = $teaserImage;
+			$originalResource = $teaserImage->getOriginalResource();
+			if ($originalResource) {
+				$teaserImage = $originalResource->getPublicUrl();
+			}
+
+			if ($teaserImage) {
+				$teaserImage = $GLOBALS['TSFE']->absRefPrefix . $teaserImage;
+			}
+		}
+
+		return [
+			'teaserImage' => $teaserImage,
+			'teaserImageObject' => $teaserImageObject,
+		];
+	}
+}
diff --git a/Classes/ViewHelpers/RelatedViewHelper.php b/Classes/ViewHelpers/RelatedViewHelper.php
index 83629d2..69b8e78 100644
--- a/Classes/ViewHelpers/RelatedViewHelper.php
+++ b/Classes/ViewHelpers/RelatedViewHelper.php
@@ -2,8 +2,11 @@
 
 namespace SGalinski\SgNews\ViewHelpers;
 
+use SGalinski\SgNews\Domain\Repository\CategoryRepository;
 use SGalinski\SgNews\Domain\Repository\NewsRepository;
+use SGalinski\SgNews\Domain\Service\NewsService;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
 use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
 use TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithRenderStatic;
 
@@ -28,12 +31,30 @@ class RelatedViewHelper extends AbstractViewHelper {
 			'The news record from which to find related news',
 			TRUE
 		);
-		$this->registerArgument('limit', 'int', 'Limit the amount of related news to display', FALSE, 5);
-		$this->registerArgument('as', 'string', 'The name of the iteration variable', TRUE);
+		$this->registerArgument(
+			'limit',
+			'int',
+			'Limit the amount of related news to display',
+			FALSE,
+			5
+		);
+		$this->registerArgument(
+			'as',
+			'string',
+			'The name of the iteration variable',
+			TRUE
+		);
 		$this->registerArgument(
 			'iteration', 'string',
 			'The name of the variable to store iteration information (index, cycle, isFirst, isLast, isEven, isOdd)'
 		);
+		$this->registerArgument(
+			'relatedNews',
+			ObjectStorage::class,
+			'An optional list of related news to take instead of finding some via the news repository',
+			FALSE,
+			NULL
+		);
 	}
 
 	/**
@@ -49,10 +70,17 @@ class RelatedViewHelper extends AbstractViewHelper {
 	public static function renderStatic(
 		array $arguments, \Closure $renderChildrenClosure, RenderingContextInterface $renderingContext
 	) {
+		$newsService = GeneralUtility::makeInstance(NewsService::class);
+		$categoryRepository = GeneralUtility::makeInstance(CategoryRepository::class);
 		$templateVariableContainer = $renderingContext->getVariableProvider();
 		$news = $arguments['news'];
 		$newsRepository = GeneralUtility::makeInstance(NewsRepository::class);
-		$related = $newsRepository->findRelated($news, (int) $arguments['limit']);
+		if ($arguments['relatedNews']) {
+			$related = $arguments['relatedNews'];
+		} else {
+			$related = $newsRepository->findRelated($news, (int) $arguments['limit']);
+		}
+
 		if (isset($arguments['iteration'])) {
 			$iterationData = [
 				'index' => 0,
@@ -62,8 +90,14 @@ class RelatedViewHelper extends AbstractViewHelper {
 		}
 
 		$output = '';
+		$categories = [];
 		foreach ($related as $relatedNews) {
-			$templateVariableContainer->add($arguments['as'], $relatedNews);
+			if (!isset($categories[$relatedNews->getPid()])) {
+				$categories[$relatedNews->getPid()] = $categoryRepository->findByUid($relatedNews->getPid());
+			}
+
+			$newsMetaData = $newsService->getMetaDataForNews($relatedNews, $categories[$relatedNews->getPid()]);
+			$templateVariableContainer->add($arguments['as'], $newsMetaData);
 			if (isset($iterationData)) {
 				$iterationData['isFirst'] = $iterationData['cycle'] === 1;
 				$iterationData['isLast'] = $iterationData['cycle'] === $iterationData['total'];
diff --git a/Resources/Private/Templates/SingleView/SingleView.html b/Resources/Private/Templates/SingleView/SingleView.html
index bc803ad..4073b8f 100644
--- a/Resources/Private/Templates/SingleView/SingleView.html
+++ b/Resources/Private/Templates/SingleView/SingleView.html
@@ -203,15 +203,19 @@
 									<h3>
 										<f:translate key="frontend.singleview.relatedArticles"/>
 									</h3>
-
-									<ul>
-										<f:for each="{newsMetaData.news.relatedNews}" as="relatedNewsEntry">
-											<li>
-												<a href="{f:uri.page(pageUid: '{relatedNewsEntry.uid}')}">
-													{relatedNewsEntry.subtitleWithFallbackToTitle}
-												</a>
+									<ul class="tx-sgnews-list tx-sgnews-overview">
+										<sg:related news="{newsMetaData.news}"
+													relatedNews="{newsMetaData.news.relatedNews}"
+													as="relatedNewsEntry">
+											<li class="col-md-4 col-sm-6 col-xs-12">
+												<f:render partial="Teaser" arguments="{
+													newsMetaData: relatedNewsEntry,
+													headerTag: '<h2>',
+													closingHeaderTag: '</h2>',
+													showCategory: '1'
+												}" />
 											</li>
-										</f:for>
+										</sg:related>
 									</ul>
 								</div>
 							</f:then>
@@ -221,13 +225,6 @@
 												iteration="iterator"
 												limit="5"
 												as="relatedNewsEntry">
-										<f:variable name="relatedNewsMetaData"
-													value="{
-														news: relatedNewsEntry,
-														teaserImage: relatedNewsEntry.teaserImage,
-														teaserImageObject: relatedNewsEntry.teaserImageObject,
-														category: relatedNewsEntry.category
-													}" />
 										<f:if condition="{iterator.isFirst}">
 											<div class="tx-sgnews-single-related">
 												<h3>
@@ -237,7 +234,7 @@
 										</f:if>
 													<li class="col-md-4 col-sm-6 col-xs-12">
 														<f:render partial="Teaser" arguments="{
-															newsMetaData: relatedNewsMetaData,
+															newsMetaData: relatedNewsEntry,
 															headerTag: '<h2>',
 															closingHeaderTag: '</h2>',
 															showCategory: '1'
-- 
GitLab