[]previous and next page

Search params, absolute URLs and the trailing slash

Dusk reads a query string in three places, and they do not give the same values.

For agentsUpdated

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).