Cheatsheet

The whole authoring language on one page.

Everything you write, on one page. Pages and templating has the rules behind each line.

Commands

crisptastic init my-site     # create a site
crisptastic dev              # serve it on 127.0.0.1:6174 and reload on save
crisptastic check .          # compile everything; use this in CI
crisptastic routes .         # list every route
crisptastic deploy           # build the signed, encrypted Capsule

Run them inside the site's folder: crisptastic.toml is read from the current directory. The command line reference has every option.

Files become URLs

FileURL
index.html.crisp/
about.html.crisp/about
contact/index.html.crisp/contact
blog/[slug].html.crisp/blog/:slug, read request.route.params.slug
api/data.json.crisp/api/data.json
sitemap.xml.crisp/sitemap.xml
_partials/_nav.html.crispnever served; the file name starts with _
404.html.crisperror page, top level only (also 400, 500, error)
assets/style.scss/style.css, compiled
assets/logo.svg/logo.svg, no /assets/ in the URL

Printing

{{ user.name }}          escaped; a dotted path or a pipe only
{{= 1 + 2 }}             escaped; any expression
{{~ post.html }}         raw; only for what you trust
{{-- a comment --}}      prints nothing

A missing path is null and prints nothing. Indexing null is an error.

Expressions

KindWrite
Equal, not equala = b, a != b. == is an error
Compare<, <=, >, >=
Logicand, or, not, !
Maths and joining+ - * /. + joins when either side is a string
Indexitems[0], a["key"]. items[0].title is an error, so write items[0]["title"]
Pipename | trim | upper is upper(trim(name))

Strings use double quotes only. Falsy values are null, false, 0, "", [] and {}. Query and form values are always strings, so use num(...).

Functions

upper lower trim len contains starts_with ends_with json str num bool exists empty merge form_urlencode sda asset secure_token. You can't define your own in a page.

Control flow

@if (user) {
  <p>Hello, {{ user.name }}</p>
} @else {
  <p><a href="/login">Sign in</a></p>
}

@for (post in posts) {
  <li>{{ post.title }}</li>
}

@for ((key, value) in settings) {
  <dt>{{ key }}</dt><dd>{{ value }}</dd>
}

@switch (order.status) {
  @case ("paid") { <b>Paid</b> }
  @default { <b>Unknown</b> }
}

@let title = "About"
@include("_partials/_head.html.crisp")

There is no else if and no loop index. @include takes a quoted path relative to the file, and the partial shares the page's scope.

What a page can read

PathHolds
request.method, request.pathThe request line
request.query.xQuery string
request.body.xSubmitted form or JSON
request.cookies.xCookies
request.route.params.xDynamic segments
context.session.xThe visitor's session
context.csrf.tokenGoes in a hidden _csrf field on every form
context.env.x[env] values, in dev, run and serve only

Effects

They apply after the page finishes, and not at all if it fails.

@status 404
@redirect "/dashboard"
@cookie theme = "dark"
@cookie theme delete
@session cart = 3
@session cart delete

Redirects are always 303. Cookies have no options. A host refuses session and cookie changes during a GET, so write them in the POST branch.

Fetching and shaping

@fetch post = get_json("https://api.example.com/posts/1")

@if (post.ok) {
  <h1>{{ post.value.title }}</h1>
} @else {
  @status 502
  <p>The API is having a moment.</p>
}

A failed fetch doesn't throw, so always check .ok. Reshape data with @shape or sda(...); Fetching and shaping data has the query language.

Declaring a route

[routes."/f/subscribe"]
methods = ["POST"]
body = "form"
max_body_bytes = 4096
rate = [{ per = "client", limit = 10, window = "1m" }]

The host answers a request that breaks this before your page runs. See [routes].

Line-directive traps

  • Put @status, @session, @cookie, @fetch, @call, @shape and @let on their own lines. They swallow a } on the same line.
  • } closes the innermost block, even in text. Print a literal one with &#125;.
  • @header, @cache and @telemetry don't exist. They print as text.
  • @include( is found by plain text search, even in comments.