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

Wisp

Getting Started

A folder is a URL, and its +page.wisp is the page: an optional block of Rust, then markup. Templates compile to Rust, and the app, styles and static files become one binary.

Install and Run

You need Rust 1.89 or later.

cargo install --git https://wisp.ar0.eu/ --tag v1.0.0-pre.3 wisp-web
wisp new my-app        # --template demo|minimal|api
cd my-app
wisp dev               # http://127.0.0.1:3000

Other systems, prerequisites for Windows, Linux and macOS, and the version history are on Changelog and Downloads.

wisp new asks a few questions. Press Enter for the defaults or add -y.

wisp dev rebuilds as you edit and keeps your $state. Alt+Shift+W opens the devtools with the routes table.

A Page

src/routes/+page.wisp:

---
let name = cx.query_or("name", "world".to_string());
let count = 0;
---

<title>Hello, ${name}</title>
<meta name="description" content="A greeting from the server, with a button that counts clicks in the browser.">
<h1>Hello, ${name}!</h1>

<button on:click="count++">Clicked {:count} times</button>

The --- block is Rust that runs per request. ${name} renders on the server. {:count} is JavaScript state in the browser. A literal let alone on its line is browser state, with nothing computed per request. With JavaScript off, the server's HTML still works.

Data and a Form

A model lives in src/db.rs, and its pub items are in every route file:

// src/db.rs
#[model]
struct Todo {
    #[validate(len = 1..=100)]
    text: String,
}
pub static TODOS: Table<Todo> = Table::saved();

An action handles the form, and fields writes a labelled input per parameter:

---
#[action]
fn add(todo: Todo) {
    TODOS.add(todo)?;
}

let count = TODOS.len();
---

<title>Your todo list (${count})</title>
<meta name="description" content="Add todos with a form; each one is kept in a saved table.">
<h1>Todos</h1>
<form action="?/add" fields><button>Add</button></form>
{#each TODOS.all() as todo}
  <p>${todo.text}</p>
{/each}

A bad value gives a 422 with each error next to its input and the typed text kept, with no JavaScript. There is no use line: a prelude brings in Cx, Table, Email, redirect, error and the rest.

Files

src/main.rs                 wisp::main!();   (generated; leave it)
src/app.html                shell with %wisp.head% %wisp.body% (optional)
src/app.css | app.scss      served at /_app/app.css (Tailwind if it imports it; Sass, no Node)
src/hooks.rs                fn init() once; fn before(cx) every request
src/db.rs                   models and tables; its pub items are in every route file
src/components/Card.wisp    <Card title=${x}>...</Card>
src/routes/.../+page.wisp   page: one file, a --- Rust block (load, actions, endpoints), then markup
src/routes/.../+layout.wisp wraps pages below; must <slot />
src/routes/.../+template.wisp like a layout, fresh on each navigation
src/routes/.../+error.wisp  error page
src/routes/.../+server.rs   endpoints: fn get/post/...; beside a page, a browser gets the page
src/routes/.../+page.md     Markdown page
static/                     served at /

Folders: blog static, [slug] param, [[lang]] optional, [...rest] rest, [id=int] digits, (group) not in the URL. More in Files and routes.

Check, Test, Build

wisp check      # templates, routes, accessibility lints
wisp test       # your tests (wisp test --browser drives a real browser)
wisp fmt        # format
wisp build      # one release binary

Other builds: wisp build --static for any static host, wisp deploy init docker for a container, wisp build --target cloudflare|deno|vercel|netlify|node|bun|lambda for edge and serverless. See Deploying.

Next

Read the Quick Start, then the Tutorial. With an AI agent, use AGENTS.md or claude mcp add wisp -- wisp mcp.

All Pages

Start

Design

Browser Code

APIs

Data and Auth

Deploy and Run

Hosting

Reference

Project

Is This Page Useful?

View This Page's Source

Type to search every docs page and heading.