Skip to content

feat(router): resolve server rewrite() targets on client navigation - #1

Closed
omerman wants to merge 2 commits into
rc3-basefrom
feat/router-rewrite-spa
Closed

omerman wants to merge 2 commits into
rc3-basefrom
feat/router-rewrite-spa

Conversation

@omerman

@omerman omerman commented Oct 17, 2026 •

Copy link
Copy Markdown
Owner

Draft for review on my fork only (not for QwikDev/qwik yet).

Problem

rewrite() only runs on the server. In v1 the client asked the server for q-data.json on every navigation, and the response carried the final href / isRewrite, so a click on a URL that only exists through a rewrite to a different route worked. Since q-data.json was split per loader (QwikDev#8501), the client matches routes from its own route table, so such a click renders the 404 page, or whatever dynamic/wildcard route happens to match. A full page load still works. The only SPA rewrite e2e (products/shirt-rewrite) rewrites to the same [id] route, so it doesn't catch this.

Real-world case: SEO URLs like /artists/<name>/<song>/chords rewritten by a DB lookup to /track/chords/<id>.

Change

  • New opt-in plugin option serverRewrites (default false).
  • Caching of the answer is up to the rewriting middleware: q-route.json is sent with the headers set before throw rewrite() (cacheControl(), CDN-Cache-Control, Cache-Tag, ...) and private if none, like loaders. Nothing is hard-coded to no-store.
  • Server: q-route.json (recognised like q-loader-*.json) answers { p: <pathname after rewrites> }. It is not an internalRequest, so rewrite middleware treats it as a normal navigation. Placed after plugins/route middleware, so any rewrite has already re-resolved the route.
  • Client: when the option is on and the URL has no renderable page, or only matches a dynamic/wildcard route, ask q-route.json, loadRoute() the returned path, keep the original URL. Loaders are still fetched through the visible URL so the server re-applies the rewrite exactly as in SSR. A redirect answer falls back to a full page load.

Tests

  • Unit: recognizeRequest / trim for q-route.json.
  • e2e rewrite-spa.e2e.ts + rewrite-spa/ routes + plugin@rewrite-spa.ts: SSR serves the target under the original URL; SPA click renders the target, keeps the URL, no reload, no page errors. Fails with serverRewrites: false ("Loader … returned empty response"), passes with it on.
  • Ran e2e/qwik-e2e/tests/qwikrouter on chromium: 196 passed, 1 failed (ssg-snapshot core bundle brotli budget; I only had a dev build, the test asks for the production build).

Known limits / open questions

  • A static page that matches the URL exactly always wins on the client, even if middleware rewrites that URL on the server (documented).
  • Each loader request still goes through the rewrite middleware (N lookups instead of 1). Alternative: fetch loaders from the resolved path and pass the visible URL as X-Qwik-fullpath, but then originalUrl differs from SSR.
  • .api.md files not regenerated.

Based on the @qwik.dev/router@2.0.0-rc.3 release commit.

🤖 Generated with Claude Code

Client navigation to a URL that only exists through a server rewrite() to a
different route rendered the 404 page (or the wrong dynamic route), because the
client matches routes from its own route table. With the new `serverRewrites`
plugin option, the client asks the server (`q-route.json`) where such a URL
finally routes to and renders that route, keeping the original URL.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@omerman
omerman changed the base branch from main to rc3-base October 17, 2026 13:21
Don't hard-code no-store. The q-route.json answer carries the headers the
middleware set before rewrite() (cacheControl(), CDN-Cache-Control, Cache-Tag,
...), and defaults to private like loaders.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@omerman
omerman marked this pull request as ready for review October 18, 2026 04:31
@omerman omerman closed this Nov 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant