diff --git a/frontend/src/components/ChannelPage.banner.dom.test.tsx b/frontend/src/components/ChannelPage.banner.dom.test.tsx
new file mode 100644
index 00000000..5d886379
--- /dev/null
+++ b/frontend/src/components/ChannelPage.banner.dom.test.tsx
@@ -0,0 +1,89 @@
+// @vitest-environment jsdom
+//
+// The channel banner is served from Google's image CDN (yt3.googleusercontent.com), which refuses a
+// request that carries a Referer from our origin — every time, not only under load (measured
+// 2026-09-26: 6/6 refused with the page's referrer or its bare origin, 8/8 served without one). So the
+// banner
must send none, as ui/Avatar does for the same CDN; with a referrer the banner never
+// showed and the page fell back to the blank bar.
+import type { ReactNode } from "react";
+import { describe, it, expect, vi, beforeAll, afterEach } from "vitest";
+import { render, cleanup, waitFor } from "@testing-library/react";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+
+vi.mock("react-i18next", async (importOriginal) => ({
+ ...(await importOriginal()),
+ useTranslation: () => ({ t: (key: string) => key, i18n: { language: "en" } }),
+}));
+vi.mock("./navigationContext", () => ({
+ useNavigationActions: () => ({ closeChannel: vi.fn(), openChannel: vi.fn() }),
+}));
+vi.mock("./confirmContext", () => ({ useConfirm: () => async () => true }));
+vi.mock("./feedFiltersContext", () => ({
+ useFeedFilters: () => ({ includeNormal: true, includeShorts: false, includeLive: false }),
+}));
+vi.mock("./Feed", () => ({ default: () => null }));
+vi.mock("./ChannelAbout", () => ({ default: () => null }));
+// The page's chrome portals into the shell's fixed toolbar band, which this test has no shell for.
+vi.mock("./ui/PageShell", async (importOriginal) => ({
+ ...(await importOriginal()),
+ PageToolbar: ({ children }: { children?: ReactNode }) => <>{children}>,
+}));
+
+import ChannelPage from "./ChannelPage";
+import { api, type ChannelDetail, type Me } from "../lib/api";
+
+beforeAll(() => {
+ class Stub {
+ observe() {}
+ unobserve() {}
+ disconnect() {}
+ takeRecords() {
+ return [];
+ }
+ }
+ globalThis.ResizeObserver ??= Stub as unknown as typeof ResizeObserver;
+ globalThis.IntersectionObserver ??= Stub as unknown as typeof IntersectionObserver;
+});
+afterEach(() => {
+ cleanup();
+ vi.restoreAllMocks();
+});
+
+const CH = "UCqoAEDirJPjEUFcF2FklnBA";
+
+describe("ChannelPage — the banner", () => {
+ it("is requested without a referrer, which Google's image CDN refuses", async () => {
+ vi.spyOn(api, "channelDetail").mockResolvedValue({
+ id: CH,
+ title: "StarTalk",
+ banner_url: "https://yt3.googleusercontent.com/banner",
+ subscribed: true,
+ blocked: false,
+ hidden: false,
+ deep_requested: false,
+ backfill_done: true,
+ stored_videos: 10,
+ } as unknown as ChannelDetail);
+ vi.spyOn(api, "tags").mockResolvedValue([] as never);
+ const { container } = render(
+
+ {}}
+ onShowInFeed={() => {}}
+ />
+ ,
+ );
+ const banner = await waitFor(() => {
+ const img = container.querySelector('img[src^="https://yt3."]');
+ expect(img).not.toBeNull();
+ return img!;
+ });
+ expect(banner.getAttribute("referrerpolicy")).toBe("no-referrer");
+ });
+});
diff --git a/frontend/src/components/ChannelPage.tsx b/frontend/src/components/ChannelPage.tsx
index 0075a98a..8fc69a34 100644
--- a/frontend/src/components/ChannelPage.tsx
+++ b/frontend/src/components/ChannelPage.tsx
@@ -79,11 +79,11 @@ export default function ChannelPage({
// where the channelScoped Feed portals ITS toolbar (via the overridden PageToolbarSlot below), so
// the toolbar lands under the tabs inside the fixed band rather than in the scroller.
const [chromeSlot, setChromeSlot] = useState(null);
- // The banner URL is valid, but googleusercontent intermittently throttles it when the channel
- // page fires the banner + avatar + ~16 thumbnails in one burst — the request is dropped and the
- // browser then NEGATIVE-CACHES the miss, so the banner stays broken even though a retry would
- // succeed. So on error we retry a few times with a cache-buster (forcing a fresh request), and
- // only fall back to the blank bar if it keeps failing (a genuinely dead URL — rare).
+ // The banner comes from Google's image CDN, which refuses any request carrying a Referer from our
+ // origin — every time, not only in a burst (measured 2026-09-26) — so the
sends none
+ // (`referrerPolicy`, as ui/Avatar). A load can still drop, and the browser NEGATIVE-CACHES the miss,
+ // so on error we retry a few times with a cache-buster (forcing a fresh request), and only fall
+ // back to the blank bar if it keeps failing (a genuinely dead URL — rare).
const MAX_BANNER_RETRIES = 3;
const [bannerAttempt, setBannerAttempt] = useState(0);
@@ -335,6 +335,7 @@ export default function ChannelPage({
// browser's negative cache (googleusercontent accepts an extra query param).
key={bannerAttempt}
src={`${ch.banner_url}=w1707${bannerAttempt ? `?r=${bannerAttempt}` : ""}`}
+ referrerPolicy="no-referrer"
alt=""
className="w-full h-full object-cover object-center"
onError={() => {
diff --git a/frontend/src/components/ui/Avatar.tsx b/frontend/src/components/ui/Avatar.tsx
index 7172df31..d81f7621 100644
--- a/frontend/src/components/ui/Avatar.tsx
+++ b/frontend/src/components/ui/Avatar.tsx
@@ -3,10 +3,11 @@ import { useState } from "react";
// Avatar image with a graceful fallback.
//
// Uses referrerPolicy="no-referrer" because Google's avatar CDNs
-// (yt3.ggpht.com / lh3.googleusercontent.com) rate-limit bursts of
-// referrer-bearing requests — when a feed page fires dozens of avatar loads at
-// once, a random subset gets throttled (HTTP 429) and the browser shows its
-// broken-image icon. Dropping the referrer makes those CDNs serve the images.
+// (yt3.ggpht.com / lh3.googleusercontent.com) refuse referrer-bearing requests
+// from our origin — yt3 refuses even a single load, not only a burst (measured
+// 2026-09-26) — and the browser shows its broken-image icon. Dropping the
+// referrer makes those CDNs serve the images. The channel banner, from the same
+// CDN, does the same (ChannelPage).
// If a load still fails we render a neutral placeholder (optionally an initial)
// instead of the broken-image icon.
export default function Avatar({