# App icon convention > Status: convention v1 > Audience: authors shipping a Reachy Mini app to the Hugging Face Hub > Implemented by: `reachy-mini-website` catalog server (this repo) + > `reachy_mini_mobile_app`, `reachy_mini_desktop_app` > Source of truth: `server/index.js` → `findIconUrl()` This document specifies how a Reachy Mini app declares a custom icon. Apps that don't follow it keep working - the surface falls back to the front-matter `emoji:` glyph, which is the existing behaviour. --- ## 1. The convention in three lines To ship a custom icon for your Reachy Mini app: 1. Commit `icon.svg` (preferred) **or** `icon.png` at the root of your Hugging Face Space repository. 2. That's it. Within ~5 minutes (the catalog cache TTL) the mobile shell, the desktop app and the website surface your icon automatically, replacing the README front-matter emoji. 3. If both files are present, `icon.svg` wins. No README change required. No tag to add. No PR to file against this repo. The catalog server scans the file list once per refresh and publishes a resolved URL on the app entry; every client consumes it. --- ## 2. Why a file convention and not `cardData.thumbnail` HF Spaces support a `thumbnail:` field in README front-matter, but: - `thumbnail` is full-bleed marketing artwork (typically 1200x630), not a square avatar. Scaling it to a 22 px or 44 px tile produces muddy thumbnails. - We want app authors to ship a dedicated, optimised glyph they control without learning the HF metadata schema. - SVG support means the icon scales cleanly across every mount point (rail tile, pinned grid, iframe header) from a single asset. `thumbnail:` keeps its existing role (banner artwork on the Space's HF page) and is not consulted by this resolution path. --- ## 3. Format & dimension recommendations | Property | Recommended | Hard requirement | |----------|-------------|------------------| | Format | `icon.svg` (vector) | `icon.svg` or `icon.png` | | Aspect ratio | 1:1 (square) | Renderers crop with `object-fit: contain`, but non-square icons render with letterboxing - prefer a true square | | Min PNG size | 256x256 | None enforced. PNGs below 64x64 will look soft on the pinned grid (44 px on retina ≈ 88 effective px) | | Background | Transparent OR solid colour | None - your call. Renderers don't add their own plate, so an icon with no background renders directly on the tile colour | | Padding | Bake ~10% inner padding into the asset | None - but icons that bleed edge-to-edge will touch the tile's rounded corners | | Light/dark variants | Single asset that works on both | None - if you must, ship two SVGs and use `prefers-color-scheme` inside the SVG via CSS | ### Style notes - **Iconic, not photographic.** A solid filled silhouette reads at 22 px; a screenshot doesn't. - **High contrast against `background.paper`.** The mobile app paints the tile background with the surface colour (very light grey on light, near-black on dark). A pure white icon disappears on light. - **No drop shadow** baked into the asset. The renderer doesn't add one either, and a baked shadow won't scale across sizes. --- ## 4. How resolution works (for the curious) 1. The catalog server calls `https://huggingface.co/api/spaces?filter=reachy_mini&full=true`. With `full=true`, the HF Hub returns `siblings: [{ rfilename: ... }]` for every Space - the complete file list. 2. For each app, `findIconUrl()` (in `server/index.js`) scans the list for root-level filenames matching `ICON_CANDIDATES` in order (`icon.svg` → `icon.png`). 3. The first match becomes: ``` https://huggingface.co/spaces///resolve/main/ ``` `resolve/main/` (not `raw/main/`) so LFS pointers follow through transparently and the `Content-Type` is set from the extension, which `` needs. 4. The URL is published on the app entry as a top-level `iconUrl` field. `null` when neither candidate exists. 5. Clients (`reachy_mini_mobile_app`, `reachy_mini_desktop_app`) read `iconUrl` and render an `` when present, falling back to the front-matter emoji otherwise. A runtime image load failure re-falls-back to the emoji without a refresh. The whole resolution path is server-side, behind the 5-minute catalog cache. Adding 100 more apps adds zero per-client probes. --- ## 5. Adding new icon formats If you need to support a new format (say, `icon.webp`), edit `ICON_CANDIDATES` in `server/index.js`: ```js const ICON_CANDIDATES = ['icon.svg', 'icon.png', 'icon.webp']; ``` Order matters - the first hit wins, so put the preferred format first. Bumping the catalog cache (POST `/api/js-apps/refresh-categories` or just wait 5 minutes) picks up the new resolution rule.