Quill docs
Installation & getting started
Quill is a Rust-native framework for building server-rendered web apps with zero JavaScript by default and interactive "islands" that hydrate on demand. There is nothing to npm-install and no framework runtime to download: a Quill app is just a plain Rust binary that depends on the Qirava engine plus the Quill crates. This page takes you from an empty checkout to a running app and an exported static site, using the real quill CLI and the exact starter it generates.
Prerequisites#
You need a local checkout of the Qirava workspace (the directory that contains qdms/, qpkgs/, and qquill/) and a working Rust toolchain (Cargo, edition 2021). That is the entire toolchain — Quill itself pulls in zero third-party crates; everything a generated app depends on is std plus Qirava crates already in your checkout.
Build the quill CLI#
The scaffolder lives in the workspace at qquill/qquill-cli. The crate is named qquill-cli, but the binary it produces is called quill (declared in its Cargo.toml [[bin]] section). Build it once:
cd qquill/qquill-cli
cargo buildThis writes the binary to qquill/qquill-cli/target/debug/quill. Put that directory on your PATH, or call the binary by its full path. The rest of this guide writes quill. Confirm it works:
Request
quill --versionResponse
quill 0.1.0Output
`--version` (or `-V`) prints the crate version baked in at compile time via `CARGO_PKG_VERSION`.Running quill, quill help, quill --help, or quill -h prints the full usage banner:
Request
quill --helpResponse
quill 0.1.0 — the Qirava Quill scaffolder
USAGE:
quill new <name> Scaffold a new Quill app in ./<name>
quill build [outdir] Export the current app to a static dist/ (default: dist)
quill --help Show this help
quill --version Show the version
A Quill app is a Rust binary that serves server-rendered HTML with zero
JavaScript by default and interactive islands that hydrate on demand. `build`
renders every page in-process and writes a CDN-ready directory of plain files
(HTML per route + copied public/ assets + a Cloudflare Pages _headers) that
serves with no server running.
EXAMPLE:
quill new hello
cd hello && cargo run # -> http://127.0.0.1:7179
quill build # -> ./dist (deploy to Cloudflare Pages)Scaffold an app#
Run quill new <name> to create a new app directory in the current working directory. Run it from anywhere — the generated Cargo.toml uses absolute path dependencies, so the app does not need to live inside the Qirava checkout. The command refuses to overwrite an existing directory.
Request
quill new myappResponse
Created Quill app `myapp`.
Next steps:
cd myapp && cargo run # serve at http://127.0.0.1:7179
cargo run -- build # export a static dist/ (no server)
open http://127.0.0.1:7179Run it (serve mode)#
cargo run with no arguments (or cargo run -- serve) boots the app in serve mode: it opens an in-memory database, registers one render handler per page, declares each page as a route, builds the worker, and serves. The first build compiles the engine and the Quill crates, then the app prints exactly the URLs it serves:
Request
cd myapp
cargo runResponse
myapp serving:
http://127.0.0.1:7179/
http://127.0.0.1:7179/counterOutput
Open http://127.0.0.1:7179/. The home page is pure server-rendered HTML — view source and there is no `<script>` tag at all. Open /counter and the increment button works: the server rendered a correct static fallback, and the runtime hydrated just that one component.The default listen address is 127.0.0.1:7179. Override it at runtime with the QUILL_ADDR environment variable — no recompile needed:
QUILL_ADDR=127.0.0.1:8080 cargo runBuild a static site (build mode)#
The same app can export itself to a directory of plain files that serve from any CDN with no server, no database, and no socket. quill build (run from inside the app directory) is a thin wrapper: it shells out to cargo run --release -- build, which drives the app binary's own build subcommand. The app renders every page through the exact same code path the server uses, so the served HTML and the exported HTML are byte-identical.
Request
cd myapp
quill buildResponse
Exported 2 page(s) to dist/
dist/index.html
dist/counter/index.html
Copied 1 asset(s) from public/
Deploy dist/ to Cloudflare Pages — it serves with no DMS running.Output
The output directory defaults to `dist`. Pass a name to override it: `quill build public_site` writes `./public_site`, and `cargo run --release -- build out` writes `./out`.quill build must be run from a directory that contains a Cargo.toml (the app root). If it is not, the CLI refuses and explains why:
Request
quill buildResponse
error: no Cargo.toml in the current directory
run `quill build` from inside a Quill app dir (the one with Cargo.toml)Serve vs build at a glance#
| Aspect | Serve (`cargo run`) | Build (`quill build`) |
|---|---|---|
| Command | `cargo run` / `cargo run -- serve` | `quill build [out]` / `cargo run --release -- build [out]` |
| Database | in-memory `Qdb::new()` | none — pages rendered in-process |
| Network | listens on `QUILL_ADDR` (default `127.0.0.1:7179`) | none — writes files to disk |
| Output | live HTTP responses | `dist/` of plain files for a CDN |
| Render path | `app::respond_html` per request | `app::respond_html` once per page (same path) |
| Default | yes (no subcommand) | explicit `build` subcommand |
Generated app structure#
quill new myapp writes a small, complete app. Every file is real and compiles immediately:
myapp/
├── Cargo.toml # path deps into your Qirava checkout (zero third-party)
├── README.md
├── .gitignore # /target, Cargo.lock, /dist
├── public/
│ └── favicon.svg # static assets copied verbatim into dist/ on build
└── src/
├── main.rs # serve + build modes, both driven by the PAGES list
└── app/
├── mod.rs # document() shell + respond_html() (the island rule)
├── theme.rs # design tokens + base CSS via style!{} — re-skin here
└── routes/
├── mod.rs # the route-module list
├── index.rs # GET / — a static, zero-JS page (view!{})
└── counter.rs # GET /counter — one hydrating island- Cargo.toml
- Path dependencies into your Qirava checkout (
qdms,qexec,qvalue, and theqquill-*crates). Zero third-party deps. The[[bin]]name matches the app name. - src/main.rs
- The whole binary: the
PAGESarray,cmd_serve(open db, register handlers, insert route rows, serve), andcmd_build(render each page in-process and export a static dist). - src/app/mod.rs
document(title, extra_head, content)builds the full HTML shell;respond_html(tree)renders it to bytes, frames the response, and injects the island runtime only when the page actually uses islands.- src/app/theme.rs
- The look-and-feel: design tokens and base CSS authored with the
style!{}macro, compiled to a CSS string and inlined into every page's<head>. - src/app/routes/
- One module per page. Each exports
pub fn respond(_input: &[u8]) -> FunctionResponse.index.rsis a static page;counter.rsis one hydrating island. - public/
- Static assets served verbatim (live, and copied into
dist/on build). The starter ships afavicon.svg.