Fix favicon not showing issue

- Use url_for() for all favicon and icon references in layout.html
- Add specific routes for favicon files to ensure proper serving
- Create dynamic webmanifest route with correct icon paths
- Import send_from_directory for serving static files
- This ensures favicons work correctly in all deployment scenarios
This commit is contained in:
2025-07-14 11:11:50 +02:00
committed by Jens Luedicke
parent 4fcf4bbf80
commit 4264357d04
9 changed files with 65 additions and 17 deletions

43
app.py
View File

@@ -1,4 +1,4 @@
from flask import Flask, render_template, request, redirect, url_for, jsonify, flash, session, g, Response, send_file, abort
from flask import Flask, render_template, request, redirect, url_for, jsonify, flash, session, g, Response, send_file, abort, send_from_directory
from flask_migrate import Migrate
from models import db, TimeEntry, WorkConfig, User, SystemSettings, Team, Role, Project, Company, CompanyWorkConfig, CompanySettings, UserPreferences, WorkRegion, AccountType, ProjectCategory, Task, SubTask, TaskStatus, TaskPriority, TaskDependency, Sprint, SprintStatus, Announcement, SystemEvent, WidgetType, UserDashboard, DashboardWidget, WidgetTemplate, Comment, CommentVisibility, BrandingSettings, CompanyInvitation, Note, NoteFolder, NoteShare
from data_formatting import (
@@ -397,6 +397,47 @@ def sitemap_xml():
return Response(sitemap_xml, mimetype='application/xml')
@app.route('/site.webmanifest')
def serve_webmanifest():
"""Serve web manifest with correct icon paths"""
manifest = {
"name": "TimeTrack",
"short_name": "TimeTrack",
"icons": [
{
"src": url_for('static', filename='android-chrome-192x192.png'),
"sizes": "192x192",
"type": "image/png"
},
{
"src": url_for('static', filename='android-chrome-512x512.png'),
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#667eea",
"background_color": "#ffffff",
"display": "standalone"
}
return jsonify(manifest)
# Favicon routes for compatibility
@app.route('/favicon.ico')
def favicon():
return send_from_directory(app.static_folder, 'favicon.ico', mimetype='image/x-icon')
@app.route('/favicon-32x32.png')
def favicon_32():
return send_from_directory(app.static_folder, 'favicon-32x32.png', mimetype='image/png')
@app.route('/favicon-16x16.png')
def favicon_16():
return send_from_directory(app.static_folder, 'favicon-16x16.png', mimetype='image/png')
@app.route('/apple-touch-icon.png')
def apple_touch_icon():
return send_from_directory(app.static_folder, 'apple-touch-icon.png', mimetype='image/png')
@app.route('/')
def home():
if g.user:

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
static/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
static/favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 B

BIN
static/favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 900 B

BIN
static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

1
static/site.webmanifest Normal file
View File

@@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

View File

@@ -4,14 +4,14 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% if title == 'Home' %}{{ g.branding.app_name if g.branding else 'TimeTrack' }} - Enterprise Time Tracking & Project Management Software{% else %}{{ title }} - {{ g.branding.app_name if g.branding else 'TimeTrack' }}{% endif %}{% if g.company %} - {{ g.company.name }}{% endif %}</title>
<!-- SEO Meta Tags -->
<meta name="description" content="{% block meta_description %}{{ g.branding.app_name if g.branding else 'TimeTrack' }} is a comprehensive time tracking solution with project management, team collaboration, billing & invoicing. Free, open-source, and enterprise-ready.{% endblock %}">
<meta name="keywords" content="{% block meta_keywords %}time tracking, project management, team collaboration, billing software, invoice management, enterprise time tracker, open source time tracking{% endblock %}">
<meta name="author" content="{{ g.branding.app_name if g.branding else 'TimeTrack' }}">
<meta name="robots" content="index, follow">
<link rel="canonical" href="{{ request.url }}">
<!-- Open Graph Meta Tags -->
<meta property="og:title" content="{% block og_title %}{{ title }} - {{ g.branding.app_name if g.branding else 'TimeTrack' }}{% endblock %}">
<meta property="og:description" content="{% block og_description %}Transform your productivity with intelligent time tracking, project management, and team collaboration tools. Enterprise-grade, open-source solution.{% endblock %}">
@@ -21,7 +21,7 @@
{% if g.branding and g.branding.logo_filename %}
<meta property="og:image" content="{{ url_for('static', filename='uploads/branding/' + g.branding.logo_filename, _external=True) }}">
{% endif %}
<!-- Twitter Card Meta Tags -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="{% block twitter_title %}{{ title }} - {{ g.branding.app_name if g.branding else 'TimeTrack' }}{% endblock %}">
@@ -38,7 +38,7 @@
<link rel="stylesheet" href="{{ url_for('static', filename='css/mobile-bottom-nav.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/tablet-optimized.css') }}">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons@latest/iconfont/tabler-icons.min.css">
<!-- PWA Support -->
<link rel="manifest" href="{{ url_for('static', filename='manifest.json') }}">
<meta name="theme-color" content="#667eea">
@@ -54,6 +54,12 @@
{% endif %}
{% if g.branding and g.branding.favicon_filename %}
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='uploads/branding/' + g.branding.favicon_filename) }}">
{% else %}
<link rel="apple-touch-icon" sizes="180x180" href="{{ url_for('static', filename='apple-touch-icon.png') }}">
<link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', filename='favicon-32x32.png') }}">
<link rel="icon" type="image/png" sizes="16x16" href="{{ url_for('static', filename='favicon-16x16.png') }}">
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}">
<link rel="manifest" href="{{ url_for('static', filename='site.webmanifest') }}">
{% endif %}
<style>
:root {
@@ -98,7 +104,7 @@
<body{% if g.user %} class="has-user has-bottom-nav"{% endif %}>
{% if g.user and g.user.preferences %}
<!-- User preferences for JavaScript -->
<div id="user-preferences" style="display: none;"
<div id="user-preferences" style="display: none;"
data-date-format="{{ g.user.preferences.date_format }}"
data-time-format-24h="{{ g.user.preferences.time_format_24h|lower }}">
</div>
@@ -109,8 +115,8 @@
<div class="mobile-nav-brand">
<a href="{{ url_for('home') }}">
{% if g.branding and g.branding.logo_filename %}
<img src="{{ url_for('static', filename='uploads/branding/' + g.branding.logo_filename) }}"
alt="{{ g.branding.logo_alt_text }}"
<img src="{{ url_for('static', filename='uploads/branding/' + g.branding.logo_filename) }}"
alt="{{ g.branding.logo_alt_text }}"
class="mobile-logo">
{% else %}
{{ g.branding.app_name if g.branding else 'TimeTrack' }}
@@ -133,8 +139,8 @@
<!-- <h2>
<a href="{{ url_for('home') }}">
{% if g.branding and g.branding.logo_filename %}
<img src="{{ url_for('static', filename='uploads/branding/' + g.branding.logo_filename) }}"
alt="{{ g.branding.logo_alt_text }}"
<img src="{{ url_for('static', filename='uploads/branding/' + g.branding.logo_filename) }}"
alt="{{ g.branding.logo_alt_text }}"
class="sidebar-logo">
{% else %}
{{ g.branding.app_name if g.branding else 'TimeTrack' }}
@@ -153,7 +159,7 @@
<img src="{{ g.user.get_avatar_url(32) }}" alt="{{ g.user.username }}" class="user-avatar">
<span class="nav-text">{{ g.user.username }}<span class="dropdown-arrow"></span></span>
</a>
<!-- User Dropdown Context Menu -->
<div class="user-dropdown-modal" id="user-dropdown-modal">
<div class="user-dropdown-header">
@@ -187,7 +193,7 @@
<li><a href="{{ url_for('sprints.sprint_management') }}" data-tooltip="Sprint Management"><i class="nav-icon ti ti-run"></i><span class="nav-text">Sprints</span></a></li>
<li><a href="{{ url_for('notes.notes_list') }}" data-tooltip="Notes"><i class="nav-icon ti ti-notes"></i><span class="nav-text">Notes</span></a></li>
<li><a href="{{ url_for('analytics') }}" data-tooltip="Time Analytics"><i class="nav-icon ti ti-chart-bar"></i><span class="nav-text">Analytics</span></a></li>
<!-- Role-based menu items -->
{% if g.user.role == Role.ADMIN or g.user.role == Role.SYSTEM_ADMIN %}
<li class="nav-divider">Admin</li>
@@ -257,7 +263,7 @@
</div>
{% endif %}
{% endwith %}
<!-- Email Nag Screens -->
{% if g.show_email_nag %}
<div class="email-nag-banner">
@@ -282,7 +288,7 @@
</div>
</div>
{% endif %}
{% block content %}{% endblock %}
</main>
@@ -300,7 +306,7 @@
</div>
</div>
</footer>
<!-- Mobile Bottom Navigation -->
{% if g.user %}
<nav class="mobile-bottom-nav">
@@ -353,7 +359,7 @@
// Store in session storage to not show again this session
sessionStorage.setItem('emailNagDismissed', 'true');
}
// Check if already dismissed this session
if (sessionStorage.getItem('emailNagDismissed') === 'true') {
const banner = document.querySelector('.email-nag-banner');
@@ -365,7 +371,7 @@
{% else %}
<script src="{{ url_for('static', filename='js/splash.js') }}"></script>
{% endif %}
<!-- Custom Tracking Script -->
{% if tracking_script_enabled and tracking_script_code %}
{{ tracking_script_code|safe }}