Template tags are logic inside HTML. Unlike {{ variable }} which only outputs a value, {% %} tags control flow: branches, loops, URLs, block composition.
{% if %} — conditional output
{% if user.is_authenticated %}
<a href="{% url 'logout' %}">Log out</a>
{% elif user.is_staff %}
<a href="/admin/">Admin panel</a>
{% else %}
<a href="{% url 'login' %}">Log in</a>
{% endif %}
Supported operators: ==, !=, <, >, and, or, not, in.
{% if posts and not page_obj.has_next %}
<p>This is the last page.</p>
{% endif %}
{% if post.status == "published" %}
<span class="badge bg-success">Published</span>
{% endif %}
{% for %} — iterating over lists
{% for post in posts %}
<article>
<h2>{{ post.title }}</h2>
<p>{{ post.summary }}</p>
</article>
{% empty %}
<p>No articles yet.</p>
{% endfor %}
{% empty %} runs when the list is empty — equivalent to if not posts.
Variables inside a loop
Django provides a forloop object automatically:
{% for post in posts %}
<div class="{% if forloop.first %}mt-0{% else %}mt-4{% endif %}">
{{ forloop.counter }}. {{ post.title }}
{% if forloop.last %}<hr>{% endif %}
</div>
{% endfor %}
| Variable | Value |
|---|---|
forloop.counter |
Iteration number (starting at 1) |
forloop.counter0 |
Iteration number (starting at 0) |
forloop.first |
True on the first iteration |
forloop.last |
True on the last iteration |
{% url %} — generating links
Don’t hardcode paths — use URL names:
<!-- urls.py: path('posts/<slug:slug>/', PostDetailView.as_view(), name='post-detail') -->
<a href="{% url 'post-detail' slug=post.slug %}">{{ post.title }}</a>
<a href="{% url 'post-list' %}">All articles</a>
<a href="{% url 'admin:index' %}">Administration</a>
If you rename a URL in urls.py, all links update automatically.
{% csrf_token %} — form protection
Every form with method POST must include this tag:
<form method="post" action="{% url 'comment-create' post.pk %}">
{% csrf_token %}
<textarea name="text"></textarea>
<button type="submit">Submit</button>
</form>
Without {% csrf_token %} Django returns a 403 error. The tag inserts a hidden field with a unique session token.
{% load static %} — loading static files
{% load static %}
<link rel="stylesheet" href="{% static 'css/main.css' %}">
<script src="{% static 'js/main.js' %}"></script>
<img src="{% static 'images/logo.png' %}" alt="Logo">
{% load static %} must appear once at the top of the file (or after {% extends %}). Details are in the static files article.
{% block %} and {% extends %} — template inheritance
A base template defines placeholder blocks:
{# base.html #}
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}DevBlog{% endblock %}</title>
</head>
<body>
{% block content %}{% endblock %}
</body>
</html>
A child template inherits and fills in the blocks:
{% extends 'base.html' %}
{% block title %}{{ post.title }} — DevBlog{% endblock %}
{% block content %}
<h1>{{ post.title }}</h1>
<div>{{ post.body }}</div>
{% endblock %}
Details are in the template inheritance article.
Full Example: DevBlog Post List Template
{% extends 'base.html' %}
{% load static %}
{% block title %}Articles — DevBlog{% endblock %}
{% block content %}
<div class="container py-4">
<h1>All Articles</h1>
{% if posts %}
<div class="row g-4">
{% for post in posts %}
<div class="col-12 col-md-6 col-lg-4">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">
<a href="{% url 'post-detail' slug=post.slug %}">
{{ post.title }}
</a>
</h5>
<p class="card-text text-muted">{{ post.summary }}</p>
</div>
<div class="card-footer text-muted small">
{{ post.published_at }}
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="alert alert-info">
No articles yet. Check back later!
</div>
{% endif %}
</div>
{% endblock %}
💬 Comments (0)
No comments yet
Be the first to share your opinion about this article!