diff --git a/ui/src/App.test.tsx b/ui/src/App.test.tsx
index 18b58404..23e24db3 100644
--- a/ui/src/App.test.tsx
+++ b/ui/src/App.test.tsx
@@ -14,6 +14,12 @@ vi.mock('./shared/routing/routing', () => ({
getAppUrl: vi.fn(),
}))
+vi.mock('./causestarter/shell/CauseShell', () => ({
+ CauseShell: ({ children }: { children: React.ReactNode }) => (
+
{children}
+ ),
+}))
+
vi.mock('./shared/components/AppShell', () => ({
AppShell: ({ branding, navigation, children }: { branding: { name: string }; navigation: { primaryNavigation: Array<{label: string; path: string}>; secondaryNavigation: Array<{label: string; path: string}>; footerText: string }; children: React.ReactNode }) => (
@@ -134,6 +140,28 @@ describe('App route composition', () => {
expect(screen.getByText('Common Sense Majority')).toBeInTheDocument()
})
+
+ it('sets the document title from domain branding', async () => {
+ mockGetActiveDomain.mockReturnValue(fakeDomain('CauseStarter', [], 'footer'))
+
+ const { default: App } = await import('./App')
+ render(React.createElement(App))
+
+ expect(document.title).toBe('CauseStarter')
+ })
+
+ it('sets the document title when using CauseShell', async () => {
+ mockGetActiveDomain.mockReturnValue({
+ ...fakeDomain('CauseStarter', [], 'footer'),
+ useCauseShell: true,
+ })
+
+ const { default: App } = await import('./App')
+ render(React.createElement(App))
+
+ expect(screen.getByTestId('cause-shell')).toBeInTheDocument()
+ expect(document.title).toBe('CauseStarter')
+ })
})
describe('primary navigation per domain', () => {
diff --git a/ui/src/App.tsx b/ui/src/App.tsx
index ea261909..b17eaa2a 100644
--- a/ui/src/App.tsx
+++ b/ui/src/App.tsx
@@ -9,6 +9,11 @@ import { CauseShell } from './causestarter/shell/CauseShell'
function DomainChrome({ children }: { children: ReactNode }) {
const domain = getActiveDomain()
+
+ useEffect(() => {
+ document.title = domain.branding.name
+ }, [domain.branding.name])
+
if (domain.useCauseShell) {
return {children}
}
diff --git a/ui/src/shared/components/AppShell.tsx b/ui/src/shared/components/AppShell.tsx
index 3a6a187c..0bb8afbe 100644
--- a/ui/src/shared/components/AppShell.tsx
+++ b/ui/src/shared/components/AppShell.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect } from 'react'
+import { useState } from 'react'
import type { MouseEvent, ReactNode } from 'react'
import {
AppBar,
@@ -208,10 +208,6 @@ export function AppShell({ children, branding, navigation }: AppShellProps) {
tagline: 'Find common ground and fund what matters.',
}
- useEffect(() => {
- document.title = brand.name
- }, [brand.name])
-
const nav = navigation ?? {
primaryNavigation: [
{ label: 'Start Here', path: '/docs' },
diff --git a/ui/vite.config.ts b/ui/vite.config.ts
index 756f060b..a30cd292 100644
--- a/ui/vite.config.ts
+++ b/ui/vite.config.ts
@@ -18,7 +18,7 @@ export default defineConfig(({ mode }) => {
build: {
outDir: `dist/${domain}`,
},
- plugins: [react(), runtimeConfigPlugin(domain, env), endUserDocsPlugin({ domain })],
+ plugins: [react(), htmlTitlePlugin(domain), runtimeConfigPlugin(domain, env), endUserDocsPlugin({ domain })],
worker: {
format: 'es',
},
@@ -102,6 +102,29 @@ function sdkSourceAliases(): Record {
)
}
+// Must match branding.name in ui/src/domains/*/manifest.tsx (Vite cannot import those TSX files here).
+const DOMAIN_TITLES: Record = {
+ commonality: 'Commonality',
+ lazyGiving: 'LazyGiving',
+ alignment: 'Aligning',
+ tally: 'Tally',
+ 'content-funding': 'Content Funding',
+ civility: 'Civility',
+ 'common-sense-majority': 'Common Sense Majority',
+ conceptspace: 'Conceptspace',
+ causestarter: 'CauseStarter',
+}
+
+function htmlTitlePlugin(buildDomain: string): Plugin {
+ const title = DOMAIN_TITLES[buildDomain] ?? 'Commonality'
+ return {
+ name: 'commonality-html-title',
+ transformIndexHtml(html) {
+ return html.replace(/[^<]*<\/title>/, `${title}`)
+ },
+ }
+}
+
function runtimeConfigPlugin(buildDomain: string, env: Record): Plugin {
return {
name: 'commonality-runtime-config',