diff --git a/frontend/src/components/NavigationProvider.dom.test.tsx b/frontend/src/components/NavigationProvider.dom.test.tsx
index 0d19f2ce..fe29cf5d 100644
--- a/frontend/src/components/NavigationProvider.dom.test.tsx
+++ b/frontend/src/components/NavigationProvider.dom.test.tsx
@@ -11,6 +11,7 @@
// bookkeeping.
import { describe, it, expect, afterEach, beforeEach, vi } from "vitest";
import { renderHook, act, cleanup } from "@testing-library/react";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import type { ReactNode } from "react";
// Partial: the provider reads `t` AND `i18n.language` (both feed the document-title effect), and the
@@ -28,8 +29,14 @@ import { NavigationProvider } from "./NavigationProvider";
import { useNavigation, useNavigationActions } from "./navigationContext";
import { useBackToClose } from "../lib/history";
+import { qk } from "../lib/queryKeys";
+
+// The provider reads the signed-in account (the tab title) from the `me` cache.
+let qc = new QueryClient();
const wrapper = ({ children }: { children: ReactNode }) => (
- {children}
+
+ {children}
+
);
const render = () =>
renderHook(() => ({ ...useNavigation(), ...useNavigationActions() }), { wrapper });
@@ -54,6 +61,8 @@ async function openOverlay() {
}
beforeEach(() => {
+ qc = new QueryClient();
+ qc.setQueryData(qk.me(), { id: 1 });
localStorage.clear();
sessionStorage.setItem("siftlode.activeAccount", "1"); // account-scoped page memory
window.history.replaceState({ sfPage: "feed" }, "", "/");
@@ -65,6 +74,22 @@ afterEach(() => {
vi.restoreAllMocks();
});
+describe("NavigationProvider · the tab title", () => {
+ // Prod UAT 2026-09-19: signed out, the tab read the Home page's question over the sign-in form.
+ it("is the brand alone while nobody is signed in, the page's name once someone is", async () => {
+ window.history.replaceState({ sfPage: "home" }, "", "/?page=home");
+ qc.setQueryData(qk.me(), null);
+ render();
+ await settle();
+ expect(document.title).toBe("Siftlode");
+
+ act(() => qc.setQueryData(qk.me(), { id: 1 }));
+ await settle();
+ expect(document.title).not.toBe("Siftlode");
+ expect(document.title.endsWith(" · Siftlode")).toBe(true);
+ });
+});
+
describe("NavigationProvider · setPage's same-page guard", () => {
it("pushes an entry for a real page change", () => {
const { result } = render();
diff --git a/frontend/src/components/NavigationProvider.tsx b/frontend/src/components/NavigationProvider.tsx
index 82faf9a8..b196bcce 100644
--- a/frontend/src/components/NavigationProvider.tsx
+++ b/frontend/src/components/NavigationProvider.tsx
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
import { useTranslation } from "react-i18next";
+import { useMe } from "../lib/useMe";
import { urlWithParams, type Page } from "../lib/urlState";
import { LOGS_KEYS } from "../lib/logsUrlState";
import { pageTitleKey } from "../lib/pageMeta";
@@ -154,16 +155,20 @@ export function NavigationProvider({ children }: { children: ReactNode }) {
}, [page]);
// Reflect the current module in the browser tab title. Feed = brand only; an open channel page
- // shows the channel name.
+ // shows the channel name. Signed out, the page on screen is the public landing whatever `page`
+ // says — the tab read "What should I watch?" over the sign-in form (prod UAT 2026-09-19).
+ const signedIn = useMe() != null;
useEffect(() => {
const brand = "Siftlode";
- const label = channelView
- ? channelView.name || t("header.channelManager")
- : page === "feed"
- ? null
- : t(pageTitleKey(page));
+ const label = !signedIn
+ ? null
+ : channelView
+ ? channelView.name || t("header.channelManager")
+ : page === "feed"
+ ? null
+ : t(pageTitleKey(page));
document.title = label ? `${label} · ${brand}` : brand;
- }, [page, channelView, i18n.language, t]);
+ }, [signedIn, page, channelView, i18n.language, t]);
// In-app Back/Forward: stamp the initial entry with the current page (mirrored to the address bar),
// then sync state from history.state on popstate — the browser restores each entry's URL for us, so
diff --git a/frontend/src/components/legal/LegalLayout.tsx b/frontend/src/components/legal/LegalLayout.tsx
index 5b0d30bd..e34b163a 100644
--- a/frontend/src/components/legal/LegalLayout.tsx
+++ b/frontend/src/components/legal/LegalLayout.tsx
@@ -71,6 +71,11 @@ export default function LegalLayout({ title, children }: { title: string; childr
const id = decodeURIComponent(window.location.hash.slice(1));
if (id) document.getElementById(id)?.scrollIntoView();
}, []);
+ // The tab names the document, like every page of the app — a bookmark or a tab strip read just
+ // "Siftlode" (prod UAT 2026-09-19).
+ useEffect(() => {
+ document.title = `${title} · Siftlode`;
+ }, [title]);
return (
diff --git a/frontend/src/i18n/locales/en/downloads.json b/frontend/src/i18n/locales/en/downloads.json
index 1e750cba..adc16396 100644
--- a/frontend/src/i18n/locales/en/downloads.json
+++ b/frontend/src/i18n/locales/en/downloads.json
@@ -18,7 +18,7 @@
"page": {
"title": "Downloads",
"subtitle": "Download videos to the server, then save them to your device.",
- "addUrl": "Paste a YouTube link or video id…",
+ "addUrl": "Paste a video link (YouTube, Facebook, …) or a YouTube video id…",
"add": "Add",
"paste": "Paste link from clipboard",
"upload": "Upload",
diff --git a/frontend/src/i18n/locales/hu/downloads.json b/frontend/src/i18n/locales/hu/downloads.json
index 7c1ec57e..00362e97 100644
--- a/frontend/src/i18n/locales/hu/downloads.json
+++ b/frontend/src/i18n/locales/hu/downloads.json
@@ -18,7 +18,7 @@
"page": {
"title": "Letöltések",
"subtitle": "Töltsd le a videókat a szerverre, majd mentsd a saját gépedre.",
- "addUrl": "Illessz be egy YouTube linket vagy videó azonosítót…",
+ "addUrl": "Illessz be egy videólinket (YouTube, Facebook, …) vagy YouTube-videóazonosítót…",
"add": "Hozzáad",
"paste": "Link beillesztése a vágólapról",
"upload": "Feltöltés",