Wisp

Type to search every docs page and heading.

Quick Start

Welcome to the Wisp docs. This page gives you a tour of the concepts you will use every day.

You Will Learn

  • How to make an app and run it
  • How to write a page and a component
  • How to add state in the browser
  • How to handle a form with validation
  • How to save data in a table
  • How to build and deploy

Create an App

You need Rust 1.88 or later. Install the wisp command, make an app and start the dev server:

cargo install --git https://wisp.ar0.eu wisp-cli
wisp new guestbook --template minimal
cd guestbook
wisp dev

Open http://127.0.0.1:3000. wisp dev rebuilds as you edit, and hot reload keeps your browser state. A folder under src/routes is a URL, and the +page.wisp inside it is the page.

Creating a Page

A page is a .wisp file. It may start with a block of Rust between two --- lines, then comes markup. src/routes/hello/+page.wisp is served at /hello:

---
let name: String = cx.query_or("name", "world".to_string());
---

<h1>Hello, {name}!</h1>

The block runs on the server for each request, and {name} writes a value into the HTML, escaped. Open /hello?name=Ada. A folder named [id=int] is a parameter that matches digits, and the block gets it as a local, id: u64.

Using a Component

A component is a .wisp file in src/components. Its {@props} line lists what it takes. src/components/Note.wisp:

{@props name, message}
<article class="note">
  <h2>{name}</h2>
  <p>{message}</p>
</article>

<style>
  .note {
    border-left: 3px solid var(--accent, #7c5cff);
    padding-left: 1rem;
  }
</style>

Use it by its file name, with props as attributes:

<Note name="Ada" message="Hello" />

The <style> block applies to this component only. Props are checked when the app builds, so a missing one is a build error and not a blank page.

Adding State

Browser code lives in the same file as the markup. A top-level let in a <script> is state, {:count} shows it, and a directive such as on:click changes it:

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

<script>
  let count = $state(0)
</script>

A write redraws only the parts of the page that read what changed. Without JavaScript the server's HTML still works, which is why forms and links do not depend on a script. See Browser code.

Server Values and Browser Values

{name} is Rust, evaluated once on the server. {:count} and the quoted value of on:click are JavaScript, evaluated in the browser. A name a page's Rust block makes is also available by name in browser code, as long as its type is a #[model] or derives Json. See Server values and client blocks.

Handling a Form

A form posts to an action. The action's parameters are the form's fields, and fields writes a labelled input for each one. Put the model in src/db.rs, where every route file can see it:

#[model]
pub struct Entry {
    #[validate(len = 1..=40)]
    name: String,
    #[validate(len = 1..=200)]
    message: String,
}

Then handle the post in a page:

---
#[action]
fn sign(entry: Entry) {
    ENTRIES.add(entry);
    redirect("/")
}
---

<form action="?/sign" fields>
  <button>Sign</button>
</form>

An empty name, or a message over 200 characters, answers with a 422. The page is drawn again with each problem beside its input and what was typed kept. The inputs also get the matching browser checks (required, minlength), and the server checks every value again.

Saving Data in a Table

A Table holds rows of a model. Table::saved() keeps them in a log file in the data folder, so they survive a restart, and Table::new() keeps them in memory. In src/db.rs:

pub static ENTRIES: Table<Entry> = Table::saved();

Read and change it from any page:

---
let entries = ENTRIES.all();
---

<title>Guestbook ({entries.len()})</title>
{#each entries.iter().rev() as entry}
  <Note name={entry.name.as_str()} message={entry.message.as_str()} />
{:else}
  <p>No notes yet. Be the first.</p>
{/each}

add, get, all, find, filter, update, set, remove and len cover most needs. A row has an id and reads as the value you stored. See Data, files and jobs.

Where the Rows Are Kept

Saved tables live in the folder named by WISP_DATA: .wisp/data in dev and data next to the binary in a release build. On a host with no disk, point a table at a database with a custom store. See Where rows are kept.

Check, Test and Build

wisp check      # templates, routes, accessibility lints
wisp test       # your tests
wisp fmt        # format
wisp build      # one release binary

wisp build writes a single binary with the styles and static files inside. Copy it to a server and run it. It listens on $HOST:$PORT, which is 0.0.0.0:3000 in a release build.

Deploying

Pick the build for your host:

You haveRun
A VPS or serverwisp build, then wisp service install to keep it running
A container hostwisp build --docker
A static hostwisp build --static (pages with forms need a server)
Cloudflare, Deno, Vercel, Netlify or Lambdawisp build --target <host>

wisp deploy init <host> writes a GitHub Actions workflow or the host's config. An app that signs cookies needs WISP_SECRET set to 32 or more random characters on every host. See Deploying.

Next Steps

You now know most of what an everyday Wisp app uses. Where to go next:

Is This Page Useful?

Edit This Page