(old-site SEO URLs) * Real page: /success-stories/story/ (subpage holding ONLY the * usecaseshow plugin) * * TYPO3's page router always resolves /success-stories/ to the LIST * page (longest page-slug prefix), so the detail subpage could never answer * that URL. This middleware rewrites the request path internally — before * page resolution — whenever matches an existing (visible) success * story. Real subpages of /success-stories are unaffected: no story match, * no rewrite. The browser URL never changes. * * Exception-safe: any failure leaves the request untouched. */ final class SuccessStoryPathRewrite implements MiddlewareInterface { private const LIST_PATH = '/success-stories'; private const DETAIL_SEGMENT = 'story'; public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { $debug = 'no-match'; try { $path = $request->getUri()->getPath(); if (preg_match('#^' . self::LIST_PATH . '/([a-zA-Z0-9\-]+)/?$#', $path, $m) === 1 && $m[1] !== self::DETAIL_SEGMENT ) { if ($this->isStorySlug($m[1])) { $uri = $request->getUri()->withPath( self::LIST_PATH . '/' . self::DETAIL_SEGMENT . '/' . $m[1] ); $request = $request->withUri($uri); $debug = 'rewritten'; } else { $debug = 'slug-not-found:' . $m[1]; } } } catch (\Throwable $e) { $debug = 'exception:' . substr($e->getMessage(), 0, 60); } // TEMP-DIAG header (remove after verification) return $handler->handle($request)->withHeader('X-Vitec-Story-Rewrite', $debug); } private function isStorySlug(string $slug): bool { $qb = GeneralUtility::makeInstance(ConnectionPool::class) ->getQueryBuilderForTable('tx_vitec_domain_model_usecase'); $row = $qb ->select('uid') ->from('tx_vitec_domain_model_usecase') ->where( $qb->expr()->eq('slug', $qb->createNamedParameter($slug)), $qb->expr()->eq('deleted', 0), $qb->expr()->eq('hidden', 0) ) ->setMaxResults(1) ->executeQuery() ->fetchAssociative(); return $row !== false; } }