Wisp is in pre-release, self-hosted since GitHub suspended us. Install it, or see the changelog.

Wisp

Knobs, Features and Settings

Const Knobs

Write a const in a page's --- block (or its mod server), a layout's block, a +server.rs or a +page.rs.

KnobWhat it does
const CACHE: u32 = 60;keep a GET's answer 60 s per worker. Never with cookies or authorization, not in dev. An {#await} that reads cx is a hole
const CACHE_PUBLIC: u32 = 60;like CACHE, for every request. Sends cache-control: public, s-maxage=60 (and stale-while-revalidate=<CACHE_STALE>) so a CDN can keep it
const CACHE_STALE: u32 = 600;seconds past CACHE to serve the old answer while one request renews it
const CACHE_TAGS: &[&str] = &["posts"];names for the answer; wisp::revalidate_tag("posts") drops it, wisp::stale_tag("posts") marks it stale
const RATE_LIMIT: u32 = 60;requests a minute per client address, then 429. An export does not count. A page's limit runs before Wisp reads a multipart upload over 64 KB
const CORS: &str = "*";same as cx.cors("*")?, and answers the preflight
const CSRF: bool = false;+server.rs or mod server: opt out of the cross-site refusal of POST, PUT, PATCH, DELETE. In a page it is a build error
const TIMEOUT: u32 = 5;503 after 5 seconds (0: no limit)
const TRAILING_SLASH: TrailingSlash = Always;page/layout trailing slash, over wisp::trailing_slash
const EARLY_HINTS: bool = false;page/layout: no early hints (see Serve)
const EARLY_HINTS_H1: bool = true;page/layout: send the 103 over HTTP/1.1 too (off by default)
const PRECACHE: bool = false;page/layout: with "offline": "all", leave this page out of the worker's precache
const COMPRESS: bool = true;gzip this page's or endpoint's answers (see Serve). Off by default
const MIDDLEWARE: &[&str] = &["auth"];run those functions of src/middleware.rs first; an Err answers
const SIGNED_IN: bool = true;in a page, layout or +server.rs: pages, actions, #[remote] fns and endpoints are for members only. Other visitors get a 303 to sign in (401 for JSON). A +server.rs under the layout passes it too
const PRERENDER: bool = true;render at build; fn entries() lists params, or the crawl finds the pages that prerendered pages link to (wisp build --no-crawl: no crawl; a dead link, or a link to an #id that the linked page lacks, fails the build, --allow-dead-links warns; Deploy). Anything per visitor is a build error: cx, {@flash}, <form once>, <form guard>, t("key") with more than one locale
const SSR: bool = false;page/layout: the browser draws the page. SSR, CSR, PRERENDER, EARLY_HINTS, PRECACHE and TRAILING_SLASH in a +server.rs are a build error
const CSR: bool = false;in a page or layout: the page gets no wisp.js, and its components start no browser code. A client script, a +page.js, SSR = false, an {#await}, CACHE or a layout or component with browser code there is a build error. A <form use:enhance> there posts and loads the whole page, with no JavaScript (No-JS pages). The client router loads such a page whole when its head has <meta name="wisp-csr" content="off">: an html hook that rewrites the head (minifies it, changes the attribute order) makes the router miss the marker
const BODY_LIMIT: usize = 20 * wisp::MB;largest body the route takes (413 past it; 1 MB)
const RUNTIME: wisp::Runtime = wisp::Runtime::Edge;on --target vercel or netlify, also an edge function

[package.metadata.wisp]

These keys live in Cargo.toml and are baked in at build. script, strict and typecheck are errors in cargo build, wisp build, wisp check, wisp openapi and wisp client, and warnings in wisp dev.

KeyWhat it does
scriptsscripts = false: no page names a script, not even wisp.js, and the policy is script-src 'none', on any site. Pages are pure HTML and CSS for visitors with JavaScript off (No-JS and Tor onion sites). The build stops on browser code (on:, {:state}, <script>, islands, src/lib/*.js, custom elements, a service worker) and on a value that is not true or false
public_envpublic_env = true: the server reads the PUBLIC_* variables of the process and the .env files once per process, at its start and writes them in each page head as one <script type="application/json">. Browser code reads them with import { env } from 'wisp' (env.PUBLIC_API_URL). No other variable goes in. Without it, that import is a build error and env.PUBLIC_X with no import is filled in at build. An edge target (wasm) refuses it: its host gives variables to each request, not at the start. The JSON is 16 KiB at most: a larger set stops the server at its start and names the variable that crosses the cap. A prerendered page (PRERENDER) that the server sends holds the server's values from its start. The server sends such a page without a brotli copy. wisp build --static runs the server on the build machine, so the exported pages hold that machine's values. wisp::test reads the environment of the test process at the first client of the process. A later client in the same process keeps those values.
version_pollversion_poll = 60: a release build's pages read /_app/version.json every 60 seconds (1 to 86400) while they show, one request at a time, and set updated when the file names another build. The build id is a hash of each file in src/ and static/, of build.rs, Cargo.toml and the nearest Cargo.lock (up to 8 parent folders, with the Cargo.toml beside it). The poll module (/_app/c/poll.js) loads only with this key. Dev builds do not poll
base = "/app"serve under a base path: / first, none last (WISP_BASE)
assets = "https://cdn.example.com"link built /_app/ files from this https host or /path (WISP_ASSET_BASE)
redirects = [...]"from to [status]" (308 by default; Design and files)
rewrites = [...]serve one path from another
headers = [...]headers by path
has:..., missing:...conditions at the end of a redirect, rewrite or header rule (a cookie, header, host or query parameter), as Next's has/missing (Design and files)
cors = [...]CORS by path: "/api/[...p] https://app.example.com" or "/pub/[...p] *" (pattern origins); answers the preflight and adds the headers (API requests)
images = [...]remote pictures wisp::img::serve may fetch, like Next's remotePatterns: "https://cdn.example.com/photos/**". With none, none is fetched. /_img never sends the query to the origin (Tooling)
i18n = [...]locale routing: default, prefix, domain, missing (Tooling)
use = [...]plugin crates the build loads
auto = [...]dependency items every file may use with no use line: "chrono::{Utc, DateTime}", "x::Y as Z" (Pages)
extends = ["../base"]layers: an app's layout, components and static files under yours
script = "ts"TypeScript only: every <script> must be lang="ts", no <script src> of a local file, and no .js, .mjs, .cjs, .jsx or .tsx in src/. Data scripts (application/ld+json, importmap) and https:// scripts are fine. "any" (default) allows both
strict = truethe build's warnings (accessibility, SEO, translations) are errors. false by default
typecheck = truewisp build and wisp check run wisp check --types and fail on errors, or on a missing tsc (TypeScript). false by default
[package.metadata.wisp.dev.proxy]wisp dev only. "/api" = "http://localhost:8788" forwards that path (longest prefix first, before app routes) to an http:// or https:// server, WebSocket included. X-Forwarded-* headers are added, and Host is the target's. An https:// target gets TLS with SNI, its certificate checked against the web's roots plus the PEM file in SSL_CERT_FILE (Environment), and a failed handshake is a 502 that says why. An unreachable target is a 502. May also be written proxy = { ... } under [package.metadata.wisp.dev] (CLI)
[package.metadata.wisp.dev] runwisp dev only: companion commands, run = ["npx wrangler@4 pages dev dist --port 8788", "npm run css:watch"], or named under [package.metadata.wisp.dev.run] (api = "npx wrangler ..."). They start before the app with WISP_DEV_PORT set, output prefixed [api] or [1], and stop when wisp dev ends. One that exits is reported once and not restarted. taskkill /T follows parent links on Windows
prefix = "ui"in a component library: the tag prefix, <ui:Button>
prefixes = { "acme-ui" = "acme" }in an app: another prefix for a library, when two share one
[package.metadata.wisp.vercel]wisp build --target vercel only: function settings (Vercel functions)

Vercel Functions

[package.metadata.wisp.vercel]
regions = ["iad1"]       # Vercel region ids; both runtimes
max_duration = 60        # seconds, or "max"; the Node function
memory = 1024            # MB, 128 to 10240; the Node function

These go into .vc-config.json as regions, maxDuration and memory. The Edge runtime takes regions only. An unknown key or out-of-range value stops the build. See Host on Vercel.

The Cloudflare targets read your Wrangler config (wrangler.jsonc, wrangler.json or wrangler.toml) and keep all of it but Wisp's keys. See Your wrangler config.

Plugin Crates and Layers

A plugin is a crate with a wisp/routes and a wisp/components folder (and a src/lib.rs, which can be empty). List it as a dependency and in use:

[dependencies]
kit = { path = "../kit" }

[package.metadata.wisp]
use = ["kit"]

The build copies kit/wisp/routes into src/routes/(kit)/ and kit/wisp/components into src/components/kit/, both git-ignored. Use <Badge label="x" /> like any component. Your own component of the same name wins, and two plugins shipping one name is a clash. The dependency must be a path or registry crate. Build once (cargo build or wisp dev) before wisp check, wisp openapi or wisp build --analyze.

extends = ["../base"] does the same with a whole app: src/routes (into (layer_base)), src/components, static/, src/app.css (first) and src/lib.

  • Your route at the same URL wins. A plugin route you also have is a duplicate-route error.
  • $lib/x.js (and .ts) is your file, else the first layer's in extends order.
  • src/fonts.txt and src/locales come from the first layer that has one, when you have none.
  • If a layer and the app pin a package at different versions, Wisp warns and uses the app's.
  • Layers do not carry Rust (db.rs, hooks.rs).

Cargo Features

FeatureWhat it does
tls (wisp)https:// for wisp::fetch (rustls)
h2 (wisp)HTTP/2 with prior knowledge (h2c) on the built-in server
img (wisp)wisp::img::serve: /_img?src=&w=&q= resizes pictures
og-png (wisp and wisp-cli)PNG Open Graph pictures for wisp::og(.., "auto")
wasm-alloc (wisp, on by default)the edge build's allocator. Use default-features = false for your own #[global_allocator]
tower (wisp)the app as a Tower service (Testing and mixing with Rust code)
browser (wisp)wisp::test::browser: tests in headless Chrome or Edge
types (wisp)used by wisp check --types, never in a served build
avif (wisp-cli)AVIF widths for images in wisp build

Smaller Public Types

NameWhat it is
Channel, Subscriptionwisp::channel(name) and what its subscribe() receives
Queuea named job queue from wisp::queue
SpanGuardfrom wisp::span; the span ends on drop
TrailingSlashAlways, Never, Ignore for wisp::trailing_slash
PrefixOptional, Always, AsNeeded, from wisp::prefix()
AsDatewhat format_date takes: epoch seconds, "2026-10-04" or (y, m, d)
Resourcethe trait #[derive(Rest)] implements
ClientModule, ExportRoutemade by generated code

wisp::native_name("fr") is a locale's name in its own language (Français).

Is This Page Useful?

View This Page's Source

Type to search every docs page and heading.