diff --git a/README.md b/README.md new file mode 100644 index 0000000..47b0323 --- /dev/null +++ b/README.md @@ -0,0 +1,179 @@ +# Servable: a simple web framework + +[![CI](https://github.com/rm-dr/servable/workflows/CI/badge.svg)](https://github.com/rm-dr/servable/actions) +[![Cargo](https://img.shields.io/crates/v/servable.svg)](https://crates.io/crates/servable) +[![API reference](https://docs.rs/servable/badge.svg)](https://docs.rs/servable/) + +A tiny, convenient web micro-framework built around [htmx](https://htmx.org), [Axum](https://github.com/tokio-rs/axum), and [Maud](https://maud.lambda.xyz). +Inspired by the "MASH" stack described [here](https://yree.io/mash) and [here](https://emschwartz.me/building-a-fast-website-with-the-mash-stack-in-rust). + + + +## 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](https://htmx.org) integration (see `htmx-*` features below) + + +------------------- + + +## Quick Start + +```rust,ignore +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: + ```rust + use servable::{StaticAsset, mime::MimeType}; + + let asset = StaticAsset { + bytes: b"body { color: red; }", + mime: MimeType::Css, + }; + ``` + +- `Redirect`, for simple http redirects: + ```rust + use servable::Redirect; + + let redirect = Redirect::new("/new-location").unwrap(); + ``` + +- `HtmlPage`, for dynamically-rendered HTML pages + ```rust + 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 `` and wraps its rendered html in ``. + + + +## `ServableRouter` + +A `ServableRouter` exposes a collection of `Servable`s under different routes. It implements `tower`'s `Service` trait, and can be easily be converted into an Axum `Router`. Construct one as follows: + +```rust +# use servable::{ServableRouter, StaticAsset, mime::MimeType}; +# let home_page = StaticAsset { bytes: b"home", mime: MimeType::Html }; +# let about_page = StaticAsset { bytes: b"about", mime: MimeType::Html }; +# let stylesheet = StaticAsset { bytes: b"css", mime: MimeType::Css }; +# let custom_404_page = StaticAsset { bytes: b"404", mime: MimeType::Html }; +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... + ```rust + # use servable::{ServableRouter, StaticAsset, mime::MimeType}; + let route = ServableRouter::new() + .add_page( + "/image.png", + StaticAsset { + bytes: b"fake image data", + mime: MimeType::Png, + } + ); + ``` + ...may be accessed as follows: + + ```r + # Original image + GET /image.png + + # Resize to max 800px on longest side + GET /image.png?t=maxdim(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);crop(400,400);format(webp) + ``` + + +- `htmx-2.0.8`: Include htmx sources in the compiled executable. \ + Use as follows: + ```rust + # 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: + +```rust +use chrono::TimeDelta; +use servable::HtmlPage; + +let page = HtmlPage::default() + .with_ttl(Some(TimeDelta::hours(1))) + .with_immutable(false); +``` + +Headers are automatically generated: +- `Cache-Control: public, max-age=3600` +- `Cache-Control: immutable, public, max-age=31536000` (for immutable assets) diff --git a/crates/servable/README.md b/crates/servable/README.md new file mode 100644 index 0000000..47b0323 --- /dev/null +++ b/crates/servable/README.md @@ -0,0 +1,179 @@ +# Servable: a simple web framework + +[![CI](https://github.com/rm-dr/servable/workflows/CI/badge.svg)](https://github.com/rm-dr/servable/actions) +[![Cargo](https://img.shields.io/crates/v/servable.svg)](https://crates.io/crates/servable) +[![API reference](https://docs.rs/servable/badge.svg)](https://docs.rs/servable/) + +A tiny, convenient web micro-framework built around [htmx](https://htmx.org), [Axum](https://github.com/tokio-rs/axum), and [Maud](https://maud.lambda.xyz). +Inspired by the "MASH" stack described [here](https://yree.io/mash) and [here](https://emschwartz.me/building-a-fast-website-with-the-mash-stack-in-rust). + + + +## 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](https://htmx.org) integration (see `htmx-*` features below) + + +------------------- + + +## Quick Start + +```rust,ignore +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: + ```rust + use servable::{StaticAsset, mime::MimeType}; + + let asset = StaticAsset { + bytes: b"body { color: red; }", + mime: MimeType::Css, + }; + ``` + +- `Redirect`, for simple http redirects: + ```rust + use servable::Redirect; + + let redirect = Redirect::new("/new-location").unwrap(); + ``` + +- `HtmlPage`, for dynamically-rendered HTML pages + ```rust + 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 `` and wraps its rendered html in ``. + + + +## `ServableRouter` + +A `ServableRouter` exposes a collection of `Servable`s under different routes. It implements `tower`'s `Service` trait, and can be easily be converted into an Axum `Router`. Construct one as follows: + +```rust +# use servable::{ServableRouter, StaticAsset, mime::MimeType}; +# let home_page = StaticAsset { bytes: b"home", mime: MimeType::Html }; +# let about_page = StaticAsset { bytes: b"about", mime: MimeType::Html }; +# let stylesheet = StaticAsset { bytes: b"css", mime: MimeType::Css }; +# let custom_404_page = StaticAsset { bytes: b"404", mime: MimeType::Html }; +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... + ```rust + # use servable::{ServableRouter, StaticAsset, mime::MimeType}; + let route = ServableRouter::new() + .add_page( + "/image.png", + StaticAsset { + bytes: b"fake image data", + mime: MimeType::Png, + } + ); + ``` + ...may be accessed as follows: + + ```r + # Original image + GET /image.png + + # Resize to max 800px on longest side + GET /image.png?t=maxdim(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);crop(400,400);format(webp) + ``` + + +- `htmx-2.0.8`: Include htmx sources in the compiled executable. \ + Use as follows: + ```rust + # 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: + +```rust +use chrono::TimeDelta; +use servable::HtmlPage; + +let page = HtmlPage::default() + .with_ttl(Some(TimeDelta::hours(1))) + .with_immutable(false); +``` + +Headers are automatically generated: +- `Cache-Control: public, max-age=3600` +- `Cache-Control: immutable, public, max-age=31536000` (for immutable assets)