From 0538d61ba6c65f961f95a7affe0e64832f8c0532 Mon Sep 17 00:00:00 2001 From: Esa Kataja Date: Sat, 5 Sep 2026 18:58:42 +0300 Subject: [PATCH] Add a favicon, home-screen icons and a web manifest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- Makefile | 17 +++- README.md | 21 +++- assets/icon.svg | 19 ++++ cmd/foodster/main.go | 5 + cmd/foodster/static/apple-touch-icon.png | Bin 0 -> 1356 bytes cmd/foodster/static/favicon.svg | 16 +++ cmd/foodster/static/icon-192.png | Bin 0 -> 1385 bytes cmd/foodster/static/icon-512.png | Bin 0 -> 3119 bytes cmd/foodster/static/manifest.webmanifest | 17 ++++ cmd/foodster/views.templ | 7 ++ mockups/favicon/a-kulho.svg | 16 +++ mockups/favicon/b-kattila.svg | 14 +++ mockups/favicon/c-lusikka.svg | 12 +++ mockups/favicon/d-ruisleipa.svg | 14 +++ mockups/favicon/index.html | 118 +++++++++++++++++++++++ scripts/smoke.sh | 11 +++ 16 files changed, 283 insertions(+), 4 deletions(-) create mode 100644 assets/icon.svg create mode 100644 cmd/foodster/static/apple-touch-icon.png create mode 100644 cmd/foodster/static/favicon.svg create mode 100644 cmd/foodster/static/icon-192.png create mode 100644 cmd/foodster/static/icon-512.png create mode 100644 cmd/foodster/static/manifest.webmanifest create mode 100644 mockups/favicon/a-kulho.svg create mode 100644 mockups/favicon/b-kattila.svg create mode 100644 mockups/favicon/c-lusikka.svg create mode 100644 mockups/favicon/d-ruisleipa.svg create mode 100644 mockups/favicon/index.html diff --git a/Makefile b/Makefile index 083c7f1..2ad5146 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,7 @@ COMPOSE ?= podman compose BIN := foodster PKG := ./cmd/foodster +STATIC := cmd/foodster/static # Vendored Datastar client. Bump, run `make vendor`, commit the result. DATASTAR_VERSION ?= v1.0.3 @@ -18,7 +19,7 @@ endif GOFILES = $(shell find . -name '*.go' -not -name '*_templ.go' 2>/dev/null) .DEFAULT_GOAL := help -.PHONY: help generate build run seed test smoke check lint fix vendor image push release up down logs clean +.PHONY: help generate build run seed test smoke check lint fix icons vendor image push release up down logs clean help: ## Show this help @grep -hE '^[a-zA-Z_-]+:.*## ' $(MAKEFILE_LIST) \ @@ -51,10 +52,20 @@ check: ## Everything that must pass before a commit @$(MAKE) --no-print-directory smoke @echo "check: all passed" +icons: ## Rasterise home-screen PNGs from assets/icon.svg and optimise them + rsvg-convert -w 180 -h 180 assets/icon.svg -o $(STATIC)/apple-touch-icon.png + rsvg-convert -w 192 -h 192 assets/icon.svg -o $(STATIC)/icon-192.png + rsvg-convert -w 512 -h 512 assets/icon.svg -o $(STATIC)/icon-512.png + # oxipng -o max alone loses to optipng on the 512; --zopfli wins at every + # size. Slow, but these are three tiny files built by hand. + oxipng -o max --zopfli --quiet \ + $(STATIC)/apple-touch-icon.png $(STATIC)/icon-192.png $(STATIC)/icon-512.png + @ls -l $(STATIC)/*.png + vendor: ## Re-download the Datastar client (DATASTAR_VERSION=v1.0.3) - curl -sSfL -o cmd/foodster/static/datastar.js \ + curl -sSfL -o $(STATIC)/datastar.js \ "https://cdn.jsdelivr.net/gh/starfederation/datastar@$(DATASTAR_VERSION)/bundles/datastar.js" - @head -1 cmd/foodster/static/datastar.js + @head -1 $(STATIC)/datastar.js lint: generate ## go vet, gofmt check, golangci-lint when installed go vet ./... diff --git a/README.md b/README.md index ae8354d..3067b0b 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,12 @@ Working: Still to build: +- **UI polish.** Checkbox chips hide the native control and signal state only + through background colour, so "Tarjoillaan lisukkeiden kanssa" gives no + clear read of on versus off. Show a real checkbox. Same applies to the + category and side chips. - Light / dark theme switch, saved per device (PRD §7.4). -- A proper app header and a `favicon.svg`. +- A proper app header. - Stage 2: the seven-meal suggester, which starts once there is history to weight against. @@ -102,6 +106,21 @@ a scratch database: make seed # or: SEED=seeds/other.json make seed ``` +## Icons + +`cmd/foodster/static/favicon.svg` is the browser-tab icon and follows the +system theme. The home-screen icons are PNGs, because a home-screen icon +cannot be transparent and must not change with the theme; they are rasterised +from `assets/icon.svg`, which is opaque and keeps the artwork inside the +central 80% so Android can mask it to any shape. + +```sh +make icons # rsvg-convert, then optipng -o7 +``` + +The PNGs are committed so the build needs no rasterizer. Re-run `make icons` +after editing `assets/icon.svg`. + ## Migrations Numbered SQL files in `cmd/foodster/migrations`, embedded in the binary and diff --git a/assets/icon.svg b/assets/icon.svg new file mode 100644 index 0000000..5560ce2 --- /dev/null +++ b/assets/icon.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + diff --git a/cmd/foodster/main.go b/cmd/foodster/main.go index 934f14e..e38b864 100644 --- a/cmd/foodster/main.go +++ b/cmd/foodster/main.go @@ -15,6 +15,7 @@ import ( "flag" "fmt" "log" + "mime" "net/http" "os" "os/signal" @@ -140,6 +141,10 @@ func openDB(path string) (*sql.DB, error) { } func routes(db *sql.DB, loc *time.Location, password string) http.Handler { + // Go's mime table has no entry for .webmanifest, and a manifest served as + // octet-stream is ignored by the browser. + _ = mime.AddExtensionType(".webmanifest", "application/manifest+json") + a := &app{db: db, loc: loc} mux := http.NewServeMux() diff --git a/cmd/foodster/static/apple-touch-icon.png b/cmd/foodster/static/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..6e893c0cd65603c794f645f114d3c5eca6625db5 GIT binary patch literal 1356 zcmV-S1+)5zP)+I*=+>)<}!7D zk+QUWq^QT$)7RkKbe^M9iICRc+fIg#%h=YQy1OQ9c29?ohpVnQcYuAQsM6foL4AhR z-rHxEoUq2ldZVYR!@{bwz!qF zw&dvJ=IQ20gNjChiL=PZ&DqvslbK3{i)EFX9cFSjcYwpv&>LlMvB$=rySzGif;@YK zM}msM(a_J?*T2xugsQEZxw?(8vO<1_t;ED0XL44Il5m`%&f3;@p`|TwdT5uN#M04I ziICIX+B0;2EO2_f&diFfur_vowaUq4l$(2_r`X`!sKLQ@p`}=jlqqg`ex<2nl$w>a zwPux@J9>k<%*)K!)#d2qYMGvtv$eFz$ZMIN7h-O%#l?W9s^RA1nz*=ur>k|Jq)38_ z=$3ld`oyeT83`!r*le1gBx`kp zsjQE&vw5SY8)b3H*VQz2f6CX@)ZW`Qc7WpN;%bL}5X`t;hxd00000003}1{Sw0>u52bG0ASCa`YczX zD1fhCZQHi{+BSdJwrz~twr%6&+s)fpNlw~0Rh1h2U$>w42J{rSLb9wBTg*KlOR{Ga zI$Gs*FUMfBAYL~y0{zSl^M+__@+wU&(9at*1x&%|Gnzhy2!u$NVY+l4PJYe=-PF@E z1&56qnm&ehmTPGW+Y4V3t3N3}u&r3Aj4X2N2 zc@Fvsrl|x@AJUQn{e;kz3@49u%)jZ-&zV|Y7lO?qctag@^z$%DJ{pEC+Pg@0oq(RE zCTFIzvE9PM1+%|GoICEg!|3hv6RvN-fJDpllwy7X(nLj4l8JH1axbO%2eL#{nrg`M zI;E+BEU!_Tc*xR7X;vYN5lu-FJdh?TgfavKMM0)N>t@^hg=(8!U50cm6sjF6{7<1e zpu!}DT7?P&6zVCc&`+U~mHZJe5CZWmlqjP>zd((LC{Q!hD5OBMP$Px{-48W9Llh=3 z9g2)mm>{TfjKZWr6(f^^1RGGMfP&0Go%}osk+%Saf+)oMP$?yr0t9>jrTi$s8r1q> zg+6Qj4T^nFpLIYrBb+{Z#{=aSTIrLOx8Z_b`s5^B@dADDBHWSw7?12L4_uOzz$5DU z8m>tU<&lIA!bM^4@d*AJhpXNV;-URF2bXQEXYo+FTAJX#><4%VOP$a{Rb>DVDB#&d z=;DhWho~DqLv~js*Y!000L0TMuJT1poj5000000KhD%FP9=!^|U(x O0000 + + + + + + + + + + + diff --git a/cmd/foodster/static/icon-192.png b/cmd/foodster/static/icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..124ee5bc80400ee0b70e8d5fed8934ebbd7e417c GIT binary patch literal 1385 zcmV-v1(y1WP)g?xal$)NqyXouZ)!y5ixw_}+<#3##C2V%)>E@=uzjB?T9cFTXr>flI z-z95x8)a{2mYmw*-O1P0(A(Iey}mDUe5}L6KYWEKZFuJCv@WIe399 zZ+eWcu_tYK)7{z{WN>PjoyynMFLHax)zfyMq{GtCzR%7>e}}ls%HihXwaUq|$Hs%H ztaqWMTaK2ez`#w0jikQ6;^*Q}hmSROfaK`ooVmL)b$&Z~gG+>r+v49bbbW!St1xqX zW0aa}nVz@G$~=05uEfP$kCor$;K$X|<>=(xae*5BK`&dr#& zxTU|qg8{npfhxTld`qw>*kNKvpRW# z#M03qXmixw+ZttXlC!lNWpO!qf`X~5slmZoj+Gf>Z{FnK%-Gd2bA2{;e`1rFY?_}( zf{Is+lO}Cv zNmteTFU|?n@W@O8t4Y4{Y7fNg|K==N{d5E?96CtmXWtFkdJZ$?*CgNugx_8p`8$7Pib)p|qJw?~RMYy^$=?HxZsVb}KxE6)S%XGyd)(3Q5hsVX2=s2H% zSnt!32e0FF?5==V$q#9p>V{V~Z8=?#tDcSp_&iKo9t3No?Q8-*2Wk5bf-TYZHTxE!x@t&f?&!gC$Ig&s zUoWIfgx`7lEv)9o7stPi;D7)7p8)mu-J!{$2cX~Zl^kl5Gc*XzA`R5)Y7%;#qh^<& z*H6^!XXtg3n&m>T6V$90dYz$Wb3&gVdlB1~pT;k6}^`mHP-LjZwK`nDi-?i^HS~R4xl9%}}`;Fe;ad z-2=1Qs8}A%s-j|9FsmY;N);8turVrC4AaJ`)B;Q!ET=-Pl`yW13VjdrDjugYEyoam z7?p`52u&?iWO5OK*h@vOBN&N3Dsec0fGlNFfy_q{m|v*CD1tN5!L&V=K!7@5XPR2~ zAW*O6GcC$g zc9u;;x@7p~6jwDhG@ryjLuc0MQd+-nQXM1o^000C80L=Ao03fLlp0{n? r*?L58bF7jc0000000000004g;?gO1y=?P|200000NkvXXu0mjfEn>cx literal 0 HcmV?d00001 diff --git a/cmd/foodster/static/icon-512.png b/cmd/foodster/static/icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..1652c5a0d955d3fa635e0c7fd3126a7e929af825 GIT binary patch literal 3119 zcmbtWX*d)L8y&f!jHQc`rDkNQggcVRl6|=`WWAwm*PqoRek?H5WQ9bs7Kw2w7T~*Z}}1 zevT&q0=z$Ipjw{sbD6cZaxj(fgzdlE8J?Zy?rq)2=D*$9*xB2{_w;79w)agk_x86V zUl6u;Hytyc8^ofnJdU~^{&b7`ww>|XCM9cbl>-h4H;zlWfruYx&ngBX%oEeKqY@hi z23()zGiIhP_=S4r6xpX^p#bNfYsOa&h4)p?-L7=2ldqT^e?9 ziWQBkxfvNBQGqv%Mc+xzOeHqg_4So>Q3$=SG8$VnBA&Ls9#W5p^U5uj^$qQ1PAqM# zVp~b|1O3#oHxj!#>R8;Pi`Mh&`Zg*j~wAv4R1(muiAOpBg>7>;QT2@$iRruh+fekg zb6$^`?D?}^kN1FxbO7L#xTT4q!xQ#?U&2q-1L7;c*PW~T#|Qr!zK0jY?^}LUiEoZb zg2^tD$H*}1;XGLnws_#ZNQMO|MkR*mp!Ro&^`DTa%H6}vg$GVr4dyiCUN%KiiueJU z)SUTRgr*V8M|ah?=(i>j>Iu0` ztQWI8vMQ};j}xOAK3F@Kd<(3QH+Fn7_`B=_P)4DNqqZG0Yw@%$EpCW>J*_d0Ku zTf)$VhYE@>FE%-v6-__u7M=QZd-&}-r1O)rOQ)0<#V>#$tSakbBxnt)Mq->Et+B4T zTzMw|;dr`m`3wGf=@MmLMiuF zby$kxmY~urXlWaVNp`nr&L;wfVbN*(T?@wHhs$%fBoyw7%QmFs=w)y{z;s&?^p1y=j68Iz zZw`#!Jr=ulIX*`sT=Xj+-4+!NpES@qW}AjH`MqkDs>0!r(!X!ZY6J>HrUS#7A4O$I zSE{?$n~i`S#ln2C0RC2=@b|)W+q9aU)%2rYV_C8haHdF@{A!YiO+6YFwI9QG!*tf( zMzM)%Nrk(>ca0ogb9?{8v#HKu0D}2iZA3148DnUbCu!h1*SFxp-kp^3YPd)G^zG=Q zKmi$5xCdryx7-}NYtVMAnc8Q@9O?>NH9~Gf)gd#pmm5*z3DO;;KWilE9_IcVCp+7+ zX1B%@3WIJp4!#`78zhm>dXZLGY^hFl?CdR>L`m31l|d205AiN3FETGF{PcmY<((xW z*muwHj`!NQuZxQagwsWG$o`LAkst_7d_m}j`$?$W7noq3t30i$XMKksj1DwY9$RId za=FC=;r!%*4KpLp z=TL!7gB10;CP?ZRwzdji;nxkEfbvz+Ji^RYafM%vEoJ zF7p|d3;=7XuZ;jP?^SuZgIOm4byRXLPgtnCG($-fw#+*GP*qj+?Z?FSf!fup6xvGu zm>@P&*Pwzbg1{&1M?&@)3P)Y8p5{F!6*@dRtOEU)0tmcV>|>#Q2K*~WcRu^;L)+vCI&0>2p>1fdU$aJ?f9O2t{Phm1?DL=d7Nza4KJ7&>mY;~M!aS*!T`exyMi zAsAy+84&4apW3ea>8Q>&ELHG6*Pb7g&?K;OA_VtLf9Ayc(2K4kf&wH}`Q#DdGx)&G z(47qosfm(^y=*5G!u9>i9K{-lq@{o#N6>P5#JH&-dztsejUXzHtth6R;su-4t_Yu--)RSiz%e@0}cW-Bx-|z>?!)~(Lo09Nq z5Y&YZwFDscyfoJ`MHzcWomP0k!3_<}{>U{%F~k=rndKkX5Dzo2-8afA`o^PfA!T%w z`&9{SSbjEEsz@Pig=zc%3L1F0rx`uu8Z5Vgk5G=LADqWtd$I+K?&p|;?uF2F^gwp} z!_TX4XeVL^eT~Qa&2Dj%k4N|f|<`2KP`zf%uOlhnRSrxDe31&0eRg>2nx-oWZU($9@@WS)HPetU&M)4mBD zrdo@o7hOELZD<9j@qYv@10eMAJE}gZpMZTs8X6zQFA7jN};JCK-cb^4IWU6s#dP;6~&O(3IEco@iYyUWEJqE>%S01-s z*)B+)ApJD>Li)&u4>%#~5=|KuwdRInKPuwdKNaOS#!9p>H3O%I + + { title } + + + + diff --git a/mockups/favicon/a-kulho.svg b/mockups/favicon/a-kulho.svg new file mode 100644 index 0000000..a5e5918 --- /dev/null +++ b/mockups/favicon/a-kulho.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/mockups/favicon/b-kattila.svg b/mockups/favicon/b-kattila.svg new file mode 100644 index 0000000..83b2a4a --- /dev/null +++ b/mockups/favicon/b-kattila.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/mockups/favicon/c-lusikka.svg b/mockups/favicon/c-lusikka.svg new file mode 100644 index 0000000..ae66deb --- /dev/null +++ b/mockups/favicon/c-lusikka.svg @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/mockups/favicon/d-ruisleipa.svg b/mockups/favicon/d-ruisleipa.svg new file mode 100644 index 0000000..a34335b --- /dev/null +++ b/mockups/favicon/d-ruisleipa.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/mockups/favicon/index.html b/mockups/favicon/index.html new file mode 100644 index 0000000..ff9acca --- /dev/null +++ b/mockups/favicon/index.html @@ -0,0 +1,118 @@ + + + + + +Foodster — favicon options + + + + +
+

Foodster — favicon, round two

+

Objects from the kitchen rather than geometry. The 16 px row and the tab strips are the only test that counts.

+ + +
+

A · Kulho

+

A bowl with steam. This household's catalog is half keitto, so soup is honest branding.

+
+
16
+
32
+
64
+
180
+
+
+ Foodster + Foodster +
+
+ +
+

B · Kattila

+

A pot with the lid on and handles out. About cooking rather than eating.

+
+
16
+
32
+
64
+
180
+
+
+ Foodster + Foodster +
+
+ +
+

C · Lusikka

+

One spoon on the diagonal. A fork is the cliché; a lone spoon is not, and it stays one solid shape at any size.

+
+
16
+
32
+
64
+
180
+
+
+ Foodster + Foodster +
+
+ +
+

D · Ruisleipä

+

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.

+
+
16
+
32
+
64
+
180
+
+
+ Foodster + Foodster +
+
+
+ + + diff --git a/scripts/smoke.sh b/scripts/smoke.sh index 47c4fc6..e9eed2e 100755 --- a/scripts/smoke.sh +++ b/scripts/smoke.sh @@ -54,6 +54,17 @@ check "healthz needs no password" \ check "datastar client is served" \ "$(curl -s -o /dev/null -w '%{http_code}' -u ":$pass" "http://$addr/static/datastar.js")" "200" +check "favicon is served" \ + "$(curl -s -o /dev/null -w '%{http_code}' -u ":$pass" "http://$addr/static/favicon.svg")" "200" + +check "apple touch icon is served" \ + "$(curl -s -o /dev/null -w '%{http_code}' -u ":$pass" "http://$addr/static/apple-touch-icon.png")" "200" + +# A manifest served as octet-stream is silently ignored by the browser. +check "manifest has the right content type" \ + "$(curl -s -o /dev/null -w '%{content_type}' -u ":$pass" "http://$addr/static/manifest.webmanifest")" \ + "application/manifest+json" + check "catalog starts empty" \ "$(curl -s -u ":$pass" "http://$addr/ruoat")" "0 pääruokaa"