How to Add a Favicon to Your Django Site

Your site’s favicon appears in the browser tab, and is a key way to brand your site. Setting up a favicon is a simple task, but once you start considering vendor-specific icons, it becomes more complicated.
In this post we’ll cover:
- what the HTML specification says about favicons
- browser support
- two simple ways to serve a favicon from Django
- the vast world of vendor-specific icons
- generating and serving a bunch of vendor-specific icons with RealFaviconGenerator
Alright, let’s get into it.
To Specify an Icon, or Not
The HTML specification defines two ways to specify a site’s icon (source).
First, you can add one or more <link>
s with rel=icon
to your page’s <head>
. The browser will then pick between these and use the most appropriate (that works):
<link rel=icon href=favicon-16.png sizes=16x16 type=image/png>
<link rel=icon href=favicon-32.png sizes=32x32 type=image/png>
The browser may pick based on size or advertised file type.
Second, if you don’t list any such <link>
s, the browser will automatically request /favicon.ico
and use that, if it’s a supported image. .ico
is the file suffix for Microsoft Windows icons, but you don’t need to use this file type. Browsers always obey the Content-Type
header, so you can serve other image types. favicon.ico
is only used for historical reasons from Internet Explorer 5 (!).
Okay, so which of these methods should you use?
Using <link>
s appeals because it allows multiple icon types. In theory, these can save bandwidth and processing time, as the browser can select the smallest appropriate icon and avoid scaling it up or down. But in practice, any such advantage is probably outweighed by the cost of including <link>
s in every page, even though only first time visitors need them (as their browser hasn’t yet cached the icon).
A second downside of using <link>
s is that they won’t apply to all pages. You may not be able to edit all pages on your site, for example if some are generated by a third party package. And, if a visitor (first) lands on a non-HTML URL, such as a JSON response, this cannot feature a <link>
, so the browser will request /favicon.ico
.
So, to apply to all pages, it’s a good idea to at least provide /favicon.ico
. With this in place, you can choose to add <link>
s on top, if required.
This whole situation is complicated by vendor-specific icons, which require <link>
s and various other tags. Put those aside though, we’ll cover them later. First, let’s just do /favicon.ico
.
What the File Type?
There are three file types commonly used for /favicon.ico
:
-
Ancient, but not due to its age and being the de facto standard, it’s supported on all browsers. It’s not compressed, and few graphic programs support it. Thankfully there are many web tools that can convert to ICO for you.
The main advantage of ICO is that a file can contain multiple resolutions.
-
Newer, an open format, and basically universally supported*. It’s compressed and well supported by graphics programs.
(*Can I Use shows Internet Explorer only supported PNG icons from version 11.)
-
Modern, an open format, and fairly well supported. Can I Use shows just Safari and Internet Explorer do not support SVG icons.
The big advantage of SVG is that it’s a vector graphics format, rather than a raster one. It’s “one size fits all”—the browser can render the same icon at any size. SVG’s can also be smaller than a corresponding PNG, especially for icons.
(Another cool SVG feature: you can style your icon differently for dark mode users.)
Unless you care about absolutely every browser, PNG is a fine choice. It’s easier to create and smaller. (At time of writing, PNG will exclude about 0.14% of world traffic on Internet Explorer <11.)
Hopefully in the future, Safari will support SVG icons, and it becomes reasonable to use them instead. Jens Oliver Meiert calls this One Favicon to Rule Them All—but we’re just not there yet (sigh).
Serve a PNG at /favicon.ico
Before adding any code, prepare your PNG. It seems fine to make it 64x64 pixels. Browsers will normally display the icon as 16x16 or 32x32 pixels, but using a larger size helps support high DPI or zoomed in displays. You can also use PNG transparency, but beware that the tab background could be any colour, due to operating system theme, dark mode, or user customization.
Once you’ve prepared your icon, place it in your static files as favicon.png
. Then, add this view to serve it:
from django.conf import settings
from django.http import FileResponse, HttpRequest, HttpResponse
from django.views.decorators.cache import cache_control
from django.views.decorators.http import require_GET
@require_GET
@cache_control(max_age=60 * 60 * 24, immutable=True, public=True) # one day
def favicon(request: HttpRequest) -> HttpResponse:
file = (settings.BASE_DIR / "static" / "favicon.png").open("rb")
return FileResponse(file)
…with this corresponding URL definition:
from django.urls import path
from example.core import views as core_views
urlpatterns = [
...,
path("favicon.ico", core_views.favicon),
...,
]
You might wonder why you need a separate view, rather than relying on Django’s staticfiles
app. The reason is that staticfiles
only serves files from within the STATIC_URL
prefix, like static/
. Thus staticfiles can only serve /static/favicon.ico
, whilst the favicon needs to be served at exactly /favicon.ico
(without a <link>
).
Let’s deconstruct the view:
@require_GET
makes the view only respond to GET requests.The
@cache_control
invocation sets theCache-Control
header to one day, with the immutable and public flags. Themax_age
value tells browsers to re-fetch the icon after a day (in seconds). One day is probably a reasonable trade-off between saving bandwidth and allowing you to change your icon in case you rebrand. Setting theimmutable
flag on tells browsers to skip checking for changes until the expiry time. And thepublic
flag tells browsers that the resource can be cached even when accessed with credentials.file
is set to the opened file, using the pathlib API. You may need to adjust the path depending on where you store your static files.This line assumes your
BASE_DIR
setting is created with pathlib. Django changed to using Pathlib for new projects in version 3.1. If your project was created before then, you may want to update it - see my past post.Django’s
FileResponse
takes the opened file, and serves the contents as the response body. It automatically adds theContent-Type
header toimage/png
, based on the filename.
Okay, that’s the code. With it set up correctly, you should be able to see the icon in your browser tab:

Yeah!
(Pear Icon from Freepik on Flaticon.com.)
To ensure the view keeps working as expected, you can add a test. Here’s a test that covers all the basics, which you could place in the corresponding views test file:
from http import HTTPStatus
from django.test import SimpleTestCase
class FaviconTests(SimpleTestCase):
def test_get(self):
response = self.client.get("/favicon.ico")
self.assertEqual(response.status_code, HTTPStatus.OK)
self.assertEqual(response["Cache-Control"], "max-age=86400, immutable, public")
self.assertEqual(response["Content-Type"], "image/png")
self.assertGreater(len(response.getvalue()), 0)
A few notes:
- The view doesn’t need to use the database, so the test case uses
SimpleTestCase
, which blocks database access (and thus runs a little faster). - The status code is checked to be OK (200) against Python’s
HTTPStatus
(as I covered previously). - The
Cache-Control
andContent-Type
headers are checked against exact values. FileResponse
is a streaming response, which makes it efficient for sending large files. As such, to check the full content in a test, you cannot access thecontent
atttribute, and instead must use thegetvalue()
method to load it all. This test simply checks there’s at least one byte of content.
Alright, that’s the test covered!
A Trick for Making an Emoji Favicon with SVG
As noted above, if you don’t care about Safari support, you can serve an SVG at /favicon.ico
. A quick way of making an SVG favicon is to this template to pick an emoji:
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<text y=".9em" font-size="90">👾</text>
</svg>
Instant icon! You can swap the emoji in the <text>
to select from the thousands of options.
This technique was first popularized by this tweet by @LeaVerou. Thanks Lea!
For demos, or internal tools where you know users won’t use Safari, this is a very easy way to set up your icon. It’s low effort and flexible, although emoji look different across platforms. I use this method in my example projects, such as the one in my django-htmx repository.
Note that you aren’t limited to emoji. You can adjust the SVG to use any character(s), or you could use an SVG icon from a set like heroicons.
You can serve an SVG icon in Django with a view like this:
from django.http import HttpRequest, HttpResponse
from django.views.decorators.cache import cache_control
from django.views.decorators.http import require_GET
@require_GET
@cache_control(max_age=60 * 60 * 24, immutable=True, public=True) # one day
def favicon(request: HttpRequest) -> HttpResponse:
return HttpResponse(
(
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">'
+ '<text y=".9em" font-size="90">👾</text>'
+ "</svg>"
),
content_type="image/svg+xml",
)
…with the corresponding URL entry:
from django.urls import path
from example.core import views as core_views
urlpatterns = [
...,
path("favicon.ico", core_views.favicon),
...,
]
Bada-bing, bada-boom:

The code is all similar to the previous PNG version. You can also add a test in the same vein:
from http import HTTPStatus
from django.test import SimpleTestCase
class FaviconTests(SimpleTestCase):
def test_get(self):
response = self.client.get("/favicon.ico")
self.assertEqual(response.status_code, HTTPStatus.OK)
self.assertEqual(response["Cache-Control"], "max-age=86400, immutable, public")
self.assertEqual(response["Content-Type"], "image/svg+xml")
self.assertTrue(response.content.startswith(b"<svg"))
Nice.
Swap Icon by Environment
Mixing up your development, staging, and production environments can be catastrophic. It’s not fun to acidentally “delete all users” on production!
As protection against such slip-ups, you can make each environment visually distinct. Within pages you can apply special styles, such as a different background colour or a banner. You can do this in your base template(s) and some CSS.
To help make the browser tabs distinct, you can also swap your favicon per environment. (Thanks to Chris Coyier for this tip.)
Here’s an example of extending the previous PNG icon view to do so:
from django.conf import settings
from django.http import FileResponse, HttpRequest, HttpResponse
from django.views.decorators.cache import cache_control
from django.views.decorators.http import require_GET
@require_GET
@cache_control(max_age=60 * 60 * 24, immutable=True, public=True) # one day
def favicon(request: HttpRequest) -> HttpResponse:
if settings.DEBUG:
name = "favicon-debug.png"
else:
name = "favicon.png"
file = (settings.BASE_DIR / "static" / name).open("rb")
return FileResponse(file)
Checking settings.DEBUG
is an easy way to distinguish development from other environments. If you want to split other environments, such a staging, you can add a custom “environment name” setting, based on an enviornment variable.
Vendor-specific Icons
Brace yourself, here’s where things get complicated. Different vendors have defined their own icon specifications, for special situations. For example:
- Apple looks for an “Apple Touch Icon”. This is used when a user pins your site to their iThing’s home screen as a “virtual app”. There have been nine different resolutions (so far) for this icon, between iOS versions and iPhone/iPad iterations.
- Android Chrome can use a different icon for similarly pinned sites. It looks for this in your web app manifest. At least this is part of the open standard for Progressive Web Applications (PWA’s).
- Windows Metro also allows users to pin sites to their desktop as a “tile”. This feature looks for a special icon. Their design guide recommends this to be a white silhouette only, and you can also specify a colour for the tile in a special
<meta>
tag. - …and so on and so forth, including variations for now-defunct browsers like Internet Explorer 10.
It’s all a bit of a mess.
Thankfully there is no requirement to specify all these icons. If you don’t care about users using “pinning” features and similar, you can skip these icons. Then, even if some do “pin”, they’ll just see your lower resolution favicon, or a fallback from their platform. But if you have a reasonable amount of traffic, or want to impress certain visitors, you might choose to do the work to add the extra icons.
Which set of extra icons to support is then the question. There are many specifications, some are defunct, and browser use varies by audience. Because of this, there are many articles and tools out there with divergent recommendations.
(This article cites many of these posts, under “Motivation”.)
One of the most prominent favicon tools is RealFaviconGenerator. It has been maintained since 2014 by its creator Philippe Bernard, updated as new standards come out, and it shows previews of your icon in use for various situations. I think it strikes a reasonable balance of supporting many vendor-specific icons whilst avoiding old ones (by default).
Let’s look at using RealFaviconGenerator to generate alternative icon formats, and then how to set them up in a Django project.
Process an Icon with RealFaviconGenerator
First, get your icon as a high resolution PNG with appropriate transparency. For examples, I’ll again use this pear icon, starting at 512x512:

Second, open up RealFaviconGenerator:

Third, upload your icon. You’ll be presented with a page where you can customize your icon for various situations:

You can adjust things like adding a background for iOS, with rationle why:

Play around until you’re satisfied with the appearances for the platforms are going to support. When you reach the end, ensure you use the default option “I will place favicon files...”:

Then, click “Generate your Favicons and HTML code”.
After generation completes, you’ll be presented with this screen:

Download the package, unzip it, and you should see it contains a bunch of files:
favicon_package_v0.16
├── android-chrome-192x192.png
├── android-chrome-512x512.png
├── apple-touch-icon.png
├── browserconfig.xml
├── favicon-16x16.png
├── favicon-32x32.png
├── favicon.ico
├── mstile-150x150.png
├── safari-pinned-tab.svg
└── site.webmanifest
Gosh, that’s a lot.
Note that RealFaviconGenerator creates favicon.ico
as an ICO file, rather than a PNG as we saw previously.
Also, note that a couple of the files are not image files, but site-wide configuration files:
browserconfig.xml
- Microsoft-specific config.site.webmanifest
- a Progressive Web App (PWA) manifest file).
For each of these, you can only have one per site. If you already have either file, you should merge in the content from RealFaviconGenerator, rather than replace them.
Alright. Let’s look at two ways to serve these files. The first uses a feature from Whitenoise, a popular package for serving static files. The second uses plain Django, and works regardless of your static file setup.
Afterwards, we’ll cover adding the HTML from RealFaviconGenerator, which you need with either method.
Serve the Icon files With Whitenoise
This section assumes you already have Whitenoise set up for your static files, as per its documentation.
Whitenoise’s static file handling uses the cache-busting pattern with hashing, from Django’s ManifestStaticFilesStorage
. This won’t work for the icon files, as they need specific URL’s. Instead, you can also configure Whitenoise to serve them as non-versioned files with the WHITENOISE_ROOT
setting.
Create a directory in your project to contain these non-versioned static files:
$ mkdir static_nonversioned
Then, configure the setting:
WHITENOISE_ROOT = BASE_DIR / "static_nonversioned"
Place the icon files from RealFaviconGenerator in the directory, and Whitenoise will serve them. You can check the icons are being served by visiting their respective URL’s. For example, you can check the apple touch icon at /apple-touch-icon.png
:

This approach is convenient, but it has one downside. By default, Whitenoise will use a short max-age value in the cache header: only 60 seconds in production. This will cause browsers to re-fetch icon files frequently - a small waste of bandwidth. Recall that we used a value of one day above, which is more reasonable.
You can change this max-age with the WHITENOISE_MAX_AGE
setting. For example, to set it to one day, except in development as per the default:
if not DEBUG:
WHITENOISE_MAX_AGE = 60 * 60 * 24 # one day
Beware this applies to all your non-versioned static files in WHITENOISE_ROOT
. You might want a shorter cache timeout for other files servd from there, such as robots.txt
. In this case, you need to opt for the lowest timeout that works.
Alternatively, you can use the below techinque whether or not you use Whitenoise. Don’t forget to then add the HTML, as below.
Serve the Icon files with Plain Django
Put the files into your project’s static folder, or a subdirectory, however you feel comfortable. (If you don’t care about a particular platform, you can drop the corresponding files.)
Next, you need a view to serve these files. You can do this like so:
from django.conf import settings
from django.http import FileResponse, HttpRequest, HttpResponse
from django.views.decorators.cache import cache_control
from django.views.decorators.http import require_GET
@require_GET
@cache_control(max_age=60 * 60 * 24, immutable=True, public=True) # one day
def favicon_file(request: HttpRequest) -> HttpResponse:
name = request.path.lstrip("/")
file = (settings.BASE_DIR / "static" / name).open("rb")
return FileResponse(file)
This code is adapted from the previous PNG view. The main change here is that the view doesn’t always serve favicon.png
any more. Instead it fetches the filename to serve from request.path
.
You may need to adjust the file =
line, depending where your static files are, and if you placed the icons in a subdirectory. (…and whether you’re using pathlib for BASE_DIR
.)
With the view in place, you should also add the corresponding URL definitions:
from django.urls import path
from example.core import views as core_views
urlpatterns = [
...,
path("android-chrome-192x192.png", core_views.favicon_file),
path("android-chrome-512x512.png", core_views.favicon_file),
path("apple-touch-icon.png", core_views.favicon_file),
path("browserconfig.xml", core_views.favicon_file),
path("favicon-16x16.png", core_views.favicon_file),
path("favicon-32x32.png", core_views.favicon_file),
path("favicon.ico", core_views.favicon_file),
path("mstile-150x150.png", core_views.favicon_file),
path("safari-pinned-tab.svg", core_views.favicon_file),
path("site.webmanifest", core_views.favicon_file),
...,
]
You can check the icons are being served by visiting their respective URL’s. For example, you can check the apple touch icon at /apple-touch-icon.png
:

Nice one.
As we saw before, it’s a good idea to add tests, to ensure that your icon files continue to work. Here’s a simple test case that checks them all:
from http import HTTPStatus
from django.test import SimpleTestCase
class FaviconFileTests(SimpleTestCase):
def test_get(self):
names = [
"android-chrome-192x192.png",
"android-chrome-512x512.png",
"apple-touch-icon.png",
"browserconfig.xml",
"favicon-16x16.png",
"favicon-32x32.png",
"favicon.ico",
"mstile-150x150.png",
"safari-pinned-tab.svg",
"site.webmanifest",
]
for name in names:
with self.subTest(name):
response = self.client.get(f"/{name}")
self.assertEqual(response.status_code, HTTPStatus.OK)
self.assertEqual(
response["Cache-Control"],
"max-age=86400, immutable, public",
)
self.assertGreater(len(response.getvalue()), 0)
Note:
- The tests use unittest’s
subTest
method to run multiple tests within one method.subTest()
is universal, but I prefer to use the parameterized package or pytest’s parametrize mark. - Each test checks for an OK response, the correct
Cache-Control
header, and at least one byte of content. TheContent-Type
header isn’t checked since it varies based on the file.
Cool, cool beans. Don’t forget to then add the HTML, as below.
Add the <link>
and <meta>
Tags From RealFaviconGenerator
Finally, it’s time to add the HTML from RealFaviconGenerator. You should place this in the <head>
tag of your base template. If you have more than one base template, you can place the HTML in a separate file and use {% include %}
in each base template.
The exact HTML depends on the options you selected during generation. Here’s what it looked like in this example:
<link rel=apple-touch-icon sizes=180x180 href=/apple-touch-icon.png>
<link rel=icon type=image/png sizes=32x32 href=/favicon-32x32.png>
<link rel=icon type=image/png sizes=16x16 href=/favicon-16x16.png>
<link rel=manifest href=/site.webmanifest>
<link rel=mask-icon href=/safari-pinned-tab.svg color=#5bbad5>
<meta name=msapplication-TileColor content=#603cba>
<meta name=theme-color content=#ffffff>
(Note I dropped all optional quote marks.)
That connects the dots, and all the vendor icons will be used! Phew.
Fin
If RealFaviconGenerator helps you, do make a donation, to help out its creator with maintenance.
May your favicon setup be as simple as possible, but no simpler.
—Adam
Improve your Django develompent experience with my new book.
One summary email a week, no spam, I pinky promise.
Related posts:
Tags: django