Server-rendered HTML is Django's default output, and the template language keeps it safe and reusable.
render(request, "blog/post_list.html", {"posts": posts}) loads a template, fills it with the context dict and returns an HttpResponse.
With APP_DIRS: True (the default in new projects), Django looks in each installed app's templates/ folder. Put templates in blog/templates/blog/... and refer to them as "blog/..."; the extra folder namespaces them, because all apps share one search path.
The Django Template Language (DTL):
{{ post.title }}: variables; dots look up dict keys, attributes, methods without arguments and list indexes{{ post.created|date:"Y-m-d" }},|default:"none",|truncatewords:30,|length,|linebreaks: filters{% for p in posts %}...{% empty %}No posts{% endfor %}and{% if %}...{% elif %}...{% endif %}{% url 'blog:detail' p.pk %}for links,{% csrf_token %}inside POST forms{% extends "base.html" %}and{% block content %}for layouts;{% include "partials/card.html" %}for snippets- Django 6.0 adds template partials:
{% partialdef card %}...{% endpartialdef %}defines a named fragment,{% partial card %}reuses it, andrender(request, "page.html#card", ...)renders just that fragment, handy for htmx.
Output is autoescaped; |safe and mark_safe turn it off, so use them only on trusted HTML.
The DTL deliberately can't call functions with arguments or run arbitrary Python: prepare data in the view. For more power, Django also supports Jinja as a template backend.
Example
<!-- blog/templates/blog/base.html -->
<!doctype html>
<title>{% block title %}My blog{% endblock %}</title>
<main>{% block content %}{% endblock %}</main>
<!-- blog/templates/blog/post_list.html -->
{% extends "blog/base.html" %}
{% block title %}Posts | {{ block.super }}{% endblock %}
{% block content %}
<ul>
{% for post in posts %}
<li>
<a href="{% url 'blog:detail' post.pk %}">{{ post.title }}</a>
by {{ post.author.name|default:"anonymous" }}, {{ post.created|date:"M j, Y" }}
{% if forloop.first %}<strong>newest</strong>{% endif %}
</li>
{% empty %}
<li>No posts yet.</li>
{% endfor %}
</ul>
{% endblock %}
<!-- view: return render(request, "blog/post_list.html", {"posts": Post.objects.all()}) -->Remember
- render(request, template, context) returns an HttpResponse
- Namespace templates as app/templates/app/name.html
- Tags {% %}, variables {{ }}, filters |name:arg; output is autoescaped
Watch out for
- Putting templates directly in app/templates/ so two apps' list.html collide.