diff --git a/vizro-core/changelog.d/20231221_052426_amward_update_meta_tags.md b/vizro-core/changelog.d/20231221_052426_amward_update_meta_tags.md
new file mode 100644
index 000000000..a567c184f
--- /dev/null
+++ b/vizro-core/changelog.d/20231221_052426_amward_update_meta_tags.md
@@ -0,0 +1,47 @@
+
+
+
+
+
+### Added
+
+- Enable adding description and image to the meta tags. ([#185](https://github.com/mckinsey/vizro/pull/185))
+
+
+
+
+
diff --git a/vizro-core/examples/assets/images/app.svg b/vizro-core/examples/assets/images/app.svg
new file mode 100644
index 000000000..9d07d6372
--- /dev/null
+++ b/vizro-core/examples/assets/images/app.svg
@@ -0,0 +1,9 @@
+
diff --git a/vizro-core/examples/assets/images/logo.svg b/vizro-core/examples/assets/images/logo.svg
new file mode 100644
index 000000000..0904b87de
--- /dev/null
+++ b/vizro-core/examples/assets/images/logo.svg
@@ -0,0 +1,3 @@
+
diff --git a/vizro-core/examples/default/app.py b/vizro-core/examples/default/app.py
index 4957f1e00..ca1df8a2e 100644
--- a/vizro-core/examples/default/app.py
+++ b/vizro-core/examples/default/app.py
@@ -24,6 +24,7 @@ def create_variable_analysis():
"""Function returns a page with gapminder data to do variable analysis."""
page_variable = vm.Page(
title="Variable Analysis",
+ description="Analyzing population, GDP per capita and life expectancy on country and continent level",
layout=vm.Layout(
grid=[
# fmt: off
@@ -194,6 +195,7 @@ def create_relation_analysis():
"""Function returns a page to perform relation analysis."""
page_relation_analysis = vm.Page(
title="Relationship Analysis",
+ description="Investigating the interconnection between population, GDP per capita and life expectancy",
layout=vm.Layout(
grid=[[0, 0, 0, 0, 1]] + [[2, 2, 3, 3, 3]] * 4 + [[4, 4, 4, 4, 4]] * 5,
row_min_height="100px",
@@ -323,6 +325,7 @@ def create_continent_summary():
"""Function returns a page with markdown including images."""
page_summary = vm.Page(
title="Continent Summary",
+ description="Summarizing the main findings for each continent",
layout=vm.Layout(grid=[[i] for i in range(5)], row_min_height="190px", row_gap="25px"),
components=[
vm.Card(
@@ -415,6 +418,7 @@ def create_benchmark_analysis():
page_country = vm.Page(
title="Benchmark Analysis",
+ description="Discovering how the metrics differ for each country and export data for further investigation",
layout=vm.Layout(grid=[[0, 1]] * 5 + [[2, -1]], col_gap="32px", row_gap="60px"),
components=[
vm.Table(
@@ -496,6 +500,7 @@ def create_home_page():
"""Function returns the homepage."""
page_home = vm.Page(
title="Homepage",
+ description="Vizro demo app for studying gapminder data",
layout=vm.Layout(grid=[[0, 1], [2, 3]], row_gap="16px", col_gap="24px"),
components=[
vm.Card(
diff --git a/vizro-core/schemas/0.1.8.dev0.json b/vizro-core/schemas/0.1.8.dev0.json
index 66b0af594..3e1bab3d3 100644
--- a/vizro-core/schemas/0.1.8.dev0.json
+++ b/vizro-core/schemas/0.1.8.dev0.json
@@ -845,7 +845,7 @@
},
"Page": {
"title": "Page",
- "description": "A page in [`Dashboard`][vizro.models.Dashboard] with its own URL path and place in the `Navigation`.\n\nArgs:\n components (List[ComponentType]): See [ComponentType][vizro.models.types.ComponentType]. At least one component\n has to be provided.\n title (str): Title to be displayed.\n layout (Layout): Layout to place components in. Defaults to `None`.\n controls (List[ControlType]): See [ControlType][vizro.models.types.ControlType]. Defaults to `[]`.\n path (str): Path to navigate to page. Defaults to `\"\"`.\n\nRaises:\n ValueError: If number of page and grid components is not the same",
+ "description": "A page in [`Dashboard`][vizro.models.Dashboard] with its own URL path and place in the `Navigation`.\n\nArgs:\n components (List[ComponentType]): See [ComponentType][vizro.models.types.ComponentType]. At least one component\n has to be provided.\n title (str): Title to be displayed.\n description (str): Description for meta tags.\n layout (Layout): Layout to place components in. Defaults to `None`.\n controls (List[ControlType]): See [ControlType][vizro.models.types.ControlType]. Defaults to `[]`.\n path (str): Path to navigate to page. Defaults to `\"\"`.\n\nRaises:\n ValueError: If number of page and grid components is not the same",
"type": "object",
"properties": {
"id": {
@@ -888,6 +888,12 @@
"description": "Title to be displayed.",
"type": "string"
},
+ "description": {
+ "title": "Description",
+ "description": "Description for meta tags.",
+ "default": "",
+ "type": "string"
+ },
"layout": {
"$ref": "#/definitions/Layout"
},
diff --git a/vizro-core/src/vizro/models/_dashboard.py b/vizro-core/src/vizro/models/_dashboard.py
index 93ac22bed..a5c3736ef 100644
--- a/vizro-core/src/vizro/models/_dashboard.py
+++ b/vizro-core/src/vizro/models/_dashboard.py
@@ -2,6 +2,7 @@
import logging
from functools import partial
+from pathlib import Path
from typing import TYPE_CHECKING, List, Literal, TypedDict
import dash
@@ -88,6 +89,8 @@ def set_navigation_pages(cls, navigation, values):
@_log_call
def pre_build(self):
+ meta_image = self._infer_image("app") or self._infer_image("logo")
+
# Setting order here ensures that the pages in dash.page_registry preserves the order of the List[Page].
# For now the homepage (path /) corresponds to self.pages[0].
# Note redirect_from=["/"] doesn't work and so the / route must be defined separately.
@@ -95,6 +98,8 @@ def pre_build(self):
dash.register_page(
module=page.id,
name=page.title,
+ description=page.description,
+ image=meta_image,
title=f"{self.title}: {page.title}" if self.title else page.title,
path=page.path if order else "/",
order=order,
@@ -193,3 +198,11 @@ def _make_page_404_layout():
],
className="page_error_container",
)
+
+ def _infer_image(self, filename: str):
+ valid_extensions = [".apng", ".avif", ".gif", ".jpeg", ".jpg", ".png", ".svg", ".webp"]
+ assets_folder = Path(dash.get_app().config.assets_folder)
+ if assets_folder.is_dir():
+ for path in Path(assets_folder).rglob(f"{filename}.*"):
+ if path.suffix in valid_extensions:
+ return str(path.relative_to(assets_folder))
diff --git a/vizro-core/src/vizro/models/_page.py b/vizro-core/src/vizro/models/_page.py
index 17a24d73d..4a24538b5 100644
--- a/vizro-core/src/vizro/models/_page.py
+++ b/vizro-core/src/vizro/models/_page.py
@@ -32,6 +32,7 @@ class Page(VizroBaseModel):
components (List[ComponentType]): See [ComponentType][vizro.models.types.ComponentType]. At least one component
has to be provided.
title (str): Title to be displayed.
+ description (str): Description for meta tags.
layout (Layout): Layout to place components in. Defaults to `None`.
controls (List[ControlType]): See [ControlType][vizro.models.types.ControlType]. Defaults to `[]`.
path (str): Path to navigate to page. Defaults to `""`.
@@ -42,6 +43,7 @@ class Page(VizroBaseModel):
components: List[ComponentType]
title: str = Field(..., description="Title to be displayed.")
+ description: str = Field("", description="Description for meta tags.")
layout: Layout = None # type: ignore[assignment]
controls: List[ControlType] = []
path: str = Field("", description="Path to navigate to page.")
diff --git a/vizro-core/tests/unit/vizro/models/test_dashboard.py b/vizro-core/tests/unit/vizro/models/test_dashboard.py
index e0cb9be91..03d48cfe1 100644
--- a/vizro-core/tests/unit/vizro/models/test_dashboard.py
+++ b/vizro-core/tests/unit/vizro/models/test_dashboard.py
@@ -1,4 +1,5 @@
import json
+from pathlib import Path
import dash
import dash_bootstrap_components as dbc
@@ -14,6 +15,7 @@
import vizro
import vizro.models as vm
+from vizro import Vizro
from vizro.actions._action_loop._action_loop import ActionLoop
from vizro.models._dashboard import _all_hidden
@@ -77,6 +79,8 @@ def test_page_registry(self, vizro_app, page_1, page_2, mocker):
mock_register_page.assert_any_call(
module=page_1.id,
name="Page 1",
+ description="",
+ image=None,
title="Page 1",
path="/",
order=0,
@@ -85,6 +89,8 @@ def test_page_registry(self, vizro_app, page_1, page_2, mocker):
mock_register_page.assert_any_call(
module=page_2.id,
name="Page 2",
+ description="",
+ image=None,
title="Page 2",
path="/page-2",
order=1,
@@ -103,12 +109,71 @@ def test_page_registry_with_title(self, vizro_app, page_1, mocker):
mock_register_page.assert_any_call(
module=page_1.id,
name="Page 1",
+ description="",
+ image=None,
title="My dashboard: Page 1",
path="/",
order=0,
layout=mocker.ANY, # partial call is tricky to mock out so we ignore it.
)
+ def test_page_registry_with_description(self, vizro_app, mocker):
+ mock_register_page = mocker.patch("dash.register_page", autospec=True)
+ vm.Dashboard(
+ pages=[vm.Page(title="Page 1", components=[vm.Button()], description="My description")]
+ ).pre_build()
+
+ mock_register_page.assert_any_call(
+ module="Page 1",
+ name="Page 1",
+ description="My description",
+ image=None,
+ title="Page 1",
+ path="/",
+ order=0,
+ layout=mocker.ANY, # partial call is tricky to mock out so we ignore it.
+ )
+
+ @pytest.mark.parametrize(
+ "image_path", ["app.png", "app.svg", "images/app.png", "images/app.svg", "logo.png", "logo.svg"]
+ )
+ def test_page_registry_with_image(self, page_1, mocker, tmp_path, image_path):
+ if Path(image_path).parent != Path("."):
+ Path(tmp_path / image_path).parent.mkdir()
+ Path(tmp_path / image_path).touch()
+ Vizro(assets_folder=tmp_path)
+ mock_register_page = mocker.patch("dash.register_page", autospec=True)
+ vm.Dashboard(pages=[page_1]).pre_build()
+
+ mock_register_page.assert_any_call(
+ module=page_1.id,
+ name="Page 1",
+ description="",
+ image=image_path,
+ title="Page 1",
+ path="/",
+ order=0,
+ layout=mocker.ANY, # partial call is tricky to mock out so we ignore it.
+ )
+
+ def test_page_registry_with_images(self, page_1, mocker, tmp_path):
+ Path(tmp_path / "app.svg").touch()
+ Path(tmp_path / "logo.svg").touch()
+ Vizro(assets_folder=tmp_path)
+ mock_register_page = mocker.patch("dash.register_page", autospec=True)
+ vm.Dashboard(pages=[page_1]).pre_build()
+
+ mock_register_page.assert_any_call(
+ module=page_1.id,
+ name="Page 1",
+ description="",
+ image="app.svg",
+ title="Page 1",
+ path="/",
+ order=0,
+ layout=mocker.ANY, # partial call is tricky to mock out so we ignore it.
+ )
+
def test_make_page_404_layout(self, vizro_app):
# vizro_app fixture is needed to avoid mocking out get_relative_path.
expected = html.Div(