add favicon and site icon generation from uploaded images
All checks were successful
deploy / deploy (push) Successful in 1m26s
All checks were successful
deploy / deploy (push) Successful in 1m26s
Upload a source image (PNG, JPEG, or SVG) and get a complete favicon setup: PNG variants at 32, 180, 192, 512px served from DB via FaviconController with ETag caching, SVG favicon for vector sources, dynamic site.webmanifest, and theme-color meta tag. Theme editor gains a site icon section with "use logo as icon" toggle, dedicated icon upload, short name, and background colour picker. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
104
lib/berrypod_web/controllers/favicon_controller.ex
Normal file
104
lib/berrypod_web/controllers/favicon_controller.ex
Normal file
@@ -0,0 +1,104 @@
|
||||
defmodule BerrypodWeb.FaviconController do
|
||||
use BerrypodWeb, :controller
|
||||
|
||||
alias Berrypod.Media
|
||||
alias Berrypod.Settings
|
||||
|
||||
@one_day 86_400
|
||||
|
||||
def favicon_svg(conn, _params) do
|
||||
case Media.get_favicon_variants() do
|
||||
%{svg: svg} when is_binary(svg) ->
|
||||
conn
|
||||
|> maybe_not_modified("svg")
|
||||
|> serve("image/svg+xml", svg)
|
||||
|
||||
_ ->
|
||||
send_resp(conn, 404, "")
|
||||
end
|
||||
end
|
||||
|
||||
def favicon_32(conn, _params), do: serve_variant(conn, :png_32)
|
||||
def apple_touch_icon(conn, _params), do: serve_variant(conn, :png_180)
|
||||
def icon_192(conn, _params), do: serve_variant(conn, :png_192)
|
||||
def icon_512(conn, _params), do: serve_variant(conn, :png_512)
|
||||
|
||||
def webmanifest(conn, _params) do
|
||||
settings = Settings.get_theme_settings()
|
||||
|
||||
short_name =
|
||||
case settings.favicon_short_name do
|
||||
name when is_binary(name) and name != "" -> name
|
||||
_ -> String.slice(settings.site_name, 0, 12)
|
||||
end
|
||||
|
||||
manifest = %{
|
||||
name: settings.site_name,
|
||||
short_name: short_name,
|
||||
theme_color: settings.accent_color || "#000000",
|
||||
background_color: settings.icon_background_color || "#ffffff",
|
||||
display: "minimal-ui",
|
||||
start_url: "/",
|
||||
icons: [
|
||||
%{src: "/icon-192.png", sizes: "192x192", type: "image/png"},
|
||||
%{src: "/icon-512.png", sizes: "512x512", type: "image/png", purpose: "maskable"}
|
||||
]
|
||||
}
|
||||
|
||||
conn
|
||||
|> put_resp_content_type("application/manifest+json")
|
||||
|> put_resp_header("cache-control", "public, max-age=#{@one_day}")
|
||||
|> json(manifest)
|
||||
end
|
||||
|
||||
defp serve_variant(conn, field) do
|
||||
case Media.get_favicon_variants() do
|
||||
nil ->
|
||||
send_resp(conn, 404, "")
|
||||
|
||||
variants ->
|
||||
case Map.get(variants, field) do
|
||||
data when is_binary(data) ->
|
||||
conn
|
||||
|> maybe_not_modified(variants)
|
||||
|> serve("image/png", data)
|
||||
|
||||
_ ->
|
||||
send_resp(conn, 404, "")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_not_modified(%{halted: true} = conn, _), do: conn
|
||||
|
||||
defp maybe_not_modified(conn, variants) do
|
||||
etag = build_etag(variants)
|
||||
|
||||
case get_req_header(conn, "if-none-match") do
|
||||
[^etag] ->
|
||||
conn
|
||||
|> put_resp_header("cache-control", "public, max-age=#{@one_day}")
|
||||
|> put_resp_header("etag", etag)
|
||||
|> send_resp(304, "")
|
||||
|> halt()
|
||||
|
||||
_ ->
|
||||
conn
|
||||
|> put_resp_header("cache-control", "public, max-age=#{@one_day}")
|
||||
|> put_resp_header("etag", etag)
|
||||
end
|
||||
end
|
||||
|
||||
defp serve(%{halted: true} = conn, _content_type, _data), do: conn
|
||||
|
||||
defp serve(conn, content_type, data) do
|
||||
conn
|
||||
|> put_resp_content_type(content_type)
|
||||
|> send_resp(200, data)
|
||||
end
|
||||
|
||||
defp build_etag(%{generated_at: %DateTime{} = dt}),
|
||||
do: ~s("fav-#{DateTime.to_unix(dt)}")
|
||||
|
||||
defp build_etag(_), do: ~s("fav-0")
|
||||
end
|
||||
Reference in New Issue
Block a user