From cb2eba1a2614be7e60129e0573941f805d53f1fb Mon Sep 17 00:00:00 2001 From: Sergei Poljanski Date: Sat, 10 Oct 2026 09:27:44 +0400 Subject: [PATCH] bingo: serve real favicon files The favicon was an emoji in a data: URI and /favicon.ico was a 404. Google only shows a site icon in results when the favicon has a crawlable URL, and the emoji looked different on every platform. Add an SVG (a 5x5 card with a crossed-out diagonal), a 16/32/48 ICO and a 180px apple-touch-icon under assets/bingo/, served by explicit routes because public/ belongs to asxp.io. Assisted-by: Claude:opus-5 --- assets/bingo/apple-touch-icon.png | Bin 0 -> 6767 bytes assets/bingo/favicon.ico | Bin 0 -> 15086 bytes assets/bingo/favicon.svg | 1 + bingo_web.rb | 10 ++++++++++ test/bingo_test.rb | 11 +++++++++++ views/bingo/layout.erb | 4 +++- 6 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 assets/bingo/apple-touch-icon.png create mode 100644 assets/bingo/favicon.ico create mode 100644 assets/bingo/favicon.svg diff --git a/assets/bingo/apple-touch-icon.png b/assets/bingo/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..4edcee5275de9c82071edc05507f4ea2e01e6477 GIT binary patch literal 6767 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 KBwZ?L=>I?V65{*- literal 0 HcmV?d00001 diff --git a/assets/bingo/favicon.ico b/assets/bingo/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..a2e3a14ff17b1cb6bd405325a7a660d9dd297642 GIT binary patch literal 15086 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 M{dxhE`kDLvZ^v(SF8}}l literal 0 HcmV?d00001 diff --git a/assets/bingo/favicon.svg b/assets/bingo/favicon.svg new file mode 100644 index 0000000..39a06b1 --- /dev/null +++ b/assets/bingo/favicon.svg @@ -0,0 +1 @@ + diff --git a/bingo_web.rb b/bingo_web.rb index 96ca286..dd0f020 100644 --- a/bingo_web.rb +++ b/bingo_web.rb @@ -111,6 +111,16 @@ class BingoWeb < Sinatra::Base erb :index end + # Icons from assets/bingo/ (public/ is asxp.io's). Search engines want + # a crawlable favicon URL; a data: URI doesn't count. + ICONS = { 'favicon.svg' => 'image/svg+xml', 'favicon.ico' => 'image/x-icon', + 'apple-touch-icon.png' => 'image/png' }.freeze + + get %r{/(favicon\.svg|favicon\.ico|apple-touch-icon\.png)} do |name| + cache_control :public, max_age: 86_400 + send_file File.join(settings.root, 'assets', 'bingo', name), type: ICONS[name] + end + # Photos are immutable per key (a new upload gets a new key). get '/p/:file' do bytes, type = Bingo::Photo.read(params[:file]) diff --git a/test/bingo_test.rb b/test/bingo_test.rb index ef13ea2..893b62b 100644 --- a/test/bingo_test.rb +++ b/test/bingo_test.rb @@ -190,6 +190,17 @@ class BingoTest < Minitest::Test assert_equal 'http://example.org/en?view=operational', last_response.location end + def test_icons + { '/favicon.svg' => 'image/svg+xml', '/favicon.ico' => 'image/x-icon', + '/apple-touch-icon.png' => 'image/png' }.each do |path, type| + get path + assert last_response.ok?, path + assert_equal type, last_response.content_type + end + get '/en' + assert_includes last_response.body, '' + end + def test_root_varies_on_language get '/' assert_equal 'Accept-Language', last_response.headers['Vary'] diff --git a/views/bingo/layout.erb b/views/bingo/layout.erb index 845342e..1e2af6a 100644 --- a/views/bingo/layout.erb +++ b/views/bingo/layout.erb @@ -34,7 +34,9 @@ - + + +