diff --git a/web/src/lib/use-document-title.test.tsx b/web/src/lib/use-document-title.test.tsx
new file mode 100644
index 0000000..5ffe5a6
--- /dev/null
+++ b/web/src/lib/use-document-title.test.tsx
@@ -0,0 +1,37 @@
+import { afterEach, expect, test } from "vitest";
+import { render } from "@testing-library/react";
+import "../i18n";
+import { ConfigProvider } from "../config/config-provider";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { useDocumentTitle } from "./use-document-title";
+
+function Titled({ page }: { page: string }) {
+ useDocumentTitle(page);
+ return null;
+}
+
+function wrap(ui: React.ReactElement) {
+ const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
+ return render(
+
+ {ui}
+ ,
+ );
+}
+
+afterEach(() => {
+ document.title = "";
+});
+
+test("sets document.title to '{page} | {app_name}'", () => {
+ wrap();
+ expect(document.title).toMatch(/^Objects \| .+/);
+});
+
+test("restores the previous title on unmount", () => {
+ document.title = "Prev";
+ const { unmount } = wrap();
+ expect(document.title).toMatch(/^Objects \| /);
+ unmount();
+ expect(document.title).toBe("Prev");
+});
diff --git a/web/src/lib/use-document-title.ts b/web/src/lib/use-document-title.ts
new file mode 100644
index 0000000..e0e413f
--- /dev/null
+++ b/web/src/lib/use-document-title.ts
@@ -0,0 +1,19 @@
+import { useEffect } from "react";
+
+import { useConfig } from "../config/config-context";
+
+export function useDocumentTitle(page: string): void {
+ const { app_name } = useConfig();
+
+ useEffect(() => {
+ if (typeof document === "undefined") return;
+
+ const previous = document.title;
+
+ document.title = `${page} | ${app_name}`;
+
+ return () => {
+ document.title = previous;
+ };
+ }, [page, app_name]);
+}