htmx Tutorial · Going Further · Lesson 26 of 30

The Server Side: Templates and Partials

Organise server templates for htmx: return fragments to htmx and full pages to normal visits, with examples in Flask, Django, Express and Laravel.

Create a free account to save this page, take notes and track your progress. Sign in

One URL, two answers

The same URL often needs to return:

  • a full page for a normal visit (typing the URL, a reload, a search engine), and
  • just the fragment that changes, for an htmx request.

Check the HX-Request header to decide. Then tell caches that the response depends on it with Vary: HX-Request, or a cache could serve a fragment to a normal visitor.

from flask import Flask, render_template, request

app = Flask(__name__)

@app.get("/contacts")
def contacts():
    q = request.args.get("q", "")
    rows = search_contacts(q)                      # your database query
    if request.headers.get("HX-Request") and not request.headers.get("HX-History-Restore-Request"):
        html = render_template("contacts/_rows.html", rows=rows)    # just the <tr>s
    else:
        html = render_template("contacts/index.html", rows=rows, q=q)  # whole page, includes _rows.html
    return html, {"Vary": "HX-Request"}

Why check HX-History-Restore-Request too? When htmx can't find a page in its history cache, it re-requests the URL to restore it, and in htmx 2 that request also carries HX-Request: true. It needs the full page.

Template partials

Keep the fragment in its own template file and include it from the full page. The fragment is written once, and both answers stay identical:

<!-- contacts/index.html -->
{% extends "base.html" %}
{% block content %}
  <input type="search" name="q" value="{{ q }}" hx-get="/contacts" hx-trigger="input changed delay:300ms"
         hx-target="#rows" hx-push-url="true">
  <table><tbody id="rows">{% include "contacts/_rows.html" %}</tbody></table>
{% endblock %}

Libraries such as django-htmx, htmx-flask and Laravel htmx packages add small helpers (request.htmx) for these checks.

Return rows to htmx, a page to everyone elseTry it yourself
<input type="search" name="q" placeholder="Filter contacts" hx-get="/contacts"
       hx-trigger="input changed delay:300ms" hx-target="#rows">
<table><tbody id="rows"><tr><td>Ada</td></tr><tr><td>Alan</td></tr><tr><td>Grace</td></tr></tbody></table>

<!-- The pretend server: on a real site this part is your backend (Python, PHP, Node, Go...) -->
<script>
  const names = ["Ada", "Alan", "Grace"];
  const rows = (q) => names.filter((n) => n.toLowerCase().includes(q.toLowerCase()))
                           .map((n) => `<tr><td>${n}</td></tr>`).join("");
  server.get("/contacts", (req) => {
    const q = req.query.q || "";
    const body = req.headers["hx-request"]
      ? rows(q)                                                     // htmx: just the rows
      : `<html><body><input name="q"><table><tbody id="rows">${rows(q)}</tbody></table></body></html>`;
    return { body, headers: { Vary: "HX-Request" } };
  });
</script>

Test yourself

Why send Vary: HX-Request when a URL returns either a fragment or a full page?

  1. So caches store the two versions separately
  2. htmx refuses responses without it
  3. It makes the response smaller
  4. Browsers require it for AJAX
Show answer

Answer: So caches store the two versions separately

Without Vary, a browser or CDN cache could hand a fragment to a normal visitor.

Was this page helpful?