# Pages and the define-page builder

`@netscript/fresh` exposes a fluent builder for declaring pages. You start a chain with `definePage()`, layer on resources, params, render layers, handlers, and metadata, and finish with `build()` to produce a Fresh-compatible page definition. The same module also defines framework-owned partial routes through `definePartial()` and `defineStatsPartial()`. Reach for this surface whenever you author a route module and want a typed pipeline rather than hand-wired loaders and handlers.

Route contracts themselves — the generated route references passed to `withRoute()` — live on `@netscript/fresh/route`; see [Routing and route contracts](https://rickylabs.github.io/netscript/web-layer/route/). The data returned by a layer follows a separate type seam: in DB-backed products, generated model schemas first become a narrowed [versioned API contract](https://rickylabs.github.io/netscript/explanation/contracts/#where-the-public-shape-begins). See the [`@database/zod` generation step](https://rickylabs.github.io/netscript/data-persistence/database/#generated-schemas-feed-public-contracts) before wiring that contract's SDK/query result into a loader.

## The builder chain

`definePage<TState>()` returns a `PageRootBuilder<TState>`, the root of a typed fluent chain. `PageRootBuilder` extends `PageBuilder`, the public fluent page builder surface. Each method returns a new `PageBuilder` whose type parameters carry forward the accumulated state, resources, path/search schemas, layer data, and a `THasRoute` flag. Because every step is typed, downstream loaders, handlers, layouts, and metadata resolvers see the exact shapes you declared earlier in the chain.

The chain ends with `build()`. When the page was bound to a route with `withRoute()` (setting `THasRoute` to `true`), `build()` returns a `RoutedPageDefinition`; otherwise it returns a `PageDefinition`. A `PageDefinition` exposes a `page` renderer and a `default` export-compatible renderer — both `(ctx: PageRequestContext<TState>) => Promise<PageRenderable>` — plus an optional `handler` built from the handlers you registered.

> `build()` is overloaded. Calling `build()` with no argument, `build(routePattern: string)`, or `build(options)` selects the routed or unrouted return type based on the arguments and the `THasRoute` flag. A page bound with `withRoute()` always builds a `RoutedPageDefinition`.

## Building a page

Here is a page declaring resources, a policy, telemetry, multiple independent render layers, a route-bound form, layout slots, and response shaping in a single fluent chain:

```tsx
import { definePage } from "@netscript/fresh/builders";
import { z } from "zod";
import { MetricChart, MetricChartSkeleton } from "../components/MetricChart.tsx";
import { EditOrderForm } from "../components/EditOrderForm.tsx";

const orderPage = definePage()
  .withRouteContract({
    $route: "/orders/[id]",
    pathSchema: z.object({ id: z.string() }),
    searchSchema: z.object({ tab: z.string().optional() }),
  })
  .withResource("metrics", async (ctx) => {
    return await loadOrderMetrics(ctx.path.id);
  })
  .withPolicy("balanced")
  .withTelemetry({ enabled: true, spanName: "order-detail-page" })
  .withLayer("chart", MetricChart, {
    loader: async (ctx) => ({ points: await ctx.resource("metrics") }),
    partial: "/partials/orders/chart",
    partialName: "orders-chart",
    fallback: <MetricChartSkeleton />,
    staleTime: 15_000,
    staleReloadMode: "background",
  })
  .withForm("editOrder", EditOrderForm, {
    schema: z.object({ status: z.string() }),
    mutate: async (values, ctx) => {
      await updateOrderStatus(ctx.path.id, values.status);
    },
  })
  .withLayout((slots, _ctx) => (
    <div class="layout">
      <aside>{slots.chart()}</aside>
      <section>{slots.editOrder()}</section>
    </div>
  ))
  .withHeader("x-page-type", "order-detail")
  .withStatus(200)
  .build();

export default orderPage.default;
```

This envelope demonstrates the core page builder capabilities:

- **Route typing:** `withRouteContract` applies `$route`, path, and search schemas so `ctx.path` and `ctx.search` are strongly typed throughout the chain.
- **Resource resolution:** `withResource` declares request-scoped data factories accessible via `ctx.resource(key)`.
- **Independent layers & partials:** `withLayer` declares parallel render units with `partial` routes and `partialName` tags.
- **Partial fallbacks & staleTime:** layer configs support JSX `fallback` elements, `staleTime` windows, and `staleReloadMode`.
- **Managed forms:** `withForm` wires Zod schema validation, CSRF headers, and `mutate` handlers into a typed form layer.
- **Telemetry:** `withTelemetry` configures OpenTelemetry span names and tracing.
- **Layout slots:** `withLayout` receives a `slots` map containing callable layer renderers for flexible page structuring.

The smallest useful page is much shorter when you only need basic rendering:

```ts
import { definePage } from "@netscript/fresh/builders";

const page = definePage()
  .withResource("metrics", async () => {
    return await loadMetrics();
  })
  .withStatus(200)
  .withMeta((ctx) => ({ title: "Dashboard" }))
  .build();

export default page.default;
```

Inside a loader, handler, layout, or metadata resolver, the runtime hands you a `PageContext`. It extends `PageRequestContext` and adds the parsed `path` and `search` state, already-resolved `layerData`, the `routePattern`, a typed `nav` href builder, the `resources` record, and a `resource(key)` accessor that resolves a single named resource with full typing.

## Builder methods

The methods on `PageBuilder` group into resources, params, routing, render layers, handlers, and response shaping. Each returns a `PageBuilder` for continued chaining.

| Method | Purpose |
| --- | --- |
| `withResource(key, factory)` | Add a single named resource to the page pipeline. |
| `withResources(factories)` | Add multiple named resources to the page pipeline. |
| `withParams({ path?, search? })` | Apply both path and search schemas in one step. |
| `withPathParams(schema)` | Apply a typed path schema to the page. |
| `withSearchParams(schema)` | Apply a typed search schema to the page. |
| `withRoute(route)` | Bind the page to a generated route reference. |
| `withRouteContract({ $route?, pathSchema?, searchSchema? })` | Bind the page to a route via an inline contract; the Vite plugin inserts `$route` from the page path. |
| `withPolicy(policy)` | Configure defer policy defaults for the page. |
| `withTelemetry(telemetry)` | Configure telemetry metadata for the page. |
| `withLayer(id, component, config?)` | Register a render layer for the page. |
| `withForm(id, component, config)` | Register a route-bound form as a typed layer. |
| `withHandler(method, handler)` | Register a page method handler. |
| `withLayout(layout)` | Register the page layout. |
| `withMeta(resolver)` | Register the page metadata resolver. |
| `withHeader(...)` | Append a static header, header map, or computed headers. |
| `withStatus(status)` | Set the default HTTP status code for Fresh `GET` rendering. |
| `withStreaming()` | Enable builder-owned HTML streaming for `delivery: 'stream'` layers. |
| `createNav(routePattern?)` | Create typed route navigation for the page. |
| `build(...)` | Build the `PageDefinition` or `RoutedPageDefinition`. |

`withHeader` is overloaded: it accepts a single `name`/`value` pair, a `HeadersInit` map, or a `PageHeaderResolver` that computes headers per request.

Resources resolve sequentially, before any layer, into a store every loader shares — the mechanism, its ordering contract, and the dedup patterns it enables are covered in [Request-scoped resources](https://rickylabs.github.io/netscript/web-layer/resources/).

Layers then resolve concurrently, each into its own named region. The loader contract, the full layer config, slot placement with `withLayout`, and the typed layer hooks are covered in [Layers, layout, and slots](https://rickylabs.github.io/netscript/web-layer/layers/).

### Params and route state

`withPathParams`, `withSearchParams`, and the combined `withParams` apply typed schemas to the page. After they run, `PageContext.path` and `PageContext.search` carry the parsed, typed state. `withRoute` binds the page to a `PageRouteReference`, which exposes a typed `nav`, an `href(...)` builder, a route-bound `Link` component, and `parsePath` / `parseSearch` helpers (plus `safeParsePath` / `safeParseSearch` returning a `PageSchemaParseResult`). Data loading details are covered in [Data loading and the query cache](https://rickylabs.github.io/netscript/web-layer/query/).

#### Codegen-owned route binding

With the NetScript Vite plugin enabled, the binding call itself is generated from the page module's path under `routes/`, so you author only the contract body. Three forms converge on the same generated `routes.<key>` tree:

- **Form A — inline.** `withRouteContract({ pathSchema?, searchSchema? })` keeps the contract body in the page module. The generator inserts `$route: routePatterns.<key>.$route` as the first field and the `routePatterns` import. The inline schemas drive the same `path` / `search` type-state promotion as `withRoute`.
- **Form B — sidecar.** A sibling `<page>.route.ts` owns the contract. The generator inserts `.withRoute(routes.<key>.$route)` and the `routes` import.
- **Form C — no contract.** The generator inserts a default `.withRoute(routes.<key>.$route)` backed by `createRouteReference(routePattern)`.

Both inline and sidecar in the same page is a build warning (inline wins, the sidecar is left in place). `.withRoute` and `.withRouteContract` in the same page is a build error. Set `pageModuleRouteBinding: false` on the Vite plugin to disable page-module rewriting and keep hand-written `.withRoute(...)` lines.

How the `routes.<key>` accessor is derived from a file path, the exact conflict messages, and what a moved route file costs at compile time are covered in [Routing and route contracts](https://rickylabs.github.io/netscript/web-layer/route/#the-generated-routes-tree).

### Defer policy and streaming

`withPolicy` accepts a `PageDeferPolicyInput` or a `PageDeferPolicyProfile`. The profile is one of `"balanced"`, `"aggressive-first-paint"`, `"background-refresh"`, or `"low-bandwidth"`. A `PageDeferPolicyInput` overrides individual fields such as `staleTimeMs`, `prewarmOnMiss`, and `prewarmOnStale`. `withStreaming()` enables builder-owned HTML streaming for layers declared with `delivery: 'stream'`. See [Deferred and streaming UI](https://rickylabs.github.io/netscript/web-layer/defer-streaming-ui/) for the policy model.

### Handlers and methods

`withHandler(method, handler)` registers a method handler. `method` is a `PageMethod`, one of `"GET"`, `"POST"`, `"PUT"`, `"PATCH"`, `"DELETE"`, `"OPTIONS"`, or `"HEAD"`. `withForm` registers a route-bound form as a typed layer, wiring a layer, method handler, CSRF headers, and form metadata; its component receives `RuntimeFormState` props. Forms are covered in [Server-validated forms](https://rickylabs.github.io/netscript/web-layer/form/).

Which combination of `withHandler`, `withHeader`, `withStatus`, and `withStreaming` produces which handler — and the three build-time errors that guard them — is covered in [Response shaping](https://rickylabs.github.io/netscript/web-layer/response/), along with `withMeta` and the `build()` overloads.

## Partials

`definePartial()` defines a framework-owned partial route backed by an async loader, and `defineStatsPartial()` defines a stats-only partial backed by a context-free query function. Both return a `DefinedPartialRoute`, which carries a Fresh `config`, an optional `handler`, a `page` renderer, and a `default` export-compatible renderer.

`DefinePartialOptions` requires a stable `name`, a `loader` `(ctx) => Promise<TProps>`, and a `component`. It also accepts an optional `errorComponent`, `errorTitle`, `handler`, and Fresh `config`. `DefineStatsPartialOptions` omits `loader` and instead requires a `query` `() => Promise<TProps>`.

The error shell, the paired route reference that types the `f-partial` link, and the layer config that turns a region into a deferred partial are covered in [Partials](https://rickylabs.github.io/netscript/web-layer/partials/).

```ts
import { definePartial } from "@netscript/fresh/builders";

export const liveCount = definePartial({
  name: "live-count",
  loader: async (ctx) => {
    return { total: await countActive(ctx) };
  },
  component: CountView,
});
```

## API summary

| Symbol | Description |
| --- | --- |
| `definePage<TState>()` | Start a new typed page builder chain. |
| `PageRootBuilder<TState>` | Root page builder returned by `definePage()`. |
| `PageBuilder<...>` | Public fluent page builder surface. |
| `PageDefinition<...>` | Unrouted page definition returned by `build()` without a route. |
| `RoutedPageDefinition<...>` | Page definition built with an explicit route. |
| `PageContext<...>` | Runtime context shared by loaders, handlers, layouts, and metadata resolvers. |
| `PageRequestContext<TState>` | Typed request context exposed to page builders. |
| `PageRenderContext<TState>` | Request context variant that can call `ctx.render()`. |
| `definePartial(options)` | Define a framework-owned partial route backed by an async loader. |
| `defineStatsPartial(options)` | Define a stats-only partial route backed by a context-free query. |
| `DefinePartialOptions<...>` | Options for creating a framework-owned partial route. |
| `DefineStatsPartialOptions<...>` | Options for creating a stats-only partial route. |
| `DefinedPartialRoute<...>` | Materialized partial route contract returned by `definePartial()`. |
| `PageMethod` | Page handler HTTP method union. |
| `PageDeferPolicyProfile` | Named defer policy profile union. |
| `PageRenderable` | Renderable value returned by page and partial renderers. |

## Related

[The Fresh page model  How a NetScript Fresh page is structured.](https://rickylabs.github.io/netscript/netscript/web-layer/server/) [Request-scoped resources  withResource, ordering, and cross-layer dedup.](https://rickylabs.github.io/netscript/netscript/web-layer/resources/) [Layers, layout, and slots  withLayer, withLayout, and the loader contract.](https://rickylabs.github.io/netscript/netscript/web-layer/layers/) [Routing and route contracts  Generated route references for withRoute().](https://rickylabs.github.io/netscript/netscript/web-layer/route/) [Data loading and the query cache  Resolve resources and cache data.](https://rickylabs.github.io/netscript/netscript/web-layer/query/) [Server-validated forms  withForm and RuntimeFormState.](https://rickylabs.github.io/netscript/netscript/web-layer/form/) [Response shaping  withMeta, withHeader, withStatus, and build().](https://rickylabs.github.io/netscript/netscript/web-layer/response/) [Deferred and streaming UI  withPolicy, withStreaming, and defer profiles.](https://rickylabs.github.io/netscript/netscript/web-layer/defer-streaming-ui/) [Live dashboard tutorial  Build a page end to end.](https://rickylabs.github.io/netscript/netscript/tutorials/live-dashboard/)

See the [Web Layer overview](https://rickylabs.github.io/netscript/web-layer/) for the full pillar map.
