diff --git a/services/web/frontend/js/marketing.ts b/services/web/frontend/js/marketing.ts
index ad32a0d29b..5e360b2dd7 100644
--- a/services/web/frontend/js/marketing.ts
+++ b/services/web/frontend/js/marketing.ts
@@ -9,4 +9,3 @@ import './features/multi-submit'
import './features/cookie-banner'
import './features/autoplay-video'
import './features/mathjax'
-import './features/header-footer-react'
diff --git a/services/web/frontend/js/pages/compromised-password.tsx b/services/web/frontend/js/pages/compromised-password.tsx
index 5c1303dc07..b163cfeb9e 100644
--- a/services/web/frontend/js/pages/compromised-password.tsx
+++ b/services/web/frontend/js/pages/compromised-password.tsx
@@ -1,13 +1,5 @@
-import '../marketing'
+import { renderInReactLayout } from '@/react'
-import { createRoot } from 'react-dom/client'
-import { CompromisedPasswordCard } from '../features/compromised-password/components/compromised-password-root'
+import { CompromisedPasswordCard } from '@/features/compromised-password/components/compromised-password-root'
-const compromisedPasswordContainer = document.getElementById(
- 'compromised-password'
-)
-
-if (compromisedPasswordContainer) {
- const root = createRoot(compromisedPasswordContainer)
- root.render()
-}
+renderInReactLayout('compromised-password', () => )
diff --git a/services/web/frontend/js/pages/socket-diagnostics.tsx b/services/web/frontend/js/pages/socket-diagnostics.tsx
index bd4f2e522a..5219e82a23 100644
--- a/services/web/frontend/js/pages/socket-diagnostics.tsx
+++ b/services/web/frontend/js/pages/socket-diagnostics.tsx
@@ -1,11 +1,5 @@
-import '../marketing'
+import { renderInReactLayout } from '@/react'
-import { createRoot } from 'react-dom/client'
import { SocketDiagnostics } from '@/features/socket-diagnostics/components/socket-diagnostics'
-const socketDiagnosticsContainer = document.getElementById('socket-diagnostics')
-
-if (socketDiagnosticsContainer) {
- const root = createRoot(socketDiagnosticsContainer)
- root.render()
-}
+renderInReactLayout('socket-diagnostics', () => )
diff --git a/services/web/frontend/js/pages/user/add-secondary-email.tsx b/services/web/frontend/js/pages/user/add-secondary-email.tsx
index 7c9d5bced9..987031dd61 100644
--- a/services/web/frontend/js/pages/user/add-secondary-email.tsx
+++ b/services/web/frontend/js/pages/user/add-secondary-email.tsx
@@ -1,13 +1,5 @@
-import '../../marketing'
+import { renderInReactLayout } from '@/react'
-import { createRoot } from 'react-dom/client'
-import { AddSecondaryEmailPrompt } from '../../features/settings/components/emails/add-secondary-email-prompt'
+import { AddSecondaryEmailPrompt } from '@/features/settings/components/emails/add-secondary-email-prompt'
-const addSecondaryEmailContainer = document.getElementById(
- 'add-secondary-email'
-)
-
-if (addSecondaryEmailContainer) {
- const root = createRoot(addSecondaryEmailContainer)
- root.render()
-}
+renderInReactLayout('add-secondary-email', () => )
diff --git a/services/web/frontend/js/pages/user/confirm-secondary-email.tsx b/services/web/frontend/js/pages/user/confirm-secondary-email.tsx
index f30203a08d..d4be9364b5 100644
--- a/services/web/frontend/js/pages/user/confirm-secondary-email.tsx
+++ b/services/web/frontend/js/pages/user/confirm-secondary-email.tsx
@@ -1,11 +1,7 @@
-import '../../marketing'
+import { renderInReactLayout } from '@/react'
-import { createRoot } from 'react-dom/client'
-import ConfirmSecondaryEmailForm from '../../features/settings/components/emails/confirm-secondary-email-form'
+import ConfirmSecondaryEmailForm from '@/features/settings/components/emails/confirm-secondary-email-form'
-const confirmEmailContainer = document.getElementById('confirm-secondary-email')
-
-if (confirmEmailContainer) {
- const root = createRoot(confirmEmailContainer)
- root.render()
-}
+renderInReactLayout('confirm-secondary-email', () => (
+
+))
diff --git a/services/web/frontend/js/pages/user/settings.jsx b/services/web/frontend/js/pages/user/settings.jsx
deleted file mode 100644
index 189799fe7c..0000000000
--- a/services/web/frontend/js/pages/user/settings.jsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import '../../marketing'
-import './../../utils/meta'
-import '../../utils/webpack-public-path'
-import './../../infrastructure/error-reporter'
-import '@/i18n'
-import '../../features/settings/components/root'
-import { createRoot } from 'react-dom/client'
-import SettingsPageRoot from '../../features/settings/components/root.tsx'
-
-const element = document.getElementById('settings-page-root')
-// For react-google-recaptcha
-window.recaptchaOptions = {
- enterprise: true,
- useRecaptchaNet: true,
-}
-if (element) {
- const root = createRoot(element)
- root.render()
-}
diff --git a/services/web/frontend/js/pages/user/settings.tsx b/services/web/frontend/js/pages/user/settings.tsx
new file mode 100644
index 0000000000..a425877180
--- /dev/null
+++ b/services/web/frontend/js/pages/user/settings.tsx
@@ -0,0 +1,13 @@
+import { renderInReactLayout } from '@/react'
+import '@/utils/meta'
+import '@/utils/webpack-public-path'
+import '@/infrastructure/error-reporter'
+import '@/i18n'
+import SettingsPageRoot from '@/features/settings/components/root'
+
+// For react-google-recaptcha
+window.recaptchaOptions = {
+ enterprise: true,
+ useRecaptchaNet: true,
+}
+renderInReactLayout('settings-page-root', () => )
diff --git a/services/web/frontend/js/pages/user/subscription/preview-change.tsx b/services/web/frontend/js/pages/user/subscription/preview-change.tsx
index 9ff5f14d45..6d7d0b175f 100644
--- a/services/web/frontend/js/pages/user/subscription/preview-change.tsx
+++ b/services/web/frontend/js/pages/user/subscription/preview-change.tsx
@@ -1,14 +1,9 @@
-import '@/marketing'
-import { createRoot } from 'react-dom/client'
+import { renderInReactLayout } from '@/react'
import PreviewSubscriptionChange from '@/features/subscription/components/preview-subscription-change/root'
import { SplitTestProvider } from '@/shared/context/split-test-context'
-const element = document.getElementById('subscription-preview-change')
-if (element) {
- const root = createRoot(element)
- root.render(
-
-
-
- )
-}
+renderInReactLayout('subscription-preview-change', () => (
+
+
+
+))
diff --git a/services/web/frontend/js/react.ts b/services/web/frontend/js/react.ts
new file mode 100644
index 0000000000..fa940e0bf4
--- /dev/null
+++ b/services/web/frontend/js/react.ts
@@ -0,0 +1,15 @@
+import './marketing'
+import './features/header-footer-react'
+import { createRoot } from 'react-dom/client'
+import { ReactNode } from 'react'
+
+export function renderInReactLayout(
+ parentId: string,
+ createChildren: () => ReactNode
+) {
+ const parentElement = document.getElementById(parentId)
+ if (parentElement) {
+ const root = createRoot(parentElement)
+ root.render(createChildren())
+ }
+}
diff --git a/services/web/frontend/js/utils/meta.ts b/services/web/frontend/js/utils/meta.ts
index f4ac2502fd..ffe1903fa5 100644
--- a/services/web/frontend/js/utils/meta.ts
+++ b/services/web/frontend/js/utils/meta.ts
@@ -133,6 +133,7 @@ export interface Meta {
'ol-hasIndividualPaidSubscription': boolean
'ol-hasManagedUsersFeature': boolean
'ol-hasPassword': boolean
+ 'ol-hasSplitTestWriteAccess': boolean
'ol-hasSubscription': boolean
'ol-hasTrackChangesFeature': boolean
'ol-hideLinkingWidgets': boolean // CI only
@@ -241,6 +242,7 @@ export interface Meta {
'ol-showUpgradePrompt': boolean
'ol-skipUrl': string
'ol-splitTestInfo': { [name: string]: SplitTestInfo }
+ 'ol-splitTestName': string
'ol-splitTestVariants': { [name: string]: string }
'ol-ssoDisabled': boolean
'ol-ssoErrorMessage': string
diff --git a/services/web/modules/user-activate/frontend/js/pages/user-activate-page.jsx b/services/web/modules/user-activate/frontend/js/pages/user-activate-page.jsx
deleted file mode 100644
index c03b5206a7..0000000000
--- a/services/web/modules/user-activate/frontend/js/pages/user-activate-page.jsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import '@/marketing'
-
-import { createRoot } from 'react-dom/client'
-import UserActivateRegister from '../components/user-activate-register'
-
-const container = document.getElementById('user-activate-register-container')
-if (container) {
- const root = createRoot(container)
- root.render()
-}
diff --git a/services/web/modules/user-activate/frontend/js/pages/user-activate-page.tsx b/services/web/modules/user-activate/frontend/js/pages/user-activate-page.tsx
new file mode 100644
index 0000000000..0172bf5573
--- /dev/null
+++ b/services/web/modules/user-activate/frontend/js/pages/user-activate-page.tsx
@@ -0,0 +1,7 @@
+import { renderInReactLayout } from '@/react'
+
+import UserActivateRegister from '../components/user-activate-register'
+
+renderInReactLayout('user-activate-register-container', () => (
+
+))