# The operator's own logo and favicon

2026-08-08. Ernest, mid-session:

> "We also need a way for someone to change their logo and favicon for their
> site. Kinda pointless without it."

He is right, and it is worse than "missing". Both were **hardcoded to
Convoro's own branding**:

```
main.cvr:49  <img src="@asset('/assets/img/logo.png')" …>
main.cvr:13  <link rel="icon" … href="@asset('/assets/img/icon-32.png')">
main.cvr:14  <link rel="apple-touch-icon" … href="@asset('/assets/img/icon-180.png')">
```

Every install of Convoro would have shipped with Convoro's logo in the header
and Convoro's icon in the browser tab, with no setting anywhere to change
either. For a self-hosted product that is not an incomplete feature, it is a
defect that makes the product unusable for its actual purpose.

---

## 🚨 The bug found on the way in, which had to be fixed first

Site settings live at `/admin/settings`. That route was declared in
**`modules/forum/Routes/admin.php`**, and so was the whole admin dashboard at
`/admin`.

`forum` is an **application**. It can be switched off. Measured, with Forums
disabled:

```
/admin             404
/admin/settings    404
/admin/themes      200
```

So a Downloads-only or Pages-only site — precisely what
[the application model](2026-08-07-the-application-model.md) exists to make
possible — had **no admin dashboard and no site settings screen at all.** No
SMTP, no site name, no policy URLs. Turning off Forums took them with it.

Putting the logo there would have hidden the branding behind the same switch,
so the dashboard and settings moved to **System** before anything else was
added. They are site-wide concerns and were never the forum's to own. Forums
keeps what is genuinely its own: forums, prefixes, tags.

This is the same defect as the widget registry, one layer up: something
everybody needs living inside something that can be turned off.

## What an operator can set

Four things, all optional, all falling back to something sensible:

| Setting | Falls back to |
|---|---|
| `site_logo` | the site name, as text |
| `site_logo_dark` | `site_logo` |
| `site_favicon` | Convoro's icon |

**The wordmark fallback matters more than the upload.** An operator who never
uploads anything should get *their own site's name* in the header, not
Convoro's logo. That single change fixes the embarrassing case; the upload is
the improvement on top of it.

### Why a separate dark logo

The theme system gives every member a light or dark mode. A logo drawn in dark
ink for a white header disappears entirely on a dark one, and it is the single
most common way a themed forum looks broken.

Two uploads, the second optional and falling back to the first. Not a colour
filter, not an automatic invert: inverting a logo with a photograph or a
gradient in it produces something worse than the original problem, and the
operator already has the file they want.

🚨 Both are rendered, and CSS picks. **Not** a server-side choice of one, because
the mode can be `auto` — resolved by the visitor's own operating system, which
the server does not know at render time. Emitting both and letting
`prefers-color-scheme` choose is the only version that is correct for `auto`.

### The favicon is one upload, two files

A square image in; a 32px and a 180px PNG out. Those are the two that matter —
the tab and the iOS home screen — and generating them removes the "why is my
favicon blurry" support question entirely.

**No `.ico`.** Every browser in use accepts a PNG favicon through a `<link>`
tag. `/favicon.ico` is still requested by some crawlers when no tag is present,
but a tag *is* present, so the fallback path is never reached.

🚨 **SVG is refused**, exactly as the cover and avatar stores refuse it. It can
carry script, and this one is served on every page of the site to everybody
including signed-out visitors. Adding it needs a sanitiser first, and that is
its own note.

## The discipline, unchanged from the stores that came before

Copied from `CoverStore` because it was right there and right:

- **MIME is sniffed from the file's own bytes**, never from what the uploader
  claimed, and the extension written to disk comes from the sniff rather than
  from the filename.
- **Stored names are generated**, 32 hex characters plus a known extension, and
  `pathFor()` refuses anything not matching that shape — so the serving route
  cannot be talked into reading a path.
- **Bounded.** 4 MB in, and the logo is scaled to at most 600×200 — it renders
  in a header at around 40px tall, and a 4000px original served to every visitor
  on every page is a real cost for no visible gain.
- Replacing an image **deletes the one it replaced**, so `content/brand/` does
  not accumulate every logo the site has ever had.

## What this note does not decide

- **A per-theme logo.** Themes already carry design tokens and could carry a
  logo too. Deliberately not done: the overwhelmingly common case is one logo
  per site, and a logo that changes with the active theme is a surprise before
  it is a feature.
- **An email logo.** Mail needs an absolute URL and inlining rules of its own,
  and `mail_from_name` is the only branding mail has today. Worth doing, its
  own change.
- **Sizing controls.** The header logo is capped by CSS. If somebody wants it
  bigger, that is the theme editor's job rather than a second place to set it.
