SurfWave
SurfWave v0
SurfWave: routes from files, data loads, actions and the server of an app.This page as MarkdownStatusv0 (ADR-0055)
Related documentsLanguage spec v0, Component reference, Security
SurfWave gives a project routes from files. A project with a src/routes folder has no src/App.surf. The compiler writes the root component.A fixed part comes before a parameter, and a parameter comes before a rest. Two pages with one URL are a compile error. surf routes lists the routes.A layout prop that has the name of a field of the data of a page gets that field while the page shows. Give it the default none.Import it with import { Wave } from "surf:wave". ~/x.surfs is a path from src.A <Link href="/x"> with no on:press goes to the route.The module also has the types and functions of the server:A form is an element with role="form", method="post" and action="/path?/name", where name is a function of actions.surfs. The value of each Input with a name is in request.form. A form works with no script. The server refuses a form from another site. Build a full app has a complete example.href of a Link and action of a form take a relative URL, http, https, mailto and tel. The element gets no attribute for another scheme, because a scheme such as javascript: runs a script in the page (ADR-0109).surf build --target server writes build/server/<name>-server. HOST is its address, and PORT is its port. ORIGIN is the address of the site behind a proxy. API_ORIGIN is the http address for a relative Http request, and for /api/. Security lists each setting and each limit of the server. surf build --target static writes each page as a file.The files of netlify, vercel and cloudflare send each unknown URL to 404.html. With --base /docs/, the rule names the prefix: cloudflare writes /docs/* /docs/404.html 200 (ADR-0064, ADR-0139).A public async function of a *.remote.surf file runs on the server, and a component calls it as a local function (ADR-0063):<Link> takes preload="off" or "tap", reload, replace, noScroll and keepFocus.Page transitions (ADR-0154).
File
Meaning
Page.surf
The page of the URL of its folder
Layout.surf
The frame of each page in its folder. It declares slot default, and it shows the page with @slot default.
Error.surf
The page for a failed load in its folder
load.surfs
The data of the folder: load for the page and layout for the layout
load.server.surfs
The same, on the server only. The browser reads /__data.json.
Page@.surf
A page with no layout around it
server.surfs
Endpoints: pub async fn get(request: Request) -> Response, also post, put, patch and delete
actions.surfs
Forms: pub async fn name(request: Request) -> Result<T, Failure>
Folder
Meaning
tickets
The fixed part tickets of the URL
[id]
A route parameter id
[...rest]
The rest of the URL, as the parameter rest. It can be empty.
(app)
A group: it can hold a layout, and it adds nothing to the URL
[id=int], [id=uuid], [id=name]
A parameter with a matcher. name is the function name of params/name.surfs.
[[lang]]
A parameter that a URL can leave out. Its value is then empty.
surf
// src/routes/tickets/[id]/load.surfsimport { AppData } from "../../load.surfs";import { ticketDetail } from "~/api.surfs";import { TicketDetail } from "~/model.surfs";pub type TicketData = { detail: TicketDetail, user: User }pub async fn load(id: String, parent: AppData) -> Result<TicketData, String> { const detail = await ticketDetail(id)?; return .ok({ detail: detail, user: parent.user });}
- load and layout must be pub async fn. They return Result<T, String> or Result<T, Failure>.
- A parameter has the name of a route parameter, or the name parent. parent is the data of the nearest outer layout with a layout function.
- later next to load gives a page a value that it does not wait for: pub async fn later(id: String) -> Result<Slow, String>. The page has prop later: Slow? = none. later takes route parameters. On a first visit, the server sends the page after load, and it sends the value in the same response. After a navigation, the value comes when it is ready. A failed later leaves the prop at none (ADR-0062).
- load.server.surfs takes parent too. When the browser asks for its data, the server runs the load functions of the outer layouts first, also for a function with no parent. The first failure stops the request, so a guard in a layout function holds for the data of each page in its folder. The server does not read a value of the browser (ADR-0109).
- A load function of the server runs only for a URL of its route. The hook before reads the path of the page, not /__data.json.
- The types of two load functions must have different names. The result type of a load.server.surfs file must be in a file that the browser can read.
- Failure is { status, message, redirect }. failure(status, message), redirect(url) and notFound() make one. A String error has status 500.
- The desktop build has each file of the server that such a function imports: a *.server.surfs file, and a file in a server folder. It has no endpoint (server.surfs), no form action (actions.surfs) and no hooks (hooks.server.surfs).
- The load functions of the outer layouts run first, also for a function with no parent, as on a server. The first failure stops the navigation. So a guard in a layout function holds on the desktop target too.
- The hooks do not run, because a desktop app gets no requests. A guard in before holds only on a server. Put a guard that each target needs in a layout function. A desktop build of an app with hooks gives a warning.
- The app keeps its cookies as a browser does: in its data folder, until maxAge ends. A maxAge of 0 or less removes the cookie. A navigation reads the cookies that the app has at its start. So a cookie of Wave.setCookie counts from the next navigation, as in a browser, which sends a new cookie with the next request.
- Wave.env reads the environment of the process of the app, in each file. The compiler checks the names in a file that the browser also reads, as for the web (section 5).
Prop
Value
A field of the data
That field
data
The whole data
A route parameter
Its text
path
The path of the URL
url
The path and the query. It changes with each navigation.
navigating
True while a new route loads
failure
The Failure, on an error page
form
The result of the last action of the page, as JSON text. It is empty before an action.
Function
Meaning
Wave.go(url), Wave.replace(url), Wave.back()
Navigation
Wave.reload()
Runs each load function of the route again
Wave.path(), Wave.url()
The URL
Wave.query(name), Wave.hasQuery(name), Wave.queryOf(url, name)
The query of the URL
Wave.title(text)
The title of the page. Call it in an effect of the page.
Wave.notify(text), on Wave.notified(text) { ... }
A message from a page to a layout
Name
Meaning
Request
method, url, path, headers, cookies, form, params (the route parameters of a server.surfs file) and body. The maps have String keys and values. path has one form (section 5).
Response
status, headers and body
textResponse(body), jsonResponse(body), response(status, kind, body)
A response. kind is the media type.
redirectResponse(url), methodNotAllowed(), failedResponse(failure)
A response for these cases
Failure, failure(status, message), redirect(url), notFound()
The error of a load or of an action
- Without HOST, the server binds to 127.0.0.1, so only its own machine reaches it (ADR-0109). A container needs HOST=0.0.0.0, and the Dockerfile of the adapters docker and fly sets it. The unit of systemd keeps the default, because a proxy on the same machine is in front of it.
- surf build --base /docs/ serves the site at the base path /docs/, also with the server target, the static target and an adapter (ADR-0139). The base path is the prefix of each address. The page, the router, each Link and each address of a file get the prefix. Each request to the server code gets it too. These requests are the data of a page (/__data.json), a form action, a remote function (/__remote/), an endpoint and /__version. An href or an action that starts with / gets the prefix, and a relative href or an href with a scheme does not change. In the browser, a relative Http request with a path that starts with / gets the prefix too, because it goes to an endpoint or to /api/. Wave.base() gives the prefix without the last /, also on the server.
- With the server target, the static target and an adapter, the base path starts and ends with /. Its parts have only letters, digits, -, ., _ and ~.
- The root of the site is the base path, for example /docs/. The server sends status 308 from /docs to /docs/. A request outside the base path gets status 404, before a hook runs.
- The code of the server sees each path without the prefix. A hook, a load, an action, an endpoint and a remote function read /about for the address /docs/about. So the code of an app does not change with its place on the host. A redirect of this code names a path without the prefix, and the server adds the prefix to the header location.
- The static target writes each page at the root of its folder, for example about/index.html. The host serves the folder at the base path.
- Not in version 0: a picture of src/assets reads its file relative to the address of the page. So on a page below the root of the site, a path such as assets/logo.png names no file. An example of such a page is /docs/notes/1. A web app with no routes gives the base path only to its script and its stylesheet, not to a Link, a form or an Http request.
- src/hooks.server.surfs can have pub async fn before(request: Request) -> Response? and pub async fn after(request: Request, response: Response) -> Response. For /__data.json, request has the path of the page.
- pub const ssr = false, csr = false, prerender = false and entries = ["/items/1"] in load.surfs apply to the folder and to its subfolders.
- A file with the name *.server.surf, and a file in a server folder, are not in the script of the page.
- Wave.cookie(name), Wave.setCookie(name, value, maxAge) and Wave.env(name) read and write the request, in a load, an action, an endpoint, a hook and a remote function. A cookie has HttpOnly, SameSite=Lax and Path=/, and Secure except on this machine. In the browser, env gives only what the page sets in surfEnv.
- In a file that the browser also reads, the argument of Wave.env must be a name in quotes that starts with PUBLIC_. Another argument is a compile error (ADR-0109). A file of the server can read each variable: *.server.surfs, server.surfs, actions.surfs, *.remote.surfs, and a file in a server folder.
- A relative Http request goes to API_ORIGIN with the cookie and authorization headers of the client. An absolute URL gets neither. The server has http: only.
- Locale (language spec, section 14) takes a locale for each request. The server takes the tags of the Accept-Language header in the order of their weights, and finds an item of the list locales of surf.toml with the lookup of RFC 4647 (language spec, section 14). So de-CH fits de. Without a fit, it is the first item of the list. A load, an action, an endpoint, a hook, a remote function and the page that the server sends use this locale. The page hydrates with the same locale: the element of the app has the tag in data-locale, and the script takes it. Locale.set in the browser changes the locale after that. The server formats with the ICU of the system, as the native targets do. So a few texts can differ from the texts of the browser, and they change at hydration as each other value that differs does. A page of surf build --target static has no request, so it has no data-locale, and the browser chooses the locale with its languages.
- After Locale.set in the browser, the client sends the header x-wave-locale with each data request (/__data.json), each action and each remote call (ADR-0132). Its value is the tag of Locale.current. For that request, the server uses this tag in place of Accept-Language, with the same rule of the fit. A new load of the page has no such header, because Locale.set does not save the locale.
- A response that depends on the locale has the header Vary: Accept-Language, x-wave-locale, so a cache keeps one copy for each locale (ADR-0132). A response depends on the locale when its code reads Locale. Each page reads it, because the page has the tag in data-locale.
- A server of an app with Locale loads ICU when it starts. Without ICU, the server stops at its start with an error that names the package (ADR-0132). This applies to a system and to the image of a container. The Dockerfile of an adapter must then install the package of ICU of its image.
- A server has no Rust crates of its own: an extern Rust file has the standard library and the runtime. A Surf package (ADR-0071) brings .surf files, not crates. A database stays behind an API, and API_ORIGIN is its address.
Adapter
Target
Files
docker
server
Dockerfile, .dockerignore. The Dockerfile sets HOST=0.0.0.0
fly
server
The files of docker, and fly.toml
systemd
server
<name>.service, with the default HOST
netlify
static
netlify.toml
vercel
static
vercel.json
cloudflare
static
web/_redirects
github-pages
static
web/.nojekyll
surf
// src/todos.remote.surfsimport { Todo } from "./todo.surfs";pub async fn addTodo(text: String) -> Result<Todo, String> { return .ok(await saveTodo(text));}
surf
import { addTodo } from "./todos.remote.surfs";
- The result must be Result<T, String>. The error of the function, or a failed request, is .err(text).
- The types of the parameters and of the result come from another file, because the browser reads them too.
- A function that is not public stays on the server.
- hooks.server.surfs runs for each call, with the path /__remote/<file>/<name>. A guard of a page does not cover a remote function, so check the session in the function or in the hook.
- The file needs --target server. On the desktop target, it is a local call, with no hooks. Its cookies follow the rules of a desktop app (section 2).
Function or event
Meaning
Wave.depends(key), Wave.invalidate(key)
A layout load that read key runs again
Wave.hold(true), on Wave.blocked(url)
A navigation stops, and the app can ask the user
on Wave.leaving(url), on Wave.arrivedAt(url)
The start and the end of a navigation
Wave.pushState(url, value), on Wave.shallow(value)
A URL with no new page
Wave.snapshot(text), Wave.restored()
The state of a page for the back button
Wave.animateRoutes(true)
Page transitions between pages
await Wave.checkUpdate()
True when the server has a new build
Wave.preload(url), Wave.base()
The code and the data of a route now, and the folder of the app, for example /docs for --base /docs/ (ADR-0139)
- Wave.animateRoutes(true) turns on the page transitions on all targets. Wave.animateRoutes(false) turns them off. They are off by default.
- A navigation that changes the path of the URL has a page transition. This includes a Link, Wave.go, Wave.replace, Wave.back and the back and forward buttons. The first page, a change of the query only, Wave.pushState and Wave.reload have no page transition.
- The transition starts when the data of the new page is ready, just before the router shows the new page. The picture of the old page shows the old page at that time.
- By default, the picture of the old page and the new page cross-fade in 250 ms with ease, as in CSS View Transitions. The two pictures blend with plus-lighter, so a part that does not change stays the same.
- An element with view-transition-<name> moves from its old box to its new box (layout spec 5.10). It is not a part of the picture of the page.
- [view-transition] in src/style.toml gives the look. old and new name keyframes for the picture of the old page and of the new page. ms and ease give the time and the easing of each part of the transition, also of the moves of the names. [view-transition.back] gives the look of a navigation back. An unknown name is a compile error. A navigation back has the type back of CSS View Transitions.
- toml[view-transition]ms = 300ease = "ease-out"old = "fade-out"new = "rise"[view-transition.back]old = "fade-out"new = "fall"
- During the transition, the new page is live. It gets each press and each key, and the accessibility tree is the tree of the new page. The pictures get no press. The focus follows the rules of the router.
- With the setting of reduced motion on, a navigation shows the new page at once.
- The web uses document.startViewTransition. A browser without it shows the new page at once. The native targets keep a picture of the window and of each named element, and they paint the transition over the new page. Only an app that calls Wave.animateRoutes has this code.
- A Transition around @slot default of a layout does not animate a new page. A new page is not a block of the layout (component reference 4.15).