Skip to main content
htmxology-expert Use when working with htmxology — the type-safe HTMX routing framework for Rust/Axum. Covers Route enums, Controller trait, RoutingController composition, template route helpers, as_htmx_attribute(), HtmxRequest detection (classic vs HTMX), Response envelope, out-of-band updates, DisplayDelegate, and ControllerRouter integration. Use when adding routes, creating controllers, building templates with route helpers, or handling HTMX vs classic requests.
インストールへ移動 Skills Marketplace コミュニティが作成したAIスキルを発見・探索
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
直接コマンドでは確認用 Prompt が省略されます。実行前にソースを確認してください。
npx skills add https://github.com/CestusIO/claude-skills --skill htmxology-expertコマンドは1行のまま表示されます。コピー前に横へスクロールして全体を確認してください。
ローカルで確認しますか?SkillsMP が現在取得できるファイルをダウンロードできます。
Zipをダウンロード ダウンロード中... このリポジトリの他の Skills Creates professional conventional commit messages based on git changes. Use when user asks to commit, create a commit message, or wants to save changes to git.
Use the Rust compiler (rustc, cargo) to compile and test code in a way that reduces AI tokens consumption.
This skill should be used when users need help with htmx development, including implementing AJAX interactions, understanding htmx attributes (hx-get, hx-post, hx-swap, hx-target, hx-trigger), debugging htmx behavior, building hypermedia-driven applications, or following htmx best practices. Use when users ask about htmx patterns, server-side HTML responses, or transitioning from SPA frameworks to htmx. (user)
name htmxology-expert description Use when working with htmxology — the type-safe HTMX routing framework for Rust/Axum. Covers Route enums, Controller trait, RoutingController composition, template route helpers, as_htmx_attribute(), HtmxRequest detection (classic vs HTMX), Response envelope, out-of-band updates, DisplayDelegate, and ControllerRouter integration. Use when adding routes, creating controllers, building templates with route helpers, or handling HTMX vs classic requests.
htmxology Expert
htmxology is a type-safe HTMX routing framework for Rust/Axum. It replaces string-based routing with enum-based routes that are verified at compile time, and provides automatic HTMX request detection, response building, and out-of-band swap support.
Core Concepts
Routes are enums — no string URLs to mistype
Controllers handle routes — match arms dispatch to handlers
RoutingController composes controllers — hierarchical nesting with path prefixes
Templates use route helpers — type-safe URL generation in Jinja2
HtmxRequest detection — automatic classic vs HTMX differentiation
OOB updates — typed out-of-band swaps via Identity + Fragment traits
Route Definition
Define routes as enum variants with #[derive(htmxology::Route)]:
#[derive(Debug, Clone, htmxology::Route)]
pub enum CoffeesRoute {
#[route("" )]
ListCoffees (#[query] SortParams),
NewCoffee,
( CreateCoffee),
(Uuid),
(Uuid),
(Uuid, CreateCoffee),
(Uuid, CreateCoffee),
(Uuid),
}
#[route("new" )]
#[route("" , method = "POST" )]
CreateCoffee
#[body]
#[route("{id}" )]
ShowCoffee
#[route("{id}/edit" )]
EditCoffee
#[route("{id}" , method = "PUT" )]
UpdateCoffee
#[body]
#[route("{id}" , method = "POST" )]
UpdateCoffeePost
#[body]
#[route("{id}" , method = "DELETE" )]
DeleteCoffee
Route Attributes Attribute Purpose Example #[route("path")]URL path pattern #[route("{id}/edit")]method = "..."HTTP method (default: GET) method = "POST", method = "DELETE"#[query]Extract query parameters ListCoffees(#[query] SortParams)#[body]Extract request body (form/JSON) CreateCoffee(#[body] CreateCoffee)Path params Tuple fields become path segments ShowCoffee(Uuid) matches {id}
Query Parameter Serialization Use serde attributes for clean URLs — defaults are skipped:
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SortParams {
#[serde(default = "default_sort" , skip_serializing_if = "is_default_sort" )]
pub sort: String ,
#[serde(default = "default_page" , skip_serializing_if = "is_default_page" )]
pub page: i64 ,
#[serde(default, skip_serializing_if = "String::is_empty" )]
pub q: String ,
}
Controller Trait Each feature implements Controller to handle its routes:
use htmxology::{Controller, ServerInfo, htmx::Request as HtmxRequest};
impl Controller for CoffeesController {
type Route = CoffeesRoute;
type Args = AppArgs;
type Response = Result <crate::response::Response, axum::response::Response>;
async fn handle_request (
&self ,
route: Self ::Route,
_htmx: HtmxRequest,
mut parts: Parts,
_server_info: &ServerInfo,
_args: Self ::Args,
) -> Self ::Response {
let state = State (self .pool.clone ());
let auth_ctx = AuthContext::from_request_parts (&mut parts, &()).await ?;
match route {
CoffeesRoute::ListCoffees (params) => {
handlers::list_coffees (state, auth_ctx, params).await
}
CoffeesRoute::NewCoffee => handlers::show_new_coffee_form (state).await ,
CoffeesRoute::CreateCoffee (body) => {
handlers::create_coffee (state, auth_ctx, Form (body)).await
}
CoffeesRoute::ShowCoffee (id) => {
handlers::show_coffee_detail (state, auth_ctx, Path (id)).await
}
CoffeesRoute::EditCoffee (id) => {
handlers::show_edit_coffee_form (state, auth_ctx, Path (id)).await
}
CoffeesRoute::UpdateCoffee (id, body) | CoffeesRoute::UpdateCoffeePost (id, body) => {
handlers::update_coffee (state, auth_ctx, Path (id), Form (body)).await
}
CoffeesRoute::DeleteCoffee (id) => {
handlers::delete_coffee (state, auth_ctx, Path (id)).await
}
}
}
}
Controller Associated Types Type Purpose RouteThe route enum this controller handles ArgsShared context passed to handlers (e.g., DB pool + user prefs) ResponseReturn type — typically Result<Response, axum::response::Response>
Controller Constructor Pattern Controllers provide a from_app factory for the RoutingController to instantiate them:
impl CoffeesController {
pub fn new (pool: sqlx::PgPool) -> Self {
Self { pool }
}
pub fn from_app (app: &AppController) -> Self {
Self ::new (app.pool ().clone ())
}
}
RoutingController (Composite) Use #[derive(RoutingController)] to compose multiple controllers into a hierarchy:
use htmxology::RoutingController;
#[derive(Clone, RoutingController)]
#[controller(AppControllerRoute, response = Result<crate::response::Response, axum::response::Response>, args = AppArgs)]
#[subcontroller(HomeController, route = Home)]
#[subcontroller(AuthController, route = Auth, path = "auth/" , convert_response = "AppController::convert_auth_response" )]
#[subcontroller(CoffeesController, route = Coffees, path = "coffees/" , convert_with = "CoffeesController::from_app" )]
#[subcontroller(RoastersController, route = Roasters, path = "roasters/" , convert_with = "RoastersController::from_app" )]
#[subcontroller(UserController, route = User, path = "user/" , convert_with = "UserController::from_app" )]
#[subcontroller(PreferencesController, route = Preferences, path = "preferences/" , convert_response = "AppController::convert_preferences_response" )]
pub struct AppController {
pool: PgPool,
}
RoutingController Attributes Attribute Purpose #[controller(RouteName, response = ..., args = ...)]Defines the auto-generated route enum name and types route = VariantVariant name in the generated AppControllerRoute enum path = "prefix/"URL path prefix for this subcontroller convert_with = "fn"Factory function to create subcontroller from parent convert_response = "fn"Custom function to convert subcontroller response to parent response
Auto-Generated Route Enum The macro generates a composite route enum:
pub enum AppControllerRoute {
Home (HomeRoute),
Auth (AuthRoute),
Coffees (CoffeesRoute),
Roasters (RoastersRoute),
User (UserRoute),
Preferences (PreferencesRoute),
}
This enum implements Display (for URL generation) and Route (for parsing).
Response Conversion When a subcontroller has a different response type, provide a converter:
impl AppController {
fn convert_auth_response (
&self ,
_htmx: &htmxology::htmx::Request,
_parts: &http::request::Parts,
_server_info: &htmxology::ServerInfo,
_args: &AppArgs,
response: <AuthController as htmxology::Controller>::Response,
) -> <Self as htmxology::Controller>::Response {
Err (response.unwrap_or_else (|e| e))
}
}
Template Route Helpers Template structs expose methods that return AppControllerRoute values for type-safe URL generation:
use askama::Template;
#[derive(Template)]
#[template(path = "coffees/templates/list.html" )]
pub struct CoffeesListTemplate {
pub coffees: Vec <CoffeeListItem>,
pub sort: String ,
pub q: String ,
}
impl CoffeesListTemplate {
pub fn new_route (&self ) -> AppControllerRoute {
AppControllerRoute::Coffees (CoffeesRoute::NewCoffee)
}
pub fn list_route (&self ) -> AppControllerRoute {
AppControllerRoute::Coffees (CoffeesRoute::ListCoffees (SortParams::default ()))
}
pub fn detail_route (&self , id: &Uuid) -> AppControllerRoute {
AppControllerRoute::Coffees (CoffeesRoute::ShowCoffee (*id))
}
pub fn edit_route (&self , id: &Uuid) -> AppControllerRoute {
AppControllerRoute::Coffees (CoffeesRoute::EditCoffee (*id))
}
pub fn delete_route (&self , id: &Uuid) -> AppControllerRoute {
AppControllerRoute::Coffees (CoffeesRoute::DeleteCoffee (*id))
}
}
Using Routes in Jinja2 Templates Routes auto-convert to URL strings via their Display implementation:
{# Regular link — route renders as URL string #}
<a href ="{{ self.new_route() }}" class ="btn btn-primary" > + Add Coffee</a >
{# Link with path parameter #}
<a href ="{{ self.detail_route(coffee.id) }}" > {{ coffee.name }}</a >
{# HTMX GET with type-safe route #}
<input hx-get ="{{ self.list_route() }}"
hx-trigger ="input changed delay:500ms, search"
hx-target ="#coffee-list-container"
hx-select ="#coffee-list-container"
hx-swap ="innerHTML"
hx-push-url ="true" />
as_htmx_attribute() for Method-Aware RoutesFor routes with non-GET methods (POST, PUT, DELETE), use as_htmx_attribute() to automatically generate the correct hx-* attribute:
{# Generates: hx-delete="/coffees/{id}" #}
<button {{ self.delete_route (coffee.id ).as_htmx_attribute () | safe }}
hx-confirm ="Delete this coffee?"
hx-target ="closest tr"
hx-swap ="outerHTML swap:1s" >
Delete
</button >
Critical : Always use | safe filter with as_htmx_attribute() — it outputs raw HTML attributes.
Cross-Controller Routes Template route helpers can reference routes from other controllers:
impl NewCoffeeTemplate {
pub fn roaster_autocomplete_route (&self ) -> AppControllerRoute {
AppControllerRoute::Roasters (RoastersRoute::Autocomplete (AutocompleteParams::default ()))
}
}
HTMX Request Detection htmxology automatically detects HTMX requests via the HX-Request header and provides a typed enum:
use htmxology::htmx::Request as HtmxRequest;
match htmx {
HtmxRequest::Classic => {
let root = Root {
breadcrumbs: breadcrumbs.unwrap_or_default (),
main_container: MainContainer (page),
color_theme,
};
root.render_into_response ()
}
HtmxRequest::Htmx { .. } => {
let main_container = MainContainer (page);
let mut response = main_container.into_htmx_response ();
if let Some (breadcrumbs) = breadcrumbs {
response = response.with_oob (breadcrumbs);
}
response.into_response ()
}
}
The HtmxRequest::Htmx variant also carries metadata:
boosted: bool — whether hx-boost triggered the request
current_url: String — the page URL when the request was made
target: Option<...> — the hx-target element ID
trigger: Option<...> — the triggering element ID
Response Envelope Use a response enum to standardize what handlers return:
pub enum Response {
Empty,
Page {
page: Page,
breadcrumbs: Option <Breadcrumbs>,
},
Htmx (htmxology::htmx::Response<String >),
}
pub type HtmxResponse = htmxology::htmx::Response<String >;
Handler Return Patterns
Ok (Response::Page {
page: Page::Coffees (Box ::new (Pages::List (template))),
breadcrumbs: None ,
})
Ok (Response::Empty)
Err (Redirect::to ("/coffees/" ).into_response ())
Err ((StatusCode::NOT_FOUND, "Coffee not found" ).into_response ())
Out-of-Band Updates OOB swaps let a single response update multiple page sections. htmxology provides typed support.
Define an OOB Fragment Implement Identity (stable HTML id) and Fragment (swap strategy) traits:
use askama::Template;
use htmxology::htmx::{Fragment, HtmlId, Identity, InsertStrategy};
#[derive(Template, Default)]
#[template(path = "breadcrumbs.html" )]
pub struct Breadcrumbs {
items: Vec <BreadcrumbItem>,
}
impl Identity for Breadcrumbs {
fn id (&self ) -> HtmlId {
HtmlId::from_static ("breadcrumbs" ).expect ("valid id" )
}
}
impl Fragment for Breadcrumbs {
fn insert_strategy (&self ) -> InsertStrategy {
InsertStrategy::OuterHtml
}
}
Attach OOB Fragments to Responses let main_container = MainContainer (page);
let mut response = main_container.into_htmx_response ();
if let Some (breadcrumbs) = breadcrumbs {
response = response.with_oob (breadcrumbs);
}
response.into_response ()
Template-Side OOB In templates, use hx-swap-oob="true" on elements with stable IDs:
<input type ="hidden" id ="current-sort" name ="sort" value ="{{ sort }}" hx-swap-oob ="true" />
<div id ="sort-buttons" class ="d-flex gap-2" hx-swap-oob ="true" >
{% include "coffees/templates/sort_buttons.html" %}
</div >
{% include "coffees/templates/list_table.html" %}
DisplayDelegate Use #[derive(DisplayDelegate)] to auto-implement Display for page enums that delegate to their inner types:
use htmxology::DisplayDelegate;
#[derive(DisplayDelegate)]
pub enum Page {
Coffees (Box <coffees::pages::Pages>),
Roasters (roasters::pages::Pages),
User (user::pages::Pages),
}
#[derive(DisplayDelegate)]
pub enum Pages {
List (CoffeesListTemplate),
Detail (CoffeeDetailTemplate),
New (NewCoffeeTemplate),
Edit (EditCoffeeTemplate),
}
This allows {{ page|safe }} in templates to render whichever variant is active.
ControllerRouter Integration Wire the controller hierarchy into Axum:
use htmxology::ControllerRouter;
let controller_router = ControllerRouter::new (controller, |c| {
let pool = c.pool ().clone ();
async move { pool }
});
let router : axum::Router = controller_router.into ();
Server Setup use htmxology::{Server, ServerOptions};
let base_url : http::Uri = "http://localhost:3000" .parse ()?;
let options = ServerOptions {
base_url: Some (base_url),
};
let server = Server::builder (listener)
.with_options (options)
.build ();
server.serve_with_router (controller_router).await ?;
Feature Module Pattern Each feature is self-contained — Rust code and HTML templates live together in the same module directory under src/. This keeps features cohesive and easy to move or refactor.
Standard directory layout for each feature:
src/{feature}/
├── mod.rs # Module exports
├── controller.rs # Route enum + Controller impl
├── handlers.rs # Async handler functions
├── pages.rs # Pages enum with DisplayDelegate
├── templates.rs # Askama template structs + route helpers
├── data.rs # Data access layer (optional)
├── queries.rs # SQL queries (optional)
└── templates/
├── list.html # List page
├── list_table.html # Table partial (HTMX swappable)
├── list_results.html # Results with OOB updates
├── sort_buttons.html # Sort controls
├── new.html # Create form
├── edit.html # Edit form
└── detail.html # Detail view
Adding a New Feature
Create src/{feature}/ directory with the files above
Define Route enum with #[derive(htmxology::Route)]
Implement Controller trait with route matching
Create template structs with route helper methods
Wire into AppController with #[subcontroller(...)]
Common Patterns
hx-boost for SPA-like Navigation Set on the body to boost all links and forms:
<body hx-boost ="true"
hx-target ="#main-container"
hx-swap ="outerHTML"
hx-indicator =".htmx-indicator" >
All regular links become HTMX requests. The App controller detects HtmxRequest::Htmx and returns partials.
Delete with Row Removal <button {{ self.delete_route (coffee.id ).as_htmx_attribute () | safe }}
hx-confirm ="Delete this coffee?"
hx-target ="closest tr"
hx-swap ="outerHTML swap:1s" >
<i class ="bi bi-trash" > </i >
</button >
Handler returns Ok(Response::Empty) — the row is removed from the DOM.
Search + Sort + Filter Combine hx-include and hx-vals to compose query parameters from multiple inputs:
<input type ="search" name ="q"
hx-get ="{{ self.list_route() }}"
hx-trigger ="input changed delay:500ms, search"
hx-target ="#coffee-list-container"
hx-select ="#coffee-list-container"
hx-swap ="innerHTML"
hx-include ="#current-sort"
hx-push-url ="true" />
<button hx-get ="{{ self.list_route() }}"
hx-target ="#coffee-list-container"
hx-select ="#coffee-list-container"
hx-swap ="innerHTML"
hx-vals ='{"sort": "name"}'
hx-include ="#coffee-filter"
hx-push-url ="true" >
Name
</button >
RenderIntoResponse for Full Pages use htmxology::RenderIntoResponse;
root.render_into_response ()
into_htmx_response() for Partials
let response = main_container.into_htmx_response ();