Static server functions for Solid. prerendered() declares that a 'use server' function runs at build time — during prerendering — and that each call's result is captured as a static artifact the deployed client fetches instead of calling a server. Pair it with prerender-crawler and vite build produces a folder of files that is a complete Solid app with typed server-side data loading and no server.
Requires @solidjs/web 2 (server functions) and @solidjs/vite-plugin with serverFunctions: true.
// vite.config.ts
import { defineConfig } from "vite";
import solid from "@solidjs/vite-plugin";
import { prerender } from "prerender-crawler/vite";
import { serverFunctions } from "@solidjs/prerender/integration";
export default defineConfig({
plugins: [
solid({ start: true, ssr: true, serverFunctions: true }),
prerender({ mode: "static", integrations: [serverFunctions()] })
]
});prerender is the generic crawler; serverFunctions() is the Solid integration that captures prerendered() calls during the crawl and guards the build.
import { query } from "@solidjs/router";
import { prerendered } from "@solidjs/prerender";
export const getPost = query(
prerendered(async (slug: string) => {
"use server";
return db.posts.find(slug); // runs at build time in a prerendered build
}),
"post"
);- Implies
GET. A prerendered call is by definition a safe read. Outside a prerendered build — the dev server, or a build without the prerender plugin — the reference behaves exactly likeGET(fn), so the app still works against a live server. Dev stays fullstack; production is static. - Arguments are the address. Each call identity (function id + arguments) becomes one artifact under
_static/. Both the build and the client derive the key from the arguments, so they must be JSON-safe and spell identically in both realms. - Results are unrestricted. JSON-safe results are stored as plain JSON; rich values (Dates, Maps, typed errors) ride the server-function codec into the artifact, so whatever survives a live call survives the artifact.
- Only calls the build made have artifacts. In a static build, a client calling with arguments no prerendered page used gets a rejected call (there is no server). In a hybrid build the call falls back to the live server.
Design pages so the crawl exercises the calls the site needs — which happens naturally when pages link to what they use.
The crawl follows links; a page nothing links to needs announcing. Call this in the app root during render with the createRouter instance (or a route-definition tree, with { base }):
import { announceRoutes } from "@solidjs/prerender";
import { Router } from "./router";
export default function App() {
announceRoutes(Router);
return <Router>{props => props.children}</Router>;
}On the server, when the request is the crawler's, the router's static pages go on the response's hint header and the crawl seeds every one of them. A visitor's response is untouched; in the browser it is a no-op. Dynamic routes (/posts/:id) are not announced — only a render knows their values; the crawl finds them by their links. Options: header (a custom crawl hintHeader), base.
It takes either router Solid apps use. With TanStack Router the instance is built per request, so the call goes where the router is — the start.setup hook:
// src/setup.tsx
const router = createAppRouter(queryClient, history);
announceRoutes(router);The enumerators behind it are exported too — solidRouterPages(router | routes, { base? }) and tanstackRouterPages(router) — for code that wants the path list itself (a sitemap of its own, a hand-rolled announcePages call from prerender-crawler/announce). Both are pure: they read the router instance's public shape and import nothing from either router package.
The integration has two jobs.
Capture. For the crawl's duration every executed prerendered call is collected (the crawl is in-process, so the built server bundle and the integration share a realm), encoded once per call identity, and emitted alongside the pages.
The guard. @solidjs/vite-plugin records every server function the client build can dispatch. After the crawl, any of those ids nothing captured is a call a static deployment cannot answer — because the function lacks prerendered(), or because no prerendered page called it. In static mode the build fails naming each one:
[@solidjs/prerender] 1 server function(s) the client can call were never captured during prerendering:
- getLiveCount (getLiveCount-1679f63b, src/data.ts)
A static deployment has no server to answer them, so these calls fail at runtime. ...
| Option | Default | |
|---|---|---|
uncaptured |
"error" static / "ignore" hybrid |
"error", "warn", or "ignore". |
codec |
Codec options for encoding rich results; must match the client's configureServerFunctionsClient({ codec }). |
The guard needs a @solidjs/vite-plugin that records function ids in .vite/solid-server-functions.json; with an older version it warns that it cannot verify and lets the build through.
prerender({ mode: "static" })— SSG. Every page written, everyprerendered()call baked, missing artifact is an error. Deploydist/clientto any static host.prerender({ mode: "hybrid" })— a live server is deployed too. Chosen calls are baked to static artifacts while everything else stays live; clients fetch artifacts first and fall back to the server. Pages aren't written by default (a static file would shadow live SSR).
The client learns the posture from import.meta.env.PRERENDER_MODE, which the crawler's Vite plugin defines. No plugin, no constant, live behavior.
solidRouterPages(router | routes, { base? }),tanstackRouterPages(router)— the static-page enumerators behindannounceRoutes, pure and isomorphic.staticCallKey(id, args)/staticArtifactPath(id, args)— the artifact key derivation, for tooling that needs to locate an artifact.- Types:
PrerenderedFunction,AnnounceableRouter,AnnounceRoutesOptions,SolidRouterLike,SolidRouteLike,TanStackRouterLike,ServerFunctionsIntegration,ServerFunctionsIntegrationOptions,CaptureSink(server).
MIT