Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .changeset/http-transport-cli-redirects.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Prerender anything over HTTP, from the command line, with redirects the host can

**Query-string pages.** `keepQuery: true` renders `/posts?page=2` apart from `/posts` (parameters sorted for dedupe), following its links and capturing its data, but writes it only when its seed entry names a `filename` — a static host serves a path the same for every query. Off by default; CLI `--keep-query`.

**Router seeding, from the server.** New `prerender-crawler/routers` (no Node imports — for application server code): `tanstackRouterPages(router)` and `solidRouterPages(router | routes, { base? })` list a router's static pages from the instance the app built for the request (leaves and indexes, no params or splats), and `announcePages(request, headers, paths)` puts them on the response's hint header when the request is the crawler's. `@solidjs/prerender` gains `announceRoutes(Router)` — one line in the app root, reading the ambient request event; a no-op in the browser. The Vite plugin, the CLI against a module, and the CLI against a running server all seed from the same header.
**Router seeding, from the server.** New `prerender-crawler/routers` (no Node imports — for application server code): `tanstackRouterPages(router)` and `solidRouterPages(router | routes, { base? })` list a router's static pages from the instance the app built for the request (leaves and indexes, no params or splats), and `announcePages(request, headers, paths)` puts them on the response's hint header when the request is the crawler's. `@solidjs/prerender` gains `announceRoutes(router)` — one line in the app root (Solid Router) or the request setup (TanStack Router); it takes either router by shape, reads the ambient request event, and is a no-op in the browser. The Vite plugin, the CLI against a module, and the CLI against a running server all seed from the same header.

**Removed:** `fileRoutePages`, `staticRoutePaths`, and the Vite plugin's `fileRoutes` option — build-time seeding that walked a `filesystem-routing` directory and re-derived its path rules. The server's own router is the source of truth for which pages exist, and the header works for crawls that never see the project's disk (the CLI against a running server). `filesystem-routing` is no longer a peer dependency.

Expand Down
8 changes: 8 additions & 0 deletions packages/solid/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ export default function App() {

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:

```ts
// src/setup.tsx
const router = createAppRouter(queryClient, history);
announceRoutes(router);
```

## `serverFunctions(options?)`

The integration has two jobs.
Expand Down
8 changes: 7 additions & 1 deletion packages/solid/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,14 @@ import type { AnnounceRoutesOptions, PrerenderedFunction } from "./shared.ts";
export { staticArtifactPath, staticCallKey } from "./shared.ts";
export type { AnnounceRoutesOptions, PrerenderedFunction } from "./shared.ts";

/** A router `announceRoutes` can read — see the server half. */
export type AnnounceableRouter = unknown;

/** The client half of `announceRoutes`: there is no request here. Always false. */
export function announceRoutes(_router: unknown, _options?: AnnounceRoutesOptions): boolean {
export function announceRoutes(
_router: AnnounceableRouter,
_options?: AnnounceRoutesOptions
): boolean {
return false;
}

Expand Down
43 changes: 29 additions & 14 deletions packages/solid/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,34 @@ import {
} from "@solidjs/web/server-functions/server";
import { getRequestEvent } from "@solidjs/web";
import type { RequestEvent, ResponseStub } from "@solidjs/web";
import { announcePages, solidRouterPages } from "prerender-crawler/routers";
import type { SolidRouteLike, SolidRouterLike } from "prerender-crawler/routers";
import { announcePages, solidRouterPages, tanstackRouterPages } from "prerender-crawler/routers";
import type {
SolidRouteLike,
SolidRouterLike,
TanStackRouterLike
} from "prerender-crawler/routers";
import { CAPTURE_SINK, PRERENDERED_META_KEY } from "./shared.ts";
import type { AnnounceRoutesOptions, CaptureSink, PrerenderedFunction } from "./shared.ts";

export { staticArtifactPath, staticCallKey } from "./shared.ts";
export type { CaptureSink, PrerenderedFunction } from "./shared.ts";
export type { AnnounceRoutesOptions } from "./shared.ts";

/** A router `announceRoutes` can read: Solid Router (instance or tree) or a TanStack Router instance. */
export type AnnounceableRouter =
SolidRouterLike | SolidRouteLike | readonly SolidRouteLike[] | TanStackRouterLike;

/**
* Tells a prerender crawl which pages this app's router has — the server
* half. Called during a server render (the app root is the natural place),
* it reads the ambient request: 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, linked or not. A visitor's request is untouched;
* on the client this is a no-op. Returns whether it announced.
* half. Called during a server render or request setup, it reads the
* ambient request: 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, linked or not. A visitor's request is untouched; on the client
* this is a no-op. Returns whether it announced.
*
* Takes either router Solid apps use — a Solid Router `createRouter`
* instance (or its route-definition tree, with `base`) or a TanStack
* Router instance — and tells them apart by shape.
*
* ```tsx
* import { announceRoutes } from "@solidjs/prerender";
Expand All @@ -41,20 +53,23 @@ export type { AnnounceRoutesOptions } from "./shared.ts";
* }
* ```
*
* Dynamic routes (`/posts/:id`) are not announced — only a render knows
* their values; the crawl finds them by their links.
* Dynamic routes (`/posts/:id`, `/posts/$id`) are not announced — only a
* render knows their values; the crawl finds them by their links.
*/
export function announceRoutes(
router: SolidRouterLike | SolidRouteLike | readonly SolidRouteLike[],
options: AnnounceRoutesOptions = {}
): boolean {
export function announceRoutes(router: AnnounceableRouter, options: AnnounceRoutesOptions = {}) {
const event = getRequestEvent() as (RequestEvent & { response?: ResponseStub }) | undefined;
if (!event?.response || event.response.committed) return false;
if (!event.request.headers.has(options.header ?? "x-prerender")) return false;
const pages = solidRouterPages(router, { base: options.base });
const pages = isTanStackRouter(router)
? tanstackRouterPages(router)
: solidRouterPages(router, { base: options.base });
return announcePages(event.request, event.response.headers, pages, { header: options.header });
}

// a TanStack instance carries its path index; nothing of Solid Router's does
const isTanStackRouter = (router: AnnounceableRouter): router is TanStackRouterLike =>
typeof router === "object" && router !== null && "routesByPath" in router;

const SERVER_FUNCTION_METADATA = Symbol.for("solid.ServerFunctionMetadata");

/**
Expand Down
15 changes: 15 additions & 0 deletions packages/solid/test/announce.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,21 @@ describe("announceRoutes", () => {
});
});

it("reads a TanStack Router instance by shape", () => {
const tanstack = {
routesByPath: {
"/": { fullPath: "/" },
"/users": { fullPath: "/users/", children: [{}] },
"/users/$id": { fullPath: "/users/$id" }
}
};
const request = new Request("http://localhost/", { headers: { "x-prerender": "1" } });
withEvent(request, event => {
expect(announceRoutes(tanstack)).toBe(true);
expect(event.response.headers.get("x-prerender")!.split(",").sort()).toEqual(["/", "/users"]);
});
});

it("is a no-op on the client", () => {
expect(announceClient(Router)).toBe(false);
});
Expand Down
Loading