fix(branding): render tenant logos unoptimized so self-hosted sidebars work (#2203) (#2246)

* 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 <Image> 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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-09-03 18:41:40 +02:00
committed by GitHub
parent cc18e9d530
commit e66195e5bb
3 changed files with 24 additions and 5 deletions
+10
View File
@@ -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
/>
) : (
<Image
+4
View File
@@ -49,6 +49,10 @@ export function BrandWordmark({
height={size === 'hero' ? 64 : 22}
className={cn('w-auto', size === 'hero' ? 'h-16' : 'h-[22px]')}
priority={size === 'hero'}
// Bypass the image optimizer: its remote-host allowlist is fixed at
// build time, which the runtime-configured Docker image cannot
// satisfy (issue #2203, same reasoning as BrandHomeLink).
unoptimized
/>
</span>
)
+10 -5
View File
@@ -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 <Image> 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 : "";