Appearance
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:
| Method | Returns |
|---|---|
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 withFilamentColor::register(), including a customsecondary, and sets the font.AdminPanelProvider:->brandName(),->brandLogo()and->darkModeBrandLogo()take closures that callSiteBranding::for(Filament::getTenant()).BrandedAvatarProvider(->defaultAvatarProvider(...)): initials avatars in the site's primary colour, when there is no uploaded image.SiteandUserimplement Filament'sHasAvatarinterface (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.cssstyles the sidebar tint, cards and active item fromvar(--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.phpapp/Models/Branding.php,Site::branding()(hasOne)- Migration
create_brandings_table(site_idunique, one row per site) app/Filament/Pages/Tenancy/EditSiteProfile.phpapp/Services/BrandedAvatarProvider.phpapp/Providers/Filament/AdminPanelProvider.phpresources/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
COLUMNSkeeps the mapping in one place.