Server Rendering
elements man html/server Read as markdownHow a page is rendered on the server and picked up by the browser runtime, how to redirect, and how html responses are cached.
Pages render on the server and attach in the browser. Pass data into the
template through the route handler. The shape is new html({ ... }).
export default function route(req: Request, res: Response) {
let users = sql<User>(`select * from users`).all();
return new html({ users });
}
Usually you'll want to provide all the data needed to render the page from the
route. This ensures the page is fully server-rendered. Occasionally you may want
to delay data fetching until after the initial page renders; in that case you
can call an @rpc function from an oninsert handler. After first render,
@rpc and LiveTable are the right tools for browser-initiated data.
Redirect
redirect(url) leaves the current page for another url. Use it in place of
assigning window.location directly.
It is one call everywhere: a template handler, a route, middleware, and an
@rpc all take the same redirect(url). In the browser it drives
window.location; in a route or middleware it writes a 302; in an @rpc the
target rides back on the reply and the browser navigates once the call settles.
import { redirect } from "@elements/app";
function attemptSignin(email: string, password: string): string {
try {
signinUser(email, password);
redirect("/");
return "";
} catch (err: any) {
return err.message;
}
}
Navigation is asynchronous: the browser fetches the next page while this one
stays live, so a reactive change made just before you leave repaints the current
page before it unloads. This matters most for auth. signinUser above is an
@rpc, so the compiler awaits it; session.login runs on the server and the
browser applies the session update, flipping session.isLoggedIn(), while the
await is still pending, before the redirect(url) line runs.
So keep session-reactive views off pages you sign in or out from. Render the
signin page as the form alone, not a page that swaps between the form and a
"you're signed in" panel based on session.isLoggedIn(). That panel is the one
thing that flashes: it paints where the form was, for the moment before the new
page loads.
<!-- Don't: the page body swaps to a signed-in view the instant login lands,
one frame before redirect() leaves. -->
<main>
<div e:if={session.isLoggedIn()}>You're signed in. <a href="/">Go home</a></div>
<form e:else onsubmit={() => { error = attemptSignin(email, password); if (!error) redirect("/"); }}>
...
</form>
</main>
<!-- Do: the page is the form. A signed-in visitor is handled at the route. -->
<main>
<form onsubmit={() => { error = attemptSignin(email, password); if (!error) redirect("/"); }}>
...
</form>
</main>
Handle an already-signed-in visitor in the route, before the page renders:
export default function route(req: Request, res: Response) {
if (session.isLoggedIn()) {
redirect("/");
return;
}
return new html({});
}
A session-reactive navbar is fine. It flips to the signed-in state and the destination shows that same state, so the change carries across the navigation rather than flashing.
redirect() is not control flow. In the browser navigation is asynchronous and
the code after it keeps running; on the server the handler keeps running too,
and a value returned afterward is discarded rather than written over the
redirect. Return early when you want the rest skipped.
redirect() always sends a 302. For any other status, a route can use the
http-specific res.redirect(url, 301).
It throws where there is no browser to navigate: a job, a test, module top level, or after a response has already started streaming.
HTML ETag Caching
Every html response carries an etag computed from the page's source, the data passed into the template, and the session state. Unchanged pages return 304 Not Modified, so a browser revisit skips both rendering and transfer when nothing relevant has changed. The page response itself doubles as the cache manifest.