From 6999b3dd73d76dec66a63ee98ed0c94778f129bd Mon Sep 17 00:00:00 2001 From: Claude Fable 5 Date: Tue, 4 Aug 2026 11:20:21 +0200 Subject: [PATCH] Document title follows the configured instance name (#323) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit useDocumentTitle pinned APP_NAME = 'Dorfteich', so every route title — tab, bookmarks, the window title a screen reader announces (WCAG 2.4.2) — named the product instead of the operator's instance. The trailing name now comes from the public branding query, exactly like the TopBar brand (#306); until the query resolves (or when it cannot, e.g. maintenance mode) the shipped default keeps the title stable, so an untouched instance reads exactly as before. The static index.html title stays the pre-JS placeholder — server-rendering it is #179's territory, deliberately out of scope (recorded in the issue). The admin-settings e2e now also asserts the title carries the new name right after saving, without a reload. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_017aviRTgWCcAHUh1SBoxf6P --- apps/web/e2e/admin-settings.spec.ts | 5 +++-- apps/web/src/lib/use-document-title.ts | 21 ++++++++++++++++----- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/apps/web/e2e/admin-settings.spec.ts b/apps/web/e2e/admin-settings.spec.ts index 2b0e2b1..8ced0c3 100644 --- a/apps/web/e2e/admin-settings.spec.ts +++ b/apps/web/e2e/admin-settings.spec.ts @@ -33,9 +33,10 @@ test('instance name changed in the general settings form persists', async ({ bro // Scoped to the card: the page has several forms with status regions. await expect(generalCard.getByRole('status')).toHaveText(/^(Saved\.|Gespeichert\.)$/); - // The TopBar shows the new name without a reload — the save invalidates - // the branding query the TopBar reads from. + // The TopBar and the document title show the new name without a reload — + // the save invalidates the branding query both read from (issue #323). await expect(page.locator('.topbar__brand')).toHaveText(newName); + await expect(page).toHaveTitle(new RegExp(`${newName}$`)); // The proof the form really persisted: the value survives a reload and // the api returns it. diff --git a/apps/web/src/lib/use-document-title.ts b/apps/web/src/lib/use-document-title.ts index a3ec0a7..4d02608 100644 --- a/apps/web/src/lib/use-document-title.ts +++ b/apps/web/src/lib/use-document-title.ts @@ -1,22 +1,33 @@ import { useEffect } from 'react'; +import { useBranding } from '../branding/use-branding'; + const APP_NAME = 'Dorfteich'; /** * Route-specific document title (issue #163, WCAG 2.4.2): joins the given - * parts with the app name ("Page — Pond — Dorfteich"). Empty/undefined + * parts with the instance name ("Page — Pond — My Wiki"). Empty/undefined * parts are skipped, so callers can pass still-loading data directly. - * Falls back to the bare app name on unmount. + * Falls back to the bare instance name on unmount. + * + * The trailing name is the OPERATOR'S instance name, not the product name + * (issue #323) — same reasoning as the TopBar brand (issue #306). Until + * the branding query resolves (or when it cannot, e.g. maintenance mode) + * the shipped default keeps the title stable, so an untouched instance + * reads exactly as before. */ export function useDocumentTitle(...parts: (string | null | undefined)[]): void { - const joined = [...parts.filter(Boolean), APP_NAME].join(' — '); + const appName = useBranding()?.instanceName.trim() || APP_NAME; + const joined = [...parts.filter(Boolean), appName].join(' — '); useEffect(() => { document.title = joined; }, [joined]); useEffect( + // On unmount only in effect: `joined` always changes with `appName`, + // so the title effect above re-runs right after this cleanup. () => () => { - document.title = APP_NAME; + document.title = appName; }, - [], + [appName], ); }