Skip to content

feat: add BrandSettings env-var surface for favicon and email branding#1720

Merged
davidpoblador merged 1 commit into
mainfrom
feat/brand-settings
Apr 28, 2026
Merged

feat: add BrandSettings env-var surface for favicon and email branding#1720
davidpoblador merged 1 commit into
mainfrom
feat/brand-settings

Conversation

@davidpoblador

Copy link
Copy Markdown
Member

Summary

DaisyUI tokens and CSS variables cover everything that renders inside the page, but a few brand surfaces are out of reach:

  • Favicon meta tags (mask-icon, msapplication-TileColor, theme-color) are read by the browser before any CSS runs.
  • PWA manifest (theme_color, background_color) is consumed by the OS install UI, also before CSS.
  • Magic-link emails render in clients (Gmail, Outlook) that strip <style> blocks and ignore CSS variables.

This PR adds BrandSettings, an app-level pydantic-settings surface that drives those specific surfaces.

Configuration

# All optional; defaults shown.
BRAND_PRIMARY_COLOR=#5b2333
BRAND_BROWSER_THEME_COLOR=#ffffff
BRAND_EMAIL_BUTTON_COLOR=  # falls back to BRAND_PRIMARY_COLOR when unset
  • BRAND_PRIMARY_COLOR → Safari pinned-tab mask-icon, Windows tile (browserconfig.xml), magic-link email button (when no override).
  • BRAND_BROWSER_THEME_COLOR → mobile browser chrome (<meta name="theme-color">), PWA manifest's theme_color / background_color.
  • BRAND_EMAIL_BUTTON_COLOR → override slot for when the email button needs to differ from the primary brand color.

Inputs accept any pydantic-Color form (named, rgb(), hex short or long). Values canonicalise to long-form #rrggbb lowercase via a HexColor subclass that pins __str__ to as_hex(format="long") — bare Color would render #ffffff as the string white, which is awkward to interpolate into HTML attributes.

settings.brand is exposed in every Jinja render via the shipped _brand_context provider, so templates read {{ brand.primary_color }} without wiring anything up. The bundled base/favicons.html.jinja, meta/site.webmanifest.jinja, and meta/browserconfig.xml.jinja already consume it; send_magic_link_email() passes settings.brand.email_button into the email template.

Why app-level, not per-tenant

Favicon assets are static files served before any tenant resolver runs, and the email service does not see request context. For per-tenant in-page colors, keep using TenantTheme.

Drive-by fix

_render_template_with_env now falls back to the bare template name after <lang>/<name> and default/<name>. Framework email templates ship flat at the namespace root (not under default/<lang>/), so without the flat fallback the magic-link email failed to render.

Test plan

  • uv run python -m pytest tests/unit — 777 passed
  • uv run python -m pytest tests/unit/test_brand_settings.py — 21 new tests cover HexColor subclass, BrandSettings defaults / env-var binding / fallbacks, favicons partial render, webmanifest + browserconfig render, magic-link email render with button color, brand context provider
  • just lint-py — passes
  • just lint-jinja — passes
  • just lint-md — passes
  • just type-check — passes

🤖 Generated with Claude Code

DaisyUI tokens and CSS variables cover everything that renders inside the
page, but a few brand surfaces are out of reach: favicon meta tags
(`mask-icon`, `msapplication-TileColor`, `theme-color`) are read by the
browser before any CSS runs; the PWA manifest's `theme_color` /
`background_color` are consumed by the OS install UI; and email clients
(Gmail, Outlook) strip `<style>` blocks and ignore CSS variables.

`BrandSettings` is an app-level pydantic-settings surface for those
specific surfaces, driven by three env vars:

- `BRAND_PRIMARY_COLOR` — Safari mask-icon, Windows tile, magic-link
  email button (when no override is set).
- `BRAND_BROWSER_THEME_COLOR` — mobile browser chrome, PWA manifest.
- `BRAND_EMAIL_BUTTON_COLOR` — explicit override slot for the email
  button when it must differ from the primary brand color.

Inputs accept any pydantic-Color form (named, `rgb()`, hex). Values are
canonicalised to long-form `#rrggbb` lowercase via a `HexColor` subclass
that pins `__str__` to `as_hex(format="long")` — bare `Color` would render
`#ffffff` as the string `white`, which is awkward to interpolate into
HTML attributes.

`settings.brand` is exposed in every Jinja render via the shipped
`_brand_context` provider; the bundled `base/favicons.html.jinja`,
`meta/site.webmanifest.jinja`, and `meta/browserconfig.xml.jinja` already
consume it. `send_magic_link_email()` passes `settings.brand.email_button`
into the email template.

Per-tenant brand colors stay with `TenantTheme` (favicon assets are
static files served before tenant resolution; the email service does not
see request context).

Drive-by fix: `_render_template_with_env` now falls back to the bare
template name after `<lang>/<name>` and `default/<name>`. Framework email
templates ship flat at the namespace root, not under `default/<lang>/`,
so without the flat fallback the magic-link email failed to render.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@davidpoblador davidpoblador merged commit 3d22717 into main Apr 28, 2026
2 checks passed
@davidpoblador davidpoblador deleted the feat/brand-settings branch April 28, 2026 09:24
davidpoblador pushed a commit that referenced this pull request Apr 28, 2026
🤖 I have created a release *beep* *boop*
---


##
[10.8.0](v10.7.0...v10.8.0)
(2026-04-28)


### Features

* add BrandSettings env-var surface for favicon and email branding
([#1720](#1720))
([3d22717](3d22717))
* i18n primitives for per-tenant locale flows
([#1718](#1718))
([177fe5e](177fe5e))


### Bug Fixes

* restore gettext plural-form support in lint-po
([#1725](#1725))
([fe0dacb](fe0dacb))
* **template:** scope babel.cfg python extraction to src/
([#1722](#1722))
([b729964](b729964))
* theme default screens with DaisyUI semantic tokens
([#1714](#1714))
([4467168](4467168))


### Performance Improvements

* **testing:** share MongoDB across vibetuner_db tests
([#1726](#1726))
([c9064e4](c9064e4))


### Miscellaneous Chores

* **py:** use PEP 639 SPDX license expression
([#1727](#1727))
([855f9f4](855f9f4))


### Documentation Updates

* **i18n:** clarify locale_names vs language_picker overlap
([#1723](#1723))
([87733bd](87733bd))
* **template:** document i18n primitives in scaffolded agent rules
([#1724](#1724))
([27a3b98](27a3b98))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant