fix(branding): serve favicon via backend route so Safari picks it up
Safari requests /favicon.ico and /apple-touch-icon*.png at the site root and is unreliable about honouring JS-injected <link rel=icon>, so an admin-set favicon never showed there (index.html only ships /favicon-32x32.png; a bare /favicon.ico 404'd). - Backend: GET /favicon.ico + /apple-touch-icon(.png|-precomposed.png) resolve the configured branding_favicon_url (redirect to its /uploads path or the absolute URL), falling back to the bundled /favicon-32x32.png. - nginx: exact-match (=) locations proxy those paths to the backend, winning over the static-asset regex that previously served them from the build dir. - DynamicFavicon also emits an apple-touch-icon link (belt-and-braces). Requires a frontend image REBUILD (nginx.conf change) in addition to backend.
This commit is contained in:
@@ -559,6 +559,34 @@ app.get('/robots.txt', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Dynamic favicon endpoints. Browsers — notably Safari — request
|
||||||
|
// /favicon.ico and /apple-touch-icon*.png directly at the site root and are
|
||||||
|
// unreliable about honouring JS-injected <link rel="icon"> tags. Serving the
|
||||||
|
// admin's configured branding favicon here makes it work without client-side
|
||||||
|
// JS (and survive aggressive favicon caches). Falls back to the bundled asset
|
||||||
|
// shipped with the frontend build when no custom favicon is set.
|
||||||
|
app.get(
|
||||||
|
['/favicon.ico', '/apple-touch-icon.png', '/apple-touch-icon-precomposed.png'],
|
||||||
|
async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { getAppSetting } = require('./src/utils/appSettings');
|
||||||
|
const raw = await getAppSetting('branding_favicon_url', null);
|
||||||
|
const url = (raw && String(raw).trim()) || null;
|
||||||
|
if (url) {
|
||||||
|
// Absolute URL → redirect as-is. Otherwise it's an /uploads path the
|
||||||
|
// backend already serves with the correct content-type + headers.
|
||||||
|
const target = /^https?:\/\//i.test(url)
|
||||||
|
? url
|
||||||
|
: (url.startsWith('/') ? url : `/uploads/${url.replace(/^uploads\//, '')}`);
|
||||||
|
return res.redirect(302, target);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('Favicon lookup failed; serving bundled default', { error: error.message });
|
||||||
|
}
|
||||||
|
return res.redirect(302, '/favicon-32x32.png');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Health check endpoint. `pid` + `uptime` let monitors (and the local E2E
|
// Health check endpoint. `pid` + `uptime` let monitors (and the local E2E
|
||||||
// watchdog) detect a silent process restart between two checks.
|
// watchdog) detect a silent process restart between two checks.
|
||||||
app.get('/health', async (req, res) => {
|
app.get('/health', async (req, res) => {
|
||||||
|
|||||||
@@ -184,6 +184,40 @@ server {
|
|||||||
proxy_set_header X-Forwarded-Proto $real_proto;
|
proxy_set_header X-Forwarded-Proto $real_proto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Dynamic favicon / apple-touch-icon served by backend (resolves the
|
||||||
|
# admin-configured branding favicon, falls back to the bundled asset).
|
||||||
|
# Exact-match (=) wins over the static-asset regex below, so these reach
|
||||||
|
# the backend instead of the build dir. Browsers (especially Safari)
|
||||||
|
# request these at the site root regardless of any JS-injected
|
||||||
|
# <link rel="icon">.
|
||||||
|
location = /favicon.ico {
|
||||||
|
set $backend_upstream backend;
|
||||||
|
proxy_pass http://$backend_upstream:3000/favicon.ico;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $real_proto;
|
||||||
|
}
|
||||||
|
location = /apple-touch-icon.png {
|
||||||
|
set $backend_upstream backend;
|
||||||
|
proxy_pass http://$backend_upstream:3000/apple-touch-icon.png;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $real_proto;
|
||||||
|
}
|
||||||
|
location = /apple-touch-icon-precomposed.png {
|
||||||
|
set $backend_upstream backend;
|
||||||
|
proxy_pass http://$backend_upstream:3000/apple-touch-icon-precomposed.png;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $real_proto;
|
||||||
|
}
|
||||||
|
|
||||||
# Delegate root requests to backend for public landing page handling
|
# Delegate root requests to backend for public landing page handling
|
||||||
location = / {
|
location = / {
|
||||||
# Use variable to force DNS resolution per request (required for Docker Swarm)
|
# Use variable to force DNS resolution per request (required for Docker Swarm)
|
||||||
|
|||||||
@@ -35,8 +35,16 @@ export const DynamicFavicon: React.FC = () => {
|
|||||||
link.rel = 'icon';
|
link.rel = 'icon';
|
||||||
if (ext && typeByExt[ext]) link.type = typeByExt[ext];
|
if (ext && typeByExt[ext]) link.type = typeByExt[ext];
|
||||||
link.href = href;
|
link.href = href;
|
||||||
|
|
||||||
document.head.appendChild(link);
|
document.head.appendChild(link);
|
||||||
|
|
||||||
|
// Safari uses apple-touch-icon for bookmarks / home-screen and is
|
||||||
|
// unreliable about JS-injected rel="icon". The backend /favicon.ico +
|
||||||
|
// /apple-touch-icon routes are the primary mechanism; this is
|
||||||
|
// belt-and-braces for browsers that do read the DOM link.
|
||||||
|
const appleLink = document.createElement('link');
|
||||||
|
appleLink.rel = 'apple-touch-icon';
|
||||||
|
appleLink.href = href;
|
||||||
|
document.head.appendChild(appleLink);
|
||||||
}
|
}
|
||||||
}, [settings?.branding_favicon_url]);
|
}, [settings?.branding_favicon_url]);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user