本文へ移動
cccskills
無料GitHub で公開

django-htmx

Use when building dynamic Django web apps with htmx - partial rendering, HTMX responses, querystring tag, CSP

インストール方法を見る

含まれるファイル(3)

  • SKILL.md14.4 KB
  • references/csp-nonce.md2.4 KB
  • references/testing-patterns.md6.1 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

Django HTMX

Django-htmx provides seamless integration between Django and htmx for building modern, dynamic web applications without writing complex JavaScript.

Versions: django-htmx 1.16.0 + Django 6.0 fully compatible. Python 3.10 → 3.14 supported.

Installation

pip install django-htmx

Add to INSTALLED_APPS:

INSTALLED_APPS = [
    ...
    "django_htmx",
]

Add the middleware (order matters - after SessionMiddleware, before AuthenticationMiddleware):

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django_htmx.middleware.HtmxMiddleware",  # Required for request.htmx
    "django.contrib.messages.middleware.MessageMiddleware",
    ...
]

Setup

Base Template

Load the template tag and include the htmx script once in your base template:

{% load django_htmx %}
<!DOCTYPE html>
<html>
<head>
    {% htmx_script %}
</head>
<body hx-headers='{"x-csrftoken": "{{ csrf_token }}"}'>
    {% block content %}{% endblock %}
</body>
</html>

The hx-headers attribute on <body> ensures all htmx requests carry the CSRF token. Without this, POST/PUT/DELETE requests fail with 403.

For debugging, use the unminified version:

{% htmx_script minified=False %}

Jinja2 Configuration

If using Jinja2 templates, configure the global:

# settings.py or jinja2 config
from django_htmx.jinja import htmx_script

def environment(**options):
    from jinja2 import Environment
    env = Environment(**options)
    env.globals.update({"htmx_script": htmx_script})
    return env

Then in templates: {{ htmx_script() }}

See: references/csp-nonce.md for Content-Security-Policy integration.

Core Concepts

Request Detection

The middleware adds request.htmx to detect htmx requests:

from django.shortcuts import render

def my_view(request):
    if request.htmx:
        template_name = "partial.html"
    else:
        template_name = "full.html"
    return render(request, template_name)

HtmxDetails Attributes

The request.htmx object provides:

  • request.htmx - Boolean, True if request is from htmx
  • request.htmx.boosted - True if from boosted element (hx-boost)
  • request.htmx.current_url - Current URL from HX-Current-URL header
  • request.htmx.current_url_abs_path - Absolute path form of current_url
  • request.htmx.history_restore_request - True for history restoration
  • request.htmx.target - Target element ID from HX-Target header
  • request.htmx.trigger - Trigger element ID from HX-Trigger header
  • request.htmx.trigger_name - Trigger element name from HX-Trigger-Name header
  • request.htmx.prompt - User response to hx-prompt attribute
  • request.htmx.triggering_event - Deserialized JSON from event-header extension

HTTP Response Classes (Decision Guide)

Choose the right response type based on what should happen on the client:

Response TypeWhen to UseWhat Breaks If Wrong
HttpResponse (normal)Full page reload needed, or swapping entire documentUsing htmx-specific responses here causes no-op or unexpected behavior
HttpResponseClientRedirectNavigate to different URL without full reloadUsing plain HttpResponseRedirect leaves stale DOM; user sees old content
HttpResponseClientRefreshForce full page reload (stale session, permission change)Using redirect instead loses form data; using HttpResponse doesn't refresh
HttpResponseLocation"Boosted" navigation to new URLUsing redirect causes full reload; using HttpResponse shows wrong URL
HttpResponseStopPollingEnd polling loop (event finished, error unrecoverable)Omitting this keeps polling, wasting resources
Plain HttpResponse + HX-TriggerUpdate fragment and trigger client eventWithout trigger, client doesn't know to update related UI (e.g., badge count)
Plain HttpResponse + HX-Redirect headerClient-side redirect from server logicUsing HttpResponseClientRedirect directly is cleaner; manual header is for custom logic

HttpResponseClientRedirect

Use for navigation that should update browser history without full reload:

from django_htmx.http import HttpResponseClientRedirect

def sensitive_view(request):
    if not sudo_mode.active(request):
        return HttpResponseClientRedirect("/activate-sudo/")
    ...

What breaks: A plain HttpResponseRedirect causes htmx to follow the redirect as a normal request, which may swap the wrong fragment or leave the original DOM intact.

HttpResponseClientRefresh

Force a full page reload:

from django_htmx.http import HttpResponseClientRefresh

def partial_table_view(request):
    if page_outdated(request):
        return HttpResponseClientRefresh()
    ...

What breaks: Without this, the client keeps showing stale data. Use when session expired, permissions changed, or cache invalidation occurred.

HttpResponseLocation

Trigger client-side "boosted" navigation (hx-boost behavior):

from django_htmx.http import HttpResponseLocation

def wait_for_completion(request, action_id):
    ...
    if action.completed:
        return HttpResponseLocation(f"/action/{action.id}/completed/")
    ...

What breaks: Using a redirect causes a full page reload, losing the htmx benefits.

HttpResponseStopPolling

End a polling loop:

from django_htmx.http import HttpResponseStopPolling

def my_pollable_view(request):
    if event_finished():
        return HttpResponseStopPolling()
    return render(request, "status.html", {"status": "running"})

Or use the constant with render():

from django_htmx.http import HTMX_STOP_POLLING

def my_pollable_view(request):
    if event_finished():
        return render(request, "event-finished.html", status=HTMX_STOP_POLLING)

Response Modifying Functions

These wrap an HttpResponse to add htmx-specific headers:

push_url / replace_url

Update browser history without navigation:

from django_htmx.http import push_url

def leaf_select(request, leaf_id):
    ...
    response = render(request, "leaf-detail.html", {"leaf": leaf})
    return push_url(response, f"/leaf/{leaf.id}")

Use replace_url() to replace current history entry instead of pushing.

reswap / retarget / reselect

Override htmx behavior server-side:

from django_htmx.http import reswap, retarget, reselect

def conditional_swap(request):
    response = render(request, "row.html", {"row": row})
    if row.is_special:
        reswap(response, "afterbegin")  # Override hx-swap
        retarget(response, "#special-container")  # Override hx-target
    return reselect(response, ".data-row")  # Override CSS selector

trigger_client_event

Trigger custom JavaScript events after swap:

from django_htmx.http import trigger_client_event

def end_of_process(request):
    response = render(request, "done.html")
    return trigger_client_event(
        response,
        "showConfetti",
        {"colours": ["purple", "red", "pink"]},
        after="swap",  # "receive", "settle", or "swap"
    )

The event name (showConfetti) must match a handler registered via htmx.on().

Template Tags

querystring Filter (Django 4.1+)

Rebuild query strings for filter/pagination links:

{% load django_htmx %}

<!-- Keep all filters, flip `outdoors` off -->
<a hx-get="{% querystring outdoors=None %}" hx-target="#list"
   hx-push-url="true">hide outdoors</a>

<!-- Paginate -->
<a hx-get="{% querystring page=page.next %}" hx-target="#list">next</a>

json_script

Embed Python data for client-side use:

{{ row.config|json_script:"row-config" }}
<script>
  const cfg = JSON.parse(document.getElementById('row-config').textContent);
  htmx.trigger('#row', 'config-ready', cfg);
</script>

Best Practices

Partial Rendering with Template Partials

Use django-template-partials for efficient fragment rendering:

pip install django-template-partials
{% extends "_base.html" %}
{% load partials %}

{% block main %}
  {% partialdef country-table inline %}
    <table id="country-data">
      {% for country in countries %}
        <tr><td>{{ country.name }}</td></tr>
      {% endfor %}
    </table>
  {% endpartialdef %}
{% endblock main %}
def country_listing(request):
    template = "countries.html"
    if request.htmx:
        template += "#country-table"  # Render partial only
    return render(request, template, {"countries": Country.objects.all()})

Caching

Keep the cached template loader enabled (it's default, but easy to break):

TEMPLATES = [{
    "BACKEND": "django.template.backends.django.DjangoTemplates",
    "DIRS": [BASE_DIR / "templates"],
    "OPTIONS": {
        "loaders": [
            ("django.template.loaders.cached.Loader", [
                "django.template.loaders.app_directories.Loader",
                "django.template.loaders.filesystem.Loader",
            ]),
        ],
    },
}]

Without it, Django recompiles templates on every request, dominating CPU for htmx partials.

Diagnosis: If py-spy shows django.template.base.compile at the top, the cache is off.

Vary Headers

Tell caches that content differs by htmx request:

from django.views.decorators.cache import cache_control
from django.views.decorators.vary import vary_on_headers

@cache_control(max_age=300)
@vary_on_headers("HX-Request")
def my_view(request):
    ...

Anti-Patterns

1. Business Logic in Template Fragments

Wrong: A partial that needs data the parent page didn't load.

<!-- BAD: template tries to access data not in context -->
{% partialdef user-stats %}
  {{ user.profile.score }}  {# user not passed to partial #}
{% endpartialdef %}

Fix: Ensure the view serving the fragment has all required context.

2. Returning JSON to hx-get

Wrong: htmx expects HTML by default.

# BAD: htmx silently ignores JSON responses
def data_view(request):
    return JsonResponse({"data": items})

Symptom: No error, but nothing updates in the DOM.

Fix: Return HTML fragments, or use hx-trigger with JavaScript to handle JSON.

3. Missing hx-swap-oob for Out-of-Band Updates

Wrong: Updating a badge without telling htmx where to put it.

# BAD: client doesn't know to update badge
return render(request, "order-status.html", {"status": "shipped"})

Fix: Use hx-swap-oob to update multiple fragments:

<!-- In the fragment response -->
<span id="badge" hx-swap-oob="true">{{ new_count }}</span>

4. Expensive Polling with every="1s"

Wrong: Self-inflicted load generator.

<div hx-get="/status" hx-trigger="every 1s">...</div>

Fix: Use longer intervals, or WebSockets for real-time:

<div hx-get="/status" hx-trigger="every 5s">...</div>

Or load the ws extension for push-based updates.

5. Missing CSRF Token

Wrong: POST requests fail with 403.

<!-- BAD: no CSRF token -->
<body>
  <form hx-post="/submit">...</form>
</body>

Symptom: 403 errors that look like template bugs.

Fix: Include token in hx-headers on <body> (see Setup section) or per-form:

<form hx-post="/submit" hx-headers='{"x-csrftoken": "{{ csrf_token }}"}'>

6. Forgetting HX-Redirect for Client-Side Redirects

Wrong: Using Django's HttpResponseRedirect in htmx context.

# BAD: causes full page reload
return HttpResponseRedirect("/done/")

Fix: Use HttpResponseClientRedirect for htmx-aware redirects.

Testing

What You Can Test Server-Side

Fragment content:

from django.test import TestCase

class HtmxTests(TestCase):
    def test_fragment_content(self):
        response = self.client.get(
            "/partial/",
            HTTP_HX_REQUEST="true",  # Sets HX-Request header
        )
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, "Expected fragment content")

HX-Response headers:

def test_redirect_header(self):
    response = self.client.post("/sensitive-action/")
    self.assertEqual(response["HX-Redirect"], "/activate-sudo/")

Out-of-band swaps:

def test_oob_swap(self):
    response = self.client.post("/update-badge/")
    # Check the response contains hx-swap-oob attribute
    self.assertIn(b'hx-swap-oob="true"', response.content)

CSRF handling:

def test_csrf_required(self):
    response = self.client.post("/action/", HTTP_HX_REQUEST="true")
    self.assertEqual(response.status_code, 403)  # No CSRF token

    response = self.client.post(
        "/action/",
        HTTP_HX_REQUEST="true",
        HTTP_X_CSRFTOKEN=self.client.cookies["csrftoken"].value,
    )
    self.assertEqual(response.status_code, 200)

What You Cannot Test Server-Side

  • Actual DOM swaps: Django tests don't render HTML in the browser
  • hx-trigger timing: Polling intervals, debouncing
  • Client-side event handlers: htmx.on() handlers triggered by HX-Trigger
  • hx-swap behavior: How content is actually inserted (use Playwright/Cypress for this)

For client-side behavior: Use end-to-end tests with Playwright or Cypress.

Type Checking

Extend HttpRequest for type hints:

from django.http import HttpRequest as HttpRequestBase
from django_htmx.middleware import HtmxDetails

class HttpRequest(HttpRequestBase):
    htmx: HtmxDetails

Deep Dives

Load these reference files for detailed guidance:

  • references/csp-nonce.md - Content-Security-Policy nonce integration (when strict CSP breaks htmx)
  • references/testing-patterns.md - Extended testing strategies and e2e setup

References

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

aiohttp

無料

Use when building Python async HTTP services or clients with aiohttp - web server routing, middleware, WebSocket, SSE, streaming, client sessions, pytest-aiohttp testing, or troubleshooting SSL and timeout issues

日本語の概要は準備中です。原文の説明を表示しています。

CodeAtCode/oss-ai-skills222026年10月9日 更新

ast-grep

無料

Use when doing structural code search and rewriting - ast-grep linting, refactoring, multi-language patterns

日本語の概要は準備中です。原文の説明を表示しています。

CodeAtCode/oss-ai-skills222026年10月9日 更新

Use when building GBA games with the BPCore Lua engine - entity, sprite and tilemap functions, SRAM save and load, link cable multiplayer protocol, camera and scrolling, or optimization patterns

日本語の概要は準備中です。原文の説明を表示しています。

CodeAtCode/oss-ai-skills222026年10月9日 更新

celery

無料

Use when running background tasks with Celery - worker and broker configuration (Redis, RabbitMQ), task routing by name vs queue, chains/groups/chords, retry patterns (autoretry_for, retry_backoff), acks_late semantics, failure detection, and monitoring with Flower

日本語の概要は準備中です。原文の説明を表示しています。

CodeAtCode/oss-ai-skills222026年10月9日 更新

django

無料

Use when building Django applications - security hardening, authentication and permissions, ORM optimization, PostgreSQL features, Django 6.0, migrations, testing, and ecosystem libraries

日本語の概要は準備中です。原文の説明を表示しています。

CodeAtCode/oss-ai-skills222026年10月9日 更新

Use when customizing Django Admin - save_formset, get_search_results, formsets, queryset optimization, db_index, custom URLs

日本語の概要は準備中です。原文の説明を表示しています。

CodeAtCode/oss-ai-skills222026年10月9日 更新

CodeAtCode のスキルをすべて見る

このスキルの問題を報告する