When a Django model needs a link to its own detail page, don’t hardcode the path in templates or views. Define a get_absolute_url() method on the model and use reverse() to point at the URL pattern by name — change the path in urls.py and every link updates itself, and the admin gets a “View on site” button for free.

The Problem

Many new Django projects start by writing paths straight into templates and views. Everything works at first, but the moment you restructure your URLs once, you have to comb through the whole codebase to fix every hardcoded spot — and you are guaranteed to miss one somewhere.

The classic mistake: the template hardcodes the /bai-viet/ prefix, the view redirects with an f-string — both assume the URL will never change, an assumption that is almost certainly wrong as the project grows.

<!-- templates/blog/post_list.html -->
<a href="/bai-viet/{{ post.slug }}/">{{ post.title }}</a>
# blog/views.py
return redirect(f"/bai-viet/{post.slug}/")

The Solution

Put the model’s “canonical address” right on the model itself, following Django’s built-in convention: the get_absolute_url() method — it only needs to return the path to the object’s detail page.

The code below does two things: it auto-generates a slug from the title with slugify() on first save — “Quản lý chi tiêu với Django” becomes “quan-ly-chi-tieu-voi-django” — and it defines get_absolute_url() with reverse() using the URL pattern’s name.

# blog/models.py
from django.db import models
from django.urls import reverse
from django.utils.text import slugify
class Post(models.Model):
title = models.CharField(max_length=200)
slug = models.SlugField(max_length=220, unique=True, blank=True)
body = models.TextField()
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.title)
super().save(*args, **kwargs)
def get_absolute_url(self):
return reverse("blog:post-detail", kwargs={"slug": self.slug})

Declare the named URL pattern in blog/urls.py, and remember to set app_name so you can use the namespace:

# blog/urls.py
from django.urls import path
from . import views
app_name = "blog"
urlpatterns = [
path("bai-viet/<slug:slug>/", views.post_detail, name="post-detail"),
]

From here on, everywhere in the project shares a single source of truth. In templates, call the method without parentheses:

<a href="{{ post.get_absolute_url }}">{{ post.title }}</a>

In views, redirect() accepts the object directly because it calls get_absolute_url() itself:

return redirect(post) # tương đương redirect(post.get_absolute_url())

Two free bonuses come with it: the admin automatically shows a “View on site” button on the object’s edit page, and django.contrib.sitemaps also uses get_absolute_url() to build the sitemap — one small method serving three places: templates, views, and SEO.

Putting It Into Practice

Apply one simple rule across the whole project: any model with a public detail page must have get_absolute_url(). Add it to Post today, then Product, Category, Author one by one — just a few lines of code each time. Keep slugs unique with unique=True, and only auto-generate a slug when the field is still empty so a slug typed by hand in the admin doesn’t get overwritten. Next time you change the URL structure, you will edit one line in urls.py instead of hunting through hundreds of templates.

Related Posts

FAQ

How is get_absolute_url() different from calling reverse() directly in a view?

Calling reverse() in a view still works, but the URL name gets repeated in many places, and templates can’t call reverse() neatly. Put the logic in the model and everywhere shares one single place, while redirect(post) works right away with no extra code.

What if two posts share the same title and their slugs collide?

With unique=True, the second post raises an IntegrityError when saving — that is deliberate behaviour so you catch the duplicate. The usual fix: append a counter suffix like quan-ly-chi-tieu-2, or append the id to the slug after the first save. Don’t drop unique=True just to “let it through”: duplicate slugs will make the detail page point at the wrong post.

Do URL patterns have to be named?

Yes, if you want to use reverse(). Without a name, reverse() doesn’t know which pattern to point at and you’re back to hardcoding paths. Name them in the app:action form, for example blog:post-detail — short and clear.

How does slugify() handle Vietnamese titles?

By default, slugify() strips Vietnamese diacritics: “Quản lý chi tiêu với Django” becomes “quan-ly-chi-tieu-voi-django” — clean and safe for URLs. To keep the diacritics, pass allow_unicode=True, but the URL will get %-encoded when shared, so most projects just stick with the default.

💬 Have you tried this approach in your own project? If you hit an error or have a neater way to do it, leave a comment below — I read them all and will add the best ideas to the post.

Sources