Table of Contents
- Introduction
- Why Use a Custom Tag Model?
- Step-by-Step Implementation
- Enhancing the Tag System
- Custom Tag Model vs. django-taggit
- Conclusion
- Tags
Introduction
Adding related articles to a blog app improves user engagement by suggesting content similar to what readers are currently viewing. In a Django blog app, this can be achieved by associating posts with tags and querying posts that share those tags. While django-taggit
is a popular choice for tagging, creating a custom tag model offers greater control and eliminates external dependencies. This post walks you through implementing related articles using a custom tag model, tailored for a Django blog app.
Why Use a Custom Tag Model?
A custom tag model provides several advantages over using django-taggit
:
- Full Control: Customize fields, relationships, and behavior (e.g., add descriptions or hierarchies).
- No Dependencies: Avoid relying on third-party packages, reducing maintenance overhead.
- Transparency: You fully understand the database schema and queries.
- Flexibility: Easily extend the system for specific needs, like tag validation or custom ranking.
However, it requires more initial setup and manual implementation of features that django-taggit
provides out of the box, such as tag clouds or generic tagging.
Step-by-Step Implementation
Define the Tag Model
Create a Tag
model to store tags with fields for name
and slug
:
from django.db import models
from django.utils.text import slugify
class Tag(models.Model):
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(max_length=50, unique=True, blank=True)
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
def __str__(self):
return self.name
The save
method auto-generates a slug for URL-friendly tags.
Update the Post Model
Add a many-to-many relationship to link posts with tags:
class Post(models.Model):
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
content = models.TextField()
published_date = models.DateTimeField(auto_now_add=True)
tags = models.ManyToManyField(Tag, related_name='posts', blank=True)
def __str__(self):
return self.title
The blank=True
option allows posts to have no tags, and related_name='posts'
enables reverse queries.
Run Migrations
Generate and apply database migrations:
python manage.py makemigrations
python manage.py migrate
Set Up Admin Integration
Register the models in admin.py
for easy management:
from django.contrib import admin
from .models import Post, Tag
@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
list_display = ['name', 'slug']
prepopulated_fields = {'slug': ('name',)}
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ['title', 'published_date']
filter_horizontal = ['tags']
The filter_horizontal
widget simplifies tag selection, and prepopulated_fields
auto-fills slugs.
Implement Related Articles Logic
Add a method to the Post
model to query related posts based on shared tags:
class Post(models.Model):
# ... other fields ...
def get_related_posts(self, limit=3):
return Post.objects.filter(
tags__in=self.tags.all()
).exclude(id=self.id).distinct()[:limit]
Alternatively, handle this in the view:
from django.shortcuts import render, get_object_or_404
def post_detail(request, slug):
post = get_object_or_404(Post, slug=slug)
related_posts = Post.objects.filter(
tags__in=post.tags.all()
).exclude(id=post.id).distinct()[:3]
return render(request, 'blog/post_detail.html', {
'post': post,
'related_posts': related_posts
})
Configure URLs
Set up the URL for the post detail view in urls.py
:
from django.urls import path
from . import views
urlpatterns = [
path('post/<slug:slug>/', views.post_detail, name='post_detail'),
]
Render Related Articles in Templates
Display related articles in blog/post_detail.html
:
{% extends 'base.html' %}
{% block content %}
<h1>{{ post.title }}</h1>
<p>{{ post.content }}</p>
<h2>Related Articles</h2>
<ul>
{% for related_post in related_posts %}
<li><a href="{% url 'post_detail' related_post.slug %}">{{ related_post.title }}</a></li>
{% empty %}
<li>No related articles found.</li>
{% endfor %}
</ul>
{% endblock %}
Display and Filter by Tags
Show tags on posts and allow filtering by tag in the post list view.
Template (blog/post_detail.html
):
<p>Tags:
{% for tag in post.tags.all %}
<a href="{% url 'post_list' %}?tag={{ tag.slug }}">{{ tag.name }}</a>
{% empty %}
No tags
{% endfor %}
</p>
View (post_list
):
def post_list(request):
posts = Post.objects.all()
tag_slug = request.GET.get('tag')
if tag_slug:
posts = posts.filter(tags__slug=tag_slug)
return render(request, 'blog/post_list.html', {'posts': posts})
URL:
urlpatterns = [
path('', views.post_list, name='post_list'),
path('post/<slug:slug>/', views.post_detail, name='post_detail'),
]
Enhancing the Tag System
Case-Insensitive Tags
Normalize tag names to lowercase and enforce case-insensitive uniqueness:
class Tag(models.Model):
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(max_length=50, unique=True, blank=True)
def save(self, *args, **kwargs):
self.name = self.name.lower()
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
class Meta:
constraints = [
models.UniqueConstraint(
models.functions.Lower('name'),
name='unique_tag_name'
)
]
Tag Validation
Add validation to prevent invalid tags (e.g., empty names) using model validation.
Hierarchical Tags
Support tag hierarchies by adding a parent
field:
class Tag(models.Model):
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(max_length=50, unique=True, blank=True)
parent = models.ForeignKey('self', null=True, blank=True, on_delete=models.CASCADE)
Improved Related Articles
Rank related posts by the number of shared tags:
from django.db.models import Count
class Post(models.Model):
# ... other fields ...
def get_related_posts(self, limit=3):
return Post.objects.filter(
tags__in=self.tags.all()
).exclude(id=self.id).annotate(
shared_tags=Count('tags')
).order_by('-shared_tags').distinct()[:limit]
Custom Tag Model vs. django-taggit
Here’s how a custom tag model compares to django-taggit
:
Feature | Custom Tag Model | django-taggit |
---|---|---|
Ease of Setup | More setup (define models, relationships) | Quick setup with TaggableManager |
Customization | Highly customizable | Limited without subclassing |
Dependencies | None | Requires django-taggit |
Generic Tagging | Specific to models | Tags any model via GenericForeignKey |
Built-in Features | Manual implementation | Tag clouds, autocomplete, etc. |
Maintenance | Your responsibility | Community-maintained |
A custom tag model is ideal if you need specific features or prefer a dependency-free solution, while django-taggit
suits rapid development with built-in functionality.
Conclusion
Implementing related articles in a Django blog app using a custom tag model is a powerful way to enhance user experience while maintaining full control over your tagging system. By defining a Tag
model, linking it to posts via a many-to-many relationship, and querying shared tags, you can efficiently suggest relevant content. This approach is lightweight, flexible, and extensible, allowing for advanced features like case-insensitive tags, hierarchies, or ranked related posts. Whether you choose a custom model or django-taggit
, the result is a more engaging blog that keeps readers exploring.