Guide

Jump start

The whole picture first: one complete Quillo app, every part of it shown and explained, before the guide takes each part on its own.

The app is a bookshelf, in five files. The home page lists books and has a form to add one. Each book has five stars you can click; the rating is saved without a page reload, and five stars throw confetti. A chart shows how the books are rated. An admin area behind a password deletes books.

It uses the parts most apps need: routes for GET, POST and /, a route group with middleware, use and import to split the program into files, a view and two components, HTML written in the code, CSS in a file and in a component, SQL on a SQLite database, and three browser libraries loaded with client, preload and preconnect.

Each file is shown whole, then explained line by line. Every word that has its own page in the docs links to it.

The files

bookshelf/├── main.qwe        the program: what it takes from the other files, the database, the routes├── lib/│   └── books.qwe   the data: a SQLite table and the SQL that reads and writes it└── ui/    ├── page.qwe    the frame every page is drawn in: a view    ├── page.css    that frame's CSS    └── shelf.qwe   what runs in the browser: two components and the libraries they call

Run it

Put the five files in a folder, then:

quillo check main.qweADMIN_PASSWORD=choose-one quillo run main.qwe --watch

quillo check reads the program and every file it uses, and says main.qwe: ok (6 routes, 8 functions, 2 components, in 4 files). quillo run starts it on http://localhost:8080; --watch starts it again when a file changes. The admin pages are at /admin, with the password you chose. Instead of typing it each time, put ADMIN_PASSWORD=... in a .env file beside main.qwe, which Quillo reads as it starts (see Settings and the environment).

main.qwe

The main file is the one quillo run is given. It takes what it needs from the other files, opens the database, and declares the routes: which address answers what.

main.qwe
// main.qwe: the program. Start it with `quillo run main.qwe --watch`.use lib/books                                   // every public name in lib/books.qweimport ui/page use Page                         // only Page from ui/page.qweimport ui/shelf use Stars, Ratingsdb.open("books.db")create_tables()route GET / {    let books = all_books()    <Page title="Bookshelf">        <h1>My bookshelf</h1>        <Ratings counts={star_counts(books)}/>        <ul>            {for b in books =>                <li><b>{b.title}</b> by {b.author} <Stars id={b.id} stars={b.stars}/></li>}        </ul>        <form method="post" action="/books">            <input name="title" placeholder="Title" required/>            <input name="author" placeholder="Author" required/>            <button>Add</button>        </form>    </Page>}route POST /books { form: { title: str, author: str } } {    if trim(title) == "" || trim(author) == "" { reply 400 "A book needs a title and an author" }    add_book(trim(title), trim(author))    redirect("/")}route GET /login =    <Page title="Sign in">        <form method="post" action="/login">            <input type="password" name="password" placeholder="Password" required/>            <button>Sign in</button>        </form>    </Page>route POST /login { form: { password: str } } {    let wanted = env("ADMIN_PASSWORD") ?? reply 503 "Set ADMIN_PASSWORD first"    if !crypto.equal(password, wanted) { reply 401 "Wrong password" }    set_session({ admin: true })    redirect("/admin")}fn admin_only(r) = session().admin == true ? r : redirect("/login")routes /admin | admin_only {    route GET / =        <Page title="Admin">            <h1>Remove books</h1>            {for b in all_books() =>                <form method="post" action={`/admin/books/${b.id}/delete`}>                    {b.title} <button>Delete</button>                </form>}        </Page>    route POST /books/:id(int)/delete {        remove_book(id)        redirect("/admin")    }}

Line by line:

A form on another site cannot post to these routes: Quillo refuses a form post the browser marks as coming from another site with 403, before any route runs (see allow_cross_site for the routes that need one).

lib/books.qwe

Everything that touches the database is in one file, so the routes read as what the site does, and the SQL is in one place.

lib/books.qwe
// lib/books.qwe: the books, kept in a SQLite table.fn create_tables() with db =    db.exec(sql`create table if not exists books (        id integer primary key,        title text not null,        author text not null,        stars integer not null default 0    )`)fn all_books() with db = db.query(sql`select id, title, author, stars from books order by id desc`)fn add_book(title: str, author: str) with db =    db.exec(sql`insert into books (title, author) values (?, ?)`, title, author)fn remove_book(id: int) with db = db.exec(sql`delete from books where id = ?`, id)fn star_counts(books: list) = for s in range(1, 6) => count(books, (b) => b.stars == s)server fn rate(id: int, stars: int) -> int ! str with db {    if stars < 1 || stars > 5 { return err("from 1 to 5 stars") }    db.exec(sql`update books set stars = ? where id = ?`, stars, id)?    stars}

Line by line:

ui/page.qwe

Every page of the app is drawn in the same frame: the <html>, the <head>, a bar of links, and the page's own content in the middle.

ui/page.qwe
// ui/page.qwe: the frame every page is drawn in.let page_css = read_file("ui/page.css")         // read once, as the program startsview Page(title: str, children) {    style global page_css    <html lang="en">        <head>            <meta charset="utf-8"/>            <meta name="viewport" content="width=device-width, initial-scale=1"/>            <title>{title}</title>        </head>        <body>            <nav><a href="/">Bookshelf</a> <a href="/admin">Admin</a></nav>            <main>{children}</main>        </body>    </html>}

Line by line:

ui/page.css

Plain CSS, in a file of its own so that an editor treats it as CSS. ui/page.qwe reads it and sends it with style global.

ui/page.css
body { margin: 0; font: 16px/1.5 system-ui, sans-serif; color: #222; }nav { display: flex; gap: 16px; padding: 12px 24px; background: #f4f1ea; }nav a { color: inherit; font-weight: 600; text-decoration: none; }main { max-width: 640px; margin: 0 auto; padding: 24px; }form { display: flex; gap: 8px; margin: 8px 0; }

Line by line:

ui/shelf.qwe

The parts of the page that run in the browser: the stars and the chart, and the three JavaScript libraries they use.

ui/shelf.qwe
// ui/shelf.qwe: the parts that run in the browser, and the libraries they call.preload "https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js" use Chartclient "https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.3/dist/confetti.browser.min.js"    use confettipreconnect "https://unpkg.com/dayjs@1.11.13/dayjs.min.js" use dayjsimport lib/books use ratecomponent Stars(id: int, stars: int) {    style css`      button { border: 0; background: none; font-size: 20px; color: #bbb; cursor: pointer; }      .on { color: #e8a317; }    `    state n = stars    state saved = null    <span>        {for s in range(1, 6) =>            <button class={s <= n && "on"} title={`${s} of 5`} onclick={() => {                n = rate(id, s) ?? n                saved = dayjs().format("HH:mm")                if s == 5 { confetti({ particleCount: 80 }) }            }}>★</button>}        {saved && <small>saved at {saved}</small>}    </span>}component Ratings(counts: list) {    <canvas height="120" onmount={(el) => {        let chart = Chart.new(el, {            type: "bar",            data: { labels: ["1 ★", "2 ★", "3 ★", "4 ★", "5 ★"], datasets: [{ label: "Books", data: counts }] },        })        () => chart.destroy()    }}></canvas>}

Line by line:

The chart is drawn from the counts the page was sent, so a new rating shows in it at the next visit; Live data is how a component follows changes as they happen.

What the browser gets

The home page's <head> ends with what Quillo added for ui/shelf.qwe and the two styles:

<link rel="preload" as="script" href="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"><link rel="preconnect" href="https://unpkg.com"><link rel="stylesheet" href="/_quillo/s/25983b01326f3815.css">

Each Stars is sent drawn, inside the island that lets the browser pick it up:

<quillo-island style="display:contents" data-c="Stars" data-p="{&quot;id&quot;:2,&quot;stars&quot;:0}" data-s="{&quot;n&quot;:0,&quot;saved&quot;:null}">  <span data-q-2ef68b9f="true"><button title="1 of 5" data-q-2ef68b9f="true">★</button>...</span></quillo-island>

And the stylesheet holds both styles: the stars' rules, scoped to their attribute, and the page's, as written:

button[data-q-2ef68b9f] { border: 0; background: none; font-size: 20px; color: #bbb; cursor: pointer; }.on[data-q-2ef68b9f] { color: #e8a317; }body { margin: 0; font: 16px/1.5 system-ui, sans-serif; color: #222; }nav { display: flex; gap: 16px; padding: 12px 24px; background: #f4f1ea; }

The sheet's name comes from what is in it, so the browser keeps it until the CSS changes.

Where each word is explained

WordIn this appExplained in
routeroute GET / { ... }Routes
GET, POSTthe methods: open a page, send a formRoutes
/the home page's path; /admin, /books/:id(int)/deleteRoutes
routesthe /admin group and its middlewareRoute groups, the guide
useuse lib/booksFiles of a program
importimport ui/page use PageWhat a file sees
clientthe confetti libraryJavaScript libraries
preloadChart.jsJavaScript libraries
preconnectDay.jsJavaScript libraries
HTML<html>, <form>, <li>: markup in the codeHTML literals
viewPageVariables, functions, views
componentStars, RatingsComponents
statethe rating shown, the time it was savedComponents, Reactivity
style, style globalStars' CSS, the page's CSSStyling a component
CSSui/page.css, css`...`css, Styling a component
SQLsql`select ...`sql, Database
db.open, db.exec, db.querythe databasedb.open, db.exec, db.query
server fnrateServer functions
with dbthe functions of lib/books.qweEffects
replyreply 400 "..."What a route returns
err, ?, ??rate's check, env(...) ?? reply 503err, Errors
redirectafter each formredirect
session, set_sessionsigning insession, set_session, Cookies and sessions
env, crypto.equalthe admin passwordenv, crypto.equal
read_filereading ui/page.cssread_file
trim, range, countsmall helperstrim, range, count
quillo check, quillo runchecking and running itChecking, Options of quillo run

From here, the Introduction covers installing and the commands, the Language tour the language, and Structuring a program how a larger program is split into files.

Thread
6Useful? Like it, or add to its thread below.

Thread 0 posts

On Jump start: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.