Search params, absolute URLs and the trailing slash
Dusk reads a query string in three places, and they do not give the same values.
Three query parsers, three rules (G-C1)#
Dusk reads a query string in three places, and they do not give the same values. Pin your code to the rule of the place it runs in.
| Where | ?page=3 |
?flag |
?a[]=1 |
Repeated ?t=a&t=b |
|---|---|---|---|---|
Route validateSearch and loaders (the router's codec) |
3 (a number) |
true |
the key a[], value 1 |
["a", "b"] |
API routes (event.url.searchParams) |
"3" |
"" |
the key a[], value "1" |
get returns the first value |
| Metadata functions (the search they receive) | "3" |
"" |
the key a[], value "1" |
["a", "b"] |
The router decodes a value that looks like JSON and reads back to the same text (so 3, true, null); "" stays
"". Brackets are not parsed by any of the three: a[] is a plain key. Unifying the three is deferred, because it would
change the values existing code receives.
Validating the search (G-C2, G-C3)#
validateSearch is a function, or a Standard Schema (Zod, Valibot, ArkType) whose output is the typed search. The loader
and the components then see the output. A schema must validate synchronously; an async schema is refused with a
TypeError.
A rejected query is a SearchValidationError with one issue per failing key. The page answers 400 with the code
SEARCH_INVALID and names the keys in its body. Any other error thrown by validateSearch is a bug and stays a 500,
reported to onError.
Absolute URLs (G-C10)#
absoluteUrl(path, source, options?) returns a full URL. The base is metadataBase when you pass it, else the origin of
the request (event), else the origin you pass as a string. A path starting with / is taken from the origin, as in the
URL standard; a query string stays and a fragment is dropped. The root is the bare origin, without its slash.
Both the protocol and the host come from the base. A production origin is https; a request that came in over plain
http (a local development server) gives an http URL.
The trailing slash in URLs (G-C11)#
With trailingSlash: "always", the canonical and alternate URLs of a page on the app's origin end with a slash
(https://example.com/about/); with "never" they do not. preserve (the default) writes the path as the page has it. The
root is never changed. A URL on another host keeps its own form. Sitemap entries on the app's origin follow the same
rule, in sitemapXml (with its trailingSlash option) and in sitemapRoute, which reads the app's policy from the
request event.
The policy is applied to the first document, which the server renders. A client-side navigation resolves its metadata in
the browser, and the browser does not receive the policy yet, so the canonical written after a navigation follows
preserve (deferred).
Relative sitemap entries resolve against the app's base path: about is /app/about under base: "/app", and
/about is an origin path (the URL standard), so it does not gain the base.
Fragments never reach the server (G-C4)#
A browser does not send a #fragment. A Request built with one still routes as the path without it: the handler's
event sees an empty url.hash, and a redirect destination with a # is refused when the config is read. Scroll to a
fragment is a client matter (see Navigating).