Skip to content

Commit 7e61c0a

Browse files
committed
README
1 parent b56ced5 commit 7e61c0a

2 files changed

Lines changed: 358 additions & 0 deletions

File tree

README.md

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
# Servable: a simple web framework
2+
3+
[![CI](https://github.com/rm-dr/servable/workflows/CI/badge.svg)](https://github.com/rm-dr/servable/actions)
4+
[![Cargo](https://img.shields.io/crates/v/servable.svg)](https://crates.io/crates/servable)
5+
[![API reference](https://docs.rs/servable/badge.svg)](https://docs.rs/servable/)
6+
7+
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).
8+
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).
9+
10+
11+
12+
## Features
13+
14+
`servable` provides abstractions that implement common utilities needed by an http server. \
15+
16+
- response headers and cache-busting utilities
17+
- client device detection (mobile / desktop)
18+
- server-side image optimization (see the `image` feature below)
19+
- ergonomic [htmx](https://htmx.org) integration (see `htmx-*` features below)
20+
21+
22+
-------------------
23+
24+
25+
## Quick Start
26+
27+
```rust,ignore
28+
use servable::{ServableRouter, servable::StaticAsset, mime::MimeType};
29+
30+
#[tokio::main]
31+
async fn main() {
32+
let route = ServableRouter::new()
33+
.add_page(
34+
"/hello",
35+
StaticAsset {
36+
bytes: b"Hello, World!",
37+
mime: MimeType::Text,
38+
},
39+
);
40+
41+
// usual axum startup routine
42+
let app = route.into_router();
43+
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
44+
.await
45+
.unwrap();
46+
47+
axum::serve(listener, app).await.unwrap();
48+
}
49+
```
50+
51+
# Core Concepts
52+
53+
## The `Servable` trait
54+
55+
The `Servable` trait is the foundation of this stack. \
56+
`servable` provides implementations for a few common servables:
57+
58+
59+
- `StaticAsset`, for static files like CSS, JavaScript, images, or plain bytes:
60+
```rust
61+
use servable::{StaticAsset, mime::MimeType};
62+
63+
let asset = StaticAsset {
64+
bytes: b"body { color: red; }",
65+
mime: MimeType::Css,
66+
};
67+
```
68+
69+
- `Redirect`, for simple http redirects:
70+
```rust
71+
use servable::Redirect;
72+
73+
let redirect = Redirect::new("/new-location").unwrap();
74+
```
75+
76+
- `HtmlPage`, for dynamically-rendered HTML pages
77+
```rust
78+
use servable::{HtmlPage, PageMetadata};
79+
use maud::html;
80+
use std::pin::Pin;
81+
82+
let page = HtmlPage::default()
83+
.with_meta(PageMetadata {
84+
title: "My Page".into(),
85+
description: Some("A great page".into()),
86+
..Default::default()
87+
})
88+
.with_render(|_page, ctx| {
89+
Box::pin(async move {
90+
html! {
91+
h1 { "Welcome!" }
92+
p { "Route: " (ctx.route) }
93+
}
94+
})
95+
});
96+
```
97+
`HtmlPage` automatically generates a `<head>` and wraps its rendered html in `<html><body>`.
98+
99+
100+
101+
## `ServableRouter`
102+
103+
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:
104+
105+
```rust
106+
# use servable::{ServableRouter, StaticAsset, mime::MimeType};
107+
# let home_page = StaticAsset { bytes: b"home", mime: MimeType::Html };
108+
# let about_page = StaticAsset { bytes: b"about", mime: MimeType::Html };
109+
# let stylesheet = StaticAsset { bytes: b"css", mime: MimeType::Css };
110+
# let custom_404_page = StaticAsset { bytes: b"404", mime: MimeType::Html };
111+
let route = ServableRouter::new()
112+
.add_page("/", home_page)
113+
.add_page("/about", about_page)
114+
.add_page("/style.css", stylesheet)
115+
.with_404(custom_404_page); // override default 404
116+
```
117+
118+
# Features
119+
- `image`: enable image transformation via query parameters. This makes `tokio` a dependency. \
120+
When this is enabled, all `StaticAssets` with a valid mimetype can take an optional `t=` query parameter. \
121+
See the `TransformerEnum` in this crate's documentation for details.
122+
123+
When `image` is enabled, the image below...
124+
```rust
125+
# use servable::{ServableRouter, StaticAsset, mime::MimeType};
126+
let route = ServableRouter::new()
127+
.add_page(
128+
"/image.png",
129+
StaticAsset {
130+
bytes: b"fake image data",
131+
mime: MimeType::Png,
132+
}
133+
);
134+
```
135+
...may be accessed as follows:
136+
137+
```r
138+
# Original image
139+
GET /image.png
140+
141+
# Resize to max 800px on longest side
142+
GET /image.png?t=maxdim(800)
143+
144+
# Crop to a 400x400 square at the center of the image
145+
GET /image.png?t=crop(400,400,c)
146+
147+
# Chain transformations and transcode
148+
GET /image.png?t=maxdim(800);crop(400,400);format(webp)
149+
```
150+
151+
152+
- `htmx-2.0.8`: Include htmx sources in the compiled executable. \
153+
Use as follows:
154+
```rust
155+
# use servable::ServableRouter;
156+
# #[cfg(feature = "htmx-2.0.8")]
157+
let route = ServableRouter::new()
158+
.add_page("/htmx.js", servable::HTMX_2_0_8)
159+
.add_page("/htmx-json-enc.js", servable::EXT_JSON_1_19_12);
160+
```
161+
162+
163+
164+
## Caching and cache-busting
165+
166+
Control caching behavior per servable:
167+
168+
```rust
169+
use chrono::TimeDelta;
170+
use servable::HtmlPage;
171+
172+
let page = HtmlPage::default()
173+
.with_ttl(Some(TimeDelta::hours(1)))
174+
.with_immutable(false);
175+
```
176+
177+
Headers are automatically generated:
178+
- `Cache-Control: public, max-age=3600`
179+
- `Cache-Control: immutable, public, max-age=31536000` (for immutable assets)

crates/servable/README.md

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
# Servable: a simple web framework
2+
3+
[![CI](https://github.com/rm-dr/servable/workflows/CI/badge.svg)](https://github.com/rm-dr/servable/actions)
4+
[![Cargo](https://img.shields.io/crates/v/servable.svg)](https://crates.io/crates/servable)
5+
[![API reference](https://docs.rs/servable/badge.svg)](https://docs.rs/servable/)
6+
7+
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).
8+
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).
9+
10+
11+
12+
## Features
13+
14+
`servable` provides abstractions that implement common utilities needed by an http server. \
15+
16+
- response headers and cache-busting utilities
17+
- client device detection (mobile / desktop)
18+
- server-side image optimization (see the `image` feature below)
19+
- ergonomic [htmx](https://htmx.org) integration (see `htmx-*` features below)
20+
21+
22+
-------------------
23+
24+
25+
## Quick Start
26+
27+
```rust,ignore
28+
use servable::{ServableRouter, servable::StaticAsset, mime::MimeType};
29+
30+
#[tokio::main]
31+
async fn main() {
32+
let route = ServableRouter::new()
33+
.add_page(
34+
"/hello",
35+
StaticAsset {
36+
bytes: b"Hello, World!",
37+
mime: MimeType::Text,
38+
},
39+
);
40+
41+
// usual axum startup routine
42+
let app = route.into_router();
43+
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
44+
.await
45+
.unwrap();
46+
47+
axum::serve(listener, app).await.unwrap();
48+
}
49+
```
50+
51+
# Core Concepts
52+
53+
## The `Servable` trait
54+
55+
The `Servable` trait is the foundation of this stack. \
56+
`servable` provides implementations for a few common servables:
57+
58+
59+
- `StaticAsset`, for static files like CSS, JavaScript, images, or plain bytes:
60+
```rust
61+
use servable::{StaticAsset, mime::MimeType};
62+
63+
let asset = StaticAsset {
64+
bytes: b"body { color: red; }",
65+
mime: MimeType::Css,
66+
};
67+
```
68+
69+
- `Redirect`, for simple http redirects:
70+
```rust
71+
use servable::Redirect;
72+
73+
let redirect = Redirect::new("/new-location").unwrap();
74+
```
75+
76+
- `HtmlPage`, for dynamically-rendered HTML pages
77+
```rust
78+
use servable::{HtmlPage, PageMetadata};
79+
use maud::html;
80+
use std::pin::Pin;
81+
82+
let page = HtmlPage::default()
83+
.with_meta(PageMetadata {
84+
title: "My Page".into(),
85+
description: Some("A great page".into()),
86+
..Default::default()
87+
})
88+
.with_render(|_page, ctx| {
89+
Box::pin(async move {
90+
html! {
91+
h1 { "Welcome!" }
92+
p { "Route: " (ctx.route) }
93+
}
94+
})
95+
});
96+
```
97+
`HtmlPage` automatically generates a `<head>` and wraps its rendered html in `<html><body>`.
98+
99+
100+
101+
## `ServableRouter`
102+
103+
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:
104+
105+
```rust
106+
# use servable::{ServableRouter, StaticAsset, mime::MimeType};
107+
# let home_page = StaticAsset { bytes: b"home", mime: MimeType::Html };
108+
# let about_page = StaticAsset { bytes: b"about", mime: MimeType::Html };
109+
# let stylesheet = StaticAsset { bytes: b"css", mime: MimeType::Css };
110+
# let custom_404_page = StaticAsset { bytes: b"404", mime: MimeType::Html };
111+
let route = ServableRouter::new()
112+
.add_page("/", home_page)
113+
.add_page("/about", about_page)
114+
.add_page("/style.css", stylesheet)
115+
.with_404(custom_404_page); // override default 404
116+
```
117+
118+
# Features
119+
- `image`: enable image transformation via query parameters. This makes `tokio` a dependency. \
120+
When this is enabled, all `StaticAssets` with a valid mimetype can take an optional `t=` query parameter. \
121+
See the `TransformerEnum` in this crate's documentation for details.
122+
123+
When `image` is enabled, the image below...
124+
```rust
125+
# use servable::{ServableRouter, StaticAsset, mime::MimeType};
126+
let route = ServableRouter::new()
127+
.add_page(
128+
"/image.png",
129+
StaticAsset {
130+
bytes: b"fake image data",
131+
mime: MimeType::Png,
132+
}
133+
);
134+
```
135+
...may be accessed as follows:
136+
137+
```r
138+
# Original image
139+
GET /image.png
140+
141+
# Resize to max 800px on longest side
142+
GET /image.png?t=maxdim(800)
143+
144+
# Crop to a 400x400 square at the center of the image
145+
GET /image.png?t=crop(400,400,c)
146+
147+
# Chain transformations and transcode
148+
GET /image.png?t=maxdim(800);crop(400,400);format(webp)
149+
```
150+
151+
152+
- `htmx-2.0.8`: Include htmx sources in the compiled executable. \
153+
Use as follows:
154+
```rust
155+
# use servable::ServableRouter;
156+
# #[cfg(feature = "htmx-2.0.8")]
157+
let route = ServableRouter::new()
158+
.add_page("/htmx.js", servable::HTMX_2_0_8)
159+
.add_page("/htmx-json-enc.js", servable::EXT_JSON_1_19_12);
160+
```
161+
162+
163+
164+
## Caching and cache-busting
165+
166+
Control caching behavior per servable:
167+
168+
```rust
169+
use chrono::TimeDelta;
170+
use servable::HtmlPage;
171+
172+
let page = HtmlPage::default()
173+
.with_ttl(Some(TimeDelta::hours(1)))
174+
.with_immutable(false);
175+
```
176+
177+
Headers are automatically generated:
178+
- `Cache-Control: public, max-age=3600`
179+
- `Cache-Control: immutable, public, max-age=31536000` (for immutable assets)

0 commit comments

Comments
 (0)