14 KiB
Django 6.0 HTML Templates
Generate production-ready Django 6.0.2 templates with Template Partials, HTMX integration, CSP support, and modern best practices for the league-planner project.
When to Use
- Creating new HTML templates with Django 6.0 features
- Refactoring templates to use Template Partials
- Building HTMX-powered dynamic components
- Setting up base templates with proper block structure
- Implementing reusable template fragments
Prerequisites
- Django 6.0+ installed
- Templates directory configured in settings
- For HTMX:
django-htmxpackage (optional but recommended)
Instructions
Step 1: Analyze Request
Parse $ARGUMENTS to determine:
- Template name: The target template file
- Type flag:
--partial: Create reusable partial fragments--htmx: HTMX-enabled template with partial endpoints--base: Base template with block structure- (none): Standard template
Step 2: Check Existing Templates
# Find existing templates in league-planner
find . -path "*/templates/*.html" -type f
Review the project's template structure and naming conventions.
Step 3: Generate Template
Apply the appropriate pattern from the examples below.
Django 6.0 Template Features
Template Partials (NEW in 6.0)
Define reusable fragments without separate files:
{# Define a partial #}
{% partialdef card %}
<div class="card">
<h3>{{ title }}</h3>
<p>{{ content }}</p>
</div>
{% endpartialdef %}
{# Render the partial multiple times #}
{% partial card %}
{% partial card %}
Inline Option - Render immediately AND save for reuse:
{% partialdef filter_controls inline %}
<form method="get" class="filters">
{{ filter_form.as_p }}
<button type="submit">Filter</button>
</form>
{% endpartialdef %}
{# Can still reuse later #}
{% partial filter_controls %}
Accessing Partials from Views (HTMX Pattern)
Render only a specific partial:
# views.py
from django.shortcuts import render
def update_component(request, pk):
obj = MyModel.objects.get(pk=pk)
# Render ONLY the partial named "item_row"
return render(request, "myapp/list.html#item_row", {"item": obj})
forloop.length (NEW in 6.0)
Access total loop count:
{% for item in items %}
<div>
{{ item.name }}
<span class="counter">({{ forloop.counter }}/{{ forloop.length }})</span>
</div>
{% endfor %}
querystring Tag Improvements
Build query strings cleanly:
{# Basic usage #}
<a href="?{% querystring page=1 %}">First Page</a>
{# Modify current query params #}
<a href="?{% querystring page=page.next_page_number %}">Next</a>
{# Remove a parameter #}
<a href="?{% querystring filter=None %}">Clear Filter</a>
{# Multiple mappings (NEW in 6.0) #}
<a href="?{% querystring base_params extra_params page=1 %}">Reset</a>
CSP Nonce Support
For inline scripts with Content Security Policy:
{# In settings: add 'django.template.context_processors.csp' #}
<script nonce="{{ csp_nonce }}">
// Safe inline script
</script>
Patterns & Best Practices
Pattern 1: Base Template with Blocks
{# templates/base.html #}
{% load static %}
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}League Planner{% endblock %}</title>
{% block extra_css %}{% endblock %}
</head>
<body>
{% block navbar %}
{% include "includes/navbar.html" %}
{% endblock %}
<main class="container">
{% block messages %}
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endblock %}
{% block content %}{% endblock %}
</main>
{% block footer %}
{% include "includes/footer.html" %}
{% endblock %}
{% block extra_js %}{% endblock %}
</body>
</html>
Pattern 2: List Template with Partials
{# templates/scheduler/scenario_list.html #}
{% extends "base.html" %}
{% block title %}Szenarien{% endblock %}
{% block content %}
<div class="scenario-list">
<h1>Szenarien</h1>
{# Define row partial for HTMX updates #}
{% partialdef scenario_row %}
<tr id="scenario-{{ scenario.pk }}">
<td>{{ scenario.name }}</td>
<td>{{ scenario.season }}</td>
<td>{{ scenario.get_status_display }}</td>
<td>
<a href="{% url 'scenario-detail' scenario.pk %}"
class="btn btn-sm btn-primary">Details</a>
<button hx-delete="{% url 'scenario-delete' scenario.pk %}"
hx-target="#scenario-{{ scenario.pk }}"
hx-swap="outerHTML"
hx-confirm="Wirklich löschen?"
class="btn btn-sm btn-danger">
Löschen
</button>
</td>
</tr>
{% endpartialdef %}
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Saison</th>
<th>Status</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody id="scenario-table-body">
{% for scenario in scenarios %}
{% partial scenario_row %}
{% empty %}
<tr><td colspan="4">Keine Szenarien vorhanden.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
Pattern 3: HTMX-Powered Form
{# templates/scheduler/scenario_form.html #}
{% extends "base.html" %}
{% block content %}
<div class="form-container">
<h1>{% if scenario.pk %}Szenario bearbeiten{% else %}Neues Szenario{% endif %}</h1>
{# Form partial for HTMX validation #}
{% partialdef scenario_form inline %}
<form method="post"
hx-post="{% url 'scenario-create' %}"
hx-target="#form-container"
hx-swap="innerHTML">
{% csrf_token %}
{% for field in form %}
<div class="form-group {% if field.errors %}has-error{% endif %}">
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
{{ field }}
{% if field.errors %}
<span class="error">{{ field.errors.0 }}</span>
{% endif %}
{% if field.help_text %}
<small class="help-text">{{ field.help_text }}</small>
{% endif %}
</div>
{% endfor %}
<div class="form-actions">
<button type="submit" class="btn btn-primary">
{% if scenario.pk %}Speichern{% else %}Erstellen{% endif %}
</button>
<a href="{% url 'scenario-list' %}" class="btn btn-secondary">Abbrechen</a>
</div>
</form>
{% endpartialdef %}
</div>
{% endblock %}
Pattern 4: Detail Page with Inline Editing
{# templates/scheduler/scenario_detail.html #}
{% extends "base.html" %}
{% block content %}
<article class="scenario-detail">
{# Header partial - editable via HTMX #}
{% partialdef scenario_header inline %}
<header id="scenario-header">
<h1>{{ scenario.name }}</h1>
<p class="meta">
Saison: {{ scenario.season }} |
Status: {{ scenario.get_status_display }} |
Erstellt: {{ scenario.created_at|date:"d.m.Y H:i" }}
</p>
<button hx-get="{% url 'scenario-edit-header' scenario.pk %}"
hx-target="#scenario-header"
hx-swap="outerHTML"
class="btn btn-sm btn-outline">
Bearbeiten
</button>
</header>
{% endpartialdef %}
{# Games list partial #}
{% partialdef games_list %}
<section id="games-list" class="games">
<h2>Spiele ({{ games|length }}/{{ games|length }})</h2>
<ul>
{% for game in games %}
<li>
{{ game.home_team }} vs {{ game.away_team }}
<span class="date">{{ game.date|date:"d.m.Y" }}</span>
</li>
{% endfor %}
</ul>
</section>
{% endpartialdef %}
{% partial games_list %}
</article>
{% endblock %}
Pattern 5: Modal with Partial
{# templates/includes/modal.html #}
{% partialdef modal_container %}
<div id="modal-container"
class="modal {% if show %}show{% endif %}"
hx-swap-oob="true">
{% if show %}
<div class="modal-backdrop" hx-get="" hx-target="#modal-container"></div>
<div class="modal-content">
{% block modal_header %}
<header class="modal-header">
<h3>{{ modal_title }}</h3>
<button hx-get="" hx-target="#modal-container" class="close">×</button>
</header>
{% endblock %}
{% block modal_body %}
{{ modal_content }}
{% endblock %}
{% block modal_footer %}
<footer class="modal-footer">
<button hx-get="" hx-target="#modal-container" class="btn btn-secondary">
Abbrechen
</button>
{% block modal_actions %}{% endblock %}
</footer>
{% endblock %}
</div>
{% endif %}
</div>
{% endpartialdef %}
Pattern 6: Pagination with querystring
{# templates/includes/pagination.html #}
{% if page_obj.has_other_pages %}
<nav class="pagination" aria-label="Seitennavigation">
<ul>
{% if page_obj.has_previous %}
<li>
<a href="?{% querystring page=1 %}">« Erste</a>
</li>
<li>
<a href="?{% querystring page=page_obj.previous_page_number %}">‹ Zurück</a>
</li>
{% endif %}
<li class="current">
Seite {{ page_obj.number }} von {{ page_obj.paginator.num_pages }}
</li>
{% if page_obj.has_next %}
<li>
<a href="?{% querystring page=page_obj.next_page_number %}">Weiter ›</a>
</li>
<li>
<a href="?{% querystring page=page_obj.paginator.num_pages %}">Letzte »</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
HTMX View Pattern
For HTMX endpoints that render partials:
# views.py
from django.shortcuts import render, get_object_or_404
from django.views.decorators.http import require_http_methods
@require_http_methods(["GET"])
def scenario_row(request, pk):
"""Render single scenario row partial for HTMX."""
scenario = get_object_or_404(Scenario, pk=pk)
# Syntax: "template.html#partial_name"
return render(request, "scheduler/scenario_list.html#scenario_row", {
"scenario": scenario
})
@require_http_methods(["DELETE"])
def scenario_delete(request, pk):
"""Delete scenario and return empty response for HTMX swap."""
scenario = get_object_or_404(Scenario, pk=pk)
scenario.delete()
# Return empty string - HTMX will remove the row
return HttpResponse("")
@require_http_methods(["GET", "POST"])
def scenario_edit_header(request, pk):
"""Inline edit scenario header via HTMX."""
scenario = get_object_or_404(Scenario, pk=pk)
if request.method == "POST":
form = ScenarioHeaderForm(request.POST, instance=scenario)
if form.is_valid():
form.save()
# Return the display partial
return render(request, "scheduler/scenario_detail.html#scenario_header", {
"scenario": scenario
})
else:
form = ScenarioHeaderForm(instance=scenario)
# Return edit form partial
return render(request, "scheduler/scenario_edit_header.html", {
"scenario": scenario,
"form": form
})
Template Organization (league-planner)
templates/
├── base.html # Main base template
├── includes/
│ ├── navbar.html
│ ├── footer.html
│ ├── pagination.html
│ └── modal.html
├── scheduler/
│ ├── scenario_list.html # With partials for rows
│ ├── scenario_detail.html # With partials for sections
│ ├── scenario_form.html # With form partial
│ └── _scenario_row.html # Standalone partial (legacy)
├── draws/
│ └── ...
└── qualifiers/
└── ...
Common Pitfalls
- Partial nicht gefunden: Partial-Namen müssen exakt matchen, keine Leerzeichen
- Context fehlt: Partials erben den Context - stelle sicher, dass Variablen verfügbar sind
- HTMX Swap-Probleme: Verwende IDs für präzises Targeting (
hx-target="#element-id") - CSP Violations: Inline-Styles/Scripts brauchen
nonce="{{ csp_nonce }}"wenn CSP aktiv - N+1 in Templates: Verwende
select_related/prefetch_relatedin Views, nicht im Template - forloop.length Performance: Bei großen Listen kann dies teuer sein (zählt vorab)
Deprecation Warning
⚠️ urlize Filter: Default-Protokoll wechselt von HTTP zu HTTPS in Django 7.0.
Setze URLIZE_ASSUME_HTTPS = True in settings.py für Vorwärtskompatibilität.
Verification
Test the template:
# Check for template syntax errors
python manage.py check --deploy
# Render template in shell
python manage.py shell
>>> from django.template.loader import render_to_string
>>> html = render_to_string('scheduler/scenario_list.html', {'scenarios': []})
>>> print(html)
# Test partial rendering
>>> html = render_to_string('scheduler/scenario_list.html#scenario_row', {'scenario': scenario})