Files
accounted/components/branding/BrandHomeLink.tsx
T
e66195e5bb 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>
2026-09-03 18:41:40 +02:00

72 lines
2.8 KiB
TypeScript

'use client'
/**
* Dashboard sidebar brand mark: the top-left home link (WL-12 slice A3).
*
* On a branded host with an uploaded logo it renders that logo; on every
* other host it renders exactly the legacy `getBranding().logoPath` image
* with the same dimensions, classes and aria-label as before, keeping
* default hosts byte-identical.
*
* With `showLabel` (expanded sidebar) a branded host without an uploaded
* logo renders the brand's app name beside the mark (byra-editable in the
* Varumärke settings). With an uploaded logo the logo carries the brand
* alone (same founder call as BrandWordmark, 2026-08-05: byrå logos usually
* carry their own name, so logo + text read as a duplicate). Unbranded
* hosts never show a label.
*/
import Link from 'next/link'
import Image from 'next/image'
import { useBranding } from '@/lib/branding/brand-context'
import { useCompanyOptional } from '@/contexts/CompanyContext'
export function BrandHomeLink({ showLabel = false }: { showLabel?: boolean }) {
const { appName, logoUrl, logoPath, brand } = useBranding()
const label = brand && !logoUrl ? brand.appName : null
// Byrå team members (any role) home to the cockpit, never to "/" (which
// would open whatever client company happens to be active). Everyone
// outside a byrå keeps the legacy home link.
const byraTeam = useCompanyOptional()?.byraTeam
const homeHref = byraTeam ? '/byra' : '/'
return (
<Link href={homeHref} aria-label={appName} className="flex items-center gap-2 rounded-lg">
{logoUrl ? (
<Image
src={logoUrl}
alt=""
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
src={logoPath}
alt=""
width={26}
height={26}
className="h-[26px] w-[26px] rounded-lg"
/>
)}
{showLabel && label && (
<span
className="truncate font-display text-base tracking-tight text-foreground"
style={{ fontWeight: 700 }}
>
{label}
</span>
)}
</Link>
)
}