sppl Svelte Themes

Sppl

sppl - supple: embedding static svelte in rust

sppl

sppl (supple) — embed static Svelte apps into your Rust binary.

A Svelte/SvelteKit app, built with adapter-static, is just a tree of HTML/CSS/JS files. sppl bakes that tree into your Rust binary at compile time and hands it to your web framework as a single Router (or a generic asset lookup) that already knows how to:

  • serve every file with the correct Content-Type,
  • resolve SvelteKit adapter-static <route>.html files for prerendered routes,
  • fall back to index.html for client-side SPA routes,
  • store one gzipped copy of each compressible asset and serve it as-is with Content-Encoding: gzip, regardless of Accept-Encoding (modern clients all decompress transparently); flip RouterConfig::never_decompress to false to opt back into on-the-fly decompression for legacy clients,
  • ship as a single self-contained binary — no extra files to deploy.

https://github.com/user-attachments/assets/010351d4-e685-4aa2-9c9e-3d1294adb904

Compression

Run sppl::build::gzip_assets from your build.rs once, after your Svelte build, and sppl takes care of the rest at request time. Because only the gzipped bytes live in the binary, the default request path is zero-CPU: every response is the stored gzipped bytes, sent with Content-Encoding: gzip. Modern clients (browsers, curl --compressed, every common HTTP library) decompress transparently; the rare client that genuinely can't accept gzip can be served via router_with(RouterConfig { never_decompress: false }), which restores on-the-fly decompression with [flate2].

Layout

crates/sppl/         # the library
examples/app/        # SvelteKit + adapter-static demo (built with deno)
examples/server/     # axum server that embeds the demo

Usage

# Cargo.toml
[dependencies]
sppl  = "0.0.3"
axum  = "0.7"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
use axum::{routing::get, Router};

#[derive(sppl::RustEmbed)]
#[folder = "$CARGO_MANIFEST_DIR/../app/build"]
#[crate_path = "sppl::rust_embed"]
struct App;

#[tokio::main]
async fn main() {
    let api = Router::new()
        .route("/api/hello", get(|| async { "hello from rust" }));

    // Serve the embedded Svelte app on every other path.
    let app = api.fallback_service(sppl::axum::router::<App>());

    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

If you'd rather wire things up yourself, sppl::resolve::<App>(path) returns the matching (path, EmbeddedFile) using the same lookup rules and is framework-agnostic.

Running the example

# from the repo root — the server's build.rs runs `deno task build` for you:
cargo run -p sppl-example-server

# …or build the svelte app yourself:
deno task --cwd=examples/app build
SPPL_SKIP_SVELTE_BUILD=1 cargo run -p sppl-example-server

Set SPPL_SKIP_SVELTE_BUILD=1 to skip the build-script step (useful in CI when the build is produced upstream).

Then open http://127.0.0.1:3000.

Testing

cargo test -p sppl

Covers accepts_gzip header parsing and the resolve lookup rules (exact path, .html extension, trailing-slash index.html, SPA fallback, and .gz preference). Fixture files live under crates/sppl/tests/fixtures/static/.

Requirements

  • Rust (1.75+ recommended) — cargo
  • Deno 2.x — drives the SvelteKit build via deno task

License

MIT

Top categories

Loading Svelte Themes