diff --git a/examples/user_guide/Deploy_and_Export.ipynb b/examples/user_guide/Deploy_and_Export.ipynb index 2af6a5828a..0af762a439 100644 --- a/examples/user_guide/Deploy_and_Export.ipynb +++ b/examples/user_guide/Deploy_and_Export.ipynb @@ -229,6 +229,14 @@ "pn.serve({'markdown': '# This is a Panel app', 'json': pn.pane.JSON({'abc': 123})})\n", "```\n", "\n", + "You can customize the HTML title of each application by supplying a dictionary where the keys represent the URL slugs and the values represent the titles, e.g.:\n", + "\n", + "```python\n", + "pn.serve(\n", + " {'markdown': '# This is a Panel app', 'json': pn.pane.JSON({'abc': 123})},\n", + " title={'markdown': 'A Markdown App', 'json': 'A JSON App'}\n", + ")\n", + "\n", "The ``pn.serve`` function accepts the same arguments as the `show` method.\n", "\n", "\n", diff --git a/panel/io/server.py b/panel/io/server.py index 3dd52bd6b6..5f16f33f6c 100644 --- a/panel/io/server.py +++ b/panel/io/server.py @@ -130,8 +130,9 @@ def serve(panels, port=0, websocket_origin=None, loop=None, show=True, Whether to open the server in a new browser tab on start start : boolean(optional, default=False) Whether to start the Server - title: str (optional, default=None) - An HTML title for the application + title: str or {str: str} (optional, default=None) + An HTML title for the application or a dictionary mapping + from the URL slug to a customized title verbose: boolean (optional, default=True) Whether to print the address and port location : boolean or panel.io.location.Location @@ -188,8 +189,9 @@ def get_server(panel, port=0, websocket_origin=None, loop=None, Whether to open the server in a new browser tab on start start : boolean(optional, default=False) Whether to start the Server - title: str (optional, default=None) - An HTML title for the application + title: str or {str: str} (optional, default=None) + An HTML title for the application or a dictionary mapping + from the URL slug to a customized title verbose: boolean (optional, default=False) Whether to report the address and port location : boolean or panel.io.location.Location @@ -210,6 +212,16 @@ def get_server(panel, port=0, websocket_origin=None, loop=None, if isinstance(panel, dict): apps = {} for slug, app in panel.items(): + if isinstance(title, dict): + try: + title_ = title[slug] + except KeyError: + raise KeyError( + "Keys of the title dictionnary and of the apps " + f"dictionary must match. No {slug} key found in the " + "title dictionnary.") + else: + title_ = title slug = slug if slug.startswith('/') else '/'+slug if 'flask' in sys.modules: from flask import Flask @@ -222,7 +234,7 @@ def get_server(panel, port=0, websocket_origin=None, loop=None, extra_patterns.append(('^'+slug+'.*', ProxyFallbackHandler, dict(fallback=wsgi, proxy=slug))) continue - apps[slug] = partial(_eval_panel, app, server_id, title, location) + apps[slug] = partial(_eval_panel, app, server_id, title_, location) else: apps = {'/': partial(_eval_panel, panel, server_id, title, location)} diff --git a/panel/tests/conftest.py b/panel/tests/conftest.py index 4054397840..b875ab17ed 100644 --- a/panel/tests/conftest.py +++ b/panel/tests/conftest.py @@ -17,6 +17,7 @@ from panel.pane import HTML, Markdown from panel.io import state +from panel import serve @pytest.fixture @@ -117,6 +118,36 @@ def markdown_server_session(): pass # tests may already close this +@pytest.fixture +def multiple_apps_server_sessions(): + """Serve multiple apps and yield a factory to allow + parameterizing the slugs and the titles.""" + servers = [] + def create_sessions(slugs, titles): + app1_slug, app2_slug = slugs + apps = { + app1_slug: Markdown('First app'), + app2_slug: Markdown('Second app') + } + server = serve(apps, port=5008, title=titles, show=False, start=False) + servers.append(server) + session1 = pull_session( + url=f"http://localhost:{server.port:d}/app1", + io_loop=server.io_loop + ) + session2 = pull_session( + url=f"http://localhost:{server.port:d}/app2", + io_loop=server.io_loop + ) + return session1, session2 + yield create_sessions + for server in servers: + try: + server.stop() + except AssertionError: + continue # tests may already close this + + @contextmanager def set_env_var(env_var, value): old_value = os.environ.get(env_var) diff --git a/panel/tests/test_server.py b/panel/tests/test_server.py index 89b4a32e65..45444cfa54 100644 --- a/panel/tests/test_server.py +++ b/panel/tests/test_server.py @@ -1,3 +1,5 @@ +import pytest + from panel.models import HTML as BkHTML from panel.io import state @@ -43,3 +45,15 @@ def test_kill_all_servers(html_server_session, markdown_server_session): state.kill_all_servers() assert server_1._stopped assert server_2._stopped + +def test_multiple_titles(multiple_apps_server_sessions): + """Serve multiple apps with a title per app.""" + session1, session2 = multiple_apps_server_sessions( + slugs=('app1', 'app2'), titles={'app1': 'APP1', 'app2': 'APP2'}) + assert session1.document.title == 'APP1' + assert session2.document.title == 'APP2' + + # Slug names and title keys should match + with pytest.raises(KeyError): + session1, session2 = multiple_apps_server_sessions( + slugs=('app1', 'app2'), titles={'badkey': 'APP1', 'app2': 'APP2'})