How to add Tags to your Blog (ManyToManyField Example)

In this tutorial, I am going to show you how to add tags to basic Django blog using a ManyToManyField.

Blogs are a common project used in beginner’s tutorials. This tutorial by Django Girls is one of my favourites, which has instructions on how build a blog.

A good way to practice Django and learn new skills is to add new features to the projects created in tutorials.

For blogs, it is good if users can quickly find more posts belonging to a topic they are interested in.

The steps we will need to take are:

  1. Add the Tag model to models.py

  2. Update the Post model

  3. Create and run migrations

  4. Register your Tag model in admin.py

  5. Create some tags in /admin

  6. Add tags to the Add Post form

  7. Display the tags on your posts

  8. Allow users to view posts by tag

Code

To follow along with this tutorial, you can clone this repository which has a basic blogging application without tags.

Finished code can be found here.

1. Add the Tag model

The first step is to add a model for the tags.

When I did a Node.js course using MongoDB, tags were stored as JSON in the same model as the blog post. When using a relational database like SQLite or PostgreSQL, it is more common to see tags stored in their own model.

This means that all tags can be managed in one place. We can use a Many-to-Many relation to link rows in the Post table with rows in the Tag table.

We use Many-to-Many because one post can have multiple tags and one tag can be applied to multiple posts. If you’d like to build a better understanding of how you can link tables in Django, I have a summary in this post. I also have a separate post that explains how to use Many-to-Many relations in more detail.

The model

The Tag model will have one field: a CharField to store the name:

# models.py

class Tag(models.Model):
    name=models.CharField(max_length=100, unique=True)

    def __str__(self):
        return self.name

We need to add a ManyToManyFieldto allow posts and tags to be linked.

You will need to make a decision about which model to add the ManyToManyField to. The model that gets the field will get the form widget.

I want to be able to add tags to a post when creating the post, so I’m going to put it onto the Post model.

If you prefer to assign posts to an individual tag like the image below, then put it on the tag model.

A screenshot from the admin panel showing the Tag model which has a form widget letting users choose which posts to associate with that tag.

If you add the ManyToManyField to the Tag model, the form widget is only available on the create tag form.

2. Update the Post model

On the Post model, I have added a ManyToManyField called “tags”. When defining a ManyToManyField, I have set the model I want to link with (Tag) and the related_name of “posts”. This means if I have a tag object, I can access all the posts that use it through tag.posts.all().

# models.py

class Post(models.Model):

    STATUS_CHOICES = [
        ("draft", "Draft"),
        ("published", "Published"),
    ]

    title = models.CharField(max_length=255)
    slug = AutoSlugField(populate_from="title", unique=True)
    status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="draft")

    body = models.TextField()

    created_date = models.DateTimeField(auto_now_add=True)
    published_date = models.DateTimeField(blank=True, null=True)
    updated_date = models.DateTimeField(blank=True, null=True)

    tags = models.ManyToManyField(to=Tag, related_name="posts", blank=True)

3. Migrate

Now that we have made our changes to our models, we need to create and run migrations to update the database. If you’re new to Django and want to build your understanding, I have a beginner’s guide to migrations.

A screenshot of the terminal showing the results of making and running the migrations.

4. Register your Tag model in admin.py

Go into admin.py and register your Tag model.

# admin.py

from django.contrib import admin

from blog import models

# Register your models here.


class PostAdmin(admin.ModelAdmin):
    list_display = ["title", "status"]


admin.site.register(models.Post, PostAdmin)
admin.site.register(models.Tag, admin.ModelAdmin)

5. Add some tags in /admin

Go into /admin and find your tag model. We need to add some tags so we can start working with them. Rather than build another form to manage tags, we’ll just add them in /admin for now.

A screenshot from admin showing the tags list page after creating 9 tags.

Now, go to your Post model in /admin and open one of the posts. We now have our new field ‘tags’ in the form:

A screenshot of the admin panel showing the form for an individual post. The form contains the widget for ManyToMany fields which allows administrators to choose tags for the post.

6. Add tags to the Add Post form

Currently, our add post form looks like this. We want to be able to assign tags to our posts in the form itself.

A screenshot of the form to add a post without the widget to add tags.

Our current add post form. The next step is to add tags to this form.

You can go into forms.py and add “tags” to the fields attribute of Meta like this:

# forms.py

from django import forms
from . import models


class PostForm(forms.ModelForm):
    class Meta:
        model = models.Post
        fields = ["title", "body", "tags"]

However, the default widget for a Many to Many field isn’t very user friendly.

What we need to do instead is specify which widget the form should use.

In the code below, we specify the ModelMultipleChoiceField for tags with the CheckboxSelectMultiple widget.

The choices for the ModelMultipleChoiceField are defined in the queryset attribute. Here, Tag.objects.all() is sufficient.

If your blog supports multiple users, then you will need to filter the tags by user. To do this, you will to pass the user object to your form, so you can construct your queryset.

This is the result:

A screenshot of the form used to add a post with checkboxes to select tags.

We can render checkboxes instead of the default widget using a ModelMultipleChoiceField

7. Display the tags on your posts

Right now, my post detail page looks like this:

A screenshot of the post detail page without tags.

I want the tags for the post to be displayed below the title. I want each tag to be a link to a page that lists all the posts that share that tag.

In the template for your post (I’m using “post_detail.html”), add the following to display the tags (I'm using Bootstrap to style my templates):

<div>
    {% for tag in post.tags.all %}
    <a href="#" class="badge text-decoration-none bg-secondary">
    {{tag}}
    </a>
    {% endfor %}
</div>

Now the tags are shown on the page!

A screenshot of the post detail page where the list of tags associated with the post are listed beneath the title.

8. Allow users to view posts by tag

The final step is to let users view posts for a particular tag.

a. Add the route to urls.py

Our route in urls.py must contain something that will identify the exact tag that we will get posts for.

As the tag name is a CharField, it is not suitable to use in URLs. Instead, I’ve chosen to use the ID of the tag.

Make sure you include “tags” at the front your path. This to prevent conflicts between URLs. For example, I could have used an ID instead of a slug for post URLs. If a user types yourwebsite.com/posts/1 and two URLs use that pattern, then Django is going to use the first URL pattern that matches. This means it will call the post_detail view and ignore the list_posts_by_tag view.

# urls.py

from django.urls import path

from . import views

urlpatterns = [
    path("", views.index, name="index"),
    path("add", views.add_post, name="add_post"),
    path("<slug:slug>", views.post_detail, name="post_detail"),
    path("<slug:slug>/publish", views.publish, name="publish"),
    path("<slug:slug>/edit", views.edit_post, name="edit"),
    path("<slug:slug>/delete", views.delete_post, name="delete"),
    path("tags/<int:tag_id>", views.list_posts_by_tag, name="tag")
]

Text-only URLs

If you would rather have the tag name in your URL, then you will need to add a slug field to your Tag model. Every slug must be unique to guarantee that a URL returns the correct tag. There is a package called django-autoslug that will help you do this. I also have a tutorial on using django-autoslug to create URL friendly slugs.

b. Create a new view

Your view will need to identify the tag from the ID supplied in the URL and raise a 404 if no such tag is found.

Once it has found the tag, it needs to filter the Post table for published posts which use that tag.

Finally, you can pass your queryset of posts and the tag name to your template. I am reusing my index.html template and will add some logic to display the correct heading.

This is the code for the view:

# views.py

def list_posts_by_tag(request, tag_id):

    tag = get_object_or_404(Tag, id=tag_id)

    posts = Post.objects.filter(status="published", tags=tag)

    context = {
        "tag_name": tag.name,
        "posts": posts
    }

    return render(request, "index.html", context)

c. Create or update a template

I’m reusing my index.html template. I added this snippet to display the correct heading:

# index.html

{% if tag_name %}
    <h1>Tag: {{tag_name}}</h1>
{% else %}
    <h1>All Posts</h1>
{% endif %}

The result:

A screenshot of the post list page. It only shows posts with a tag of CSS.

d. Add the url to your post detail page

The final step is to update the links we added to the post detail page in Step 7.

# post_detail.html

<div>
    {% for tag in post.tags.all %}
    <a href="{% url 'tag' tag.id %}" class="badge text-decoration-none bg-secondary">
    {{tag}}
    </a>
    {% endfor %}
</div>

Now the links are clickable!

Conclusion

We can use a ManyToManyField to add tags our blog posts. This allows us to store our tags in a separate table to our blog posts. This makes it easy to query the database to find which posts to belong to a given tag. This would be much harder if you stored all tags in the Post table.

After adding the Tag model, we updated the form to add a post to allow users to select tags. This involved specifying the form widget as the default widget isn’t very user friendly.

With the form fixed, we updated our templates to render the tags on our post detail page and added a view to display posts belonging to a particular tag.