{"id":1471,"date":"2026-11-12T11:49:50","date_gmt":"2026-11-12T11:49:50","guid":{"rendered":"https:\/\/www.taywa.ch\/blog\/?p=1471"},"modified":"2026-11-12T11:49:50","modified_gmt":"2026-11-12T11:49:50","slug":"keep-your-head","status":"publish","type":"post","link":"https:\/\/www.taywa.ch\/blog\/allgemein\/keep-your-head\/","title":{"rendered":"Keep your head"},"content":{"rendered":"<p>Wenn man ein Django\/Wagtail Webprojekt startet, steht man vor der Entscheidung, welche Art von Frontend-Stil man im Projekt verwenden m\u00f6chte.<\/p>\n<p>Es gibt den traditionellen Weg, der in Django standardm\u00e4ssig verwendet wird: Das Backend rendert die gesamte Seite und liefert sie im Request\/Response-Zyklus an den Client aus. Dies ist der einfachste und geradlinigste Weg, eine Website zu erstellen.<\/p>\n<p>Oder es gibt den \u201eheadless\u201c Weg mit JavaScript Focus beim Datenaustausch, bei dem man den API-Weg geht und JSON statt HTML zwischen Server und Client austauscht. Wagtail unterst\u00fctzt dies auch.<\/p>\n<p>Wir haben Projekte auch mit diesem Ansatz gebaut. In der Regel mit:<\/p>\n<ul>\n<li>Nuxt frontend framework (VueJS)<\/li>\n<li>GraphQL als API-Layer zwischen Client und Server \/ oder REST<\/li>\n<\/ul>\n<p>Die Vorteile des API\/JavaScript-Ansatzes sind:<\/p>\n<ul>\n<li>kein Neuladen der gesamten Seite, schnellere Seiten\u00fcberg\u00e4nge, nur json payloads<\/li>\n<li>in der Regel komponentenbasierte Entwicklung, ideal f\u00fcr gr\u00f6ssere Projekte und Teams<\/li>\n<li>gut f\u00fcr komplexe Interaktionen<\/li>\n<\/ul>\n<p>Aber es gibt auch Nachteile:<\/p>\n<ul>\n<li>im Fall von Vue.js braucht man f\u00fcr SEO zus\u00e4tzlich Nuxt, damit es f\u00fcr bots\/SEO eine HTML-Repr\u00e4sentation der Seite gibt<\/li>\n<li>man hat zwei Kontexte: Client und Server (SSR), dies macht die Dinge komplizierter<\/li>\n<li>das Routing der Seiten f\u00e4llt nun auch in die eigene Verantwortlichkeit, was zus\u00e4tzlichen Code erfordert<\/li>\n<li>zus\u00e4tzliche grosse Frontend-Stacks mit all den Node.js-Bibliotheken erh\u00f6hen die Komplexit\u00e4t<\/li>\n<\/ul>\n<p><span style=\"font-weight: 400;\">Je nach Anforderungen des Projekts, Budget und Teamgr\u00f6sse, macht das eine oder andere Sinn.<\/span><\/p>\n<h2><span style=\"font-weight: 400;\">Mittelweg: HTML over the wire<br \/>\n<\/span><\/h2>\n<p>Es gibt auch etwas dazwischen, bei dem man viele Vorteile des API\/JavaScript-Weges erh\u00e4lt, aber ohne dessen Nachteile. Insbesondere f\u00fcr nicht zu komplexe Projekte.<\/p>\n<p>Mit z.b. htmx k\u00f6nnen wir nur Teile der Seite laden, anstatt die gesamte Seite. Das bringt die meisten Vorteile der Benutzererfahrung des API\/JavaScript-Ansatzes. Aber anstatt JSON zwischen Client und Server zu senden, senden wir HTML-Snippets (HTML over the wire). Das ist die \u201ealte\u201c AJAX-Art, Dinge zu tun, die um 2005 herum popul\u00e4r war.<\/p>\n<p>Weil wir HTML senden, k\u00f6nnen wir alle Werkzeuge nutzen, die Django und Wagtail von Haus aus anbieten, dies f\u00fchrt zu weniger Aufwand.<\/p>\n<p>Es ist so nicht n\u00f6tig, Routing und SSR f\u00fcr SEO selbst zu erstellen; alles ist bereits im Django\/Wagtail HTML-Stack enthalten.<\/p>\n<h2>htmx Minitutorial<\/h2>\n<p>Es gibt eine Django-App f\u00fcr die Serverseite von htmx: `django-htmx`.<\/p>\n<p>Installiere die Python-Bibliothek mit:<\/p>\n<pre class=\"brush: python; title: ; notranslate\" title=\"\">python -m pip install django-htmx<\/pre>\n<p>F\u00fcge sie zu deinen Apps in settings.py hinzu:<\/p>\n<pre class=\"brush: python; title: ; notranslate\" title=\"\">INSTALLED_APPS = &#x5B;\r\n    ...,\r\n    \"django_htmx\",\r\n    ...,\r\n]\r\n\r\nMIDDLEWARE = &#x5B;\r\n    ...,\r\n    \"django_htmx.middleware.HtmxMiddleware\",\r\n    ...,\r\n]\r\n<\/pre>\n<h3>Templates &amp; Views<\/h3>\n<h4>Templates<\/h4>\n<p>F\u00fcge Folgendes in den head-Bereich deines base.html-Templates ein:<\/p>\n<pre class=\"brush: jscript; title: ; notranslate\" title=\"\">&lt;script src=\"{% static 'js\/htmx.min.js' %}\" defer&gt;&lt;\/script&gt;\r\n{% django_htmx_script %}<\/pre>\n<p>Um htmx zu aktivieren, musst du nur Folgendes zu deinem body-Tag hinzuf\u00fcgen:<\/p>\n<pre class=\"brush: jscript; title: ; notranslate\" title=\"\">&lt;body hx-boost=\"true\" hx-target=\"#main\" hx-swap=\"show:window:top\"&gt;<\/pre>\n<ul>\n<li>hx-boost: aktiviert das Laden aller &lt;a&gt;-Links und &lt;form&gt;-Formulare \u00fcber AJAX<\/li>\n<li>hx-target: #main, dein Inhaltsziel innerhalb deines HTMLs, passe es an dein benutzerdefiniertes HTML an<\/li>\n<li>hx-swap: show:window:top, nach dem HTML-Austausch nach oben scrolle<\/li>\n<\/ul>\n<p>Du musst deine Templates in zwei Teile aufteilen: eines f\u00fcr die vollst\u00e4ndige Seite, das base.html erweitert, und das andere, das nur das Inhaltsfragment enth\u00e4lt.<\/p>\n<p>Eine Beispielimplementierung:<\/p>\n<pre class=\"brush: python; title: ; notranslate\" title=\"\"># product_detail.html\r\n{% extends \"base.html\" %}\r\n{% block content %}\r\n    {% include 'myproject\/product_detail_fragment.html' %}\r\n{% endblock content %}\r\n\r\n# myproject\/product_detail_fragment.html\r\n\u2026 dein gewohnter Django\/Wagtail-Template-Code\r\n<\/pre>\n<h4>Views<\/h4>\n<p>Du musst lediglich den Aufruf des Templates anpassen, um das vollst\u00e4ndige Template oder das Fragment-Template zu erhalten.<\/p>\n<pre class=\"brush: python; title: ; notranslate\" title=\"\">class ProductView(DetailView):\r\n    model = Product\r\n    \u2026\r\n    def get_template_names(self):\r\n        if self.request.htmx:\r\n            return \"libucore\/product_detail_fragment.html\"\r\n        return \"libucore\/product_detail.html\"<\/pre>\n<h4>JavaScript<\/h4>\n<p>Auf der Frontend-Seite kannst du reines JavaScript oder auch ein leichtgewichtiges Framework verwenden (z. B. <a href=\"https:\/\/alpinejs.dev\">https:\/\/alpinejs.dev<\/a>, <a href=\"https:\/\/stimulus.hotwired.dev\">https:\/\/stimulus.hotwired.dev<\/a>).<\/p>\n<p>Die Beispiele hier verwenden reines JavaScript. Wenn nach dem Laden von Inhalten etwas passieren soll, kannst du einen Event Listener verwenden:<\/p>\n<pre class=\"brush: jscript; title: ; notranslate\" title=\"\">document.body.addEventListener('htmx:load', function (e) {\r\n\/\/ dein Code\r\n});<\/pre>\n<p>htmx k\u00fcmmert sich um die URL in der Adressleiste. Wenn du nur einen Teil der Seite erstellst und die Navigation beibeh\u00e4ltst, musst du dich selbst um den Navigationsstatus k\u00fcmmern. Wenn du Statistiken zur Seitennutzung m\u00f6chtest, musst du Pageviews ebenfalls selbst ausl\u00f6sen. Beides wird mit dem <code>htmx:pushedIntoHistory<\/code> Event erreicht (weil hier die aktuelle URL \/ Position verf\u00fcgbar ist).<\/p>\n<pre class=\"brush: python; title: ; notranslate\" title=\"\">document.body.addEventListener(\r\n    'htmx:pushedIntoHistory', function(e) {\r\n\r\n        \/\/ Navigation aktualisieren\r\n        const segments = e.detail.path.split('\/')\r\n        segments.forEach(part =&gt; {\r\n            document.querySelectorAll('#mainnav &#x5B;data-slug=\"' + part + '\"]').forEach(el =&gt; el.classList.add('active'));\r\n        });\r\n\r\n        \/\/ add your analytics pageview event here\r\n    }\r\n); \r\n<\/pre>\n<p>F\u00fcr Seiten\u00fcbergangsanimationen kannst du die Events <code>htmx:beforeRequest<\/code> und <code>htmx:afterSettle<\/code> verwenden.<\/p>\n<p>Eine m\u00f6gliche Falle ist: Wenn du eine Seite mit vielen schweren Inhalten (Video\/Audio\/Iframe) hast, l\u00e4dt\/preloaded der Browser diese standardm\u00e4ssig. Wenn auf einer \u201enormalen\u201c Seite ein Link angeklickt wird, stoppt der Browser das Laden\/Preloaden dieser Ressourcen und beginnt mit dem Laden der neuen Seite. Nicht so bei htmx. Die XMLHttpRequest wird in eine Warteschlange gestellt und wartet darauf, dass der Rest geladen wird. Es gibt verschiedene Ans\u00e4tze, dies zu l\u00f6sen. In unserem Fall hatten wir ein Problem mit dem Audio-Tag, daher haben wir dem Tag einfach das Attribut preload=&#8220;none&#8220; hinzugef\u00fcgt.<\/p>\n<h4>Dokumente &amp; Dateien<\/h4>\n<p>Ein Problem, auf das du stossen wirst, ist, dass verkn\u00fcpfte Dokumente und Dateien ebenfalls als XMLHttpRequest-Anfragen geladen werden. Dies f\u00fchrt dazu, dass Bin\u00e4rdaten auf deiner Website angezeigt werden.<br \/>\nUm das zu beheben, musst du eine Umleitung hinzuf\u00fcgen, um htmx-boost f\u00fcr Dokumente zu deaktivieren.<\/p>\n<p>Eine beispielhafte Nginx-Konfiguration daf\u00fcr:<\/p>\n<pre class=\"brush: python; title: ; notranslate\" title=\"\">location \/media\/ {\r\n    alias \/\/media\/;\r\n    location ~* ^\/media\/documents\/.*$ {\r\n        add_header HX-Redirect $request_uri;\r\n    }\r\n}\r\n<\/pre>\n<p>Dies behebt den Download \/ die Anzeige im Browser von Dokumenten und Dateien (PDF, Excel, etc\u2026).<\/p>\n<h2>Fazit<\/h2>\n<p>htmx bietet einen guten Mittelweg an Komplexit\u00e4t, der sich gut f\u00fcr Inhaltsseiten mit einfacher JavaScript-Interaktion eignet.<\/p>\n<p>F\u00fcr ein Live-Beispiel einer Wagtail + htmx Website haben wir z.B. <a href=\"https:\/\/www.buchbasel.ch\">www.buchbasel.ch<\/a> mit diesem Ansatz erstellt.<\/p>\n<h2>Links<\/h2>\n<ul>\n<li><a href=\"https:\/\/htmx.org\">htmx.org<\/a><\/li>\n<li><a href=\"https:\/\/django-htmx.readthedocs.io\">django-htmx.readthedocs.io\/<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Wenn man ein Django\/Wagtail Webprojekt startet, steht man vor der Entscheidung, welche Art von Frontend-Stil man im Projekt verwenden m\u00f6chte. Es gibt den traditionellen Weg, der in Django standardm\u00e4ssig verwendet wird: Das Backend rendert die gesamte Seite und liefert sie im Request\/Response-Zyklus an den Client aus. Dies ist der einfachste und geradlinigste Weg, eine Website<\/p>\n","protected":false},"author":4,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-1471","post","type-post","status-publish","format-standard","hentry","category-allgemein"],"_links":{"self":[{"href":"https:\/\/www.taywa.ch\/blog\/wp-json\/wp\/v2\/posts\/1471","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.taywa.ch\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.taywa.ch\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.taywa.ch\/blog\/wp-json\/wp\/v2\/users\/4"}],"replies":[{"embeddable":true,"href":"https:\/\/www.taywa.ch\/blog\/wp-json\/wp\/v2\/comments?post=1471"}],"version-history":[{"count":15,"href":"https:\/\/www.taywa.ch\/blog\/wp-json\/wp\/v2\/posts\/1471\/revisions"}],"predecessor-version":[{"id":1486,"href":"https:\/\/www.taywa.ch\/blog\/wp-json\/wp\/v2\/posts\/1471\/revisions\/1486"}],"wp:attachment":[{"href":"https:\/\/www.taywa.ch\/blog\/wp-json\/wp\/v2\/media?parent=1471"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.taywa.ch\/blog\/wp-json\/wp\/v2\/categories?post=1471"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.taywa.ch\/blog\/wp-json\/wp\/v2\/tags?post=1471"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}