IAN WALDRON IAN WALDRON

Efficient URL Management In Django

A centralized approach to URL management in Django for frequently referenced URL patterns.
June 25, 2024

Background

I've encountered situations in Django where using the URL template tag created a bit of work when code needed to be refactored. When you first start a project, your architecture is relatively flat and URL pattern references are easy to make and maintain. But as your project grows, you may find that a particular app is better suited as one nested in another. If you're using URL namespaces, you'll need to refactor any templates referencing old URL pattern names.

For these situations, I decided to centralize my most commonly referenced URL patterns that would have otherwise relied on the URL template tag. That way any updates to project architecture or pattern naming only need be updated in a single location. To accomplish this, I explored a couple options including placing the anchor HTML tag containing the URL template tag in an HTML template fragment, using context processors to supply context containing URLs to templates globally, and using template tags that accept arguments and reverse URLs as functions. But first, let's look at an example.

Scenario

Consider the situation and the effect on URL patterns where app_a, originally a top-level app, becomes nested within app_b. If both are using URL namespaces, the full URL name to be used when reversing would change from 'app_a:some-url-name'  to 'app_b:app_a:some-url-name.' With this change, you'll need to go back and refactor any templates with the old reference and change it over to the new reference.

Depending on the project, the refactoring might not be that big of a deal. Especially for URL patterns pointing to create/update/delete operations, these references likely aren't being repeated numerously and instead tend to be local to a feature. Rather, the use case I'm thinking about is for situations where you have a URL pattern that will referenced in a lot of places throughout your project where refactoring would take a great deal of effort.

Templates

The first option I explored was to use the Django URL template tag but dump it into its own template fragment stored in a central location. I then access the link using the include template tag. My thought process was that if the whole of the link being an anchor tag is HTML then we should to take an HTML-centric approach.

First, I created a centrally located template directory named "common-links/" and specified its location in my settings.py file.

# settings.py

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [
            os.path.join(BASE_DIR, 'common-links/')
        ],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

Then in my "common-links/" directory, I'll nest a file "article.html" to be used for storing an anchor tag pointing to blog articles.

To retrieve the HTML files with common-links/article.html, you actually have to add a second, nested "common-links" directory because the "DIRS" value in the "TEMPLATES" setting is already pointing to "common-links." So the path would be "common-links/common-links/article.html" similar to how you approach app-specific templates.

Because my particular URL pattern accepts two arguments for the article's ID and slug, this template needs context passed to it. Additionally, I want the ability to pass any desired attributes as well as the text content of the anchor tag. If you're only ever going to pass a single attribute, then perhaps define that one attribute as its own argument like "css" or similar. However, I'm going to structure my template to accept an "attrs" argument containing both the key and value of the attribute so any number of attributes and values can be supplied ("css," "title," and so on). 

<!-- common-links/article.html -->
<a href="{% url 'public:article' id=id slug=slug %}" {{ attrs }}>{{ text }}</a>

Here we have an anchor tag for the URL pattern named "article" in the namespace "public" that requires as arguments both the ID and slug. It further accepts two more arguments "attrs" and "text" to construct the anchor tag. 

Then in a higher-order template where we want to add an article link, we would use the Django "include" template tag to pull in our anchor tag template fragment.

{% include 'common-links/article.html' with id=<id> slug=<some-slug> attrs='<key>="<value>"' text="" %}

With this structure, I can now include the anchor tag using this template fragment and if and when there are any changes to the URL Pattern, I will need only to make a change in the one location.

For dynamic URLs related to a model, the easiest way by far is to use the instance's model method get_absolute_url(). This way you can simply adjust the model method if changes occur. If you need to specify more than one URL pattern, then add model methods.

Concerns

While this is a completely valid and functional approach, I decided to go in another direction for a couple of reasons. First, the approach isn't very semantic and the readability of the templates is diminished. Second, the approach is less performant than alternatives. There's a bit of overhead involved with using the template engine so heavily and you'll run into problems when traffic scales. Django has a couple tricks available to help alleviate performance issues associated with rendering templates. For example, you can cache template fragments you frequently use. But even with caching, be aware of what overhead you're adding.

Context Processors

The next option I explored was building a custom context processor. Let's say we have some URL pattern name "some-url-pattern" and we want this URL to be available to our templates globally. We could build the following context processor:

# app/context_processors.py

from django.urls import reverse


# noinspection PyUnusedLocal
def common_links(request):
    return {
        "common_links": {
            "some_url_pattern": reverse('some-url-pattern'),
        }
    }

Then we need to register the context processor:

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
                'app.context_processors.common_links'
            ]
        },
    },
]

And now we can access our URL in any template using this context dictionary like so:

<!-- some-template.html -->

<a href="">Some URL Pattern</a>

Limitations

While this is probably the simplest way to get up and running quickly, there is a major limitation to the approach; You can't have URL patterns that accept arguments. Depending on your objectives, this limitation could defeat the entire purpose. But if your particular URL patterns don't need to accept arguments, for example as would be the case with more static-oriented content, then this is a completely valid approach.

Template Tags

The last approach I'll explore is template tags. The benefit of template tags over context processors is that template tags can accept arguments so you would be able to reverse dynamic URLs. Using our earlier "article" example, we could build a simple template tag:

# app/templatetags/common_links.py

from django import template
from django.urls import reverse

register = template.Library()


@register.simple_tag
def article_link(**kwargs):
    return reverse('public:article', kwargs=kwargs)

Then in our template:


<!-- some-template.html -->
{% load common_links %}

<a href="{% article_link id=<id> slug=<some-slug> %}"><text></a>

With this approach we have a pretty light-weight solution for managing URL patterns centrally. This is my preferred solution for dealing with dynamic URLs that are repeated often in templates.

Final Thoughts

When you have a URL pattern that's being repeated many times in a lot of different locations, you may want to pause and consider whether it's worth the effort and complexity to centralize those references in case you need to make changes down the line. The approach you choose will depend on your use case. If traffic isn't particularly high, the template approach might suit your needs well. If your patterns are static, then the context processor may get the job done. And finally if you're working with higher traffic volumes and your URL patterns are dynamic, consider my last example using template tags.

Again, and I can't emphasize this enough, please don't use this for constructing dynamic URL for objects stored in your own database. This scenario should be handled using the model method get_absolute_url(). However for the scenarios where you may not have a model instance you control, like with external or user supplied data, and you need to construct a dynamic URL, consider one of the above approaches for your project.