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. } } }