Spaces:
Running
Running
| # 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/<author>/<repo>/resolve/main/<filename> | |
| ``` | |
| `resolve/main/` (not `raw/main/`) so LFS pointers follow through | |
| transparently and the `Content-Type` is set from the extension, | |
| which `<img>` 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 `<img>` 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. | |