Skip to main content
doc0blade47/next.jsdocumentation home
Sign in

Routing Utility

Last updated Oct 12, 2026100% grounded
Relevant source files

The following files were used as context for generating this wiki page:

Overview

Routing utilities orchestrate incoming HTTP request evaluation, path matching, middleware coordination, and destination compilation across Next.js routing layers. Provided primarily via @next/routing and core router subsystems, these utilities resolve route definitions, handle internationalization rules, normalize data URLs, and synchronize parameter extraction between client and server runtime components.

Sources: packages/next-routing/package.json:1-15, packages/next-routing/src/resolve-routes.ts:557-601

Public API Surface and Types

Public Export Contract

The @next/routing package exposes route resolution functionality, middleware helpers, internationalization detectors, and core TypeScript definitions through its primary entry point. The public export contract includes the resolveRoutes function alongside type definitions for route matching conditions (RouteHas), route rules (Route), middleware context and results (MiddlewareContext, MiddlewareResult), and resolution parameters and outcomes (ResolveRoutesParams, ResolveRoutesResult, ResolveRoutesQuery, RouteInvocationTarget). Additionally, internationalization utilities (detectLocale, detectDomainLocale, normalizeLocalePath, getAcceptLanguageLocale, getCookieLocale, I18nConfig, I18nDomain) and the middleware response mapper (responseToMiddlewareResult) are exported for external consumers.

export { resolveRoutes } from './resolve-routes'
export type {
  RouteHas,
  Route,
  MiddlewareContext,
  MiddlewareResult,
  ResolveRoutesParams,
  ResolveRoutesResult,
  ResolveRoutesQuery,
  ResolveRoutesQueryValue,
  RouteInvocationTarget,
} from './types'
export type { I18nConfig, I18nDomain } from './i18n'
export {
  detectLocale,
  detectDomainLocale,
  normalizeLocalePath,
  getAcceptLanguageLocale,
  getCookieLocale,
} from './i18n'
export { responseToMiddlewareResult } from './middleware'

Sources: packages/next-routing/src/index.ts:1-21

RouteHas Conditions and Route Configurations

Conditional route matching relies on RouteHas union types, which distinguish between request headers, cookies, query parameters, and hostnames. A standard matcher configuration evaluates type fields against key and optional value constraints, whereas host-based matchers omit the key and enforce a strict value string match.

export type RouteHas =
  | {
      type: 'header' | 'cookie' | 'query'
      key: string
      value?: string
    }
  | {
      type: 'host'
      key?: undefined
      value: string
    }

Sources: packages/next-routing/src/types.ts:1-11

Individual route entries (Route) define regex patterns, destination templates, response headers, status codes, and positive or negative match conditions (has and missing).

FieldTypeDescription
sourceRegexstringRegular expression string with named or unnamed capture matches
destinationstring (optional)Replacement destination template using $1 or $name tokens
headersRecord<string, string> (optional)Custom headers to inject upon matching
hasRouteHas[] (optional)Required conditions that must be satisfied
missingRouteHas[] (optional)Conditions that must not be present
statusnumber (optional)HTTP status code associated with the route

Sources: packages/next-routing/src/types.ts:13-23

Invocation Types and Resolution Parameters

Parameters for route resolution (ResolveRoutesParams) aggregate request properties, build identifiers, base paths, internationalization settings, and categorical route arrays (beforeMiddleware, middlewareMatchers, beforeFiles, afterFiles, dynamicRoutes, onMatch, and fallback). Route matching defaults to case-insensitivity to mirror default Next.js behavior unless caseSensitive is explicitly enabled.

export type ResolveRoutesParams = {
  url: URL
  buildId: string
  basePath: string
  requestBody: ReadableStream
  headers: Headers
  pathnames: string[]
  i18n?: {
    defaultLocale: string
    domains?: Array<{
      defaultLocale: string
      domain: string
      http?: true
      locales?: string[]
    }>
    localeDetection?: false
    locales: string[]
  }
  routes: {
    /**
     * When false (default), route matching is case-insensitive to mirror
     * Next.js default behavior. When true, matches are case-sensitive.
     */
    caseSensitive?: boolean
    beforeMiddleware: Array<Route>
    /**
     * Middleware matcher definitions used to decide whether middleware should
     * be invoked for the current request.
     */
    middlewareMatchers?: Array<Route>
    beforeFiles: Array<Route>
    afterFiles: Array<Route>
    dynamicRoutes: Array<Route>
    onMatch: Array<Route>
    fallback: Array<Route>
    shouldNormalizeNextData?: boolean
  }
  invokeMiddleware: (ctx: MiddlewareContext) => Promise<MiddlewareResult>
}

Sources: packages/next-routing/src/types.ts:42-80

The outcome of the resolution pipeline is structured via ResolveRoutesResult, which encapsulates middleware responses, external rewrites, redirects, resolved pathnames, merged query parameters, and a concrete RouteInvocationTarget.

export type RouteInvocationTarget = {
  /**
   * Concrete pathname that should be invoked after routing resolution.
   */
  pathname: string
  /**
   * Concrete query that should be invoked after routing resolution.
   */
  query: ResolveRoutesQuery
}

Sources: packages/next-routing/src/types.ts:85-94

Note

For dynamic routes, resolvedPathname returns the matched template pathname rather than the instantiated concrete URL path.

Sources: packages/next-routing/src/types.ts:103-107

Route Matching and Evaluation Pipeline

Request Evaluation Algorithm

Incoming requests flow through an ordered pipeline within resolveRoutes that handles URL initialization, internationalization prefixing, preprocessing via beforeMiddleware, middleware invocation, and localized redirects. Initial parameters such as headers, cookies, base paths, and build identifiers are unpacked, and initialUrl is copied into a mutable currentUrl instance for sequential transformations.

export async function resolveRoutes(
  params: ResolveRoutesParams
): Promise<ResolveRoutesResult> {
  const {
    url: initialUrl,
    basePath,
    requestBody,
    headers: initialHeaders,
    pathnames,
    routes,
    invokeMiddleware,
    buildId,
    i18n,
  } = params
 
  const { shouldNormalizeNextData, caseSensitive = false } = routes
 
  let currentUrl = new URL(initialUrl.toString())
  let currentRequestHeaders = new Headers(initialHeaders)
  let currentResponseHeaders = new Headers()
  let currentStatus: number | undefined
  let pendingLocaleRedirect:
    | {
        url: URL
        status: number
      }
    | undefined
  let pendingBeforeMiddlewareStop:
    | {
        status: number | undefined
      }
    | undefined
  const initialOrigin = initialUrl.origin
 
  // Check if the original URL is a data URL and normalize if so
  let isDataUrl = false
  if (shouldNormalizeNextData) {
    const dataPrefix = `${basePath}/_next/data/${buildId}/`
    isDataUrl = initialUrl.pathname.startsWith(dataPrefix)
 
    if (isDataUrl) {
      currentUrl = normalizeNextDataUrl(currentUrl, basePath, buildId)
    }
  }
 

Sources: packages/next-routing/src/resolve-routes.ts:557-601

If internationalization is configured and the route is not a Next.js data request or API path, the pipeline extracts locale information from the pathname or host headers. When locale auto-detection runs on index paths (/), a mismatched preferred locale triggers a pending redirect status of 307 to either a configured multi-domain target or a prefixed local path. Otherwise, the target locale is prefixed internally for subsequent route resolution phases without issuing an immediate client redirect.

// Handle i18n locale detection and redirects
if (i18n && !isDataUrl) {
  const pathname = currentUrl.pathname.startsWith(basePath)
    ? currentUrl.pathname.slice(basePath.length) || '/'
    : currentUrl.pathname
 
  // Skip locale handling for _next and api routes
  if (!pathname.startsWith('/_next/') && !pathname.startsWith('/api/')) {
    const hostname = currentUrl.hostname
    const cookieHeader = currentRequestHeaders.get('cookie') || undefined
    const acceptLanguageHeader =
      currentRequestHeaders.get('accept-language') || undefined
 
    // Detect locale from path first
    const pathLocaleResult = normalizeLocalePath(pathname, i18n.locales)
    const localeInPath = !!pathLocaleResult.detectedLocale
 
    // Detect domain locale
    const domainLocale = detectDomainLocale(i18n.domains, hostname)
    const defaultLocale = domainLocale?.defaultLocale || i18n.defaultLocale
 
    // Determine target locale if locale detection is enabled
    let targetLocale = pathLocaleResult.detectedLocale || defaultLocale
 
    // Match Next.js behavior: preferred-locale auto-detection redirects only
    // on index requests, not on arbitrary non-locale pathnames.
    const shouldDetectPreferredLocale =
      i18n.localeDetection !== false &&
      !localeInPath &&
      pathLocaleResult.pathname === '/'
 
    if (shouldDetectPreferredLocale) {
      const detectedResult = detectLocale({
        pathname,
        hostname,
        cookieHeader,
        acceptLanguageHeader,
        i18n,
      })
 
      targetLocale = detectedResult.locale
 
      // Check if we need to redirect based on domain or locale mismatch
      if (targetLocale !== defaultLocale) {
        const targetDomain = detectDomainLocale(
          i18n.domains,
          undefined,
          targetLocale
        )
 
        // Redirect to different domain if target locale has a different configured domain
        if (targetDomain && targetDomain.domain !== hostname) {
          const scheme = targetDomain.http ? 'http' : 'https'
          const localePrefix =
            targetLocale === targetDomain.defaultLocale
              ? ''
              : `/${targetLocale}`
          const redirectUrl = new URL(
            `${scheme}://${targetDomain.domain}${basePath}${localePrefix}${pathname}${currentUrl.search}`
          )
 
          pendingLocaleRedirect = {
            url: redirectUrl,
            status: 307,
          }
        }
 
        // If no dedicated domain for target locale, or we're already on the right domain,
        // redirect to add locale prefix on same domain
        if (
          !targetDomain ||
          (targetDomain && targetDomain.domain === hostname)
        ) {
          const redirectUrl = new URL(currentUrl.toString())
          redirectUrl.pathname = `${basePath}/${targetLocale}${pathname}`
 
          pendingLocaleRedirect = {
            url: redirectUrl,
            status: 307,
          }

Sources: packages/next-routing/src/resolve-routes.ts:602-693

Warning

Preferred-locale auto-detection only triggers redirect behavior on index requests where pathpathname equals '/', and explicitly skips arbitrary non-locale pathnames to prevent infinite redirection loops on deep links.

Sources: packages/next-routing/src/resolve-routes.ts:627-631

Middleware Matching and Invocation Checks

Before invoking middleware functions, requests pass through the beforeMiddleware route processor. If a redirect or external rewrite occurs during this phase, execution halts immediately and returns the resolved headers and status.

// Process beforeMiddleware routes
const beforeMiddlewareResult = processRoutes(
  routes.beforeMiddleware,
  currentUrl,
  currentRequestHeaders,
  currentResponseHeaders,
  initialOrigin,
  caseSensitive
)
 
if (beforeMiddlewareResult.status) {
  currentStatus = beforeMiddlewareResult.status
}
 
if (beforeMiddlewareResult.redirect) {
  return {
    redirect: beforeMiddlewareResult.redirect,
    resolvedHeaders: currentResponseHeaders,
    status: currentStatus,
  }
}
 
if (beforeMiddlewareResult.externalRewrite) {
  return {
    externalRewrite: beforeMiddlewareResult.externalRewrite,
    resolvedHeaders: currentResponseHeaders,
    status: currentStatus,
  }
}
 
if (beforeMiddlewareResult.stopped) {
  pendingBeforeMiddlewareStop = {
    status: currentStatus,
  }
}
 
currentUrl = beforeMiddlewareResult.url
 

Sources: packages/next-routing/src/resolve-routes.ts:695-732

If data URL normalization was active, URLs are denormalized before evaluating middleware matchers via shouldInvokeMiddlewareForRequest. When middleware runs, it can mutate request headers, append response set-cookie headers, or return redirect and rewrite destinations.

let middlewareInvocationUrl = currentUrl
 
// Denormalize before invoking middleware if this was originally a data URL
if (isDataUrl && shouldNormalizeNextData) {
  middlewareInvocationUrl = denormalizeNextDataUrl(
    currentUrl,
    basePath,
    buildId
  )
}
 
const shouldInvokeMiddleware = shouldInvokeMiddlewareForRequest(
  routes.middlewareMatchers,
  currentUrl,
  currentRequestHeaders,
  caseSensitive
)
 
if (shouldInvokeMiddleware) {
  // Invoke middleware
  const middlewareResult = await invokeMiddleware({
    url: middlewareInvocationUrl,
    headers: currentRequestHeaders,
    requestBody,
  })
 
  // Check if middleware sent the response body
  if (middlewareResult.bodySent) {
    return { middlewareResponded: true }
  }
 
  // Apply request headers from middleware
  if (middlewareResult.requestHeaders) {
    currentRequestHeaders = new Headers(middlewareResult.requestHeaders)
  }
 
  // Apply response headers from middleware
  if (middlewareResult.responseHeaders) {
    middlewareResult.responseHeaders.forEach((value, key) => {
      if (key.toLowerCase() === 'set-cookie') {
        currentResponseHeaders.append(key, value)
      } else {
        currentResponseHeaders.set(key, value)
      }
    })
  }
 
  // Handle middleware redirect
  if (middlewareResult.redirect) {
    if (!currentResponseHeaders.has('location')) {
      currentResponseHeaders.set(
        'Location',
        middlewareResult.redirect.url.toString()
      )
    }
    return {
      resolvedHeaders: currentResponseHeaders,
      status: middlewareResult.redirect.status,
    }
  }
 
  // Handle middleware rewrite
  if (middlewareResult.rewrite) {
    currentUrl = middlewareResult.rewrite
 
    // Check if it's an external rewrite
    if (currentUrl.origin !== initialOrigin) {
      return {
        externalRewrite: currentUrl,
        resolvedHeaders: currentResponseHeaders,
        status: currentStatus,
      }
    }
  }
}

Sources: packages/next-routing/src/resolve-routes.ts:733-807

Note

Response headers returned by middleware are appended normally for standard keys, but set-cookie headers utilize .append() to preserve multiple cookie definitions instead of overwriting existing entries.

Sources: packages/next-routing/src/resolve-routes.ts:770-778

Destination Replacement and URL Construction

Dynamic Token Substitution and Placeholder Parsing

Destination string processing relies on token substitution mechanisms that map regular expression captures, named capture groups, and conditional has matches onto placeholder tokens. The replaceDestination utility extracts keys from hasCaptures and index-based or group-based regular expression matches, sorts them by length in descending order, and constructs an alternation regex pattern. Placeholder tokens matching digits are constrained with negative lookaheads to prevent partial numerical suffix collisions.

export function replaceDestination(
  destination: string,
  regexMatches: RegExpMatchArray | null,
  hasCaptures: Record<string, string>
): string {
  const captureKeys = Object.keys(hasCaptures)
 
  if (regexMatches) {
    for (let index = 1; index < regexMatches.length; index++) {
      captureKeys.push(String(index))
    }
    if (regexMatches.groups) {
      captureKeys.push(...Object.keys(regexMatches.groups))
    }
  }
 
  if (captureKeys.length === 0) {
    return destination
  }
 
  const capturePattern = captureKeys
    .sort((first, second) => second.length - first.length)
    .map((key) => {
      const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
      return /^\d+$/.test(key) ? `${escapedKey}(?!\\d)` : escapedKey
    })
    .join('|')
 
  // Replace placeholders once. Captured paths can contain literal text such as
  // $2 or $d$segment that must not become another substitution.
  return destination.replace(
    new RegExp(`\\$(${capturePattern})`, 'g'),
    (placeholder, key: string) => {
      if (regexMatches) {
        const index = Number(key)
        if (
          Number.isInteger(index) &&
          index > 0 &&
          index < regexMatches.length &&
          String(index) === key
        ) {
          return regexMatches[index] ?? ''
        }
        if (regexMatches.groups && Object.hasOwn(regexMatches.groups, key)) {
          return regexMatches.groups[key] ?? ''
        }
      }
      if (Object.hasOwn(hasCaptures, key)) {
        return hasCaptures[key]
      }
      return placeholder
    }
  )
}

Sources: packages/next-routing/src/destination.ts:5-58

Warning

Placeholder replacement runs exactly once to prevent security vulnerabilities or replacement loops where captured paths containing literal strings like $2 or $d$segment get re-evaluated as nested substitution tokens.

Sources: packages/next-routing/src/destination.ts:33-35

In more complex routing configurations, prepareDestination parses destination templates and parameter dictionaries through path-to-regexp compilers while disabling validation flags. Disabling compile validation prevents failures when merging named pattern parameters from a source route regex into an entirely separate destination template structure.

Sources: packages/next/src/shared/lib/router/utils/prepare-destination.ts:254-263

Query Merging and External Destination Handling

Once dynamic tokens are resolved, destination application handles URL mutation or absolute redirection based on protocol inspection. The isExternalDestination function checks whether a destination string begins with explicit protocol prefixes.

export function isExternalDestination(destination: string): boolean {
  return destination.startsWith('http://') || destination.startsWith('https://')
}

Sources: packages/next-routing/src/destination.ts:63-65

When applying internal or external destinations via applyDestination, the utility splits the destination string into pathname and search components. Query parameters parsed from the destination search string are merged into the target URL parameters using URLSearchParams iteration, updating existing keys or appending new values accordingly.

export function applyDestination(currentUrl: URL, destination: string): URL {
  if (isExternalDestination(destination)) {
    return new URL(destination)
  }
 
  // Create a new URL with the updated pathname
  const newUrl = new URL(currentUrl.toString())
 
  // Handle destinations with query strings
  const [pathname, search] = destination.split('?')
  newUrl.pathname = pathname
 
  if (search) {
    // Merge query parameters
    const newParams = new URLSearchParams(search)
    for (const [key, value] of newParams.entries()) {
      newUrl.searchParams.set(key, value)
    }
  }
 
  return newUrl
}

Sources: packages/next-routing/src/destination.ts:71-92

Note

If a destination is identified as an external rewrite or redirect target via isExternalDestination, applyDestination bypasses query parameter merging on the current URL base and immediately constructs and returns a brand-new URL instance pointing to the external destination.

Sources: packages/next-routing/src/destination.ts:72-74

Next Data URL Normalization Cycle

Data URL Detection and Path Normalization

When Next.js routing handles requests, standalone data route resolution depends on normalizing incoming client requests that point to data route bundles. The routing lifecycle inspects whether data route normalization is enabled via shouldNormalizeNextData, identifying requests starting with the data prefix ${basePath}/_next/data/${buildId}/.

let isDataUrl = false
if (shouldNormalizeNextData) {
  const dataPrefix = `${basePath}/_next/data/${buildId}/`
  isDataUrl = initialUrl.pathname.startsWith(dataPrefix)
 
  if (isDataUrl) {
    currentUrl = normalizeNextDataUrl(currentUrl, basePath, buildId)
  }
}

Sources: packages/next-routing/src/resolve-routes.ts:592-600

When a request matches this structure, normalizeNextDataUrl strips the /_next/data/${buildId}/ segment and removes any trailing .json file extension from the path. If the resulting path equals index, it maps back to the root basePath or /. Otherwise, it reconstructs the standard pathname combining the base path and the remaining route segments.

export function normalizeNextDataUrl(
  url: URL,
  basePath: string,
  buildId: string
): URL {
  const newUrl = new URL(url.toString())
  let pathname = newUrl.pathname
 
  // Pattern: ${basePath}/_next/data/${buildId}/${path}.json
  const dataPrefix = `${basePath}/_next/data/${buildId}/`
 
  if (pathname.startsWith(dataPrefix)) {
    // Remove the /_next/data/${buildId}/ part, keeping what comes after
    let pathAfterData = pathname.slice(dataPrefix.length)
 
    // Remove .json extension if present
    if (pathAfterData.endsWith('.json')) {
      pathAfterData = pathAfterData.slice(0, -5)
    }
 
    if (pathAfterData === 'index') {
      pathname = basePath || '/'
    } else {
      pathname = basePath ? `${basePath}/${pathAfterData}` : `/${pathAfterData}`
    }
    newUrl.pathname = pathname
  }
 
  return newUrl
}

Sources: packages/next-routing/src/next-data.ts:5-34

Note

During normalization, an empty or root-level index data request like /_next/data/build-id/index.json is explicitly transformed to match the root basePath rather than an empty string pathname.

Sources: packages/next-routing/src/next-data.ts:25-27

Denormalization and Re-attachment Cycle

Before middleware invocation, the routing cycle denormalizes the normalized URL back into its data route format so that middleware matchers and rewrite rules inspect the original data path structure.

// Denormalize before invoking middleware if this was originally a data URL
if (isDataUrl && shouldNormalizeNextData) {
  middlewareInvocationUrl = denormalizeNextDataUrl(
    currentUrl,
    basePath,
    buildId
  )
}

Sources: packages/next-routing/src/resolve-routes.ts:735-742

The denormalizeNextDataUrl function checks if the current URL lacks the data prefix. If absent, it strips the basePath, maps empty or root paths to /index, and re-attaches the /_next/data/${buildId}/ prefix alongside the .json extension.

export function denormalizeNextDataUrl(
  url: URL,
  basePath: string,
  buildId: string
): URL {
  const newUrl = new URL(url.toString())
  let pathname = newUrl.pathname
 
  // Only denormalize if it's not already a data URL
  const dataPrefix = `${basePath}/_next/data/${buildId}/`
  if (!pathname.startsWith(dataPrefix)) {
    // Remove basePath if present
    let pathWithoutBase = pathname
    if (basePath && pathname.startsWith(basePath)) {
      pathWithoutBase = pathname.slice(basePath.length)
    }
 
    if (pathWithoutBase === '' || pathWithoutBase === '/') {
      pathWithoutBase = '/index'
    }
 
    // Add the /_next/data/${buildId}/ prefix and .json extension
    pathname = `${basePath}/_next/data/${buildId}${pathWithoutBase}.json`
    newUrl.pathname = pathname
  }
 
  return newUrl

Sources: packages/next-routing/src/next-data.ts:40-66

Warning

Denormalization execution is conditional: denormalizeNextDataUrl checks whether !pathname.startsWith(dataPrefix) to prevent recursive prefix duplication if a URL has already been transformed.

Sources: packages/next-routing/src/next-data.ts:49-50

Following middleware execution, if the request was originally flagged as a data URL, the routing engine normalizes the path a second time prior to evaluating beforeFiles route matching rules.

// Normalize again after middleware if this was originally a data URL
if (isDataUrl && shouldNormalizeNextData) {
  currentUrl = normalizeNextDataUrl(currentUrl, basePath, buildId)
}

Sources: packages/next-routing/src/resolve-routes.ts:829-832

Server and Client Routing Integration

Server Route Resolution and Filesystem Synchronization

Server-side routing operations coordinate request paths with concrete filesystem outputs using filesystem checkers and custom route tables. When resolveRoutes executes, it evaluates route execution flows sequentially against custom rewrites, redirects, before_files, middleware, and static filesystem assets via fsChecker.getItem(pathname).

const output = await fsChecker.getItem(pathname)

Sources: packages/next/src/server/lib/router-utils/resolve-routes.ts:504-504

Note

Only page and app file outputs participate in route rendering. Public, static, image, and virtual filesystem items bypass template rendering and are served directly as static filesystem assets.

Sources: packages/next/src/server/lib/router-utils/filesystem.ts:845-849

Client-side navigation state bridges runtime parameter parsing through helper utilities like adaptForPathParams and adaptForSearchParams. These adapters translate the current browser router state into structured Params and URLSearchParams instances matching server expectations.

export function adaptForPathParams(
  router: Pick<NextRouter, 'isReady' | 'pathname' | 'query' | 'asPath'>
): Params | null {
  if (!router.isReady || !router.query) {
    return null
  }
  const pathParams: Params = {}
  const routeRegex = getRouteRegex(router.pathname)
  const keys = Object.keys(routeRegex.groups)
  for (const key of keys) {
    pathParams[key] = router.query[key]!
  }
  return pathParams
}

Sources: packages/next/src/shared/lib/router/adapters.tsx:57-70

URL Normalization and Canonical Parameter Decoding

Client-side href resolution handles parameter interpolation via resolveHref, normalizing paths and testing local URL validity before routing.

export function resolveHref(
  router: NextRouter,
  href: Url,
  resolveAs: true
): [string, string] | [string]
export function resolveHref(
  router: NextRouter,
  href: Url,
  resolveAs?: false
): string
export function resolveHref(
  router: NextRouter,
  href: Url,
  resolveAs?: boolean
): [string, string] | [string] | string {

Sources: packages/next/src/client/resolve-href.ts:18-32

To maintain consistent encodings between client URL parts and server-side route definitions, canonicalizeURLPart decodes incoming path segments and re-encodes them using encodeURIComponent. This prevents double-encoding issues for percent-encoded sequences like %2F.

export function canonicalizeURLPart(part: string): string {
  try {
    return encodeURIComponent(decodeURIComponent(part))
  } catch {
    // `decodeURIComponent` throws on malformed sequences. Fall back to the
    // already-encoded form rather than failing the navigation.
    return part
  }
}

Sources: packages/next/src/client/route-params.ts:86-94

Warning

The browser URL parser and server-side dynamic param decoders handle special characters differently; URL parsers leave characters like , and : untouched, whereas encodeURIComponent percent-encodes them.

Sources: packages/next/src/client/route-params.ts:78-85

Path and Search Param Synchronization

Adapter / FunctionTarget TypeInput SourceSynchronization Behavior
adaptForSearchParamsURLSearchParamsrouter.asPathExtracts query parameters from the active browser path string.
adaptForPathParamsParamsrouter.queryExtracts dynamic route parameters keyed by route regex group names.
canonicalizeURLPartstringURL pathname partDecodes and re-encodes path segments to match server-side parameters.
getRenderedPathnameNormalizedPathnameRSCResponseResolves pathnames using server headers or response URLs.

Sources: packages/next/src/shared/lib/router/adapters.tsx:47-70, packages/next/src/client/route-params.ts:48-63, packages/next/src/client/route-params.ts:86-94

Was this page helpful?