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