Add a favicon, home-screen icons and a web manifest

The favicon is a bowl of soup: this household's catalog is half keitto, so it
is at least honest. favicon.svg follows the system theme in the tab strip.

Home-screen icons cannot do the same. They must be opaque and must not change
with the theme, so assets/icon.svg is a separate fixed-colour source, with the
artwork inside the central 80% for Android to mask to any launcher shape. The
same 512 is declared maskable in the manifest.

`make icons` rasterises with rsvg-convert and compresses with
`oxipng -o max --zopfli`, which beat `optipng -o7` at every size — 5860 bytes
against 6109 for the three files. Plain `oxipng -o max` was not an upgrade: it
won the two small icons and lost the 512, ending up larger overall. All output
verified pixel-identical to the rasterizer. The PNGs are committed so the
build needs no rasterizer.

Two things that only fail on a real device, so both are covered by smoke
checks: Go has no mime entry for .webmanifest and served it as octet-stream,
which browsers ignore; and a manifest fetch carries no credentials by default,
so behind Basic auth it needs crossorigin="use-credentials" or it 401s.
This commit is contained in:
Esa Kataja
2026-09-05 18:58:42 +03:00
parent d15f741ab1
commit 0538d61ba6
16 changed files with 283 additions and 4 deletions
+16
View File
@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-label="Foodster">
<!-- Kulho: a bowl with steam. Finnish dinner is soup more often than not,
and this household's own catalog is half keitto. -->
<style>
svg { color: #15616D; }
@media (prefers-color-scheme: dark) { svg { color: #58C6D2; } }
</style>
<g fill="currentColor">
<rect x="1.5" y="12" width="29" height="3.2" rx="1.6"/>
<path d="M3.5 15.2 h25 a12.5 12.5 0 0 1 -25 0 z"/>
</g>
<g fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round">
<path d="M11.5 9 c0 -2.1 2.1 -2.6 2.1 -4.6"/>
<path d="M19 9 c0 -2.1 2.1 -2.6 2.1 -4.6"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 688 B

+14
View File
@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-label="Foodster">
<!-- Kattila: a cooking pot, lid on, handles out. Cooking rather than eating. -->
<style>
svg { color: #15616D; }
@media (prefers-color-scheme: dark) { svg { color: #58C6D2; } }
</style>
<g fill="currentColor">
<circle cx="16" cy="6" r="2.5"/>
<rect x="2" y="9" width="28" height="3.4" rx="1.7"/>
<rect x="0" y="14" width="5.5" height="3.2" rx="1.6"/>
<rect x="26.5" y="14" width="5.5" height="3.2" rx="1.6"/>
<path d="M5 13.6 h22 v6.9 a5.5 5.5 0 0 1 -5.5 5.5 h-11 a5.5 5.5 0 0 1 -5.5 -5.5 z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 639 B

+12
View File
@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-label="Foodster">
<!-- Lusikka: one spoon, set on the diagonal. A fork is the cliché; a spoon
on its own is not, and it stays a single solid shape at any size. -->
<style>
svg { color: #15616D; }
@media (prefers-color-scheme: dark) { svg { color: #58C6D2; } }
</style>
<g fill="currentColor" transform="rotate(34 16 16)">
<ellipse cx="16" cy="9.5" rx="5.4" ry="7"/>
<rect x="13.9" y="15" width="4.2" height="14" rx="2.1"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 545 B

+14
View File
@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-label="Foodster">
<!-- Ruisleipä: the Finnish rye loaf, hole in the middle. Nothing else in a
tab strip looks like this — which is the point, and also the risk. -->
<style>
.hole { fill: #ECEDE8; }
.loaf { fill: #6B4636; }
@media (prefers-color-scheme: dark) {
.hole { fill: #121316; }
.loaf { fill: #9A6E52; }
}
</style>
<circle class="loaf" cx="16" cy="16" r="14"/>
<circle class="hole" cx="16" cy="16" r="3.8"/>
</svg>

After

Width:  |  Height:  |  Size: 546 B

+118
View File
@@ -0,0 +1,118 @@
<!doctype html>
<html lang="en" data-theme="auto">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Foodster — favicon options</title>
<link rel="icon" href="a-kulho.svg" type="image/svg+xml">
<style>
:root{
color-scheme: light dark;
--paper: light-dark(#ECEDE8, #121316);
--card: light-dark(#FFFFFF, #1C1E22);
--ink: light-dark(#14161A, #E9EAE5);
--muted: light-dark(#6B6F6A, #8B8F89);
--line: light-dark(#D5D6D0, #2C2F34);
}
html[data-theme="light"]{color-scheme:only light;}
html[data-theme="dark"] {color-scheme:only dark;}
*{box-sizing:border-box;}
body{margin:0;background:var(--paper);color:var(--ink);
font-family:system-ui,sans-serif;font-size:16px;line-height:1.45;}
.wrap{max-width:900px;margin:0 auto;padding:24px 20px 60px;}
h1{font-size:20px;letter-spacing:-.02em;margin:0 0 4px;}
.lede{margin:0 0 22px;color:var(--muted);font-size:14px;}
.themebtn{font:inherit;font-size:13px;background:var(--card);border:1px solid var(--line);
color:var(--muted);padding:7px 12px;border-radius:7px;cursor:pointer;margin-bottom:22px;}
.opt{background:var(--card);border:1px solid var(--line);border-radius:12px;
padding:18px;margin-bottom:14px;}
.opt h2{font-size:16px;margin:0 0 2px;letter-spacing:-.02em;}
.opt p{margin:0 0 14px;font-size:13.5px;color:var(--muted);}
.sizes{display:flex;align-items:flex-end;gap:22px;flex-wrap:wrap;}
.size{text-align:center;}
.size img{display:block;}
.size span{display:block;margin-top:6px;font-size:10px;letter-spacing:.06em;
text-transform:uppercase;color:var(--muted);}
/* A browser tab is the real test: 16px against the tab strip. */
.tabsim{margin-top:16px;display:flex;gap:10px;flex-wrap:wrap;}
.tab{display:flex;align-items:center;gap:7px;border-radius:8px 8px 0 0;
padding:7px 14px 7px 10px;font-size:12.5px;}
.tab img{width:16px;height:16px;}
.onwhite{background:#fff;color:#333;border:1px solid #ddd;}
.ondark{background:#1b1d22;color:#bbb;border:1px solid #33363c;}
</style>
</head>
<body>
<div class="wrap">
<h1>Foodster — favicon, round two</h1>
<p class="lede">Objects from the kitchen rather than geometry. The 16&nbsp;px row and the tab strips are the only test that counts.</p>
<button class="themebtn" id="t">Theme: auto</button>
<div class="opt">
<h2>A · Kulho</h2>
<p>A bowl with steam. This household's catalog is half <em>keitto</em>, so soup is honest branding.</p>
<div class="sizes">
<div class="size"><img src="a-kulho.svg" width="16" height="16"><span>16</span></div>
<div class="size"><img src="a-kulho.svg" width="32" height="32"><span>32</span></div>
<div class="size"><img src="a-kulho.svg" width="64" height="64"><span>64</span></div>
<div class="size"><img src="a-kulho.svg" width="180" height="180"><span>180</span></div>
</div>
<div class="tabsim">
<span class="tab onwhite"><img src="a-kulho.svg" alt="">Foodster</span>
<span class="tab ondark"><img src="a-kulho.svg" alt="">Foodster</span>
</div>
</div>
<div class="opt">
<h2>B · Kattila</h2>
<p>A pot with the lid on and handles out. About cooking rather than eating.</p>
<div class="sizes">
<div class="size"><img src="b-kattila.svg" width="16" height="16"><span>16</span></div>
<div class="size"><img src="b-kattila.svg" width="32" height="32"><span>32</span></div>
<div class="size"><img src="b-kattila.svg" width="64" height="64"><span>64</span></div>
<div class="size"><img src="b-kattila.svg" width="180" height="180"><span>180</span></div>
</div>
<div class="tabsim">
<span class="tab onwhite"><img src="b-kattila.svg" alt="">Foodster</span>
<span class="tab ondark"><img src="b-kattila.svg" alt="">Foodster</span>
</div>
</div>
<div class="opt">
<h2>C · Lusikka</h2>
<p>One spoon on the diagonal. A fork is the cliché; a lone spoon is not, and it stays one solid shape at any size.</p>
<div class="sizes">
<div class="size"><img src="c-lusikka.svg" width="16" height="16"><span>16</span></div>
<div class="size"><img src="c-lusikka.svg" width="32" height="32"><span>32</span></div>
<div class="size"><img src="c-lusikka.svg" width="64" height="64"><span>64</span></div>
<div class="size"><img src="c-lusikka.svg" width="180" height="180"><span>180</span></div>
</div>
<div class="tabsim">
<span class="tab onwhite"><img src="c-lusikka.svg" alt="">Foodster</span>
<span class="tab ondark"><img src="c-lusikka.svg" alt="">Foodster</span>
</div>
</div>
<div class="opt">
<h2>D · Ruisleipä</h2>
<p>The rye loaf with its hole. Nothing else in a tab strip looks like this — which is the appeal and also the risk of reading as a ring.</p>
<div class="sizes">
<div class="size"><img src="d-ruisleipa.svg" width="16" height="16"><span>16</span></div>
<div class="size"><img src="d-ruisleipa.svg" width="32" height="32"><span>32</span></div>
<div class="size"><img src="d-ruisleipa.svg" width="64" height="64"><span>64</span></div>
<div class="size"><img src="d-ruisleipa.svg" width="180" height="180"><span>180</span></div>
</div>
<div class="tabsim">
<span class="tab onwhite"><img src="d-ruisleipa.svg" alt="">Foodster</span>
<span class="tab ondark"><img src="d-ruisleipa.svg" alt="">Foodster</span>
</div>
</div>
</div>
<script>
const h=document.documentElement,b=document.getElementById('t'),m=['auto','light','dark'];
b.onclick=()=>{const n=m[(m.indexOf(h.dataset.theme)+1)%3];h.dataset.theme=n;b.textContent='Theme: '+n;};
</script>
</body>
</html>