Мой эксперимент с HTMX и Astro

Делюсь первым опытом работы с библиотекой htmx. Я создал простой сайт, состоящий из главной страницы с первоначальным набором карточек квартир и 5 кнопок, которые выводят отфильтрованные по количеству комнат квартиры.

Стек: Astro + HTMX + Tailwind

Реализация очень простая:

// src/pages/index.astro <form id='filter-form' hx-trigger='change' hx-post='/api/cases' hx-target='#serch-results' hx-swap='innerHTML' hx-indicator='#loading' > ... <label> <input type='radio' name='rooms' value='1' class='hidden peer' /> <div class='hover:border-slate-500 peer-checked:opacity-100 peer-checked:shadow-xl peer-checked:border-slate-400'> 1 комната </div> </label> ... </form> <div id='loading'> Загрузка... </div> <div id='serch-results'></div>

Соответственно, при выборе нового варианта фильтра, по адресу `/api/cases` выполняется POST-запрос с данными о выбранном варианте.


Индикатором рабты запроса является `<div id='loading'>`. Результат (готовый HTML) выводится внутри `<div id='serch-results'></div>`.

На стороне `api` код выглядит примерно так:

// src/pages/api/cases.ts export const POST: APIRoute = async ({ request }) => { const formData = await request.formData() const rooms = formData.get('rooms') // Получаем список комнат на основе rooms return new Response( ` <div> ${filteredFlats.map(flat => ` {some flat html here} `)} </div> `, { status: 200, headers: { 'Content-Type': 'text/html+htmx', }, } )

И это все.

Впечатления

Данный подход к созданию веб-приложений скорее не для меня. Реализация через тот же React кажется проще и с большим потенциалом для доработок. Из того, что мне не понравилось:

  • Размытие логики приложения. Backend не просто возвращает данные, но также занимается и решением задач фронтенда;
  • Необходимость согласования верстки на стороне клиента, и на стороне сервера (в нашем случае это отображение карточек по умолчанию и вывод результатов поиска)

Да, здесь я сам немного усложнил себе задачу, не использовав Page Partials, но их применение в рамках того же Astro в связке с HTMX мне показалось черезчур надуманным.

Но, несмотря на сказанное выше, у меня есть ряд задач, где HTMX подходит идеально: у меня есть несколько старых статичных сайтов и сайтов, реализованных на CMS вроде WordPress. Для них возомжность добавить интерактивности (тем более с использованием Page Partials) является отличной перспективой. Для сайтов, написанных на Next или Astro, я пока что не смог увидеть применения.

P.S. Неожиданная проблема

В данном проекте я подключил View Transitions и это, как окалалось, ломает работу библиотеки HTMX. Стоит вам осуществить переход между страницами, как сайт окончательно превращается в статичный. К счастью, я наткнулся на эту статью, где описано решение: необходимо на страницу с кодом HTXM добавить следующий скрипт

// src/pages/index.astro <script> document.addEventListener('astro:page-load', () => { const contentElement = document.getElementById('filter-form') if (contentElement) { htmx.process(document.body) } }) </script>
2
5 комментариев
","lang":""}}],"summaryContent":null,"isExistSummaryContent":false,"warningFromEditor":null,"warningFromEditorTitle":null,"counters":{"comments":5,"favorites":0,"reposts":0,"views":281,"hits":355,"reads":null,"online":0},"dateFavorite":0,"hitsCount":355,"isCommentsEnabled":true,"isLikesEnabled":true,"isRemovedByUserRequest":false,"isFavorited":false,"isPinned":false,"repostId":null,"repostData":null,"subscribedToTreads":false,"isEditorial":false,"isAudioAvailable":false,"audioUrl":null,"isAudioAvailableToGenerate":false,"commentEditor":{"enabled":true,"who":null,"text":"","until":null,"reason":null,"type":"everybody"},"isBlur":false,"isPublished":true,"isDisabledAd":false,"withheld":[],"ogTitle":null,"ogDescription":null,"url":"https://vc.ru/id206685/1248402-moi-eksperiment-s-htmx-i-astro","author":{"id":206685,"name":"Petr Tcoi","nickname":null,"description":null,"uri":"","avatar":{"type":"image","data":{"uuid":"ffbf4738-e2bd-4ce6-12ae-5b41b2ec2db6","width":200,"height":200,"size":70783,"type":"png","color":"dfc5b2","hash":"","external_service":[]}},"cover":null,"achievements":[{"title":"Год на vc.ru","code":"registration_1_year","description":"Первый год с vc.ru. Получена 24 июля 2025.","previewUuid":"0d11c244-49de-50e7-894e-b9b27945d42b","formats":{"glb":"https://static.vc.ru/achievements/fish.glb","usdz":"https://static.vc.ru/achievements/fish.usdz"},"viewData":{"contentColor":"#C67AA3","textMaxWidth":0.634765625,"textX":0.5888671875,"textY":0.54296875,"logoX":0.5859375,"logoY":0.6669921875,"logoXNoText":0.6044921875,"logoYNoText":0.5439453125},"id":5216331,"userId":206685,"count":0,"shareImage":"https://api.vc.ru/achievements/share/5216331"},{"title":"3 года на vc.ru","code":"registration_3_years","description":"Провёл 3 года вместе с vc.ru. Получена 23 июля 2025.","previewUuid":"d9d72ac5-bcb5-55e0-8c72-b99251e5cdd9","formats":{"glb":"https://static.vc.ru/achievements/shark.glb","usdz":"https://static.vc.ru/achievements/shark.usdz"},"viewData":{"contentColor":"#8E6F09","textMaxWidth":0.66796875,"textX":0.5205078125,"textY":0.341796875,"logoX":0.5205078125,"logoY":0.4609375,"logoXNoText":0.5,"logoYNoText":0.3662109375},"id":1592244,"userId":206685,"count":0,"shareImage":"https://api.vc.ru/achievements/share/1592244"},{"title":"5 лет на vc.ru","code":"registration_5_years","description":"Провёл 5 лет вместе с vc.ru. Получена 23 июля 2025.","previewUuid":"a9140d54-73b8-5f40-afa8-449fbaafd42b","formats":{"glb":"https://static.vc.ru/achievements/whale.glb","usdz":"https://static.vc.ru/achievements/whale.usdz"},"viewData":{"contentColor":"#8E6F09","textMaxWidth":0.66796875,"textX":0.533203125,"textY":0.658203125,"logoX":0.533203125,"logoY":0.77734375,"logoXNoText":0.4375,"logoYNoText":0.66015625},"id":365536,"userId":206685,"count":0,"shareImage":"https://api.vc.ru/achievements/share/365536"}],"lastModificationDate":1764930193,"isSubscribed":false,"isSubscribedToNewPosts":false,"isMuted":false,"isAvailableForMessenger":true,"badgeId":null,"isDonationsEnabled":false,"isPlusGiftEnabled":true,"isUnverifiedBlogForCompanyWithoutPro":false,"isRemovedByUserRequest":false,"isFrozen":false,"isDisabledAd":false,"isPlus":false,"isVerified":false,"isPro":false,"yandexMetricaId":null,"badge":null,"isOnline":false,"tgChannelShortname":null,"isUnsubscribable":true,"type":1,"subtype":"personal_blog"},"subsite":{"id":206685,"name":"Petr Tcoi","nickname":null,"description":null,"uri":"","avatar":{"type":"image","data":{"uuid":"ffbf4738-e2bd-4ce6-12ae-5b41b2ec2db6","width":200,"height":200,"size":70783,"type":"png","color":"dfc5b2","hash":"","external_service":[]}},"cover":null,"achievements":[{"title":"Год на vc.ru","code":"registration_1_year","description":"Первый год с vc.ru. Получена 24 июля 2025.","previewUuid":"0d11c244-49de-50e7-894e-b9b27945d42b","formats":{"glb":"https://static.vc.ru/achievements/fish.glb","usdz":"https://static.vc.ru/achievements/fish.usdz"},"viewData":{"contentColor":"#C67AA3","textMaxWidth":0.634765625,"textX":0.5888671875,"textY":0.54296875,"logoX":0.5859375,"logoY":0.6669921875,"logoXNoText":0.6044921875,"logoYNoText":0.5439453125},"id":5216331,"userId":206685,"count":0,"shareImage":"https://api.vc.ru/achievements/share/5216331"},{"title":"3 года на vc.ru","code":"registration_3_years","description":"Провёл 3 года вместе с vc.ru. Получена 23 июля 2025.","previewUuid":"d9d72ac5-bcb5-55e0-8c72-b99251e5cdd9","formats":{"glb":"https://static.vc.ru/achievements/shark.glb","usdz":"https://static.vc.ru/achievements/shark.usdz"},"viewData":{"contentColor":"#8E6F09","textMaxWidth":0.66796875,"textX":0.5205078125,"textY":0.341796875,"logoX":0.5205078125,"logoY":0.4609375,"logoXNoText":0.5,"logoYNoText":0.3662109375},"id":1592244,"userId":206685,"count":0,"shareImage":"https://api.vc.ru/achievements/share/1592244"},{"title":"5 лет на vc.ru","code":"registration_5_years","description":"Провёл 5 лет вместе с vc.ru. Получена 23 июля 2025.","previewUuid":"a9140d54-73b8-5f40-afa8-449fbaafd42b","formats":{"glb":"https://static.vc.ru/achievements/whale.glb","usdz":"https://static.vc.ru/achievements/whale.usdz"},"viewData":{"contentColor":"#8E6F09","textMaxWidth":0.66796875,"textX":0.533203125,"textY":0.658203125,"logoX":0.533203125,"logoY":0.77734375,"logoXNoText":0.4375,"logoYNoText":0.66015625},"id":365536,"userId":206685,"count":0,"shareImage":"https://api.vc.ru/achievements/share/365536"}],"lastModificationDate":1764930193,"isSubscribed":false,"isSubscribedToNewPosts":false,"isMuted":false,"isAvailableForMessenger":true,"badgeId":null,"isDonationsEnabled":false,"isPlusGiftEnabled":true,"isUnverifiedBlogForCompanyWithoutPro":false,"isRemovedByUserRequest":false,"isFrozen":false,"isDisabledAd":false,"isPlus":false,"isVerified":false,"isPro":false,"yandexMetricaId":null,"badge":null,"isOnline":false,"tgChannelShortname":null,"isUnsubscribable":true,"type":1,"subtype":"personal_blog"},"reactions":{"counters":[{"id":1,"count":2}],"reactionId":0},"isNews":false,"source":null,"clusters":[],"donations":{"amount":0,"isDonated":false},"commentsSeenCount":null,"keywords":[],"media":null,"customCover":null,"robotsTag":null,"categories":[10],"isAnonymized":true}};