Khi một model Django cần link tới trang chi tiết của chính nó, đừng hardcode đường dẫn trong template hay view. Hãy định nghĩa phương thức get_absolute_url() trên model và dùng reverse() để trỏ tới URL pattern theo tên — đổi đường dẫn trong urls.py thì mọi link tự cập nhật, và trang admin tự có nút “View on site”.

Vấn đề

Nhiều project Django mới bắt đầu bằng cách viết thẳng đường dẫn vào template và view. Lúc đầu mọi thứ chạy tốt, nhưng chỉ cần đổi cấu trúc URL một lần là bạn phải rà soát lại toàn bộ codebase để sửa từng chỗ hardcode — và chắc chắn sẽ sót một chỗ nào đó.

Mẫu lỗi điển hình: template ghi cứng tiền tố /bai-viet/, view redirect bằng f-string — cả hai đều giả định URL không bao giờ đổi, một giả định gần như chắc chắn sai khi dự án lớn dần.

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

Giải pháp

Đặt “địa chỉ chuẩn” của model ngay trên chính model đó, theo quy ước sẵn của Django: phương thức get_absolute_url() — chỉ cần trả về đường dẫn tới trang chi tiết của object.

Đoạn code dưới đây làm hai việc: tự sinh slug từ tiêu đề bằng slugify() khi lưu lần đầu — “Quản lý chi tiêu với Django” thành “quan-ly-chi-tieu-voi-django” — và định nghĩa get_absolute_url() bằng reverse() theo tên URL pattern.

# 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})

Khai báo URL pattern có tên trong blog/urls.py, nhớ đặt app_name để dùng 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"),
]

Từ đây, mọi nơi trong dự án dùng chung một “nguồn sự thật” duy nhất. Trong template, gọi phương thức không cần dấu ngoặc:

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

Trong view, redirect() nhận thẳng object vì nó tự gọi get_absolute_url():

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

Hai lợi ích miễn phí đi kèm: trang admin tự hiện nút “View on site” trên trang sửa object, và django.contrib.sitemaps cũng dùng get_absolute_url() để dựng sitemap — một phương thức nhỏ phục vụ cả ba nơi: template, view và SEO.

Áp dụng vào dự án của bạn

Áp dụng quy tắc một dòng cho cả dự án: model nào có trang chi tiết công khai thì model đó phải có get_absolute_url(). Thêm vào Post hôm nay, rồi lần lượt Product, Category, Author — mỗi lần chỉ vài dòng code. Nhớ giữ slug duy nhất bằng unique=True, và chỉ tự sinh slug khi trường còn trống để slug nhập tay trong admin không bị ghi đè. Lần tới đổi cấu trúc URL, bạn sửa một dòng trong urls.py thay vì rà soát cả trăm template.

Bài viết liên quan

Câu hỏi thường gặp

get_absolute_url() khác gì gọi reverse() trực tiếp trong view?

Gọi reverse() trong view vẫn chạy, nhưng tên URL bị lặp lại ở nhiều nơi, còn template thì không gọi reverse() gọn gàng được. Đặt logic vào model, mọi nơi dùng chung một chỗ duy nhất, và redirect(post) hoạt động ngay không cần code thêm.

Hai bài viết cùng tiêu đề thì slug trùng nhau, xử lý thế nào?

Với unique=True, bài thứ hai lỗi IntegrityError khi lưu — đó là hành vi có chủ đích để bạn phát hiện trùng lặp. Cách xử lý thường gặp: thêm hậu tố đếm như quan-ly-chi-tieu-2, hoặc ghép id vào slug sau lần lưu đầu tiên. Đừng bỏ unique=True để “cho qua”: slug trùng sẽ làm trang chi tiết trỏ nhầm bài.

Có bắt buộc đặt tên cho URL pattern không?

Có, nếu muốn dùng reverse(). Không có tên, reverse() không biết trỏ tới pattern nào và bạn lại quay về hardcode đường dẫn. Đặt tên theo dạng app:tên-hành-động, ví dụ blog:post-detail, ngắn và rõ nghĩa.

slugify() xử lý tiêu đề tiếng Việt thế nào?

Mặc định slugify() bỏ dấu tiếng Việt: “Quản lý chi tiêu với Django” thành “quan-ly-chi-tieu-voi-django” — sạch và an toàn cho URL. Muốn giữ dấu thì truyền allow_unicode=True, nhưng URL sẽ bị mã hoá % khi chia sẻ, nên đa số dự án cứ để mặc định.

💬 Bạn đã thử cách này trong dự án của mình chưa? Nếu gặp lỗi hoặc có cách làm gọn hơn, để lại bình luận bên dưới — mình đọc hết và sẽ bổ sung những ý hay vào bài.

Nguồn tham khảo