1 puntos por GN⁺ 4 시간 전 | 1 comentarios | Compartir por WhatsApp
  • Manteniendo el renderizado del lado del servidor basado en html/template de Go, se agregan interacciones dinámicas con HTMX, y desde un único renderizador se devuelven de forma selectiva páginas completas o fragmentos HTML.
  • Se divide el marcado en base.tmpl, plantillas de páginas y fragmentos reutilizables, y se aprovechan embed.FS de Go 1.16 y un conjunto compartido de plantillas para simplificar la estructura de despliegue y renderizado.
  • Según HX-Request, los resultados de búsqueda se devuelven como página completa o como filas de tabla, y se evitan errores de caché y restauración al volver atrás con Vary: HX-Request y historyRestoreAsHxRequest: false.
  • Al mover una solicitud HTMX a otra página, se usan HX-Redirect y una respuesta 2xx en lugar de un 3xx normal, y se controla el alcance del reemplazo y la visualización de errores con responseHandling para 204, 422, 4xx y 5xx.
  • Se toma como punto de partida una configuración que desactiva la caché de almacenamiento local y la herencia de atributos, y que define un tiempo límite para las solicitudes; cuando la escala crece, se pueden agregar plantillas de layout intermedio para admitir incluso estructuras de pantalla separadas, como áreas de administración.

Por qué combinar el renderizado de servidor de Go con HTMX

  • HTMX permite agregar interacciones dinámicas con muy poco JavaScript, manteniendo la consistencia y la seguridad que ofrece el renderizado HTML del lado del servidor de html/template en Go.
  • La implementación aborda tres ejes:
    • Una estructura de plantillas que admite tanto páginas completas como respuestas HTML parciales.
    • Redirecciones y manejo de errores en un entorno HTMX.
    • Configuración que cambia los valores predeterminados de HTMX y las razones para hacerlo.
  • Se empieza con una funcionalidad básica que reemplaza un botón por una imagen, y luego se amplía a una pantalla de búsqueda que filtra una lista mientras se ingresa un nombre de usuario o correo electrónico.
  • El mismo patrón de plantillas se puede aplicar no solo a HTMX, sino también a herramientas HTML-over-the-wire como Unpoly y Hotwire.
  • Si no conoces el uso básico de HTMX, conviene revisar primero la documentación oficial.

Directorio del proyecto y configuración de archivos estáticos

  • El proyecto se divide en las siguientes áreas según su función:
    • assets/html/base.tmpl: esqueleto HTML compartido por todas las páginas.
    • assets/html/pages/: contenido de páginas individuales.
    • assets/html/partials/: fragmentos HTML reutilizados en varios lugares.
    • assets/static/css, assets/static/img, assets/static/js: recursos estáticos.
    • cmd/web/: inicio del servidor, handlers y código del renderizador HTML.
  • El esqueleto básico se genera con los siguientes comandos:
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 también se puede instalar vía CDN o NPM, pero normalmente se usa el enfoque de descargar una copia y servirla directamente como archivo estático de la aplicación.
    • La configuración es simple y se evitan las desventajas de usar un CDN.
    • Se usan htmx.org@2.0.10 y el framework CSS sin clases 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

Separación de plantillas base, de página y parciales

  • base.tmpl define el esqueleto completo del documento, como <head>, el título común y <main>, e inserta page:title y page:content.
  • Bamboo CSS y el script de HTMX se cargan desde la plantilla base, y al script de HTMX se le asigna el atributo defer.
    • El navegador descarga el script en paralelo mientras analiza el HTML.
    • El script se ejecuta después de que finalizan el análisis del HTML y la construcción del DOM.
  • Todas las plantillas reciben un nombre explícito con {{define}}...{{end}}, sin depender del nombre del archivo.
    • El código Go puede referenciar las plantillas de forma consistente solo por el nombre definido.
    • El : de page:title es un separador arbitrario; también podrían usarse nombres como page_title, page-title, pageTitle o title.
  • En el botón de la página de inicio se definen dos comportamientos:
    • hx-get="/gopher": al hacer clic, envía una solicitud GET /gopher.
    • hx-swap="outerHTML": reemplaza el propio botón con el HTML de respuesta.
  • partial:image:gopher es un fragmento de imagen reutilizable, y mediante width="{{.}}" recibe el ancho de la imagen en el momento de la llamada.
  • Los fragmentos HTML usados solo en una página específica pueden colocarse en el archivo de esa página, no en el directorio común partials.
    • La distinción es dejar en el directorio común los fragmentos usados por varias páginas y ubicar los fragmentos exclusivos de una página junto al contenido relacionado.

Incluir HTML y recursos estáticos en el binario de Go

  • Desde que en Go 1.16 se agregó el embedding de archivos, HTML y recursos estáticos pueden incluirse en el binario de Go en lugar de leerse desde disco en tiempo de ejecución.
  • Con la directiva //go:embed "html" "static", ambos directorios se colocan en embed.FS, y con fs.Sub() se separan en dos subsistemas de archivos:
    • HTMLFiles: sistema de archivos de plantillas que usa html como raíz.
    • StaticFiles: sistema de archivos estáticos que usa static como raíz.
  • Separarlos aporta dos beneficios:
    • El código de HTML y el de archivos estáticos no acceden innecesariamente a los archivos del otro.
    • Al abrir archivos no hace falta agregar los prefijos html/ o static/.
  • Si no se quiere usar panic() en sub(), también se puede devolver el error e inicializar las dos variables en main().
  • En la implementación actual, para que fs.Sub() falle, el valor del directorio tendría que no ser una ruta válida; pero las cadenas fijas "html" y "static" pasan esa validación, por lo que el riesgo de pánico en tiempo de ejecución es muy bajo.

Conjunto compartido de plantillas y htmlRenderer

  • htmlRenderer administra el flujo de renderizado de la siguiente manera:
    • Al iniciar, analiza una vez el conjunto compartido de plantillas.
    • En cada renderizado, clona el conjunto compartido.
    • Analiza adicionalmente las plantillas de página necesarias para la solicitud.
    • Ejecuta la plantilla con el nombre indicado y la envía como respuesta HTTP.
  • newHTMLRenderer() registra template.FuncMap y luego analiza las plantillas comunes.
    • Conecta now con time.Now.
    • Otras funciones de plantilla personalizadas también pueden agregarse en el mismo lugar.
  • render() crea un conjunto de plantillas por solicitud con sharedTemplates.Clone() y, si hay additionalTemplateFiles, lo extiende con ParseFS().
  • Primero se ejecuta la plantilla en un bytes.Buffer, luego se registra el código de estado y se envía el cuerpo de la respuesta.
  • Al inicializar el servidor, "base.tmpl" y "partials/*.tmpl" se analizan como conjunto compartido.
    • La plantilla base y todos los fragmentos comunes siempre están disponibles en el renderizador.
    • Solo las plantillas específicas de cada página se agregan al llamar al handler.
  • Los archivos estáticos se sirven con http.FileServerFS(assets.StaticFiles) y la ruta /static/, y el servidor corre en :5051.

Renderizado de páginas completas y fragmentos HTML

  • El handler de la página de inicio devuelve el documento completo con la siguiente llamada:
app.html.render(w, 200, nil, "base", "pages/home.tmpl")
  • Es una llamada que agrega pages/home.tmpl al conjunto compartido y ejecuta la plantilla base con 200 OK.
  • El handler /gopher no analiza archivos adicionales y solo ejecuta partial:image:gopher, que ya está incluido en el conjunto compartido.
width := 100
app.html.render(w, http.StatusOK, width, "partial:image:gopher")
  • HTMX reemplaza el botón por la imagen recibida desde /gopher según la configuración hx-swap="outerHTML"
  • Esta estructura ofrece las siguientes ventajas
    • Las plantillas y los recursos estáticos quedan incluidos en el binario, lo que facilita el despliegue
    • Con el mismo render() se puede devolver tanto una página HTML completa como un fragmento HTML específico
    • Se reduce la duplicación de marcado mediante una plantilla base y plantillas parciales
    • Las plantillas parciales se pueden reutilizar en la plantilla base, en el contenido de páginas y también en otras plantillas parciales

Búsqueda de usuarios que se actualiza al escribir

  • La función de búsqueda se divide en dos rutas
    • GET /users: página HTML completa con la información de todos los usuarios
    • GET /users/search: fragmento HTML que devuelve solo las filas de la tabla de los usuarios cuyo nombre o email coincide con el término de búsqueda
  • El campo de búsqueda combina los siguientes atributos de HTMX
    • Envía la solicitud de búsqueda con hx-get="/users/search"
    • Solicita después de que pasen 500 ms tras un cambio en la entrada, o al presionar Enter, con hx-trigger="input changed delay:500ms, keyup[key=='Enter']"
    • El término de búsqueda se pasa como cadena de consulta con la forma GET /users/search?query=foo
    • Inserta la respuesta dentro de <tbody id="search-results"> con hx-target="#search-results"
    • Actualiza la barra de direcciones y agrega una entrada al historial del navegador en cada solicitud con hx-push-url="true"
  • users:rows es una plantilla separada que renderiza solo las filas de usuarios
    • En cada fila se muestran el nombre y el email
    • Si IsGopher es verdadero, renderiza partial:image:gopher con ancho 24
  • listUsers y searchUsers agregan pages/users.tmpl al conjunto compartido, pero ejecutan plantillas diferentes
    • listUsers: ejecuta base y devuelve la página completa
    • searchUsers: ejecuta users:rows y devuelve solo las filas coincidentes
  • Si el término de búsqueda está vacío, se usa la lista completa de usuarios; si tiene un valor, se revisan el nombre y el email con strings.Contains()
  • En una aplicación real, se pueden considerar las siguientes mejoras
    • Unificar los handlers de listado y búsqueda
    • Centralizar los helpers de manejo de errores y logging
    • Agregar encabezados de Content Security Policy y recuperación ante pánicos mediante middleware
    • Configurar adecuadamente los timeouts del servidor

Distinguir entre página completa y respuesta parcial con HX-Request

  • Si el usuario abre /users/search?query=leo directamente en el navegador o llega mediante un enlace compartido, es posible que solo vea el HTML parcial correspondiente a las filas de la tabla
  • Las solicitudes de HTMX siempre incluyen el encabezado HX-Request: true, por lo que se decide el tipo de respuesta con base en él
func isHTMXRequest(r *http.Request) bool {
    return r.Header.Get("HX-Request") == "true"
}
  • Se deja base como plantilla de ejecución predeterminada del handler de búsqueda y se cambia a users:rows solo cuando es una solicitud HTMX
    • Si se visita directamente, se recibe una página completa con los resultados de búsqueda
    • En una solicitud HTMX, solo se devuelve el fragmento de filas para insertar en el <tbody>

Encabezado Vary y restauración al volver atrás

  • Como una misma URL devuelve respuestas distintas según el valor de HX-Request, también hay que informar esa diferencia a la caché
  • Se agrega Vary: HX-Request a todas las respuestas renderizadas
    • En rigor, configurarlo solo en los handlers donde haga falta genera menos desperdicio
    • Configurarlo de forma global en el renderizador evita bugs que podrían surgir por omisiones
  • Cuando hx-push-url o hx-boost agregan entradas al historial del navegador, HTMX guarda en caché el HTML de la página completa en el almacenamiento local del navegador
    • La caché predeterminada es de hasta 10 páginas
    • Si se retrocede hasta un punto que no está en caché, HTMX vuelve a solicitar esa URL al servidor
  • La solicitud de restauración predeterminada incluye HX-Request: true, por lo que el servidor podría devolver HTML parcial en lugar de una página completa
  • Si se elige el tipo de respuesta mediante HX-Request, se debe configurar historyRestoreAsHxRequest: false
    • Excluye HX-Request: true en solicitudes de restauración por fallo de caché
    • Hace que el servidor devuelva una página HTML completa para esa solicitud
  • La configuración de HTMX se coloca en la metaetiqueta htmx-config de base.tmpl

Redirigir una solicitud HTMX a otra página

  • HTMX puede reemplazar de inmediato fragmentos HTML como mensajes de éxito, lo que reduce los casos en los que se necesita el flujo habitual Post/Redirect/Get
  • Cuando se debe enviar al usuario a una página completamente distinta, como ir al perfil después de iniciar sesión, no se puede resolver solo con una respuesta 3xx normal
    • El navegador sigue la respuesta 3xx antes que HTMX
    • HTMX solo ve la respuesta final después de la redirección y reemplaza con ese contenido el destino existente
  • En solicitudes HTMX se usa una respuesta 2xx junto con el encabezado HX-Redirect
    • HX-Redirect: /foo/bar mueve el navegador a esa URL y vuelve a cargar la página completa
    • La solicitud al destino no incluye HX-Request: true
  • Para soportar también entornos con JavaScript desactivado o donde HTMX no se haya cargado, las solicitudes que no sean HTMX deben mantener una respuesta 3xx normal
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)
}
  • Al ir a /profile después del inicio de sesión, se puede llamar así
redirect(w, r, "/profile", http.StatusSeeOther)

Diferencia entre HX-Redirect y HX-Location

  • HX-Location realiza un comportamiento similar a una redirección sin recargar la página completa
    • Obtiene el HTML de la URL de destino
    • Reemplaza la respuesta en <body>
    • Agrega una nueva entrada al historial del navegador
  • Al solicitar la URL de destino, se incluye HX-Request: true
  • Si la ruta de destino devuelve HTML parcial según HX-Request, pueden surgir problemas
    • El handler no puede distinguir entre una solicitud que viene de HX-Location y una solicitud HTMX normal
    • La primera necesita una página completa, pero la segunda necesita una respuesta parcial
  • A diferencia de la restauración del historial, no existe una configuración para desactivar HX-Request: true en una navegación con HX-Location
  • En la mayoría de los casos, es más fácil y seguro aceptar la recarga de página completa y usar HX-Redirect
  • Si la ruta de destino está configurada para devolver siempre HTML completo, también se puede usar HX-Location

Manejo de respuestas 204, 422, 4xx y 5xx

  • De forma predeterminada, HTMX no reemplaza en el DOM las respuestas 4xx o 5xx
    • Mantiene la pantalla actual tal como está
    • El error se registra en la consola de herramientas para desarrolladores
  • Aunque el servidor devuelva 500 Internal Server Error por el nombre de una plantilla inexistente, con la configuración predeterminada el usuario no puede ver el error en pantalla
  • Para mostrar mensajes de error al usuario, las respuestas 4xx y 5xx se configuran para reemplazar todo el <body>
  • La excepción es 422 Unprocessable Content
    • Se usa al devolver un formulario que contiene errores de validación
    • La respuesta debe reemplazarse normalmente en el destino HTMX existente
  • responseHandling se configura así según el código de estado
    • 204 No Content: no modifica el DOM
    • 422 Unprocessable Content: reemplazo normal en el destino existente
    • Otros 4xx y 5xx: reemplaza la respuesta en <body>
    • Las demás respuestas: reemplazo normal en el destino existente
"responseHandling":[
    {"code":"204", "swap": false},
    {"code":"422", "swap": true},
    {"code":"[45]..", "swap": true, "target": "body"},
    {"code":"...", "swap": true}
]
  • Si en una interacción específica necesitas colocar los errores en un destino distinto de <body>, puedes sobrescribir la configuración predeterminada con la extensión response target.

Comprobar la URL actual en la barra de direcciones del navegador

  • HTMX no cambia la URL del navegador durante las solicitudes AJAX ni el reemplazo de respuestas si no se usan hx-boost, hx-push-url o hx-replace-url.
  • Por eso, r.URL del handler de Go puede ser distinto de la URL que el usuario ve en la barra de direcciones.
  • HTMX envía la URL que el navegador muestra actualmente en el encabezado HX-Current-URL de cada solicitud.
  • La función auxiliar parsea este encabezado como url.URL y, si no existe, devuelve r.URL.
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
}

Configuración para cambiar los valores predeterminados de HTMX

  • Con historyCacheSize: 0, se desactiva por completo la caché de páginas en el almacenamiento local de HTMX.
    • La caché en almacenamiento local puede ser una fuente de bugs y problemas de seguridad.
    • Al presionar Atrás, el HTML se vuelve a obtener del servidor en lugar de usar la caché.
    • En futuras versiones de HTMX, el cacheo en almacenamiento local vendrá desactivado por defecto por el mismo motivo.
  • Con disableInheritance: true, se desactiva la herencia de atributos de HTMX.
    • Es más claro declarar siempre los atributos de forma explícita.
    • Reduce el riesgo de bugs o comportamientos no deseados.
    • En futuras versiones de HTMX, la herencia de atributos también vendrá desactivada por defecto.
  • Con includeIndicatorStyles: false, se evita que HTMX inyecte estilos para los indicadores.
    • Los estilos de los indicadores se definen directamente junto con otras reglas CSS para mantener la consistencia.
  • Las solicitudes HTMX no tienen tiempo límite de forma predeterminada y esperan hasta que el servidor responda.
    • Según cómo la aplicación Go maneje los tiempos límite y los deadlines, se puede especificar timeout en milisegundos.
    • Como punto de partida, se usa timeout: 5000.
  • La configuración base también incluye historyRestoreAsHxRequest: false y responseHandling por código de estado.
<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
    }'
>

Layouts por página para aplicaciones grandes

  • Si no alcanza con una plantilla base y plantillas por página, se puede agregar una plantilla de layout entre ambas capas.
  • Es adecuada para áreas que necesitan un título, un menú de navegación y una estructura de cuerpo distintos a los de las páginas comunes, como una página de administración.
  • base llama a layout en lugar de llamar directamente a page:content.
<body>
    {{template "layout" .}}
</body>
  • layouts/admin.tmpl define la estructura común del área de administración y llama internamente a page:content.
    • Título del área de administración
    • Menú de navegación hacia usuarios y pedidos
    • Un <main> donde va el contenido específico de cada página
  • El handler pasa a render() tanto el layout como la plantilla de página que se usarán.
app.html.render(
    w,
    200,
    nil,
    "base",
    "layouts/admin.tmpl",
    "pages/admin-orders.tmpl",
)
  • Al mantener una plantilla base compartida y combinar layouts por ruta con contenido de página, las pantallas que necesitan una estructura separada, como el área de administración, también pueden ampliarse con el mismo patrón de renderizado.

1 comentarios

 
GN⁺ 4 시간 전
Opiniones en Hacker News
  • Me gusta Go + HTMX, y lo uso junto con a-h/templ para aumentar la seguridad de tipos en plantillas, componentes y HTML parcial. Publiqué este conjunto completo de herramientas, compuesto por Go, Unix y SQLite, llamándolo stack GUS: https://housecat.com/blog/the-gus-stack-go-unix-sqlite Está muy inspirado en el stack GUTS de exe.dev, pero usa HTMX en lugar de TypeScript: https://exe.dev/docs/guts Uso cockroachdb/errors para errores con stack trace, templ para HTML con seguridad de tipos, fuego para generar especificaciones OpenAPI, sqlc para generar código SQL, modernc.org/sqlite para SQLite puro en Go, goose para migraciones, dbos para flujos de trabajo de persistencia, y rod para pruebas y automatización con Chrome/CDP. Esta combinación resulta productiva tanto para programar directamente o con ayuda de agentes, como para compilar y desplegar un único binario.

    • Es una lástima que Hyperscript no reciba la atención que merece cuando se habla de HTMX. Permite manipular el DOM sin viajes de ida y vuelta al servidor ni funciones JavaScript separadas, así que encaja muy bien con Go + HTMX y se siente como una extensión del HTML en el que ya estás trabajando. Habrá gente a la que no le guste la programación declarativa, pero en Open Payment Host, que maneja varias pasarelas de pago, pude implementar incluso manipulaciones complejas del DOM solo con Hyperscript. https://hyperscript.org/, https://openpaymenthost.com/
    • También se podría renombrar como stack HUGS, agrupando HTMX, hipertexto, Unix, Go y SQLite.
    • También he visto que a Go, Templ y HTMX le llaman stack GoTH; si se suma sqlc, se convierte en GoTHs o GHosT. Se gana mucho si se acepta go generate ./... como paso de compilación, y con goverter también genero conversiones entre modelos de sqlc y objetos/valores de retorno de plantillas. Alrededor del 50% del código repetitivo se genera automáticamente, lo que mejora mucho la seguridad de tipos.
    • Últimamente disfruto usar jet para SQL con seguridad de tipos. Requiere una base de datos en ejecución para la generación de código, pero también veo como ventaja que obligue a aplicar primero las migraciones antes de escribir código. https://github.com/go-jet/jet
    • Para apps web reactivas pequeñas, como dashboards de APIs en tiempo real que quizá ni siquiera necesiten su propia base de datos, últimamente uso mucho la plantilla PAHG. Es una combinación de Pico.css, Alpine.js, HTMX y Go, y por supuesto también se puede conectar SQLite. https://github.com/hiAndrewQuinn/pahg-template
  • HTMX es excelente para muchos usos, pero es muy difícil adoptarlo si tus compañeros de equipo no están de acuerdo. La resistencia de que no es una tecnología seria continúa, y todo tipo de bugs se atribuyen primero a HTMX; aunque después se demuestre que era un malentendido, la confianza ya queda dañada. Incluso me pasó en el mejor equipo en el que he trabajado hasta ahora, y al final aprendí que hay que elegir qué batallas pelear. También considero que html/template de Go tiene una interfaz especialmente poco natural. Como recomienda 《A Philosophy of Software Design》, la complejidad debería empujarse hacia el interior, pero una estructura en la que hay que preocuparse por clonar plantillas cada vez que se renderiza HTML es innecesariamente compleja.

    • A mí también me gusta HTMX, pero viví la misma realidad al intentar adoptarlo en un equipo. Lo propuse para una app web sencilla, pero un desarrollador que durante años había devuelto JSON y renderizado con JS/jQuery tuvo dificultades en cada paso para resolver problemas existentes con el nuevo enfoque; otro desarrollador ni siquiera lo intentó, y en el equipo seguían hablando solo de Blazor. Cualquier enfoque nuevo les habría resultado difícil, pero HTMX prácticamente no tuvo oportunidad. Es una pena, porque al ser un equipo pequeño podríamos evitar el 95% del JavaScript y generar código y HTML en el servidor, con lo que el codebase quedaría más ordenado y las pruebas serían más fáciles. En proyectos personales sigo usando HTMX, encontré un buen flujo y siento que el desarrollo web se simplificó mucho.
    • En un rol de consultoría, guié activamente al equipo y expliqué que aprender HTMX profundiza la comprensión del navegador, que es el entorno real de trabajo, mientras que React y similares aíslan al desarrollador del navegador; aun así viví lo mismo: https://www.reddit.com/r/htmx/s/DuXyGgsCWK Me gusta que html/template tenga seguridad básica y venga incluido como estándar, pero por su naturaleza dinámica hay demasiadas partes sin una respuesta definida; y después de escribir varias apps comerciales grandes con Templ, también me pareció que la experiencia de desarrollo dejaba que desear. Por eso creé gsx: https://github.com/gsxhq/gsx
    • No hace falta clonar las plantillas cada vez que se renderiza; basta con compilarlas una vez y luego ejecutarlas. El problema del artículo original se lo creó él mismo al intentar definir el título de la página desde la plantilla principal. Se puede pasar el título por contexto y dividir base.tmpl en BASE_BEGIN y BASE_END para usarlos en la plantilla final. Hay que pasar el título de cada página en tiempo de ejecución, pero el enfoque del artículo original también termina rompiéndose cuando se empieza con el soporte multidioma.
    • En mi empresa pasa exactamente lo contrario: aunque tenga todo tipo de problemas, es imposible convencer de que una SPA en React no escala, porque se la considera una tecnología establecida. Ven cada problema por separado, pero no logran conectarlos con el panorama general de que todos provienen de la misma estructura.
  • En un proyecto reciente disfruté muchísimo usar HTMX. Como alguien que conoce la web anterior a AngularJS y React, me gusta especialmente poder crear páginas reales y minimizar la cantidad de JavaScript. También se puede hacer con JavaScript puro, pero HTMX evita el código repetitivo de manejo de eventos. Si no te gusta la filosofía moderna del frontend, vale la pena probarlo; los primeros ejemplos del sitio oficial son muy básicos, pero al aprender un poco más se vuelve mucho más potente.

    • Con cautela, también quisiera recomendar SvelteKit. Por fuera parece otro framework frontend de JavaScript más, pero al usarlo vi que soporta muy bien el diseño centrado en hipertexto que busca la familia de HTMX.
  • Probé crear un proyecto bastante grande con HTMX + Go, pero todavía no fue suficiente, y no estoy seguro de que en el futuro llegue al nivel que quiero. Es excelente para apps CRUD simples y dashboards de administración, pero cuando empiezan a abundar los componentes conectados entre sí, el estado compartido y las interacciones complejas, se vuelve difícil de gestionar muy rápido. Elegí HTMX porque no me gusta React, pero al final mantuve el backend en Go y cambié el frontend a SvelteKit en modo SPA; al separar limpiamente ambos lados, pude desarrollar y mantener una UI compleja con mucha más facilidad. Svelte se siente como una extensión natural de HTML, más que como otro lenguaje llamado JSX; su gestión de estado y modelo de componentes son simples, y la nueva sintaxis $state me gusta especialmente.

    • El sitio de HTMX también deja claro ese compromiso, diciendo que no es adecuado para esos casos de uso, pero hay que buscar bastante para encontrar esos textos.
    • Hyperscript, el proyecto hermano de frontend del equipo de HTMX, maneja cosas como el estado en el frontend. Me pregunto si también lo probaste: https://hyperscript.org/docs/reactivity/
    • Creo que el núcleo de la filosofía de HTMX está en preguntarse si de verdad hacen falta componentes conectados por estado compartido desde el principio. Al final, la mayoría de las apps hacen CRUD contra una base de datos.
  • Últimamente escribo casi todos mis proyectos personales en Go + HTMX, con o sin ayuda de LLMs. Opus y GPT también manejan muy bien esta combinación; permite construir y arrancar rápido, y como es un único binario, el despliegue y el hosting también son cómodos. Es un stack muy bueno para iterar rápido.

  • Si vas a usar HTMX, recomiendo firmemente una forma de generar HTML que permita extraer fragmentos HTML comunes en funciones y componentizarlos fácilmente en el backend, como en React. HTMX necesita flexibilidad en el HTML que genera el backend, por ejemplo para agregar o quitar etiquetas junto a cierto HTML según la situación. Por ejemplo, si encuentra una etiqueta `` en el nivel superior, actualiza el título de la página. En los motores de plantillas tradicionales basados en strings, este tipo de trabajo es difícil, pero con una biblioteca de HTML integrada en el lenguaje es sencillo, y se pueden aprovechar todas las capacidades de abstracción del lenguaje, por lo que encaja especialmente bien con HTMX. Si el backend es JavaScript, se puede usar una función de tagged template literal como https://github.com/WebReflection/uhtml-ssr; en Go, https://www.gomponents.com/; y en Scala, ScalaTags.

  • Conocí Go por primera vez con el libro de Alex Edwards y Learn Go with Tests, y todavía los recomiendo. Me dieron ganas de intentar convertir a HTMX algunos proyectos antiguos.

    • Si tienes aunque sea un poco de interés en el desarrollo web con Go, deberías leer tanto Let's Go como Let's Go Further de Alex Edwards. Son de los libros introductorios de lenguajes de programación más accesibles, agradables y prácticos que he leído.
  • Creé para uso personal un framework basado en Kotlin + HTMX: https://github.com/reubenfirmin/zoned El objetivo era comprobar si podía hacer una app web completa con seguridad de tipos de extremo a extremo, usando Kotlinx.html para escribir HTML con un DSL similar a JSX. Hice por mi cuenta el primer 90%, y para el último 10% y el README recibí ayuda de Claude para terminar de publicarlo.

  • Me gustan tanto Go como Alex Edwards, pero los resultados al usar HTMX siempre me decepcionaron. Siento que la complejidad del código base crece el doble de rápido que la propia app, y siempre aparecen casos límite de los que no se puede salir sin trucos raros. Entiendo por qué a la gente no le gustan los paquetes de Node, pero HTMX me parece una sobrecompensación frente a eso. El tiempo que se ahorra al no pelearse con JSON se multiplica por tres en lograr que la app se vea y se sienta bien. Con un template de Mantine puedes configurarlo en 2 minutos y aprovechar excelentes componentes de UI; si incluyes los assets estáticos generados, puedes obtener igualmente un único binario de Go: https://github.com/mantinedev/vite-min-template

    • Cuando conocí el hipermedia con Rails me pareció cómodo y fácil, así que me sorprendió la fricción de Go/HTMX, pero tras varios intentos aprendí que las abstracciones importan. Sin generadores de componentes y un router inverso, resulta bastante doloroso; Rails los ofrece de serie, así que no había que preocuparse por eso. La mayor ventaja es que los cálculos y los datos están en un solo lugar. Los LLMs también usan y prueban bien apps hipermedia autocontenidas, y al no tener que esperar a JavaScript, el ciclo de iteración se acorta. Dicho eso, coincido en que es difícil empezar porque no hay una biblioteca de componentes lista para usar.
    • Para usar HTMX tuve que cambiar toda la filosofía de diseño. No necesariamente es una desventaja; incluso puede ser un buen cambio, pero es una transición considerable, y el resultado se acerca más a una forma propia de la web que a imitar una app móvil.
    • Me da curiosidad saber algún ejemplo concreto de UI que pudiste crear con Mantine pero no con HTMX.
  • Prefiero mucho más Go + Datastar porque es más simple.

    • Go y Datastar encajan como un guante hecho a medida. A las plantillas me gustaría sumarles a-h/templ, que Datastar también soporta. Me sorprende que Datastar sea tan poco conocido.
    • Yo también prefiero Datastar, pero las nuevas funciones de HTMX4 están reduciendo mucho la brecha. Da gusto ver que el ecosistema converge hacia buenos patrones.