diff --git a/README.md b/README.md
index ca855927..d4c92a54 100644
--- a/README.md
+++ b/README.md
@@ -40,6 +40,7 @@ Did you decide to start using Unfold but you don't have time to make the switch
- **Environment label**: distinguish between environments by displaying a label
- **Nonrelated inlines**: displays nonrelated model as inline in changeform
- **Parallel admin**: support for default admin in parallel with Unfold. [Admin migration guide](https://unfoldadmin.com/blog/migrating-django-admin-unfold/?utm_medium=github&utm_source=unfold)
+- **Favicons**: built-in support for configuring various site favicons
- **VS Code**: project configuration and development container is included
## Table of contents
@@ -186,6 +187,14 @@ UNFOLD = {
"dark": lambda request: static("logo-dark.svg"), # dark mode
},
"SITE_SYMBOL": "speed", # symbol from icon set
+ "SITE_FAVICONS": [
+ {
+ "rel": "icon",
+ "sizes": "32x32",
+ "type": "image/svg+xml",
+ "href": lambda request: static("favicon.svg"),
+ },
+ ],
"SHOW_HISTORY": True, # show/hide "History" button, default: True
"SHOW_VIEW_ON_SITE": True, # show/hide "View on site" button, default: True
"ENVIRONMENT": "sample_app.environment_callback",
diff --git a/src/unfold/dataclasses.py b/src/unfold/dataclasses.py
index 663f1c67..5c300921 100644
--- a/src/unfold/dataclasses.py
+++ b/src/unfold/dataclasses.py
@@ -1,5 +1,5 @@
from dataclasses import dataclass
-from typing import Dict, Optional, Union
+from typing import Callable, Dict, Optional, Union
from .typing import ActionFunction
@@ -12,3 +12,11 @@ class UnfoldAction:
path: str
attrs: Optional[Dict] = None
object_id: Optional[Union[int, str]] = None
+
+
+@dataclass
+class Favicon:
+ href: Union[str, Callable]
+ rel: Optional[str] = None
+ type: Optional[str] = None
+ sizes: Optional[str] = None
diff --git a/src/unfold/settings.py b/src/unfold/settings.py
index 5da9f979..5a31a11b 100644
--- a/src/unfold/settings.py
+++ b/src/unfold/settings.py
@@ -7,6 +7,7 @@
"SITE_ICON": None,
"SITE_SYMBOL": None,
"SITE_LOGO": None,
+ "SITE_FAVICONS": [],
"SHOW_HISTORY": True,
"SHOW_VIEW_ON_SITE": True,
"COLORS": {
diff --git a/src/unfold/sites.py b/src/unfold/sites.py
index 4fe5d330..630f76ee 100644
--- a/src/unfold/sites.py
+++ b/src/unfold/sites.py
@@ -10,6 +10,7 @@
from django.utils.functional import lazy
from django.utils.module_loading import import_string
+from .dataclasses import Favicon
from .settings import get_config
from .utils import hex_to_rgb
from .widgets import CHECKBOX_CLASSES, INPUT_CLASSES
@@ -66,6 +67,9 @@ def each_context(self, request: HttpRequest) -> Dict[str, Any]:
"site_symbol": self._get_value(
get_config(self.settings_name)["SITE_SYMBOL"], request
),
+ "site_favicons": self._process_favicons(
+ request, get_config(self.settings_name)["SITE_FAVICONS"]
+ ),
"show_history": get_config(self.settings_name)["SHOW_HISTORY"],
"show_view_on_site": get_config(self.settings_name)[
"SHOW_VIEW_ON_SITE"
@@ -350,6 +354,19 @@ def _replace_values(self, target: Dict, source: Dict, request: HttpRequest):
return target
+ def _process_favicons(
+ self, request: HttpRequest, favicons: List[Dict]
+ ) -> List[Favicon]:
+ return [
+ Favicon(
+ href=self._get_value(item["href"], request),
+ rel=item.get("rel"),
+ sizes=item.get("sizes"),
+ type=item.get("type"),
+ )
+ for item in favicons
+ ]
+
def _process_colors(
self, colors: Dict[str, Dict[str, str]]
) -> Dict[str, Dict[str, str]]:
diff --git a/src/unfold/templates/unfold/layouts/skeleton.html b/src/unfold/templates/unfold/layouts/skeleton.html
index 29f33749..a8ebb907 100644
--- a/src/unfold/templates/unfold/layouts/skeleton.html
+++ b/src/unfold/templates/unfold/layouts/skeleton.html
@@ -24,6 +24,10 @@
{% endfor %}
+ {% for favicon in site_favicons %}
+
+ {% endfor %}
+
diff --git a/tests/test_site_branding.py b/tests/test_site_branding.py
index 4a64fe62..4529e96d 100644
--- a/tests/test_site_branding.py
+++ b/tests/test_site_branding.py
@@ -143,3 +143,29 @@ def test_incorrect_mode_site_logo(self):
request.user = AnonymousUser()
context = admin_site.each_context(request)
self.assertIsNone(context["site_logo"])
+
+ @override_settings(
+ UNFOLD={
+ **CONFIG_DEFAULTS,
+ **{
+ "SITE_FAVICONS": [
+ {
+ "rel": "icon",
+ "sizes": "32x32",
+ "type": "image/svg+xml",
+ "href": lambda request: static("favicon.svg"),
+ }
+ ]
+ },
+ }
+ )
+ def test_favicons(self):
+ admin_site = UnfoldAdminSite()
+ request = RequestFactory().get("/rand")
+ request.user = AnonymousUser()
+ context = admin_site.each_context(request)
+
+ self.assertEqual(context["site_favicons"][0].rel, "icon")
+ self.assertEqual(context["site_favicons"][0].sizes, "32x32")
+ self.assertEqual(context["site_favicons"][0].type, "image/svg+xml")
+ self.assertEqual(context["site_favicons"][0].href, "/static/favicon.svg")