Skip to content

Branding ​

What it does ​

Each site's admin panel uses its own primary and secondary colour, font, logos and avatar. Site admins edit these under Site settings > Branding.

How it works ​

SiteBranding produces one array of brand values per site by merging three layers. Later layers win:

MethodReturns
raw($site)Merged values. Logos are left as stored (a hardcoded URL or an uploaded path).
for($site)raw() with logo, dark_logo and avatar resolved to URLs. Hardcoded http URLs pass through. Uploaded paths go through MediaService::url(). If URL resolution fails it reports the error and returns null, so the panel never locks up.
overrides($site)Non-empty values from the site's brandings row, mapped to service keys.
save($site, $values)Writes the brandings row with updateOrCreate. Blank values become null, so they fall back to the layer below.

Service keys map to table columns in the COLUMNS constant: primary to primary_color, secondary to secondary_color, font, logo to logo_path, dark_logo to dark_logo_path, avatar to avatar_path. The colour columns are not named primary because that is a reserved word in MySQL.

Where the values are used

  • ApplySiteBranding (tenant middleware, persistent): registers colours with FilamentColor::register(), including a custom secondary, and sets the font.
  • AdminPanelProvider: ->brandName(), ->brandLogo() and ->darkModeBrandLogo() take closures that call SiteBranding::for(Filament::getTenant()).
  • BrandedAvatarProvider (->defaultAvatarProvider(...)): initials avatars in the site's primary colour, when there is no uploaded image.
  • Site and User implement Filament's HasAvatar interface (singular) so uploaded images are shown. The method returns null when there is no image, which triggers the initials fallback.
  • resources/css/filament/admin/theme.css styles the sidebar tint, cards and active item from var(--primary-*).

The form: app/Filament/Pages/Tenancy/EditSiteProfile.php. The Branding section has two colour pickers, a font select, and three FileUpload fields (logo, dark logo, avatar). mutateFormDataBeforeFill() loads branding into branding.* state, filling the upload fields with uploaded paths only (never hardcoded URLs, which the upload field can't preview). handleRecordUpdate() strips branding out and calls SiteBranding::save() before updating the site itself.

Files ​

  • app/Services/SiteBranding.php
  • app/Models/Branding.php, Site::branding() (hasOne)
  • Migration create_brandings_table (site_id unique, one row per site)
  • app/Filament/Pages/Tenancy/EditSiteProfile.php
  • app/Services/BrandedAvatarProvider.php
  • app/Providers/Filament/AdminPanelProvider.php
  • resources/css/filament/admin/theme.css
  • Middleware: ApplySiteBranding

Why it's built this way ​

  • Three layers let a site work with no database row (hardcoded BRANDS), and let admins override any single value without losing the rest.
  • Blank means "use the default", which is how removing a custom logo restores the hardcoded one.
  • One service means the middleware, panel closures and avatars can't disagree about a site's colours.
  • Service keys differ from column names to avoid the reserved word, and COLUMNS keeps the mapping in one place.