From 45e83b50c89c7cd78a64fc4dfbd0e571ac7ad774 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Wed, 14 Oct 2026 13:32:15 +0800 Subject: [PATCH] fix: serve pages in SPA mode when the route also has an API handler `matchAPIRoute` only counted a route as a page when it had a page component. SPA builds leave page components out of the server routes, so a route file exporting both a page and `GET` was treated as API-only. When its `GET` returned nothing, the handler stopped there and h3 answered 404. A catch-all like `[...404].tsx` with a `GET` turned every page into a 404. Use `route.page` alone. SSR routes with a page always have the component, so nothing changes there. Co-Authored-By: Claude Opus 5.5 (1M context) --- .changeset/spa-page-api-route.md | 5 ++++ packages/start/src/server/routes.spec.ts | 34 ++++++++++++++++++++++++ packages/start/src/server/routes.ts | 5 ++-- 3 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 .changeset/spa-page-api-route.md create mode 100644 packages/start/src/server/routes.spec.ts diff --git a/.changeset/spa-page-api-route.md b/.changeset/spa-page-api-route.md new file mode 100644 index 000000000..774a9b043 --- /dev/null +++ b/.changeset/spa-page-api-route.md @@ -0,0 +1,5 @@ +--- +"@solidjs/start": patch +--- + +Serve pages in SPA mode (`ssr: false`) when a route file exports both a page and an API handler, such as a `[...404].tsx` with a `GET`. The server treated those routes as API-only, so a `GET` that returned nothing gave a 404 for every URL the route matched. diff --git a/packages/start/src/server/routes.spec.ts b/packages/start/src/server/routes.spec.ts new file mode 100644 index 000000000..367601bc9 --- /dev/null +++ b/packages/start/src/server/routes.spec.ts @@ -0,0 +1,34 @@ +import { describe, expect, it, vi } from "vitest"; + +const GET = { import: () => Promise.resolve({}) }; + +// What the server file router produces for `routes/[...404].tsx` exporting +// both a page and `GET`, with and without the page component. +vi.mock("solid-start:routes", () => ({ + default: [ + { + path: "/*404", + page: true, + $component: { src: "routes/[...404].tsx", import: () => Promise.resolve({}) }, + $GET: GET, + }, + { path: "/spa/*404", page: true, $GET: GET }, + { path: "/api/*rest", $GET: GET }, + ], +})); + +const { matchAPIRoute } = await import("./routes.ts"); + +describe("matchAPIRoute", () => { + it("marks a route with a page and an API handler as a page", () => { + expect(matchAPIRoute("/missing", "GET")?.isPage).toBe(true); + }); + + it("marks it as a page in SPA builds, where the server routes have no page components", () => { + expect(matchAPIRoute("/spa/missing", "GET")?.isPage).toBe(true); + }); + + it("does not mark an API-only route as a page", () => { + expect(matchAPIRoute("/api/missing", "GET")?.isPage).toBe(false); + }); +}); diff --git a/packages/start/src/server/routes.ts b/packages/start/src/server/routes.ts index 0b44eb18d..4521cff8e 100644 --- a/packages/start/src/server/routes.ts +++ b/packages/start/src/server/routes.ts @@ -111,8 +111,9 @@ export function matchAPIRoute( if (handler === undefined) return; - // Check if this is a page route - const isPage = route.page === true && route.$component !== undefined; + // Check if this is a page route. SPA builds leave page components out of + // the server routes, so this can't depend on `$component`. + const isPage = route.page === true; // Return comprehensive route information return {