Skip to content

Commit 78bcad4

Browse files
authored
Allow custom favicons (#1062)
* Allow custom favicons If static folder is enabled, then we will check if there is a favicon.ico in the designated static folder. If it exists, we will use the user's favicon instead of the default. Also: - Moved the static folder functions to server_utils.py - Improved validation slightly for static URL paths - Updated docs with custom favicon example Closes #563
1 parent fcb1c61 commit 78bcad4

File tree

8 files changed

+114
-51
lines changed

8 files changed

+114
-51
lines changed

docs/guides/static-assets.md

+17-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
Mesop allows you to specify a folder for storing static assets that will be served by
44
the Mesop server.
55

6-
This feature provides a simple way to serving images, CSS stylesheets, and other files
7-
without having to rely on CDNs, external servers, or mounting Mesop onto FastAPI/Flask.
6+
This feature provides a simple way to serving images, favicons, CSS stylesheets, and
7+
other files without having to rely on CDNs, external servers, or mounting Mesop onto
8+
FastAPI/Flask.
89

910
## Enable a static folder
1011

@@ -86,6 +87,20 @@ def foo():
8687
me.image(src="/static/logo.png")
8788
```
8889

90+
### Use a custom favicon
91+
92+
This example shows you how to use a custom favicon in your Mesop app.
93+
94+
Let's assume you have a directory like this:
95+
96+
- static/favicon.ico
97+
- main.py
98+
- requirements.txt
99+
100+
If you have a static folder enabled, Mesop will look for a `favicon.ico` file in your
101+
static folder. If the file exists, Mesop will use your custom favicon instead of the
102+
default Mesop favicon.
103+
89104
### Load a Tailwind stylesheet
90105

91106
This example shows you how to use [Tailwind CSS](https://tailwindcss.com/) with Mesop.

mesop/server/server.py

+10-43
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import base64
2-
import os
32
import secrets
43
import threading
54
from typing import Generator, Sequence
@@ -12,7 +11,6 @@
1211
request,
1312
stream_with_context,
1413
)
15-
from werkzeug.security import safe_join
1614

1715
import mesop.protos.ui_pb2 as pb
1816
from mesop.component_helpers import diff_component
@@ -23,14 +21,15 @@
2321
MESOP_WEBSOCKETS_ENABLED,
2422
)
2523
from mesop.events import LoadEvent
26-
from mesop.exceptions import MesopDeveloperException, format_traceback
24+
from mesop.exceptions import format_traceback
2725
from mesop.runtime import runtime
28-
from mesop.server.config import app_config
2926
from mesop.server.constants import WEB_COMPONENTS_PATH_SEGMENT
3027
from mesop.server.server_debug_routes import configure_debug_routes
3128
from mesop.server.server_utils import (
3229
STREAM_END,
3330
create_update_state_event,
31+
get_static_folder,
32+
get_static_url_path,
3433
is_same_site,
3534
make_sse_response,
3635
serialize,
@@ -44,10 +43,15 @@
4443
def configure_flask_app(
4544
*, prod_mode: bool = True, exceptions_to_propagate: Sequence[type] = ()
4645
) -> Flask:
46+
static_folder = get_static_folder()
47+
static_url_path = get_static_url_path()
48+
if static_folder and static_url_path:
49+
print(f"Static folder enabled: {static_folder}")
50+
4751
flask_app = Flask(
4852
__name__,
49-
static_folder=get_static_folder(),
50-
static_url_path=get_static_url_path(),
53+
static_folder=static_folder,
54+
static_url_path=static_url_path,
5155
)
5256

5357
def render_loop(
@@ -322,40 +326,3 @@ def ws_generate_data(ws, ui_request):
322326
runtime().delete_context(websocket_session_id)
323327

324328
return flask_app
325-
326-
327-
def get_static_folder() -> str | None:
328-
static_folder_name = app_config.static_folder.strip()
329-
if not static_folder_name:
330-
print("Static folder disabled.")
331-
return None
332-
333-
if static_folder_name in {
334-
".",
335-
"..",
336-
"." + os.path.sep,
337-
".." + os.path.sep,
338-
}:
339-
raise MesopDeveloperException(
340-
"Static folder cannot be . or ..: {static_folder_name}"
341-
)
342-
if os.path.isabs(static_folder_name):
343-
raise MesopDeveloperException(
344-
"Static folder cannot be an absolute path: static_folder_name}"
345-
)
346-
347-
static_folder_path = safe_join(os.getcwd(), static_folder_name)
348-
349-
if not static_folder_path:
350-
raise MesopDeveloperException(
351-
"Invalid static folder specified: {static_folder_name}"
352-
)
353-
354-
print(f"Static folder enabled: {static_folder_path}")
355-
return static_folder_path
356-
357-
358-
def get_static_url_path() -> str | None:
359-
if not app_config.static_folder:
360-
return None
361-
return app_config.static_url_path

mesop/server/server_utils.py

+63
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
import base64
22
import json
3+
import os
34
import secrets
45
import urllib.parse as urlparse
56
from typing import Any, Generator, Iterable
67
from urllib import request as urllib_request
78

89
from flask import Response, abort, request
10+
from werkzeug.security import safe_join
911

1012
import mesop.protos.ui_pb2 as pb
1113
from mesop.env.env import EXPERIMENTAL_EDITOR_TOOLBAR_ENABLED
14+
from mesop.exceptions import MesopDeveloperException
1215
from mesop.runtime import runtime
1316
from mesop.server.config import app_config
1417

@@ -124,3 +127,63 @@ def make_sse_response(
124127
# See https://nginx.org/en/docs/http/ngx_http_proxy_module.html
125128
headers={"X-Accel-Buffering": "no"},
126129
)
130+
131+
132+
def get_static_folder() -> str | None:
133+
static_folder_name = app_config.static_folder.strip()
134+
if not static_folder_name:
135+
return None
136+
137+
if static_folder_name in {
138+
".",
139+
"..",
140+
"." + os.path.sep,
141+
".." + os.path.sep,
142+
}:
143+
raise MesopDeveloperException(
144+
"Static folder cannot be . or ..: {static_folder_name}"
145+
)
146+
if os.path.isabs(static_folder_name):
147+
raise MesopDeveloperException(
148+
"Static folder cannot be an absolute path: static_folder_name}"
149+
)
150+
151+
static_folder_path = safe_join(os.getcwd(), static_folder_name)
152+
153+
if not static_folder_path:
154+
raise MesopDeveloperException(
155+
"Invalid static folder specified: {static_folder_name}"
156+
)
157+
158+
return static_folder_path
159+
160+
161+
def get_static_url_path() -> str | None:
162+
if not app_config.static_folder:
163+
return None
164+
165+
static_url_path = app_config.static_url_path.strip()
166+
if not static_url_path.startswith("/"):
167+
raise MesopDeveloperException(
168+
"Invalid static url path. It must start with a slash: {static_folder_name}"
169+
)
170+
171+
if not static_url_path.endswith("/"):
172+
static_url_path += "/"
173+
174+
return static_url_path
175+
176+
177+
def get_favicon() -> str | None:
178+
default_favicon_path = "./favicon.ico"
179+
180+
static_folder = get_static_folder()
181+
static_url_path = get_static_url_path()
182+
if not static_folder or not static_url_path:
183+
return default_favicon_path
184+
185+
favicon_path = safe_join(static_folder, "favicon.ico")
186+
if not favicon_path or not os.path.isfile(favicon_path):
187+
return default_favicon_path
188+
189+
return static_url_path + "favicon.ico"

mesop/server/static_file_serving.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from mesop.exceptions import MesopException
1616
from mesop.runtime import runtime
1717
from mesop.server.constants import WEB_COMPONENTS_PATH_SEGMENT
18+
from mesop.server.server_utils import get_favicon
1819
from mesop.utils import terminal_colors as tc
1920
from mesop.utils.runfiles import get_runfile_location, has_runfiles
2021
from mesop.utils.url_utils import sanitize_url_for_csp
@@ -58,7 +59,10 @@ def retrieve_index_html() -> io.BytesIO | str:
5859
lines[i] = (
5960
f'<script src="{livereload_script_url}" nonce={g.csp_nonce}></script>\n'
6061
)
61-
62+
if line.strip() == "<!-- Inject favicon -->":
63+
lines[i] = (
64+
f'<link rel="shortcut icon" href="{get_favicon()}" type="image/x-icon" />\n'
65+
)
6266
if (
6367
page_config
6468
and page_config.stylesheets

mesop/static/favicon.ico

14.7 KB
Binary file not shown.

mesop/tests/e2e/static_folder_test.ts

+17-3
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,22 @@ import {test, expect} from '@playwright/test';
22

33
test.describe('Static Folders', () => {
44
test.describe('MESOP_STATIC_FOLDER disabled', () => {
5+
if (process.env['MESOP_STATIC_FOLDER'] !== undefined) {
6+
test.skip('Test skipped because MESOP_STATIC_FOLDER is set.');
7+
}
58
test('static folder not viewable if MESOP_STATIC_FOLDER not enabled', async ({
69
page,
710
}) => {
8-
if (process.env['MESOP_STATIC_FOLDER'] !== undefined) {
9-
test.skip('Test skipped because MESOP_STATIC_FOLDER is set.');
10-
}
1111
const response = await page.goto('/static/tailwind.css');
1212
expect(response!.status()).toBe(500);
1313
});
14+
test('default favicon is used', async ({page}) => {
15+
await page.goto('/tailwind');
16+
const faviconLink = await page
17+
.locator('link[rel="shortcut icon"]')
18+
.first();
19+
await expect(faviconLink).toHaveAttribute('href', './favicon.ico');
20+
});
1421
});
1522

1623
test.describe('MESOP_STATIC_FOLDER enabled', () => {
@@ -30,5 +37,12 @@ test.describe('Static Folders', () => {
3037
const response = await page.goto('/static/../config.py');
3138
expect(response!.status()).toBe(500);
3239
});
40+
test('custom favicon is used', async ({page}) => {
41+
await page.goto('/tailwind');
42+
const faviconLink = await page
43+
.locator('link[rel="shortcut icon"]')
44+
.first();
45+
await expect(faviconLink).toHaveAttribute('href', '/static/favicon.ico');
46+
});
3347
});
3448
});

mesop/web/src/app/editor/index.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
/>
1414
<link rel="stylesheet" href="./styles.css" />
1515
<!-- Inject stylesheet (if needed) -->
16-
<link rel="shortcut icon" href="./favicon.ico" type="image/x-icon" />
16+
<!-- Inject favicon -->
1717
</head>
1818
<body>
1919
<mesop-editor-app ngCspNonce="$$INSERT_CSP_NONCE$$"></mesop-editor-app>

mesop/web/src/app/prod/index.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
/>
1414
<link rel="stylesheet" href="./styles.css" />
1515
<!-- Inject stylesheet (if needed) -->
16-
<link rel="shortcut icon" href="./favicon.ico" type="image/x-icon" />
16+
<!-- Inject favicon -->
1717
</head>
1818
<body>
1919
<mesop-app ngCspNonce="$$INSERT_CSP_NONCE$$"></mesop-app>

0 commit comments

Comments
 (0)