Tutorials
A static site: Handbook, from soda new --static to deployed
A static Soda site is drawn once, as it's built, into files any host serves: Vercel, Netlify,
GitHub Pages, Cloudflare Pages, a bucket. Its pages are written as on any backend (routes as
folders, view! and components, layouts, typed links), and soda dev patches each change into
the open pages while you work. What a page reads is what the site is made of, loaded once as it's
built, and its address. sodakit.dev is one: its docs are this repository's docs/, read as it's
built (site/).
This builds the template's handbook and says what each part does. It's
templates/static in the SDK.
1. A new site#
&& field-guide/
Cargo.toml soda, soda-static, and pulldown-cmark for the Markdown
pages/ the chapters, a Markdown file each
src/
main.rs soda_static::app!(handbook::Handbook)
handbook.rs what the pages are drawn from: the chapters, read from pages/
components.rs what the pages share
routes/
layout.rs around every page
page.rs /, the contents
[chapter]/
page.rs /:chapter, a page for each chapter
not_found.rs built as 404.html
web/
app.html the shell each page is drawn into
app.css the look: its design tokens
tests/e2e.mjs real browsers on the built site2. What the pages are drawn from#
The site's state is a soda_static::App: any Send + Sync type, loaded by load as the site is
built. It's read from the site's folder (the one with its Cargo.toml), however the program is
run, and an error from it stops the build, saying why:
WATCH names what it's read from. Under soda dev, a change there loads it again and draws every
open page again, as a change to the code does: a chapter saved is in the open page in about a
quarter of a second.
A page reads it as cx.state, and src/main.rs names it:
app!;3. Pages#
A page is a function of what it reads and its address, as on any backend:
// src/routes/page.rs: the contents
Its head is written into the page's own file as it's built: the title in <title> (after the
site's name, Contents · Handbook) and og:title, the description in
<meta name="description"> and og:description, wherever the shell has the tag. A search engine
and a shared link read them before anything has run.
4. A page for each#
src/routes/[chapter] is an address with a param. Which pages to build is the page's params, a
Params for each:
// src/routes/[chapter]/page.rs
use Params;
use crate*;
A route with params and no params is a compile error that says so: there'd be no telling which
pages to build. A link to one is a function, as anywhere: link::chapter("welcome"). params
may be async, as pages and layouts may.
5. Building it#
dist/ is then the whole site:
index.html | /, the contents |
welcome/index.html, … | each chapter, in its own folder, so /welcome is served as it is |
404.html | not_found.rs's page, which every static host serves for an address that isn't there |
app.css, soda.js, … | the look, and Soda in the page |
sitemap.xml, robots.txt | with the site's url (below) |
A page its route's params named that then says it isn't there (NotFound), or goes wrong, fails
the build, each said with its address. Nothing posts to a site of files, so it has no actions: a
route with an #[action] is a compile error that says so, and the same pages move to a server or
local-first when it needs some.
Say where it's served and each page says its own address too (<link rel="canonical"> and
og:url, where the shell has them), and the build writes the sitemap:
app!;A download (#[download]) is built once, into a file at its address, so it takes only its cx:
// src/routes/downloads.rs
href=Search links to it, and the build writes it to dist/api/_/search.
6. In the browser#
Each page is a file, and moving between them is the browser's own: a link loads the next page,
and Back goes back. #app says the page is built ahead of time (data-static), so soda.js
leaves links to the browser and tells no backend where the page is. A crossfade between pages is
one line of the site's CSS (the template's web/app.css has it):
@ }A page is drawn at its path alone, the same for everyone: its query, cx.recall and anything of a
reader's own are the browser's, and cx.now() is when the site was built.
7. Under soda dev#
soda dev runs the site's program as a server while you work on it: each page drawn as it's asked
for, as the build would draw it. dx patches each change to its code into the running program, and
every open page is drawn again in place (about 0.6 s for a page), as on a server. A chapter saved
(WATCH) is in the open pages at once. A page that panics shows where, in its place.
8. Testing#
soda check runs the site's own tests (cargo test: the template reads its own chapters), Clippy
and the token check. soda test then builds it and runs tests/e2e.mjs: real browsers on the
built site, served as a static host serves it (each file as it is, 404.html for what isn't one),
checking each page's own head, the links between them, the 404, and that nothing asked for a
backend.
9. Deploying#
dist/ goes anywhere that serves files. soda deploy publishes it with Vercel (the Vercel CLI,
linked to the site's project): it builds the site here as vercel.json says (scripts/build.sh,
into dist/), then publishes what it built, with the headers vercel.json sends (its
Content-Security-Policy among them). To build it on a host instead (a Vercel or Netlify project
connected to its repository, CI), depend on the SDK's repository rather than a path, so Cargo.lock
pins the commit every build uses (the template's Cargo.toml says how), and have the host run
scripts/vercel-install.sh, then scripts/build.sh, and serve dist/.
This page is docs/static.md in the SDK, built into this site as it is.