How to Add a Favicon to Your Django Site

“Paint me an icon!”

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:

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:

  1. 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.

  2. PNG

    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.)

  3. SVG

    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. SVGs 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:

Okay, that’s the code. With it set up correctly, you should be able to see the icon in your browser tab:

Firefox with PNG favicon

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:

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:

Firefox with SVG emoji favicon

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:

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:

Pear icon

Second, open up RealFaviconGenerator:

RealFaviconGenerator home page.

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

RealFaviconGenerator first icon page.

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

RealFaviconGenerator modify iOS options.

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...”:

RealFaviconGenerator modify iOS options.

Then, click “Generate your Favicons and HTML code”.

After generation completes, you’ll be presented with this screen:

RealFaviconGenerator install instructions page.

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:

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

Update (2022-01-20): Added this section, thanks to the tip from Julian Wachholz on Twitter.

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 URLs. 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 URLs. For example, you can check the apple touch icon at /apple-touch-icon.png:

Firefox with apple touch icon

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 URLs. For example, you can check the apple touch icon at /apple-touch-icon.png:

Firefox with apple touch icon

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:

Cool, cool beans. Don’t forget to then add the HTML, as below.

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


Read my book Boost Your Git DX to Git better.


Subscribe via RSS, Twitter, Mastodon, or email:

One summary email a week, no spam, I pinky promise.

Related posts:

Tags: