# 102. Data and rendering

**For:** app developers. Where the markup comes from and what the browser ends up with.

## Where a page is rendered

**Prerendered.** A route whose loader reads nothing request-specific (no query, no session, no identity, no clock) can be rendered once at build time and answered from a file. `fsr prerender <app>` writes them, `[server] prerender` names the directory and the response carries `x-sf-prerendered: 1`. The boot report lists which routes qualified.

A route with a parameter needs one more thing, since nothing can tell which values the parameter takes. Its page loader says so with `paths` and the command writes a file per set:

```
$ fsr prerender app
/                      app/dist/prerender/index.html
/post/hello            app/dist/prerender/post/hello/index.html
/post/again            app/dist/prerender/post/again/index.html
prerendered.json       app/dist/prerender/prerendered.json
```

Each run lists what it wrote in `prerendered.json` and deletes those files before writing again, so a slug that drops out of the set leaves no stale page behind. A path outside the set is rendered live, so this is safe to use for a set that rarely changes.

**Server-rendered.** This is the normal case. The host evaluates the lowered loader, walks the plan and writes HTML. This is Rust interpreting data, with no JavaScript VM starting.

**Hydrated.** This covers anything the browser has to keep interactive. Only islands are shipped as code.

Very little falls into the third group. A page is server-rendered whenever the build could lower it. Lowering is the default.

```diagram
request-lifecycle
```

## What the browser actually receives

The browser receives HTML, one stylesheet link per file in `app/styles/`, one inline import map and one entry module. The entry registers the islands, boots and turns links into payload navigations. Everything else is markup.

A navigation does not load a new document. The client asks the same route for a payload instead of a document and fills the changed part of the tree in place. The layout's DOM, its islands and their state all survive. Text typed into a header search box is still there after the page under it changes.

## Rendering everything in the browser

Which of the three a component gets is decided by what the build could lower. One setting renders the whole application in the browser:

```toml
[server]
render = "islands"
```

With it no evaluator is registered, so every lowered component falls to the null evaluator and becomes a node naming a module with its props. The build does not change: components still lower and the report still says so. The only difference is that nothing asks the interpreter to render them.

The server still does most of the work. Loaders and actions run in Rust, because they never went through an evaluator. Metadata, the store seed, the session and routing also stay on the server. The document carries the shell, the head, the store seed, the loader data as each island's props and one region per plan child. The browser mounts each module rather than hydrating it, since there is no server markup to hydrate against.

The benefit is that no component has to render twice and match, so residue stops mattering: every component is treated as residue. The cost is the first paint, since the document has no page content until the bundle has loaded and run. This suits an application behind a login, where nothing is indexed and the first paint is a spinner anyway. It is also how to run a component library that will never lower.

## What a build produces

```diagram
two-artifacts
```

The plan is data, so it can be diffed, hashed, validated and swapped into a running process. The bundle is code and it never runs on the server.

## The store

A loader's `store` export seeds keys the browser can read without another request.

```ts
export const store = ({ data }: { data: { cart: bigint } }) => ({
  "shop/cart_count": Number(data.cart),
});
```

An island reads it with a typed key:

```ts
import { key } from "@snapfire/fsr-client/store";
export const cartCount = key<number>("shop/cart_count");
```

The store is one global map keyed by string. Nothing namespaces the keys for you, so prefix them yourself. Two features using `"count"` will find each other's value.

## Errors and misses

An `error.tsx` receives `{ error: string }` when the loader fails: a service that is down, a response the contract rejected or a `fail` the body raised. The document renders around it, so an error is a page with a message rather than a blank tab.

A `routes/not-found.tsx` answers a path no route matches, with status 404 and `params.path` carrying what was asked for.

Those two cases can look alike. A path that matches a route pattern but finds nothing (`/product/does-not-exist`) goes through the loader, so `fail("not_found", …)` renders the error boundary rather than the not-found page. The status still follows the failure: the page's own loader failing answers with the kind's status, 404 here and 503 for `unavailable`, because a crawler, a cache and the browser's history read the status rather than the markup.

A layout's loader failing is different. That segment degrades and the status stays 200, since the page beneath it still renders. `fsr prerender` writes no file for a path whose loader failed.

## Try it

Run `fsr prerender <app>` and read the `prerender` list in the boot report `fsr serve <app> --verbose` prints. Then make one of those loaders read `query` and build again: it drops off the list, because it can no longer be answered from a file.

Give a `[slug]` route a `paths` and prerender again: the report marks that route `per paths` and one file exists per slug. Ask for a slug the set does not hold and read the status with `curl -i`. It is a 404, rendered live, with no `x-sf-prerendered` header on it.
