Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,6 @@ jobs:
buildandtest:
name: "Build and test"
runs-on: ubuntu-latest
permissions:
contents: write

steps:
- uses: actions/checkout@v4
Expand All @@ -71,3 +69,6 @@ jobs:

- name: Test
run: cargo test-all-features -- --release

- name: Publish
run: cargo publish --dry-run
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 14 additions & 14 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ resolver = "2"
[workspace.package]
rust-version = "1.90.0"
edition = "2024"
version = "0.0.2"
version = "0.0.3"
license = "GPL-3.0"
repository = "https://github.com/rm-dr/servable"
readme = "README.md"
Expand Down Expand Up @@ -70,16 +70,16 @@ cargo_common_metadata = "deny"

[workspace.dependencies]

axum = "0.8.7"
chrono = "0.4.42"
image = "0.25.9"
maud = "0.27.0"
rand = "0.9.0"
serde = { version = "1.0.228", features = ["derive"] }
serde_urlencoded = "0.7.1"
strum = { version = "0.27.2", features = ["derive"] }
thiserror = "2.0.17"
tokio = "1.48.0"
tower = "0.5.2"
tower-http = { version = "0.6.7", features = ["compression-full"] }
tracing = "0.1.41"
axum = "0.8"
chrono = "0.4"
image = "0.25"
maud = "0.27"
rand = "0.9"
serde = { version = "1.0", features = ["derive"] }
serde_urlencoded = "0.7"
strum = { version = "0.27", features = ["derive"] }
thiserror = "2.0"
tokio = "1.48"
tower = "0.5"
tower-http = { version = "0.6", features = ["compression-full"] }
tracing = "0.1"
179 changes: 0 additions & 179 deletions README.md

This file was deleted.

1 change: 1 addition & 0 deletions README.md
57 changes: 43 additions & 14 deletions crates/servable/README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
# Servable: a simple web framework

### TODO:
- cache-bust fonts in css (dynamic replace in css (fonts))


[![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).


A minimal, convenient web micro-framework built around [htmx](https://htmx.org), [Axum](https://github.com/tokio-rs/axum), and [Maud](https://maud.lambda.xyz). \
This powers [my homepage](https://betalupi.com). See example usage [here](https://git.betalupi.com/Mark/webpage/src/branch/main/crates/service/service-webpage/src/routes/mod.rs).

## Features

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

- response headers and cache-busting utilities
- client device detection (mobile / desktop)
Expand Down Expand Up @@ -63,6 +65,7 @@ The `Servable` trait is the foundation of this stack. \
let asset = StaticAsset {
bytes: b"body { color: red; }",
mime: MimeType::Css,
ttl: StaticAsset::DEFAULT_TTL
};
```

Expand Down Expand Up @@ -104,10 +107,10 @@ A `ServableRouter` exposes a collection of `Servable`s under different routes. I

```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 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)
Expand All @@ -129,6 +132,7 @@ let route = ServableRouter::new()
StaticAsset {
bytes: b"fake image data",
mime: MimeType::Png,
ttl: StaticAsset::DEFAULT_TTL
}
);
```
Expand All @@ -139,13 +143,13 @@ let route = ServableRouter::new()
GET /image.png

# Resize to max 800px on longest side
GET /image.png?t=maxdim(800)
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);crop(400,400);format(webp)
GET /image.png?t=maxdim(800,800);crop(400,400);format(webp)
```


Expand All @@ -171,9 +175,34 @@ use servable::HtmlPage;

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

Headers are automatically generated:
- `Cache-Control: public, max-age=3600`
- `Cache-Control: immutable, public, max-age=31536000` (for immutable assets)
- `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:

```rust
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);
```
6 changes: 6 additions & 0 deletions crates/servable/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
#![doc = include_str!("../README.md")]
// readme is symlinked to the root of this repo
// because `cargo publish` works from a different dir,
// and needs a different relative path than cargo build.
// https://github.com/rust-lang/cargo/issues/13309

pub mod mime;

Expand Down Expand Up @@ -44,6 +48,7 @@ pub static CACHE_BUST_STR: std::sync::LazyLock<String> = std::sync::LazyLock::ne
pub const HTMX_2_0_8: servable::StaticAsset = servable::StaticAsset {
bytes: include_str!("../htmx/htmx-2.0.8.min.js").as_bytes(),
mime: mime::MimeType::Javascript,
ttl: StaticAsset::DEFAULT_TTL,
};

/// HTMX json extension, 1.19.2.
Expand All @@ -53,4 +58,5 @@ pub const HTMX_2_0_8: servable::StaticAsset = servable::StaticAsset {
pub const EXT_JSON_1_19_12: servable::StaticAsset = servable::StaticAsset {
bytes: include_str!("../htmx/json-enc-1.9.12.js").as_bytes(),
mime: mime::MimeType::Javascript,
ttl: StaticAsset::DEFAULT_TTL,
};
7 changes: 7 additions & 0 deletions crates/servable/src/mime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,13 @@ impl<'de> Deserialize<'de> for MimeType {

impl Default for MimeType {
fn default() -> Self {
Self::const_default()
}
}

impl MimeType {
/// [Default::default], but const
pub const fn const_default() -> Self {
Self::Blob
}
}
Expand Down
Loading