1
0
mirror of https://github.com/rm-dr/servable.git synced 2025-11-28 05:19:33 -08:00
Files
servable/crates/servable/README.md
rm-dr 8674bd9c85
All checks were successful
CI / Check typos (push) Successful in 32s
CI / Check links (push) Successful in 13s
CI / Clippy (push) Successful in 2m58s
CI / Build and test (push) Successful in 8m36s
README
2025-11-27 20:39:36 -08:00

5.7 KiB

Servable: a simple web framework

CI Cargo API reference

A minimal, convenient web micro-framework built around htmx, Axum, and Maud.
This powers my homepage. See example usage here.

Features

servable provides abstractions that implement common utilities needed by an http server.

  • response headers and cache-busting utilities
  • client device detection (mobile / desktop)
  • server-side image optimization (see the image feature below)
  • ergonomic htmx integration (see htmx-* features below)

Quick Start

use servable::{ServableRouter, servable::StaticAsset, mime::MimeType};

#[tokio::main]
async fn main() {
	let route = ServableRouter::new()
		.add_page(
			"/hello",
			StaticAsset {
				bytes: b"Hello, World!",
				mime: MimeType::Text,
			},
		);

	// usual axum startup routine
	let app = route.into_router();
	let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
		.await
		.unwrap();

	axum::serve(listener, app).await.unwrap();
}

Core Concepts

The Servable trait

The Servable trait is the foundation of this stack.
servable provides implementations for a few common servables:

  • StaticAsset, for static files like CSS, JavaScript, images, or plain bytes:

    use servable::{StaticAsset, mime::MimeType};
    
    let asset = StaticAsset {
    	bytes: b"body { color: red; }",
    	mime: MimeType::Css,
    	ttl: StaticAsset::DEFAULT_TTL
    };
    
  • Redirect, for simple http redirects:

    use servable::Redirect;
    
    let redirect = Redirect::new("/new-location").unwrap();
    
  • HtmlPage, for dynamically-rendered HTML pages

    use servable::{HtmlPage, PageMetadata};
    use maud::html;
    use std::pin::Pin;
    
    let page = HtmlPage::default()
    	.with_meta(PageMetadata {
    		title: "My Page".into(),
    		description: Some("A great page".into()),
    		..Default::default()
    	})
    	.with_render(|_page, ctx| {
    		Box::pin(async move {
    			html! {
    				h1 { "Welcome!" }
    				p { "Route: " (ctx.route) }
    			}
    		})
    	});
    

    HtmlPage automatically generates a <head> and wraps its rendered html in <html><body>.

ServableRouter

A ServableRouter exposes a collection of Servables under different routes. It implements tower's Service trait, and can be easily be converted into an Axum Router. Construct one as follows:

# use servable::{ServableRouter, StaticAsset, mime::MimeType};
# let home_page = StaticAsset { bytes: b"home", mime: MimeType::Html, ttl: StaticAsset::DEFAULT_TTL};
# let about_page = StaticAsset { bytes: b"about", mime: MimeType::Html, ttl: StaticAsset::DEFAULT_TTL };
# let stylesheet = StaticAsset { bytes: b"css", mime: MimeType::Css, ttl: StaticAsset::DEFAULT_TTL };
# let custom_404_page = StaticAsset { bytes: b"404", mime: MimeType::Html, ttl: StaticAsset::DEFAULT_TTL };
let route = ServableRouter::new()
	.add_page("/", home_page)
	.add_page("/about", about_page)
	.add_page("/style.css", stylesheet)
	.with_404(custom_404_page); // override default 404

Features

  • image: enable image transformation via query parameters. This makes tokio a dependency.
    When this is enabled, all StaticAssets with a valid mimetype can take an optional t= query parameter.
    See the TransformerEnum in this crate's documentation for details.

    When image is enabled, the image below...

    # use servable::{ServableRouter, StaticAsset, mime::MimeType};
    let route = ServableRouter::new()
    	.add_page(
    		"/image.png",
    		StaticAsset {
    			bytes: b"fake image data",
    			mime: MimeType::Png,
    			ttl: StaticAsset::DEFAULT_TTL
    		}
    	);
    

    ...may be accessed as follows:

    # Original image
    GET /image.png
    
    # Resize to max 800px on longest side
    GET /image.png?t=maxdim(800,800)
    
    # Crop to a 400x400 square at the center of the image
    GET /image.png?t=crop(400,400,c)
    
    # Chain transformations and transcode
    GET /image.png?t=maxdim(800,800);crop(400,400);format(webp)
    
  • htmx-2.0.8: Include htmx sources in the compiled executable.
    Use as follows:

    # use servable::ServableRouter;
    # #[cfg(feature = "htmx-2.0.8")]
    let route = ServableRouter::new()
    	.add_page("/htmx.js", servable::HTMX_2_0_8)
    	.add_page("/htmx-json-enc.js", servable::EXT_JSON_1_19_12);
    

Caching and cache-busting

Control caching behavior per servable:

use chrono::TimeDelta;
use servable::HtmlPage;

let page = HtmlPage::default()
	.with_ttl(Some(TimeDelta::hours(1)))
	.with_private(false);

Headers are automatically generated:

  • Cache-Control: public, max-age=3600 (default)
  • Cache-Control: private, max-age=31536000 (if private is true)

We also provide a static CACHE_BUST_STR, which may be formatted into urls to force cache refresh whenever the server is restarted:

use chrono::TimeDelta;
use servable::{HtmlPage, CACHE_BUST_STR, ServableWithRoute, StaticAsset, ServableRouter};
use servable::mime::MimeType;

pub static HTMX: ServableWithRoute<StaticAsset> = ServableWithRoute::new(
	|| format!("/{}/main.css", *CACHE_BUST_STR),
	StaticAsset {
		bytes: "div{}".as_bytes(),
		mime: MimeType::Css,
		ttl: StaticAsset::DEFAULT_TTL,
	},
);


let route = HTMX.route();
println!("Css is at {route}");

let router = ServableRouter::new()
	.add_page_with_route(&HTMX);

TODO:

  • cache-busting fonts in css is not possible, we need to dynamic replace urls