diff --git a/packages/vitec/Classes/Command/SsrFlagCommand.php b/packages/vitec/Classes/Command/SsrFlagCommand.php new file mode 100644 index 0000000..b421497 --- /dev/null +++ b/packages/vitec/Classes/Command/SsrFlagCommand.php @@ -0,0 +1,58 @@ +addOption('sync', null, InputOption::VALUE_NONE, 'Create/remove the flag file so it matches the setting'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $setting = $this->ssrFlagService->isEnabledInSettings(); + $flag = $this->ssrFlagService->isFlagSet(); + + $output->writeln('Setting vitec.ssr.enabled: ' . ($setting ? 'ON' : 'off')); + $output->writeln('Flag ' . $this->ssrFlagService->getFlagPath() . ': ' . ($flag ? 'EXISTS (SSR routing active)' : 'absent (SPA as before)')); + + if ($setting === $flag) { + $output->writeln('In sync.'); + return Command::SUCCESS; + } + + if (!$input->getOption('sync')) { + $output->writeln('OUT OF SYNC - run with --sync (or just hit any page: the middleware repairs it).'); + return Command::FAILURE; + } + + $new = $this->ssrFlagService->sync(); + $output->writeln('Synced: flag is now ' . ($new ? 'SET - SSR routing active.' : 'removed - SPA as before.') . ''); + return Command::SUCCESS; + } +} diff --git a/packages/vitec/Classes/Middleware/SyncSsrFlag.php b/packages/vitec/Classes/Middleware/SyncSsrFlag.php new file mode 100644 index 0000000..876d5ff --- /dev/null +++ b/packages/vitec/Classes/Middleware/SyncSsrFlag.php @@ -0,0 +1,34 @@ +ssrFlagService->syncQuietly(); + return $handler->handle($request); + } +} diff --git a/packages/vitec/Classes/Service/SsrFlagService.php b/packages/vitec/Classes/Service/SsrFlagService.php new file mode 100644 index 0000000..adbdfc3 --- /dev/null +++ b/packages/vitec/Classes/Service/SsrFlagService.php @@ -0,0 +1,102 @@ + Settings). + * + * The flag file is what public/.htaccess checks per request (RewriteCond + * ... -f): if it exists, page requests are proxied to the SSR node on the + * Solr VPS; if not, the SPA fallback serves _frontend/index.html as + * before. TYPO3 itself never routes differently - the flag only steers + * Apache, which is why a plain file is used: zero runtime cost in the + * hot path and trivially inspectable on the server. + * + * Saving site settings in the TYPO3 v14 backend fires NO event (core + * SiteWriter::writeSettings dispatches nothing, checked 2026-11-09), so + * the sync runs self-healingly from the SyncSsrFlag middleware on every + * frontend and backend request - a mismatch costs one file operation + * once, agreement costs a file_exists(). The vitec:ssr-flag CLI command + * offers the same sync plus a status view. + */ +final class SsrFlagService +{ + public function __construct(private readonly SiteFinder $siteFinder) + { + } + + public function getFlagPath(): string + { + return Environment::getVarPath() . '/vitec-ssr-on.flag'; + } + + /** + * True as soon as ANY site enables the setting (there is only the + * "vitec" site; iterating keeps us independent of the identifier). + */ + public function isEnabledInSettings(): bool + { + try { + foreach ($this->siteFinder->getAllSites() as $site) { + if ((bool)$site->getSettings()->get('vitec.ssr.enabled', false)) { + return true; + } + } + } catch (SiteNotFoundException) { + // No sites configured - treat as disabled. + } + return false; + } + + public function isFlagSet(): bool + { + return is_file($this->getFlagPath()); + } + + /** + * Creates or removes the flag file so it matches the setting. + * + * @return bool|null The new state if something changed, null if the + * flag already matched the setting. + */ + public function sync(): ?bool + { + $desired = $this->isEnabledInSettings(); + if ($desired === $this->isFlagSet()) { + return null; + } + if ($desired) { + file_put_contents( + $this->getFlagPath(), + "SSR routing is ON (site setting vitec.ssr.enabled).\n" + . "Managed by SsrFlagService - do not create/delete manually,\n" + . "flip the switch in Site Management > Settings instead.\n" + . 'Last sync: ' . date('c') . "\n" + ); + } else { + @unlink($this->getFlagPath()); + } + return $desired; + } + + /** + * Sync variant for the request hot path: never lets an IO hiccup + * (read-only FS, concurrent unlink ...) break request handling. + */ + public function syncQuietly(): void + { + try { + $this->sync(); + } catch (\Throwable) { + // Intentionally silent - next request retries. + } + } +} diff --git a/packages/vitec/Configuration/RequestMiddlewares.php b/packages/vitec/Configuration/RequestMiddlewares.php index 416997a..49d6d08 100755 --- a/packages/vitec/Configuration/RequestMiddlewares.php +++ b/packages/vitec/Configuration/RequestMiddlewares.php @@ -46,5 +46,23 @@ return [ 'typo3/cms-frontend/site', ], ], + // Self-healing sync of the SSR routing flag file against the + // Site Set switch vitec.ssr.enabled (no core event fires on + // settings save, hence per-request sync; cheap file_exists when + // nothing changed). Also registered for the backend below. + 'vitec/sync-ssr-flag' => [ + 'target' => \Evomedien\Vitec\Middleware\SyncSsrFlag::class, + 'after' => [ + 'typo3/cms-core/normalized-params-attribute', + ], + ], + ], + 'backend' => [ + 'vitec/sync-ssr-flag' => [ + 'target' => \Evomedien\Vitec\Middleware\SyncSsrFlag::class, + 'after' => [ + 'typo3/cms-core/normalized-params-attribute', + ], + ], ], ]; diff --git a/packages/vitec/Configuration/Sets/Vitecset/settings.definitions.yaml b/packages/vitec/Configuration/Sets/Vitecset/settings.definitions.yaml index beabfc6..b8a302e 100644 --- a/packages/vitec/Configuration/Sets/Vitecset/settings.definitions.yaml +++ b/packages/vitec/Configuration/Sets/Vitecset/settings.definitions.yaml @@ -132,3 +132,10 @@ settings: category: seo type: string default: '' + + vitec.ssr.enabled: + label: 'SSR delivery (serve pages from the SSR node)' + description: 'ON: page requests are proxied by .htaccess to the SSR server (React Router SSR on the Solr VPS) - the flag file var/vitec-ssr-on.flag is kept in sync automatically within seconds. OFF: pages are served as today (SPA shell from _frontend/). JSON under /_json, /typo3, /api etc. are never affected. Safe rollback switch.' + category: VITEC + type: bool + default: false diff --git a/public/.htaccess b/public/.htaccess index e6bc0b8..20d9563 100644 --- a/public/.htaccess +++ b/public/.htaccess @@ -345,6 +345,29 @@ AddDefaultCharset utf-8 RewriteCond %{QUERY_STRING} __typo3_install RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^.*$ %{ENV:CWD}index.php [QSA,L] + # --- VITEC SSR routing (flag-gated) ----------------------------------- + # Active ONLY while the flag file below exists. Mirrors the SPA + # fallback's exclusions but PROXIES page requests to the SSR node on + # the Solr VPS instead of serving the React shell (mod_proxy incl. + # https target verified on this host 2026-11-09, see Nextcloud + # vitec-solr/ssr/README-SSR-Setup.md). The flag is written/removed by + # EXT:vitec (Site Set switch "vitec.ssr.enabled" -> SsrFlagService, + # synced by the SyncSsrFlag middleware / vitec:ssr-flag command). + # Emergency off without TYPO3: delete the flag file. + RewriteCond /usr/www/users/vitecevo/live/var/vitec-ssr-on.flag -f + RewriteRule ^$ https://vitec-ssr.evomedien.de/ [P,L] + RewriteCond /usr/www/users/vitecevo/live/var/vitec-ssr-on.flag -f + RewriteCond %{REQUEST_FILENAME} !-f + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-l + RewriteCond %{REQUEST_URI} !^/_json(/|$) + RewriteCond %{REQUEST_URI} !^/typo3(/|$) + RewriteCond %{REQUEST_URI} !^/download/ + RewriteCond %{REQUEST_URI} !^/api/ + RewriteCond %{REQUEST_URI} !^/_frontend/ + RewriteCond %{REQUEST_URI} !^/sitemap + RewriteRule ^(.*)$ https://vitec-ssr.evomedien.de/$1 [P,L] + # ---------------------------------------------------------------------- # The root path needs its own rule: for "/" REQUEST_FILENAME is the # docroot DIRECTORY, so the !-d guard below skips it and DirectoryIndex # would hand it to TYPO3 (JSON). The homepage JSON lives at /_json/.