Skip to main content
0.0.x

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. 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. See the @database/zod generation step 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.

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:

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:

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.

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.

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.

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.

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 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.

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, 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.

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.

See the Web Layer overview for the full pillar map.