From 48e049671ee3010908ab3c5ec6563fd60157cc8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Thu, 22 Oct 2026 15:43:24 +0200 Subject: [PATCH] feat: search bar on TYPO3 13, loaded into the static pages, searching by GET The search bar in the page header is back (SearchbarController, lib.searchBar and lib.searchBarCollapsed), in the Subpage, SubpageSidebarRight and Detail layouts as on TYPO3 10; product pages get the collapsed bar and its toggle button in the main navigation again. - The bar takes no arguments, so it is the same on every page of a site and cacheable. Page types 1706/1707 (/searchbar, /searchbar-collapsed on the ep-reisen root, also under /en/) render it alone for the static pages, which replace their exported bar with it via htmx. The suffixes have no leading slash: on the root page the route path has none, so "/searchbar" would never match (as /form and /snowreport do not there either). - The form sends dateFrom, dateTo, pax, destinationType, destinationUid and priceRange by GET to the search page instead of POSTing to processSearch: no signed fields and no redirect, and URLs of about 100 characters instead of about 870. SearchController reads them (SEARCHBAR_PARAMETERS, excluded from the cHash; the results are not cached) and drops anything but a real Y-m-d date, which made DateUtility::formatDateRange() throw. processSearch is removed. - The date range label is empty without arguments, so the template no longer needs ep:dateRange. Compared with TYPO3 10: the bar's markup matches apart from the form (open, collapsed, /en/); seven searches by GET load the same filter settings and hit counts as the TYPO3 10 POST. The static pages get the island from the exporter in EP/website, see .ddev/plans/static-export-searchbar-island.patch (local only). Co-Authored-By: Claude Opus 5.5 --- config/sites/ep-reisen/config.yaml | 2 + .../Classes/Controller/SearchController.php | 50 ++-- .../Controller/SearchbarController.php | 66 +++++ .../RequestArgumentTypeConversionTrait.php | 19 -- packages/ep_products/ext_localconf.php | 20 +- .../TypoScript/Library/SearchBar.typoscript | 14 + .../TypoScript/Page/Searchbar.typoscript | 25 ++ .../Private/Layouts/Page/Detail.html | 7 +- .../Private/Layouts/Page/Subpage.html | 5 + .../Layouts/Page/SubpageSidebarRight.html | 5 + .../Private/Templates/Searchbar/Index.html | 280 ++++++++++++++++++ 11 files changed, 444 insertions(+), 49 deletions(-) create mode 100644 packages/ep_products/Classes/Controller/SearchbarController.php create mode 100644 packages/ep_theme/Configuration/TypoScript/Library/SearchBar.typoscript create mode 100644 packages/ep_theme/Configuration/TypoScript/Page/Searchbar.typoscript create mode 100644 packages/ep_theme/Resources/Private/Templates/Searchbar/Index.html diff --git a/config/sites/ep-reisen/config.yaml b/config/sites/ep-reisen/config.yaml index 8d85bbb..f6acfe2 100644 --- a/config/sites/ep-reisen/config.yaml +++ b/config/sites/ep-reisen/config.yaml @@ -284,6 +284,8 @@ routeEnhancers: /html: 1702 /form: 1704 /captcha: 3413 + searchbar: 1706 + searchbar-collapsed: 1707 /snowreport: 1705 .json: 1803 .ical: 1710 diff --git a/packages/ep_products/Classes/Controller/SearchController.php b/packages/ep_products/Classes/Controller/SearchController.php index 0c89099..2bc11f8 100644 --- a/packages/ep_products/Classes/Controller/SearchController.php +++ b/packages/ep_products/Classes/Controller/SearchController.php @@ -40,6 +40,12 @@ class SearchController extends ActionController use RequestArgumentTypeConversionTrait; + /** + * The fields of the search bar (Templates/Searchbar/Index.html), sent by GET to the search + * page and excluded from the cHash: the visitor chooses the values + */ + public const SEARCHBAR_PARAMETERS = ['dateFrom', 'dateTo', 'pax', 'destinationType', 'destinationUid', 'priceRange']; + /** * @var FilterService */ @@ -59,32 +65,6 @@ class SearchController extends ActionController $this->searchResultService = $searchResultService; } - public function initializeProcessSearchAction() - { - $this->processSearchParamsArgument(); - } - - /** - * @param array $searchParams - */ - public function processSearchAction(array $searchParams): ResponseInterface - { - $filterSettings = $this->filterService->getFilterSettingsFromSearchParams($searchParams); - - $redirectUri = $this->uriBuilder - ->reset() - ->setCreateAbsoluteUri(true) - ->uriFor( - 'searchresult', - ['filterSettings' => $filterSettings], - null, - null, - 'searchresult' - ); - - return $this->redirectToUri($redirectUri); - } - public function initializeSearchresultAction() { // Process encoded filtersettings from static search result. @@ -98,6 +78,24 @@ class SearchController extends ActionController $this->request = $this->request->withArgument('filterSettings', $filterSettings); } } + // The fields of the search bar, which sends them here by GET + $searchbarParameters = array_filter( + array_intersect_key($this->request->getQueryParams(), array_flip(self::SEARCHBAR_PARAMETERS)), + 'is_string' + ); + // Anything but a real Y-m-d date is dropped: the URL is easy to edit, and + // DateUtility::formatDateRange() throws on what DateTime cannot read + foreach (['dateFrom', 'dateTo'] as $name) { + $date = \DateTime::createFromFormat('!Y-m-d', $searchbarParameters[$name] ?? ''); + if (!$date || $date->format('Y-m-d') !== $searchbarParameters[$name]) { + unset($searchbarParameters[$name]); + } + } + if ($searchbarParameters !== [] && !$this->request->hasArgument('filterSettings')) { + $searchParams = $this->searchResultService->getNormalizedSearchParams($searchbarParameters); + $filterSettings = $this->filterService->getFilterSettingsFromSearchParams($searchParams); + $this->request = $this->request->withArgument('filterSettings', $filterSettings); + } $forcedConceptUids = GeneralUtility::intExplode(',', $this->settings['forcedConceptUids'] ?? '', true); $this->processFilterSettingsArgument($forcedConceptUids); } diff --git a/packages/ep_products/Classes/Controller/SearchbarController.php b/packages/ep_products/Classes/Controller/SearchbarController.php new file mode 100644 index 0000000..7c7e671 --- /dev/null +++ b/packages/ep_products/Classes/Controller/SearchbarController.php @@ -0,0 +1,66 @@ +, dreipunktnull + * + * 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 EP\EpProducts\Service\FilterService; +use EP\EpProducts\Service\SearchResultService; +use Psr\Http\Message\ResponseInterface; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Extbase\Mvc\Controller\ActionController; + +/** + * The search bar in the page header. It takes no arguments: the options come from the products + * the site can show, so the output is the same on every page of a site (cacheable), and the + * static pages of the hybrid setup load it from /searchbar (Page/Searchbar.typoscript). + * The form sends its fields by GET to the search page (SearchController::SEARCHBAR_PARAMETERS). + */ +class SearchbarController extends ActionController +{ + public function __construct( + private readonly FilterService $filterService, + private readonly SearchResultService $searchResultService, + ) { + } + + public function indexAction(): ResponseInterface + { + $searchParams = $this->searchResultService->getNormalizedSearchParams(); + $forcedConceptUids = GeneralUtility::intExplode(',', $this->settings['forcedConceptUids'] ?? '', true); + $filterSettings = $this->filterService->getFilterSettingsFromSearchParams($searchParams, $forcedConceptUids); + $excludedConceptUids = GeneralUtility::intExplode(',', $this->settings['excludedConceptUids'] ?? '', true); + + $searchResult = $this->searchResultService->getResult($filterSettings, $excludedConceptUids); + + $this->view->assignMultiple([ + 'filterOptions' => $this->filterService->collectFilterOptionsFromSearchResult($searchResult), + 'searchParams' => $searchParams, + 'collapsed' => (bool)($this->settings['collapsed'] ?? false), + ]); + return $this->htmlResponse(); + } +} diff --git a/packages/ep_products/Classes/Traits/RequestArgumentTypeConversionTrait.php b/packages/ep_products/Classes/Traits/RequestArgumentTypeConversionTrait.php index d03fbbb..62bc0a7 100644 --- a/packages/ep_products/Classes/Traits/RequestArgumentTypeConversionTrait.php +++ b/packages/ep_products/Classes/Traits/RequestArgumentTypeConversionTrait.php @@ -3,30 +3,11 @@ namespace EP\EpProducts\Traits; use EP\EpProducts\Service\FilterService; -use EP\EpProducts\Service\SearchResultService; use TYPO3\CMS\Core\Utility\GeneralUtility; trait RequestArgumentTypeConversionTrait { - public function processSearchParamsArgument(): void - { - - /** @var SearchResultService $searchResultService */ - $searchResultService = GeneralUtility::makeInstance(SearchResultService::class); - - if (!$this->request->hasArgument('searchParams')) { - $searchParams = $searchResultService->getNormalizedSearchParams(); - } else { - $requestArgument = $this->request->getArgument('searchParams'); - $searchParamsFromRequest = is_array($requestArgument) ? - $requestArgument : json_decode($requestArgument, true, 512, JSON_THROW_ON_ERROR); - $searchParams = $searchResultService->getNormalizedSearchParams($searchParamsFromRequest); - } - - $this->request = $this->request->withArgument('searchParams', $searchParams); - } - public function processFilterSettingsArgument(array $forcedConceptUids = []): void { diff --git a/packages/ep_products/ext_localconf.php b/packages/ep_products/ext_localconf.php index c6c21d6..1bd9fe9 100644 --- a/packages/ep_products/ext_localconf.php +++ b/packages/ep_products/ext_localconf.php @@ -2,6 +2,7 @@ use TYPO3\CMS\Extbase\Utility\ExtensionUtility; use EP\EpProducts\Controller\ProductController; use EP\EpProducts\Controller\SearchController; +use EP\EpProducts\Controller\SearchbarController; use EP\EpProducts\Controller\SnowreportController; use EP\EpProducts\Controller\TravelinfoController; use EP\EpProducts\Controller\WatchlistController; @@ -59,10 +60,19 @@ $boot = function () { 'EpProducts', 'searchresult', [ - SearchController::class => 'searchresult,processSearch', + SearchController::class => 'searchresult', ], [ - SearchController::class => 'searchresult,processSearch', + SearchController::class => 'searchresult', + ] + ); + + // The search bar in the page header (lib.searchBar), not a content element + ExtensionUtility::configurePlugin( + 'EpProducts', + 'searchbar', + [ + SearchbarController::class => 'index', ] ); @@ -123,6 +133,12 @@ $boot = function () { // The client-side callers that vary month/year (calendar_controller.js) send them in a // POST body, which the cHash never inspects, so nothing depends on the exclusion. $GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['excludedParameters'][] = 'f'; + // The search bar sends its fields by GET to the search page, whose results are not cached + // (SearchController::SEARCHBAR_PARAMETERS) + array_push( + $GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['excludedParameters'], + ...SearchController::SEARCHBAR_PARAMETERS + ); $GLOBALS['TYPO3_CONF_VARS']['SYS']['routing']['aspects']['PassthroughMapper'] = PassthroughMapper::class; diff --git a/packages/ep_theme/Configuration/TypoScript/Library/SearchBar.typoscript b/packages/ep_theme/Configuration/TypoScript/Library/SearchBar.typoscript new file mode 100644 index 0000000..1e69b05 --- /dev/null +++ b/packages/ep_theme/Configuration/TypoScript/Library/SearchBar.typoscript @@ -0,0 +1,14 @@ +# The search bar in the page header, open (sticky once scrolled past) or collapsed behind the +# search icon of the main navigation (product and hotel pages) +lib.searchBar = USER +lib.searchBar { + userFunc = TYPO3\CMS\Extbase\Core\Bootstrap->run + extensionName = EpProducts + pluginName = searchbar + settings =< plugin.tx_epproducts.settings + persistence =< plugin.tx_epproducts.persistence + view =< plugin.tx_epproducts.view +} + +lib.searchBarCollapsed < lib.searchBar +lib.searchBarCollapsed.settings.collapsed = 1 diff --git a/packages/ep_theme/Configuration/TypoScript/Page/Searchbar.typoscript b/packages/ep_theme/Configuration/TypoScript/Page/Searchbar.typoscript new file mode 100644 index 0000000..d9cf1a3 --- /dev/null +++ b/packages/ep_theme/Configuration/TypoScript/Page/Searchbar.typoscript @@ -0,0 +1,25 @@ +# The search bar without the page around it, on the site root (/searchbar, /searchbar-collapsed). +# The static pages of the hybrid setup load it with htmx where the export had the bar: it is the +# same on every page of a site, and its options follow the imported products (ep:import-dates +# flushes the page cache). The form sends its fields by GET to the search page. + +tx_eptheme_searchbar = PAGE +tx_eptheme_searchbar { + typeNum = 1706 + config { + disableAllHeaderCode = 1 + disablePrefixComment = 1 + additionalHeaders.10.header = Content-Type: text/html; charset=utf-8 + additionalHeaders.20.header = X-Robots-Tag: noindex, nofollow + admPanel = 0 + debug = 0 + } + + 10 =< lib.searchBar +} + +tx_eptheme_searchbar_collapsed < tx_eptheme_searchbar +tx_eptheme_searchbar_collapsed { + typeNum = 1707 + 10 =< lib.searchBarCollapsed +} diff --git a/packages/ep_theme/Resources/Private/Layouts/Page/Detail.html b/packages/ep_theme/Resources/Private/Layouts/Page/Detail.html index 64be84e..6818ea7 100644 --- a/packages/ep_theme/Resources/Private/Layouts/Page/Detail.html +++ b/packages/ep_theme/Resources/Private/Layouts/Page/Detail.html @@ -2,8 +2,11 @@ xmlns="http://www.w3.org/1999/xhtml" xmlns:f="http://typo3.org/ns/fluid/ViewHelpers"> - - + + + + +
diff --git a/packages/ep_theme/Resources/Private/Layouts/Page/Subpage.html b/packages/ep_theme/Resources/Private/Layouts/Page/Subpage.html index 6317c28..215d855 100644 --- a/packages/ep_theme/Resources/Private/Layouts/Page/Subpage.html +++ b/packages/ep_theme/Resources/Private/Layouts/Page/Subpage.html @@ -5,6 +5,11 @@
+ + + + +
diff --git a/packages/ep_theme/Resources/Private/Layouts/Page/SubpageSidebarRight.html b/packages/ep_theme/Resources/Private/Layouts/Page/SubpageSidebarRight.html index 41b820f..9336a40 100644 --- a/packages/ep_theme/Resources/Private/Layouts/Page/SubpageSidebarRight.html +++ b/packages/ep_theme/Resources/Private/Layouts/Page/SubpageSidebarRight.html @@ -5,6 +5,11 @@
+ + + + +
diff --git a/packages/ep_theme/Resources/Private/Templates/Searchbar/Index.html b/packages/ep_theme/Resources/Private/Templates/Searchbar/Index.html new file mode 100644 index 0000000..9139ae7 --- /dev/null +++ b/packages/ep_theme/Resources/Private/Templates/Searchbar/Index.html @@ -0,0 +1,280 @@ + + + + + +
+ data-searchbar-collapsed-value="true" + + data-action="datepicker:range-select->searchbar#setDateRange searchbar-toggle:toggle@window->searchbar#toggle"> + + GET to the search page, read by SearchController::initializeSearchresultAction() + (SEARCHBAR_PARAMETERS). searchbar_controller.js finds the fields by their target. + +
+ + + + + + +
+ + data-controller="sticky" + data-sticky-offset-value="650" + data-sticky-padding-value="7.5rem" + data-sticky-disabled-value="false" + data-sticky-classes-value="sticky reduced" + + + data-searchbar-target="container"> +
+
+
+ +
+
+ + + + + +
+
+ +
+
+ + + + + +
+
+ +
+
+ + + + + +
+
+ +
+
+ + + + + +
+
+ +
+ +
+
+ + +
+
+
+
+
+
+ +