diff --git a/Classes/Common/PageTitleProvider.php b/Classes/Common/PageTitleProvider.php new file mode 100644 index 0000000..589aa17 --- /dev/null +++ b/Classes/Common/PageTitleProvider.php @@ -0,0 +1,16 @@ +title = $title; + } +} diff --git a/Classes/Controller/SearchController.php b/Classes/Controller/SearchController.php index 4c4fd63..e3f928e 100644 --- a/Classes/Controller/SearchController.php +++ b/Classes/Controller/SearchController.php @@ -12,90 +12,104 @@ use TYPO3\CMS\Core\Utility\GeneralUtility; use Elastic\Elasticsearch\Exception\ClientResponseException; use Elastic\Transport\Exception\RuntimeException; - -// ToDo: -// Organize the transfer of the necessary parameters (index name, fields, etc.) from the other extensions (ExtensionConfiguration?) -> see in ElasticSearchServic -// Elastic Search Index return standardized fields? Standardized search fields or own params from the respective extension? -// process search parameters from the URL query parameters to search +use TYPO3\CMS\Core\MetaTag\MetaTagManagerRegistry; +use Slub\LisztCommon\Common\PageTitleProvider; +use TYPO3\CMS\Core\Page\AssetCollector; +use TYPO3\CMS\Core\Cache\CacheManager; +use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface; final class SearchController extends ClientEnabledController { // set resultLimit as intern variable from $this->settings['resultLimit']; protected int $resultLimit; + private FrontendInterface $runtimeCache; // Dependency Injection of Repository // https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/ApiOverview/DependencyInjection/Index.html#Dependency-Injection - public function __construct( private readonly ElasticSearchServiceInterface $elasticSearchService, - protected ExtensionConfiguration $extConf - ) - { + protected ExtensionConfiguration $extConf, + private readonly PageTitleProvider $titleProvider, + private readonly AssetCollector $assetCollector, + CacheManager $cacheManager, + ) { $this->resultLimit = $this->settings['resultLimit'] ?? 25; + $this->runtimeCache = $cacheManager->getCache('runtime'); } public function indexAction(array $searchParams = []): ResponseInterface { - $language = $this->request->getAttribute('language'); - $locale = $language->getLocale(); - if ( - isset($searchParams['page']) && - (int) $searchParams['page'] > 0 - ) { - $currentPage = (int) $searchParams['page']; - } else { - $currentPage = 1; - } - - // $totalItems = $this->elasticSearchService->count($searchParams, $this->settings); - //$totalItems = 100; + $locale = $this->request->getAttribute('language')->getLocale(); + $currentPage = $this->getCurrentPage($searchParams); + $this->addViewTransitionStyle(); $elasticResponse = $this->elasticSearchService->search($searchParams, $this->settings); - $paginator = (new Paginator())-> - setPage($currentPage)-> - setTotalItems($elasticResponse['hits']['total']['value'])-> - setExtensionConfiguration($this->extConf); - $pagination = $paginator->getPagination(); - $showPagination = $paginator->getTotalPages() > 1 ? true : false; - - $this->view->assign('locale', $locale); - $this->view->assign('totalItems', $elasticResponse['hits']['total']['value']); - $this->view->assign('searchParams', $searchParams); - $this->view->assign('searchResults', $elasticResponse); - $this->view->assign('pagination', $pagination); - $this->view->assign('showPagination', $showPagination); - // $this->view->assign('totalItems', $totalItems); - $this->view->assign('currentString', Paginator::CURRENT_PAGE); - $this->view->assign('dots', Paginator::DOTS); - $this->view->assign('detailPageId', $this->extConf->get('liszt_common','detailPageId')); + $totalItems = 0; + if (isset($elasticResponse['hits'], $elasticResponse['hits']['total'], $elasticResponse['hits']['total']['value'])) { + $totalItems = (int)$elasticResponse['hits']['total']['value']; + } + $paginator = (new Paginator()) + ->setPage($currentPage) + ->setTotalItems($totalItems) + ->setExtensionConfiguration($this->extConf); + $pagination = $paginator->getPagination(); + $showPagination = $paginator->getTotalPages() > 1; + + $this->view->assignMultiple([ + 'locale' => $locale, + 'totalItems' => $totalItems, + 'searchParams' => $searchParams, + 'searchResults' => $elasticResponse, + 'pagination' => $pagination, + 'showPagination'=> $showPagination, + 'currentString' => Paginator::CURRENT_PAGE, + 'dots' => Paginator::DOTS, + 'detailPageId' => $this->extConf->get('liszt_common', 'detailPageId'), + ]); return $this->htmlResponse(); } + public function searchBarAction(array $searchParams = []): ResponseInterface { $this->view->assign('searchParams', $searchParams); return $this->htmlResponse(); } - public function detailsAction(array $searchParams = []): ResponseInterface + + public function detailsHeaderAction(array $searchParams = []): ResponseInterface { + $documentId = $this->getDocumentIdFromRouting(); + if (!$documentId) { + return $this->redirectToNotFoundPage(); + } + + try { + $elasticResponse = $this->loadDetailPageFromElastic($documentId); + } catch (ClientResponseException $e) { + if ($e->getCode() === 404) { + return $this->redirectToNotFoundPage(); + } + throw $e; + } + + $this->view->assign('searchResult', $elasticResponse); + return $this->htmlResponse(); + } - $routing = $this->request->getAttribute('routing'); - $routingArgs = $routing->getArguments(); - // Check if 'tx_lisztcommon_searchdetails' exists and if 'detailId' has a valid value. - $documentId = null; - if (!empty($routingArgs['tx_lisztcommon_searchdetails']['documentId'])) { - $documentId = $routingArgs['tx_lisztcommon_searchdetails']['documentId']; - } else { + public function detailsAction(array $searchParams = []): ResponseInterface + { + $documentId = $this->getDocumentIdFromRouting(); + if (!$documentId) { return $this->redirectToNotFoundPage(); } try { - $elasticResponse = $this->elasticSearchService->getDocumentById($documentId, []); + $elasticResponse = $this->loadDetailPageFromElastic($documentId); } catch (ClientResponseException $e) { // Handle 404 errors if ($e->getCode() === 404) { @@ -105,25 +119,82 @@ public function detailsAction(array $searchParams = []): ResponseInterface } - $this->view->assign('searchParams', $searchParams); - $this->view->assign('routingArgs', $routingArgs); - $this->view->assign('detailId', $documentId); - $this->view->assign('searchResult', $elasticResponse); - $this->view->assign('detailPageId', $this->extConf->get('liszt_common','detailPageId')); + // set page title + $pageTitle = $this->formatPageTitle($elasticResponse['_source']['title'] ?? 'Details'); + $this->titleProvider->setTitle($pageTitle); + + // set meta description + $metaDescription = ''; + if (isset($elasticResponse['_source'], $elasticResponse['_source']['tx_lisztcommon_searchable'])) { + $metaDescription = (string)$elasticResponse['_source']['tx_lisztcommon_searchable']; + } + $metaTagManager = GeneralUtility::makeInstance(MetaTagManagerRegistry::class) + ->getManagerForProperty('description'); + $metaTagManager->addProperty('description', $metaDescription); + + $this->addViewTransitionStyle(); + + $this->view->assignMultiple([ + 'searchParams' => $searchParams, + 'routingArgs' => $this->request->getAttribute('routing')->getArguments(), + 'detailId' => $documentId, + 'searchResult' => $elasticResponse, + 'detailPageId' => $this->extConf->get('liszt_common', 'detailPageId'), + ]); return $this->htmlResponse(); + } + + private function getCurrentPage(array $params): int + { + return (isset($params['page']) && (int)$params['page'] > 0) ? (int)$params['page'] : 1; + } + // add view transition styles as inline style for animation + private function addViewTransitionStyle(): void + { + $this->assetCollector->addInlineStyleSheet( + 'view-transitions-root', + '@media screen and (prefers-reduced-motion: no-preference) { @view-transition { navigation: auto; } }' + ); } + private function getDocumentIdFromRouting(): ?string + { + $routingArgs = $this->request->getAttribute('routing')->getArguments(); + return $routingArgs['tx_lisztcommon_searchdetails']['documentId'] ?? null; + } + + private function formatPageTitle(string $title): string + { + $maxTitleLength = 50; + return (mb_strlen($title) > $maxTitleLength) + ? mb_substr($title, 0, $maxTitleLength - 3) . '...' + : $title; + } + + private function loadDetailPageFromElastic(string $documentId): Collection + { + $cachedResult = $this->runtimeCache->get($documentId); + if (!$cachedResult instanceof Collection) { + $result = $this->elasticSearchService->getDocumentById($documentId, []); + if (!$result instanceof Collection) { + throw new \UnexpectedValueException("The return value of getDocumentById does not correspond to the expected collection type."); } + $this->runtimeCache->set($documentId, $result); + return $result; + } + return $cachedResult; + } + + public function redirectToNotFoundPage(): ResponseInterface { - // see: https://docs.typo3.org/m/typo3/reference-coreapi/12.4/en-us/ExtensionArchitecture/Extbase/Reference/Controller/ActionController.html + // see: https://docs.typo3.org/m/typo3/reference-coreapi/12.4/en-us/ExtensionArchitecture/Extbase/Reference/Controller/ActionController.html // $uri could also be https://example.com/any/uri // $this->resourceFactory is injected as part of the `ActionController` inheritance return $this->responseFactory->createResponse(301) ->withHeader('Location', '/404/'); // the uri of the 404 page from typo installation } - } diff --git a/Classes/Event/Listener/SiteConfigurationLoadedEventListener.php b/Classes/Event/Listener/SiteConfigurationLoadedEventListener.php new file mode 100644 index 0000000..686913b --- /dev/null +++ b/Classes/Event/Listener/SiteConfigurationLoadedEventListener.php @@ -0,0 +1,25 @@ +getConfiguration(); + $fileLoader = GeneralUtility::makeInstance(YamlFileLoader::class); + $routeEnhancersConfiguration = $fileLoader->load('EXT:liszt_common/Configuration/Routing/routeEnhancers.yaml'); + ArrayUtility::mergeRecursiveWithOverrule($configuration, $routeEnhancersConfiguration); + $event->setConfiguration($configuration); + } + +} diff --git a/Classes/ViewHelpers/GetDetailBreadCrumbArrayViewHelper.php b/Classes/ViewHelpers/GetDetailBreadCrumbArrayViewHelper.php new file mode 100644 index 0000000..548cc4a --- /dev/null +++ b/Classes/ViewHelpers/GetDetailBreadCrumbArrayViewHelper.php @@ -0,0 +1,23 @@ +getRequest();$frontendController = $request->getAttribute('frontend.controller'); + $rootline = $frontendController->rootLine; + return array_reverse($rootline); + } +} diff --git a/Classes/Routing/Configuration/config.yaml b/Configuration/Routing/routeEnhancers.yaml similarity index 100% rename from Classes/Routing/Configuration/config.yaml rename to Configuration/Routing/routeEnhancers.yaml diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml index 5ce66db..afc2520 100644 --- a/Configuration/Services.yaml +++ b/Configuration/Services.yaml @@ -6,5 +6,14 @@ services: Slub\LisztCommon\: resource: '../Classes/*' exclude: '../Classes/Domain/Model/*' + Slub\LisztCommon\Event\Listener\SiteConfigurationLoadedEventListener: + tags: + - name: event.listener + method: modify + identifier: 'ext-lisztcommon/site-configuration-loaded' + event: TYPO3\CMS\Core\Configuration\Event\SiteConfigurationLoadedEvent + + + # if more than one! class implement the same interface (i.e.Search Interface) read: https://usetypo3.com/dependency-injection/ # and https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/ApiOverview/DependencyInjection/Index.html diff --git a/Configuration/TCA/Overrides/tt_content.php b/Configuration/TCA/Overrides/tt_content.php index cd6c19d..c90b05a 100644 --- a/Configuration/TCA/Overrides/tt_content.php +++ b/Configuration/TCA/Overrides/tt_content.php @@ -26,6 +26,13 @@ 'Liszt Search Details' ); + +ExtensionUtility::registerPlugin( + 'liszt_common', + 'SearchDetailsHeader', + 'Liszt Search Details Header' +); + // Adds the content element to the "Type" dropdown ExtensionManagementUtility::addTcaSelectItem( 'tt_content', diff --git a/Configuration/TypoScript/constants.typoscript b/Configuration/TypoScript/constants.typoscript index 405f315..604c114 100644 --- a/Configuration/TypoScript/constants.typoscript +++ b/Configuration/TypoScript/constants.typoscript @@ -9,12 +9,4 @@ }*/ -/* -plugin.tx_liszt_common { -view { -templateRootPath = EXT:liszt_common/Resources/Private/Templates/ -partialRootPath = EXT:liszt_common/Resources/Private/Partials/ -layoutRootPath = EXT:liszt_common/Resources/Private/Layouts/ -} -}*/ diff --git a/Configuration/TypoScript/setup.typoscript b/Configuration/TypoScript/setup.typoscript index 1b8ab80..646257d 100644 --- a/Configuration/TypoScript/setup.typoscript +++ b/Configuration/TypoScript/setup.typoscript @@ -17,10 +17,18 @@ page { partialRootPaths { 20 = EXT:liszt_common/Resources/Private/Partials/ } + layoutRootPaths { + 20 = EXT:liszt_common/Resources/Private/Layouts/ + } + } + +// 20 < tt_content.list.20.lisztcommon_searchdetails + } + lib.contentElement { templateRootPaths { 200 = EXT:liszt_common/Resources/Private/ExtensionOverrides/fluid_styled_content/Templates/ @@ -54,6 +62,15 @@ plugin.tx_lisztcommon_searchlisting.view.pluginNamespace = tx_liszt_common_searc plugin.tx_lisztcommon_searchbar.mvc.callDefaultActionIfActionCantBeResolved = 1; +// set own pageTitlePovider +config { + pageTitleProviders { + liszt_common { + provider = Slub\LisztCommon\Common\PageTitleProvider + before = record + } + } +} # get the selected frontend layout from page table for show/hide SearchBar, because bootstrap package not use this param #lib.selectedFrontendLayout = TEXT @@ -61,6 +78,17 @@ plugin.tx_lisztcommon_searchbar.mvc.callDefaultActionIfActionCantBeResolved = 1; +// Partial Root Path to find logo and other Partials from liszt_web +/*plugin.tx_lisztcommon_searchdetails { + view { + partialRootPaths { + 10 = EXT:liszt_common/Resources/Private/Partials/ + 20 = EXT:liszt_web/Resources/Private/Partials/ + } + } +} +*/ + # copy element in PAGE object to display in fluid template # page = PAGE /*page { diff --git a/Resources/Private/Language/de.locallang.xlf b/Resources/Private/Language/de.locallang.xlf index 5e85920..7b61673 100644 --- a/Resources/Private/Language/de.locallang.xlf +++ b/Resources/Private/Language/de.locallang.xlf @@ -72,6 +72,9 @@ Elemente pro Seite der Ergebnisliste + + Id der Suchseite + Id der Detailseite diff --git a/Resources/Private/Layouts/DetailPage.html b/Resources/Private/Layouts/DetailPage.html new file mode 100644 index 0000000..b1801da --- /dev/null +++ b/Resources/Private/Layouts/DetailPage.html @@ -0,0 +1,17 @@ + + + +{f:translate(key: 'skipNavTitle', extensionName: 'liszt_web')} + + + + + + + + + + + + + diff --git a/Resources/Private/Partials/DetailBreadcrumb.html b/Resources/Private/Partials/DetailBreadcrumb.html new file mode 100644 index 0000000..b4200b9 --- /dev/null +++ b/Resources/Private/Partials/DetailBreadcrumb.html @@ -0,0 +1,24 @@ +{namespace lc=Slub\LisztCommon\ViewHelpers} + +{lc:getDetailBreadCrumbArray()} + + diff --git a/Resources/Private/Partials/DetailPageHeader.html b/Resources/Private/Partials/DetailPageHeader.html new file mode 100644 index 0000000..36a7d54 --- /dev/null +++ b/Resources/Private/Partials/DetailPageHeader.html @@ -0,0 +1,15 @@ + + + diff --git a/Resources/Private/Templates/Search/Details.html b/Resources/Private/Templates/Search/Details.html index 782b3b1..288dff6 100644 --- a/Resources/Private/Templates/Search/Details.html +++ b/Resources/Private/Templates/Search/Details.html @@ -5,9 +5,13 @@ xmlns:i="http://typo3.org/ns/Quellenform/Iconpack/ViewHelpers" data-namespace-typo3-fluid="true"> LLL:EXT:{settings.entityTypes.1.extensionName}/Resources/Private/Language/{settings.entityTypes.1.languageFile}.xlf -{_all} + + +
+ + +
diff --git a/Resources/Private/Templates/Search/DetailsHeader.html b/Resources/Private/Templates/Search/DetailsHeader.html new file mode 100644 index 0000000..0a7e872 --- /dev/null +++ b/Resources/Private/Templates/Search/DetailsHeader.html @@ -0,0 +1,19 @@ + + +
+ +

{searchResult._source.tx_lisztcommon_header}

+
+

+ + + {searchResult._source.tx_lisztcommon_body} + + + {searchResult._source.title} + + +

+
+ + diff --git a/Resources/Private/Templates/Search/Index.html b/Resources/Private/Templates/Search/Index.html index 7bdf673..0bdb39d 100644 --- a/Resources/Private/Templates/Search/Index.html +++ b/Resources/Private/Templates/Search/Index.html @@ -57,19 +57,18 @@
{f:translate(key: 'filter_container_label', extensio
- ToDo: pageUid should not hardcodet here -> from config? + class="searchresults-item-detail-link prefetch-link" >
-
+
{hit._source.tx_lisztcommon_header}
-
+

diff --git a/Resources/Private/Templates/SearchDetails.html b/Resources/Private/Templates/SearchDetails.html index 0ebcd35..012e89d 100644 --- a/Resources/Private/Templates/SearchDetails.html +++ b/Resources/Private/Templates/SearchDetails.html @@ -3,19 +3,15 @@ xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" xmlns:vac="http://typo3.org/ns/Praetorius/ViteAssetCollector/ViewHelpers" > - + + -
- -
-
- -
+ {f:cObject(typoscriptObjectPath: 'lib.dynamicContent', data: '{colPos: 0, slide: -1, max: 1}')} + + diff --git a/ext_conf_template.txt b/ext_conf_template.txt index d312e67..817530c 100644 --- a/ext_conf_template.txt +++ b/ext_conf_template.txt @@ -12,3 +12,5 @@ paginationRange = 2,3,4,5 itemsPerPage = 10 # cat=Search; type=int; label=LLL:EXT:liszt_common/Resources/Private/Language/locallang.xml:config.detailPageId detailPageId = 38 +/*# cat=Search; type=int; label=LLL:EXT:liszt_common/Resources/Private/Language/locallang.xml:config.searchPageId +searchPageId = 18*/ diff --git a/ext_localconf.php b/ext_localconf.php index 775f7a0..9f94711 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -36,6 +36,14 @@ [ ], ); +// cache Detail Pages? +ExtensionUtility::configurePlugin( + 'LisztCommon', + 'SearchDetailsHeader', + [ SearchController::class => 'detailsHeader' ], + [ ], +); + ExtensionManagementUtility::addPageTSConfig( ''