From e66195e5bb9cd646e8d9c88f6460ff915cc46fb3 Mon Sep 17 00:00:00 2001 From: Jakob Wennberg Date: Thu, 3 Sep 2026 18:41:40 +0200 Subject: [PATCH] fix(branding): render tenant logos unoptimized so self-hosted sidebars work (#2203) (#2246) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(branding): render tenant logos unoptimized so self-hosted sidebars work (#2203) On the official Docker image a byrÄ logo uploaded under Settings > Brand worked as favicon but rendered broken in the sidebar. BrandHomeLink (and BrandWordmark) sent the Supabase Storage URL through the Next.js image optimizer, whose remote-host allowlist is derived from NEXT_PUBLIC_SUPABASE_URL at BUILD time. The generic image bakes a sentinel there and docker-entrypoint.sh substitutes the real URL only at container start, so /_next/image answered 400 '"url" parameter is not allowed'. Both tenant-logo elements now pass `unoptimized`: the browser fetches the public object directly, which is exactly what the favicon already did, and CSP img-src already permits https:. The remotePatterns block in next.config.ts stays for builds that know the URL, with its comment updated to say what it still covers. Closes #2203 Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01VnConrmMCxJRQ5kfiPPWyy * chore: carry the DECISIONS.md line for this PR in #2247 instead (append-only log conflicts on every merge) --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 --- components/branding/BrandHomeLink.tsx | 10 ++++++++++ components/branding/BrandWordmark.tsx | 4 ++++ next.config.ts | 15 ++++++++++----- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/components/branding/BrandHomeLink.tsx b/components/branding/BrandHomeLink.tsx index 1e9f1bdf..a2c92ecd 100644 --- a/components/branding/BrandHomeLink.tsx +++ b/components/branding/BrandHomeLink.tsx @@ -38,6 +38,16 @@ export function BrandHomeLink({ showLabel = false }: { showLabel?: boolean }) { width={26} height={26} className="h-[26px] w-[26px] rounded-lg object-contain" + // Tenant logos live on the deployment's Supabase Storage host, and + // the image optimizer only allows that host when + // NEXT_PUBLIC_SUPABASE_URL was known at BUILD time. The generic + // Docker image bakes a sentinel and substitutes the real URL at + // container start, so on self-hosted installs /_next/image answered + // 400 '"url" parameter is not allowed' and the sidebar mark rendered + // broken while the favicon (same URL, no optimizer) worked (issue + // #2203). The browser fetches the public object directly instead; at + // 26px there is nothing to optimize anyway. + unoptimized /> ) : ( ) diff --git a/next.config.ts b/next.config.ts index ea86644a..b21623ab 100644 --- a/next.config.ts +++ b/next.config.ts @@ -35,11 +35,16 @@ const supabaseWsUrl = supabaseUrl.replace(/^http(s?):/, "ws$1:"); // Brand logos (WL-12 slice A3) are served from Supabase Storage public -// objects and rendered via next/image, which requires the host to be -// allowlisted. The pattern stays narrow: public storage objects only. -// try/catch because Docker builds may bake a sentinel value that is not a -// parseable URL; no hostname simply means no remote images are allowed, -// exactly as before. +// objects. The tenant-logo elements (components/branding/) render +// them `unoptimized` since issue #2203: this allowlist is fixed at build +// time, and the generic Docker image bakes a sentinel for +// NEXT_PUBLIC_SUPABASE_URL that docker-entrypoint.sh substitutes only at +// container start, so the optimizer rejected the runtime host with 400 +// '"url" parameter is not allowed'. The pattern is kept for builds where the +// real URL is present at build time (hosted, local) so any other remote +// image from the same public bucket still passes. Narrow on purpose: public +// storage objects only. try/catch because the sentinel is not a parseable +// URL; no hostname simply means no remote images are allowed, as before. let supabaseImageHostname = ""; try { supabaseImageHostname = supabaseUrl ? new URL(supabaseUrl).hostname : "";