1 Punkte von GN⁺ 4 시간 전 | 1 Kommentare | Auf WhatsApp teilen
  • Beibehaltung des auf Go html/template basierenden serverseitigen Renderings, Ergänzung dynamischer Interaktionen mit HTMX und selektive Rückgabe ganzer Seiten oder HTML-Fragmente über einen einzigen Renderer
  • Aufteilung des Markups in base.tmpl, Seiten-Templates und wiederverwendbare Fragmente; Nutzung von Go 1.16 embed.FS und einer gemeinsamen Template-Menge, um Deployment- und Rendering-Struktur zu vereinfachen
  • Rückgabe von Suchergebnissen je nach HX-Request als ganze Seite oder Tabellenzeilen; Vermeidung von Cache- und Zurück-Navigation-Wiederherstellungsfehlern mit Vary: HX-Request und historyRestoreAsHxRequest: false
  • Beim Weiterleiten von HTMX-Requests auf eine andere Seite statt regulärer 3xx-Antworten HX-Redirect mit einer 2xx-Antwort verwenden; Steuerung von Austauschbereich und Fehleranzeige über responseHandling für 204, 422, 4xx und 5xx
  • Eine Konfiguration, die Local-Storage-Cache und Attributvererbung deaktiviert sowie Request-Timeouts setzt, als Ausgangspunkt verwenden; bei wachsendem Umfang können Zwischen-Layout-Templates ergänzt werden, um auch separate Bildschirmstrukturen wie Admin-Bereiche zu unterstützen

Warum Go-Server-Rendering mit HTMX kombinieren?

  • HTMX ermöglicht dynamische Interaktionen mit wenig JavaScript und bewahrt zugleich die Konsistenz und Sicherheit des serverseitigen HTML-Renderings mit Go html/template
  • Die Implementierung behandelt drei Achsen
    • Eine Template-Struktur, die sowohl ganze Seiten als auch partielle HTML-Antworten unterstützt
    • Redirects und Fehlerbehandlung in einer HTMX-Umgebung
    • Konfiguration, die HTMX-Defaults ändert, und die Gründe dafür
  • Ausgehend von der Grundfunktion, einen Button durch ein Bild zu ersetzen, wird dies zu einer Suchansicht erweitert, die die Liste während der Eingabe von Benutzernamen oder E-Mail filtert
  • Dasselbe Template-Muster lässt sich nicht nur auf HTMX anwenden, sondern auch auf HTML-over-the-wire-Tools wie Unpoly und Hotwire
  • Wer die Grundlagen von HTMX nicht kennt, sollte sich zuerst die offizielle Dokumentation ansehen

Projektverzeichnis und Aufbau statischer Dateien

  • Das Projekt ist nach Rollen in folgende Bereiche unterteilt
    • assets/html/base.tmpl: HTML-Grundgerüst, das alle Seiten gemeinsam nutzen
    • assets/html/pages/: Inhalte einzelner Seiten
    • assets/html/partials/: HTML-Fragmente, die an mehreren Stellen wiederverwendet werden
    • assets/static/css, assets/static/img, assets/static/js: statische Assets
    • cmd/web/: Serverstart, Handler und HTML-Renderer-Code
  • Das Grundgerüst wird mit folgenden Befehlen erstellt
go mod init example.com/htmx
mkdir -p assets/static/css assets/static/img assets/static/js assets/html/partials assets/html/pages cmd/web
touch assets/efs.go assets/html/base.tmpl assets/html/partials/images.tmpl assets/html/pages/home.tmpl cmd/web/main.go cmd/web/handlers.go cmd/web/html.go
  • HTMX kann per CDN oder NPM installiert werden, meist wird jedoch eine Kopie heruntergeladen und direkt als statische Datei der Anwendung ausgeliefert
    • Die Konfiguration ist einfach und Nachteile der CDN-Nutzung lassen sich vermeiden
    • Verwendet werden htmx.org@2.0.10 und das klassenlose CSS-Framework bamboo.css@1.4.0
wget -P assets/static/js https://cdn.jsdelivr.net/npm/htmx.org@2.0.10/dist/htmx.min.js
wget -P assets/static/css https://cdn.jsdelivr.net/npm/bamboo.css@1.4.0/dist/bamboo.min.css

Trennung von Basis-, Seiten- und Partial-Templates

  • base.tmpl definiert das gesamte Dokumentgerüst wie <head>, gemeinsamen Titel und <main> und fügt page:title sowie page:content ein
  • Bamboo CSS und das HTMX-Skript werden im Basis-Template geladen; beim HTMX-Skript wird das Attribut defer gesetzt
    • Der Browser lädt das Skript parallel, während er das HTML parst
    • Nach Abschluss von HTML-Parsing und DOM-Aufbau wird das Skript ausgeführt
  • Alle Templates erhalten unabhängig vom Dateinamen mit {{define}}...{{end}} explizite Namen
    • Go-Code kann Templates konsistent nur über die definierten Namen referenzieren
    • Der : in page:title ist ein beliebiges Trennzeichen; auch Namen wie page_title, page-title, pageTitle oder title sind möglich
  • Für den Button auf der Startseite werden zwei Verhaltensweisen festgelegt
    • hx-get="/gopher": sendet beim Klick einen GET /gopher-Request
    • hx-swap="outerHTML": ersetzt den Button selbst durch das Antwort-HTML
  • partial:image:gopher ist ein wiederverwendbares Bildfragment; über width="{{.}}" wird beim Aufruf die Bildbreite übergeben
  • HTML-Fragmente, die nur auf einer bestimmten Seite verwendet werden, können in der jeweiligen Seitendatei liegen, statt im gemeinsamen Verzeichnis partials
    • Fragmente, die auf mehreren Seiten genutzt werden, liegen im gemeinsamen Verzeichnis; seitenspezifische Fragmente werden neben dem zugehörigen Inhalt platziert

HTML und statische Assets in das Go-Binary einbetten

  • Seit der Einführung von File-Embedding in Go 1.16 lassen sich HTML und statische Assets in das Go-Binary einbetten, statt sie zur Laufzeit von der Festplatte zu lesen
  • Mit der Direktive //go:embed "html" "static" werden die beiden Verzeichnisse in ein embed.FS aufgenommen und mit fs.Sub() in zwei Unter-Dateisysteme getrennt
    • HTMLFiles: Template-Dateisystem mit html als Root
    • StaticFiles: statisches Dateisystem mit static als Root
  • Die Trennung hat zwei Vorteile
    • HTML-Code und Code für statische Dateien greifen nicht unnötig auf die Dateien des jeweils anderen Bereichs zu
    • Beim Öffnen von Dateien muss kein Präfix html/ oder static/ angehängt werden
  • Wer in sub() kein panic() verwenden möchte, kann auch Fehler zurückgeben und die beiden Variablen in main() initialisieren
  • Damit fs.Sub() in der aktuellen Implementierung fehlschlägt, müsste der Verzeichniswert kein gültiger Pfad sein; die festen Strings "html" und "static" bestehen diese Prüfung, daher ist das Risiko einer Runtime-Panic sehr gering

Gemeinsame Template-Menge und htmlRenderer

  • htmlRenderer verwaltet den Rendering-Ablauf wie folgt
    • Beim Start wird einmal eine gemeinsame Template-Menge geparst
    • Bei jedem Rendering wird die gemeinsame Menge geklont
    • Die für den Request benötigten Seiten-Templates werden zusätzlich geparst
    • Das Template mit dem angegebenen Namen wird ausgeführt und als HTTP-Antwort gesendet
  • newHTMLRenderer() registriert eine template.FuncMap und parst anschließend die gemeinsamen Templates
    • now wird mit time.Now verbunden
    • Weitere benutzerdefinierte Template-Funktionen können an derselben Stelle hinzugefügt werden
  • render() erstellt mit sharedTemplates.Clone() eine request-spezifische Template-Menge und erweitert sie bei vorhandenen additionalTemplateFiles mit ParseFS()
  • Templates werden zunächst in einen bytes.Buffer ausgeführt; anschließend wird der Statuscode geschrieben und der Antwort-Body gesendet
  • Bei der Serverinitialisierung werden "base.tmpl" und "partials/*.tmpl" als gemeinsame Menge geparst
    • Das Basis-Template und alle gemeinsamen Fragmente stehen dem Renderer immer zur Verfügung
    • Nur seitenspezifische Templates werden beim Handler-Aufruf ergänzt
  • Statische Dateien werden über http.FileServerFS(assets.StaticFiles) und den Pfad /static/ bereitgestellt; der Server läuft auf :5051

Ganze Seiten und HTML-Fragmente rendern

  • Der Startseiten-Handler gibt mit folgendem Aufruf das gesamte Dokument zurück
app.html.render(w, 200, nil, "base", "pages/home.tmpl")
  • Dieser Aufruf fügt der gemeinsamen Menge pages/home.tmpl hinzu und führt das Template base mit 200 OK aus
  • Der Handler /gopher parst keine zusätzlichen Dateien, sondern führt nur das bereits in der gemeinsamen Menge enthaltene partial:image:gopher aus
width := 100
app.html.render(w, http.StatusOK, width, "partial:image:gopher")
  • HTMX ersetzt das von /gopher erhaltene Bild gemäß der Einstellung hx-swap="outerHTML" durch den Button
  • Diese Struktur bietet folgende Vorteile
    • Templates und statische Assets sind in der Binary enthalten, was die Bereitstellung vereinfacht
    • Mit demselben render() können sowohl vollständige HTML-Seiten als auch bestimmte HTML-Fragmente zurückgegeben werden
    • Basis-Template und Partial-Templates reduzieren doppelte Markup-Strukturen
    • Partial-Templates können im Basis-Template, im Seiteninhalt und in anderen Partial-Templates wiederverwendet werden

Benutzersuche mit Aktualisierung während der Eingabe

  • Die Suchfunktion ist in zwei Routen aufgeteilt
    • GET /users: eine vollständige HTML-Seite mit allen Benutzerinformationen
    • GET /users/search: ein HTML-Fragment, das nur die Tabellenzeilen der Benutzer zurückgibt, deren Name oder E-Mail zur Suchanfrage passt
  • Das Sucheingabefeld kombiniert folgende HTMX-Attribute
    • Mit hx-get="/users/search" wird die Suchanfrage gesendet
    • Mit hx-trigger="input changed delay:500ms, keyup[key=='Enter']" wird die Anfrage ausgelöst, wenn nach einer Eingabeänderung 500 ms vergangen sind oder Enter gedrückt wird
    • Der Suchbegriff wird als Query-String in der Form GET /users/search?query=foo übergeben
    • Mit hx-target="#search-results" wird die Antwort in <tbody id="search-results"> eingefügt
    • Mit hx-push-url="true" wird bei jeder Anfrage die Adressleiste aktualisiert und ein Eintrag zur Browser-History hinzugefügt
  • users:rows ist ein separates Template, das nur Benutzerzeilen rendert
    • Jede Zeile zeigt Name und E-Mail an
    • Wenn IsGopher wahr ist, wird partial:image:gopher mit Breite 24 gerendert
  • listUsers und searchUsers fügen beide pages/users.tmpl zur gemeinsamen Menge hinzu, führen aber unterschiedliche Templates aus
    • listUsers: führt base aus und gibt die vollständige Seite zurück
    • searchUsers: führt users:rows aus und gibt nur die passenden Zeilen zurück
  • Ist der Suchbegriff leer, wird die vollständige Benutzerliste verwendet; ist ein Wert vorhanden, werden Name und E-Mail mit strings.Contains() geprüft
  • In einer echten Anwendung könnten folgende Verbesserungen erwogen werden
    • Listen- und Such-Handler zusammenführen
    • Fehlerbehandlung und Logging-Helfer zentralisieren
    • Per Middleware Content-Security-Policy-Header und Panic-Recovery hinzufügen
    • Server-Timeouts angemessen konfigurieren

Vollständige Seiten und Teilantworten mit HX-Request unterscheiden

  • Wenn ein Nutzer /users/search?query=leo direkt im Browser öffnet oder über einen geteilten Link besucht, ist möglicherweise nur das Teil-HTML für die Tabellenzeilen sichtbar
  • HTMX-Anfragen enthalten immer den Header HX-Request: true, sodass die Antwortform anhand dessen bestimmt wird
func isHTMXRequest(r *http.Request) bool {
    return r.Header.Get("HX-Request") == "true"
}
  • Das Standard-Ausführungstemplate des Such-Handlers wird auf base gesetzt und nur bei HTMX-Anfragen auf users:rows geändert
    • Bei einem direkten Besuch erhält man eine vollständige Seite mit den Suchergebnissen
    • Bei einer HTMX-Anfrage wird nur das Zeilenfragment zurückgegeben, das in <tbody> eingefügt wird

Vary-Header und Wiederherstellung beim Zurückgehen

  • Da dieselbe URL je nach HX-Request-Wert unterschiedliche Antworten zurückgibt, muss auch der Cache über diesen Unterschied informiert werden
  • Allen Rendering-Antworten wird Vary: HX-Request hinzugefügt
    • Streng genommen ist es weniger verschwenderisch, ihn nur in einzelnen Handlern zu setzen, wenn er nötig ist
    • Wird er pauschal im Renderer gesetzt, lassen sich Bugs durch Vergessen vermeiden
  • Wenn hx-push-url oder hx-boost Browser-History-Einträge hinzufügen, cached HTMX das fertige Seiten-HTML im lokalen Speicher des Browsers
    • Der Standard-Cache umfasst maximal 10 Seiten
    • Wenn man zu einem Punkt zurücknavigiert, der nicht im Cache ist, fordert HTMX die entsprechende URL erneut vom Server an
  • Die standardmäßige Wiederherstellungsanfrage enthält HX-Request: true, sodass der Server statt einer vollständigen Seite Teil-HTML zurückgeben kann
  • Wenn die Antwortart über HX-Request ausgewählt wird, sollte historyRestoreAsHxRequest: false gesetzt werden
    • Dadurch wird HX-Request: true bei Wiederherstellungsanfragen nach einem Cache-Miss weggelassen
    • Der Server gibt für diese Anfrage dann eine vollständige HTML-Seite zurück
  • Die HTMX-Konfiguration wird im Meta-Tag htmx-config in base.tmpl platziert

HTMX-Anfragen auf eine andere Seite umleiten

  • HTMX kann HTML-Fragmente wie Erfolgsmeldungen sofort ersetzen und reduziert dadurch Fälle, in denen ein klassischer Post/Redirect/Get-Ablauf nötig ist
  • Wenn man nach einem erfolgreichen Login wie etwa zum Profil auf eine komplett andere Seite wechseln muss, lässt sich das nicht allein mit einer normalen 3xx-Antwort behandeln
    • Der Browser folgt der 3xx-Antwort, bevor HTMX sie verarbeitet
    • HTMX sieht nur die finale Antwort nach der Weiterleitung und ersetzt deren Inhalt im bisherigen Ziel
  • Für HTMX-Anfragen verwendet man eine 2xx-Antwort zusammen mit dem Header HX-Redirect
    • HX-Redirect: /foo/bar navigiert den Browser zu dieser URL und lädt die gesamte Seite neu
    • Die Anfrage an das Ziel enthält kein HX-Request: true
  • Wenn auch Umgebungen unterstützt werden sollen, in denen JavaScript deaktiviert ist oder HTMX nicht geladen wurde, sollten Nicht-HTMX-Anfragen weiterhin eine normale 3xx-Antwort erhalten
func redirect(w http.ResponseWriter, r *http.Request, url string, code int) {
    if isHTMXRequest(r) {
        w.Header().Set("HX-Redirect", url)
        w.WriteHeader(http.StatusNoContent)
        return
    }

    http.Redirect(w, r, url, code)
}
  • Für eine Weiterleitung nach /profile nach dem Login kann es so aufgerufen werden
redirect(w, r, "/profile", http.StatusSeeOther)

Unterschied zwischen HX-Redirect und HX-Location

  • HX-Location führt ein redirect-ähnliches Verhalten aus, ohne die ganze Seite neu zu laden
    • Es holt das HTML der Ziel-URL
    • Es ersetzt die Antwort in <body>
    • Es fügt der Browser-History einen neuen Eintrag hinzu
  • Bei der Anfrage an die Ziel-URL wird HX-Request: true mitgesendet
  • Wenn die Zielroute je nach HX-Request Teil-HTML zurückgibt, kann das zu Problemen führen
    • Der Handler kann eine von HX-Location kommende Anfrage nicht von einer normalen HTMX-Anfrage unterscheiden
    • Für erstere wird eine vollständige Seite benötigt, für letztere eine Teilantwort
  • Anders als bei der History-Wiederherstellung gibt es bei HX-Location keine Einstellung, um HX-Request: true abzuschalten
  • In den meisten Fällen ist es einfacher und sicherer, das vollständige Neuladen der Seite in Kauf zu nehmen und HX-Redirect zu verwenden
  • Wenn die Zielroute so konfiguriert ist, dass sie immer vollständiges HTML zurückgibt, kann auch HX-Location verwendet werden

Verarbeitung von Antworten mit 204, 422, 4xx und 5xx

  • HTMX ersetzt 4xx- oder 5xx-Antworten standardmäßig nicht im DOM
    • Der aktuelle Bildschirm bleibt unverändert
    • Der Fehler wird in der Konsole der Entwicklertools protokolliert
  • Selbst wenn der Server wegen eines nicht vorhandenen Template-Namens 500 Internal Server Error zurückgibt, sieht der Nutzer den Fehler bei der Standardeinstellung nicht auf dem Bildschirm
  • Um Fehlermeldungen dem Nutzer anzuzeigen, werden 4xx- und 5xx-Antworten so konfiguriert, dass sie den gesamten <body> ersetzen
  • Die Ausnahme ist 422 Unprocessable Content
    • Dieser Status wird verwendet, wenn ein Formular mit Validierungsfehlern zurückgegeben wird
    • Die Antwort sollte normal im bestehenden HTMX-Ziel ersetzt werden
  • responseHandling wird je nach Statuscode wie folgt konfiguriert
    • 204 No Content: ändert das DOM nicht
    • 422 Unprocessable Content: normal im bestehenden Ziel ersetzen
    • alle übrigen 4xx und 5xx: Antwort in <body> ersetzen
    • sonstige Antworten: normal im bestehenden Ziel ersetzen
"responseHandling":[
    {"code":"204", "swap": false},
    {"code":"422", "swap": true},
    {"code":"[45]..", "swap": true, "target": "body"},
    {"code":"...", "swap": true}
]
  • Wenn bei einer bestimmten Interaktion Fehler in ein anderes Ziel als <body> eingefügt werden sollen, kann die Standardeinstellung mit der Response-Target-Erweiterung überschrieben werden

Aktuelle URL in der Adressleiste des Browsers prüfen

  • HTMX ändert die Browser-URL während AJAX-Anfragen und dem Ersetzen von Antworten nicht, sofern nicht hx-boost, hx-push-url oder hx-replace-url verwendet werden
  • Dadurch kann sich r.URL im Go-Handler von der URL unterscheiden, die der Nutzer in der Adressleiste sieht
  • HTMX übermittelt die aktuell im Browser angezeigte URL im Header HX-Current-URL jeder Anfrage
  • Eine Hilfsfunktion parst diesen Header als url.URL und gibt, falls der Header fehlt, r.URL zurück
func browserURL(r *http.Request) (*url.URL, error) {
    cu := r.Header.Get("HX-Current-URL")
    if cu != "" {
        return url.Parse(cu)
    }

    return r.URL, nil
}

Konfiguration zum Ändern der HTMX-Standardwerte

  • Mit historyCacheSize: 0 wird der Seitencache im lokalen Speicher von HTMX vollständig deaktiviert
    • Der Cache im lokalen Speicher kann eine Quelle für Bugs und Sicherheitsprobleme sein
    • Beim Zurücknavigieren wird das HTML erneut vom Server geladen, statt aus dem Cache zu kommen
    • In künftigen HTMX-Versionen soll das Caching im lokalen Speicher aus demselben Grund standardmäßig deaktiviert werden
  • Mit disableInheritance: true wird die Vererbung von HTMX-Attributen deaktiviert
    • Es ist klarer, Attribute immer explizit zu deklarieren
    • Das senkt das Risiko von Bugs oder unbeabsichtigtem Verhalten
    • In künftigen HTMX-Versionen soll auch die Attributvererbung standardmäßig deaktiviert werden
  • Mit includeIndicatorStyles: false wird verhindert, dass HTMX Indicator-Styles injiziert
    • Die Indicator-Styles werden zusammen mit anderen CSS-Regeln selbst definiert, um Konsistenz zu wahren
  • HTMX-Anfragen haben standardmäßig kein Timeout und warten, bis der Server antwortet
    • Je nachdem, wie die Go-Anwendung Timeouts und Deadlines behandelt, kann timeout in Millisekunden angegeben werden
    • Als Ausgangspunkt wird timeout: 5000 verwendet
  • Die Basiskonfiguration enthält außerdem historyRestoreAsHxRequest: false sowie responseHandling je nach Statuscode
<meta
    name="htmx-config"
    content='{
        "includeIndicatorStyles": false,
        "historyCacheSize": 0,
        "historyRestoreAsHxRequest": false,
        "responseHandling":[
            {"code":"204", "swap": false},
            {"code":"422", "swap": true},
            {"code":"[45]..", "swap": true, "target": "body"},
            {"code":"...", "swap": true}
        ],
        "timeout": 5000
    }'
>

Seitenspezifische Layouts für größere Anwendungen

  • Wenn ein Basis-Template und seitenspezifische Templates nicht ausreichen, kann zwischen diesen beiden Ebenen ein Layout-Template eingefügt werden
  • Das eignet sich für Bereiche wie Admin-Seiten, die einen anderen Titel, ein anderes Navigationsmenü oder eine andere Inhaltsstruktur benötigen als normale Seiten
  • base ruft nicht direkt page:content auf, sondern layout
<body>
    {{template "layout" .}}
</body>
  • layouts/admin.tmpl definiert die gemeinsame Struktur des Admin-Bereichs und ruft darin page:content auf
    • Titel des Admin-Bereichs
    • Navigationsmenü zu Nutzern und Bestellungen
    • Ein <main> für seitenspezifische Inhalte
  • Der Handler übergibt das zu verwendende Layout und das Seiten-Template gemeinsam an render()
app.html.render(
    w,
    200,
    nil,
    "base",
    "layouts/admin.tmpl",
    "pages/admin-orders.tmpl",
)
  • So lassen sich bei beibehaltener gemeinsamer Basisvorlage routenspezifische Layouts und Seiteninhalte kombinieren; auch Ansichten, die wie der Admin-Bereich eine eigene Struktur benötigen, können mit demselben Rendering-Muster erweitert werden

1 Kommentare

 
GN⁺ 4 시간 전
Meinungen auf Hacker News
  • Man mag Go + HTMX und nutzt es zusammen mit a-h/templ, um die Typsicherheit von Templates, Komponenten und partiellem HTML zu erhöhen. Die gesamte Toolchain aus Go, Unix und SQLite wurde als GUS-Stack veröffentlicht: https://housecat.com/blog/the-gus-stack-go-unix-sqlite Er ist stark vom GUTS-Stack von exe.dev inspiriert, verwendet aber HTMX statt TypeScript: https://exe.dev/docs/guts Für Stacktrace-Fehler kommt cockroachdb/errors zum Einsatz, für typsicheres HTML templ, für die Generierung von OpenAPI-Spezifikationen fuego, für SQL-Codegenerierung sqlc, für reines Go-SQLite modernc.org/sqlite, für Migrationen goose, für Persistenz-Workflows dbos und für Chrome/CDP-Tests sowie Automatisierung rod. Diese Kombination ist sowohl beim direkten Programmieren als auch mit Unterstützung durch Agenten produktiv, ebenso beim Bauen und Deployen eines einzelnen Binaries.

    • Schade, dass Hyperscript bei Diskussionen über HTMX nicht die Aufmerksamkeit bekommt, die es verdient. Damit lässt sich das DOM ohne Roundtrips zum Server oder separate JavaScript-Funktionen manipulieren; es passt daher sehr gut zu Go + HTMX und fühlt sich wie eine Erweiterung des HTML an, an dem man ohnehin gerade arbeitet. Manche mögen deklarative Programmierung vielleicht nicht, aber bei Open Payment Host, das mehrere Payment-Gateways verarbeitet, ließen sich selbst komplexe DOM-Manipulationen ausschließlich mit Hyperscript umsetzen. https://hyperscript.org/, https://openpaymenthost.com/
    • Man könnte HTMX oder Hypertext, Unix, Go und SQLite auch bündeln und in HUGS-Stack umbenennen.
    • Ich habe auch gesehen, dass Go, Templ und HTMX als GoTH-Stack bezeichnet werden; nimmt man sqlc hinzu, wird daraus GoTHs oder GHosT. Wenn man go generate ./... als Build-Schritt akzeptiert, gewinnt man viel; mit goverter werden außerdem Konvertierungen zwischen sqlc-Modellen und Template-Objekten bzw. Rückgabewerten generiert. Etwa 50 % des Boilerplates werden automatisch erzeugt, wodurch die Typsicherheit sehr gut wird.
    • In letzter Zeit nutze ich für typsicheres SQL gern jet. Für die Codegenerierung ist zwar eine laufende Datenbank nötig, aber ich sehe es auch als Vorteil, dass man dadurch gezwungen wird, Migrationen anzuwenden, bevor man Code schreibt. https://github.com/go-jet/jet
    • Für kleine reaktive Web-Apps wie Echtzeit-API-Dashboards, die eventuell nicht einmal eine eigene Datenbank brauchen, verwende ich in letzter Zeit häufig das PAHG-Template. Es kombiniert Pico.css, Alpine.js, HTMX und Go; SQLite lässt sich natürlich ebenfalls anbinden. https://github.com/hiAndrewQuinn/pahg-template
  • HTMX ist für viele Einsatzzwecke großartig, lässt sich aber sehr schwer einführen, wenn die Teamkollegen nicht mitziehen. Es gibt anhaltenden Widerstand nach dem Motto, das sei keine ernsthafte Technologie, und verschiedenste Bugs werden zunächst HTMX angelastet; selbst wenn sich das später als Missverständnis herausstellt, ist das Vertrauen bereits beschädigt. Das passierte sogar im besten Team, in dem ich bisher gearbeitet habe, und am Ende habe ich gelernt, dass man sich seine Kämpfe aussuchen muss. Auch Gos html/template hat aus meiner Sicht eine ungewöhnlich unnatürliche Schnittstelle. Wie 《A Philosophy of Software Design》 empfiehlt, sollte Komplexität nach innen verlagert werden; eine Struktur, bei der man sich bei jedem Rendern von HTML um das Klonen von Templates kümmern muss, ist unnötig kompliziert.

    • Ich mag HTMX ebenfalls, habe bei der Einführung im Team aber dieselbe Realität erlebt. Ich schlug es für eine einfache Web-App vor, doch Entwickler, die jahrelang JSON zurückgegeben und mit JS/jQuery gerendert hatten, taten sich bei jedem Schritt schwer damit, bestehende Probleme auf die neue Weise zu lösen; ein anderer Entwickler versuchte es nicht einmal, und im Team wurde weiterhin nur Blazor ins Spiel gebracht. Jede neue Herangehensweise wäre schwierig gewesen, aber HTMX hatte praktisch keine Chance. Schade, denn in einem kleinen Team ließen sich 95 % des JavaScripts vermeiden, und wenn Code und HTML auf dem Server erzeugt würden, wäre die Codebasis aufgeräumter und leichter zu testen. In persönlichen Projekten nutze ich HTMX weiter, habe einen guten Flow gefunden und empfinde Webentwicklung dadurch als deutlich einfacher.
    • In einer beratenden Rolle habe ich Teams aktiv angeleitet und erklärt, dass man durch das Lernen von HTMX ein tieferes Verständnis für die tatsächliche Arbeitsumgebung, den Browser, entwickelt, während React und Ähnliches Entwickler vom Browser abschirmen; trotzdem habe ich dasselbe erlebt: https://www.reddit.com/r/htmx/s/DuXyGgsCWK An html/template gefällt mir die Standardsicherheit und dass es in der Standardbibliothek enthalten ist, aber wegen seiner dynamischen Natur gibt es zu viele offene Punkte ohne klare Antwort. Auch bei Templ fehlte mir nach einigen großen kommerziellen Apps die Entwicklerfreundlichkeit. Deshalb habe ich gsx gebaut: https://github.com/gsxhq/gsx
    • Man muss Templates nicht bei jedem Rendern klonen; man kompiliert sie einmal und führt sie danach nur noch aus. Das Problem im Original ist selbst verursacht, weil versucht wird, den Seitentitel im Haupt-Template festzulegen. Man kann den Titel über den Kontext übergeben und base.tmpl in BASE_BEGIN und BASE_END aufteilen, die dann im finalen Template verwendet werden. Der Seitentitel muss zwar zur Laufzeit übergeben werden, aber die Vorgehensweise aus dem Original bricht ohnehin zusammen, sobald man mit Mehrsprachigkeit beginnt.
    • In unserer Firma ist es genau umgekehrt: Trotz aller Probleme ist es unmöglich zu vermitteln, dass eine React-SPA nicht skaliert, weil React SPA als etablierte Technologie gilt. Jedes Problem wird einzeln betrachtet, aber der große Zusammenhang, dass alle aus derselben Struktur entstehen, wird nicht hergestellt.
  • In einem aktuellen Projekt habe ich HTMX mit großer Freude eingesetzt. Als jemand, der das Web vor AngularJS und React kennt, gefällt mir besonders, dass man echte Seiten bauen und die Menge an JavaScript minimieren kann. Mit reinem JavaScript wäre das ebenfalls möglich, aber HTMX nimmt einem den wiederkehrenden Boilerplate für Event-Handling ab. Wenn man die moderne Frontend-Philosophie nicht mag, ist es einen Versuch wert; die ersten Beispiele auf der offiziellen Website sind sehr grundlegend, aber wenn man etwas tiefer einsteigt, ist es deutlich mächtiger.

    • Vorsichtig würde ich auch SvelteKit empfehlen. Oberflächlich wirkt es wie noch ein weiteres JavaScript-Frontend-Framework, aber aus eigener Nutzung unterstützt es das hypertextzentrierte Design, das die HTMX-Familie anstrebt, sehr gut.
  • Ich habe ein ziemlich großes Projekt mit HTMX + Go gebaut, aber es war noch nicht ausreichend, und ich bin mir nicht sicher, ob es künftig das gewünschte Niveau erreichen wird. Für einfache CRUD-Apps und Admin-Dashboards ist es großartig, aber sobald es viele miteinander verbundene Komponenten, geteilten State und komplexe Interaktionen gibt, wird die Verwaltung schnell schwierig. Weil ich React nicht mochte, hatte ich HTMX gewählt, bin am Ende aber beim Backend bei Go geblieben und habe das Frontend auf SvelteKit im SPA-Modus umgestellt. Durch die saubere Trennung beider Seiten ließ sich die komplexe UI deutlich leichter entwickeln und warten. Svelte fühlt sich eher wie eine natürliche Erweiterung von HTML an als wie eine andere Sprache namens JSX; State-Management und Komponentenmodell sind einfach, und die neue $state-Syntax gefällt mir besonders gut.

    • Auch die HTMX-Website benennt klar den Trade-off, dass es für solche Einsatzzwecke nicht passend ist, aber man muss etwas tiefer graben, um die entsprechenden Texte zu finden.
    • Das Frontend-Schwesterprojekt der HTMX-Entwickler, Hyperscript, behandelt Funktionen wie Frontend-State. Ich frage mich, ob du das auch ausprobiert hast: https://hyperscript.org/docs/reactivity/
    • Ich glaube, der Kern der HTMX-Philosophie besteht darin, überhaupt zu fragen, ob man wirklich über geteilten State verbundene Komponenten braucht. Am Ende führen die meisten Apps CRUD gegen eine Datenbank aus.
  • Heutzutage schreibe ich Nebenprojekte fast alle mit Go + HTMX, unabhängig davon, ob ich dabei Hilfe von einem LLM bekomme. Auch Opus und GPT kommen mit dieser Kombination sehr gut zurecht; man kann schnell bauen und loslegen, und weil es ein einzelnes Binary ist, sind Deployment und Hosting bequem. Für schnelle Iterationen ist das ein sehr guter Stack.

  • Wenn man HTMX verwendet, empfehle ich dringend eine Art der HTML-Erzeugung, mit der sich auch im Backend gemeinsame HTML-Fragmente wie in React leicht in Funktionen auslagern und komponentisieren lassen. HTMX braucht je nach Situation Flexibilität beim vom Backend erzeugten HTML, etwa weil man Tags direkt neben bestimmtem HTML hinzufügen oder entfernen muss. Wenn es zum Beispiel auf oberster Ebene ein ``-Tag findet, aktualisiert es den Seitentitel. Mit traditionellen stringbasierten Template-Engines ist so etwas schwierig, mit sprachintegrierten HTML-Bibliotheken dagegen einfach; außerdem kann man die Abstraktionsmöglichkeiten der ganzen Sprache nutzen, was besonders gut zu HTMX passt. Bei einem JavaScript-Backend kann man eine Tagged-Template-Literal-Funktion wie https://github.com/WebReflection/uhtml-ssr verwenden, bei Go https://www.gomponents.com/ und bei Scala ScalaTags.

  • Mit den Büchern von Alex Edwards und Learn Go with Tests bin ich zum ersten Mal mit Go in Berührung gekommen, und ich empfehle sie immer noch. Jetzt bekomme ich Lust, einige ältere Projekte auf HTMX umzustellen.

    • Wenn man auch nur ein bisschen Interesse an Go-Webentwicklung hat, sollte man sowohl Let's Go als auch Let's Go Further von Alex Edwards lesen. Das sind die zugänglichsten, angenehmsten und praktischsten Einführungsbücher zu einer Programmiersprache, die ich bisher gelesen habe.
  • Ich habe für den Eigenbedarf ein Framework auf Basis von Kotlin + HTMX gebaut: https://github.com/reubenfirmin/zoned Ziel war zu prüfen, ob sich eine Web-App durchgängig typsicher machen lässt; für HTML verwende ich Kotlinx.html, das HTML in einer JSX-ähnlichen DSL schreibt. Die ersten 90 % habe ich selbst gebaut, für die letzten 10 % und das README habe ich Claude hinzugezogen, um die Veröffentlichung abzuschließen.

  • Ich mag sowohl Go als auch Alex Edwards sehr, aber die Ergebnisse mit HTMX waren für mich immer enttäuschend. Es fühlt sich an, als wachse die Komplexität der Codebase doppelt so schnell wie die der App selbst, und man stößt ständig auf Randfälle, aus denen man ohne merkwürdige Workarounds nicht herauskommt. Ich verstehe, warum man Node-Pakete nicht mag, aber HTMX wirkt wie eine Überkompensation dafür. Die Zeit, die man dadurch spart, nicht mit JSON zu ringen, gibt man dreifach dafür aus, Aussehen und Bediengefühl der App richtig hinzubekommen. Das Mantine-Template ist in zwei Minuten eingerichtet und bietet hervorragende UI-Komponenten; bindet man die gebauten statischen Assets ein, kann man daraus genauso ein einzelnes Go-Binary machen: https://github.com/mantinedev/vite-min-template

    • Als ich über Rails zum ersten Mal mit Hypermedia in Kontakt kam, war es bequem und einfach, daher hat mich die Reibung bei Go/HTMX überrascht. Nach mehreren Versuchen habe ich aber gelernt, dass Abstraktion wichtig ist. Ohne Komponentengenerator und Reverse Router ist es ziemlich schmerzhaft, und Rails liefert beides standardmäßig mit, sodass man sich darum nicht kümmern musste. Der größte Vorteil ist, dass Berechnung und Daten an einem Ort liegen. Auch LLMs können eigenständige Hypermedia-Apps gut nutzen und testen, und weil man nicht auf JavaScript wartet, sind die Iterationszyklen kürzer. Allerdings stimme ich zu, dass der Einstieg schwierig ist, weil es keine sofort nutzbare Komponentenbibliothek gibt.
    • Um HTMX zu verwenden, musste ich die gesamte Designphilosophie ändern. Das ist nicht unbedingt ein Nachteil und kann auch eine gute Veränderung sein, aber es ist eine erhebliche Umstellung; das Ergebnis ist eher web-nativ, statt eine Mobile-App nachzuahmen.
    • Ich frage mich, welches konkrete Beispiel einer UI es gab, die man mit Mantine bauen konnte, mit HTMX aber nicht.
  • Go + Datastar ist einfacher, deshalb bevorzuge ich es deutlich.

    • Go und Datastar passen wie angegossen zusammen. Für Templates würde ich gern a-h/templ ergänzen, das Datastar ebenfalls unterstützt. Es ist überraschend, wie wenig bekannt Datastar ist.
    • Ich bevorzuge Datastar auch, aber die neuen Funktionen von HTMX4 verringern den Abstand deutlich. Es ist erfreulich zu sehen, wie das Ökosystem auf gute Patterns konvergiert.