Repository: holoviz/panel

Issue:
Improve Title functionality of panel.serve to be specific for each app served
THE BELOW IS COPIED FROM https://discourse.holoviz.org/t/how-do-i-set-the-app-title-with-panel-serve/550

IN THAT POST I HAVE TRIED DIFFERENT APPROACHES WITHOUT LUCK.

#### SOLUTION

Provide functionality to support use case

-----------

I'm using `panel.serve` to serve a list of Panel applications.

I would like the *index* page to show a custom title in the browser tab like `Corp Analytics`

I would like each app to show a custom title in the browser tab like `App Name`.

**How do I achieve that?**

What I can do and would like is shown below

#### Index Page

![image](https://user-images.githubusercontent.com/42288570/79939878-80bfc600-8460-11ea-9ef6-0bafdf907667.png)

#### App 1 Page

![image](https://user-images.githubusercontent.com/42288570/79939884-85847a00-8460-11ea-8a0f-bcc5f639a792.png)

#### App 2 Page

![image](https://user-images.githubusercontent.com/42288570/79939898-8cab8800-8460-11ea-98c6-3f04e77a1a09.png)

#### Code

```python
import panel as pn

def create_app():

    return pn.Spacer(background="green", sizing_mode="stretch_both")

APP_ROUTES = {

    "app1": create_app,

    "app2": create_app

}

pn.serve(APP_ROUTES, port=14033, title="My App")
```


Discussion:
#### Additional Context

I will be creating a custom *index* page using a custom `Template` one day.

So the important thing for me is that sub apps and/ or apps based on a custom `Template` can have a custom title.

Resolving patch:
diff --git a/panel/io/server.py b/panel/io/server.py
--- 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)}
 


Test patch:
diff --git a/panel/tests/conftest.py b/panel/tests/conftest.py
--- 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
--- 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'})