From 3493a7e913c5b5c97b9e9650c5c94bb8252720bb Mon Sep 17 00:00:00 2001 From: Peter Amiri Date: Fri, 12 Jun 2026 02:54:45 -0700 Subject: [PATCH] docs(web/guides): correct routing guides to verified router behavior MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Guide-behavioral-audit batch 2 (p1-10-routing) fixes for basics/routing.mdx and core-concepts/how-routing-works.mdx, verified live on Lucee 7 and Adobe 2023: - Namespaced sections: replace the .namespace(name="admin", callback=...) example with the working .namespace("admin")...end() form. scope()/namespace() silently ignore callback= and the unclosed scope swallows subsequent routes (#3072); add a caution Aside and the extends="app.controllers.Controller" note for subfolder controllers. - Match algorithm + order rules: document the static-route O(1) index — literal paths resolve before the ordered placeholder scan regardless of declaration position; declaration order governs placeholder-vs-placeholder and ties between identical static patterns (#3073). Invert the /posts/featured order-rules bullet accordingly. - Resource routes: update registers both PATCH and PUT; seven REST actions (not seven routes) and every route gets a .[format] twin. - Placeholder default match is [^\./]+ (non-slash AND non-dot), not "any non-slash string". verify:docs: 7 tagged blocks pass. Co-Authored-By: Claude Fable 5 Signed-off-by: Peter Amiri --- .../content/docs/v4-0-0/basics/routing.mdx | 22 ++++++++++++------- .../core-concepts/how-routing-works.mdx | 17 +++++++++----- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/web/sites/guides/src/content/docs/v4-0-0/basics/routing.mdx b/web/sites/guides/src/content/docs/v4-0-0/basics/routing.mdx index 140e45638b..ec8f26550a 100644 --- a/web/sites/guides/src/content/docs/v4-0-0/basics/routing.mdx +++ b/web/sites/guides/src/content/docs/v4-0-0/basics/routing.mdx @@ -13,7 +13,7 @@ Define the URL surface of your app in `config/routes.cfm`. This page shows how t **You'll learn:** - How to define a single route with `get`, `post`, `patch`, `delete` -- How `resources()` produces seven REST routes in one line +- How `resources()` produces the seven REST actions in one line - How to nest resources and namespace an admin section - How to constrain placeholder patterns with regex - Which helper reads which route at call time @@ -39,7 +39,7 @@ mapper() ## Resource routes -`resources()` is where most routing happens. One call generates seven REST routes plus the named helpers that go with them. +`resources()` is where most routing happens. One call generates routes for the seven REST actions plus the named helpers that go with them. ```cfm {test:compile} @@ -51,7 +51,7 @@ mapper() ``` -This gives you `GET /posts`, `GET /posts/new`, `POST /posts`, `GET /posts/:key`, `GET /posts/:key/edit`, `PATCH /posts/:key`, and `DELETE /posts/:key` — plus the named routes `posts`, `newPost`, `post`, and `editPost`. `binding=true` turns on route model binding: the member actions (`show`, `edit`, `update`, `delete`) see a pre-loaded `params.post` instance without calling `findByKey` themselves. +This gives you `GET /posts`, `GET /posts/new`, `POST /posts`, `GET /posts/:key`, `GET /posts/:key/edit`, `PATCH/PUT /posts/:key` (both verbs map to `update`), and `DELETE /posts/:key` — plus the named routes `posts`, `newPost`, `post`, and `editPost`. Each route is also registered with a `.[format]` twin (`/posts.json`, `/posts/:key.xml`, …), so the actual table holds more rows than seven. `binding=true` turns on route model binding: the member actions (`show`, `edit`, `update`, `delete`) see a pre-loaded `params.post` instance without calling `findByKey` themselves. ## Trim the resource with `only` or `except` @@ -88,14 +88,14 @@ The nested resource produces `POST /posts/:postKey/comments → comments##create ## Namespaced sections -Group a set of controllers under a shared URL prefix and a subfolder. `.namespace(name="admin")` prefixes URLs with `/admin` and loads controllers from `app/controllers/admin/`. +Group a set of controllers under a shared URL prefix and a subfolder. `.namespace("admin")` prefixes URLs with `/admin` and loads controllers from `app/controllers/admin/`. Declare the namespaced routes between the `.namespace()` call and a matching `.end()`: ```cfm {test:compile} mapper() - .namespace(name="admin", callback=function(map) { - map.resources("posts"); - }) + .namespace("admin") + .resources("posts") + .end() .resources("posts") .wildcard() .end(); @@ -104,9 +104,15 @@ mapper() URLs become `/admin/posts`, `/admin/posts/:key`, and so on; the controller class is `admin/Posts.cfc`. The outer `.resources("posts")` still serves the public `/posts` URLs — namespaces don't replace the non-namespaced routes, they sit alongside them. + + +Controllers that live in `app/controllers/admin/` must extend the base controller with its full path — `extends="app.controllers.Controller"`. The bare `extends="Controller"` fails to resolve from the subfolder on Lucee (`invalid component definition, can't find component [Controller]`). + ## Constrain pattern placeholders -Pattern placeholders are written as `[name]`. By default any non-slash string matches. Pass a `constraints` struct to restrict a placeholder to a regex. +Pattern placeholders are written as `[name]`. By default a placeholder matches any string without a slash or dot (`[^\./]+` — dots are reserved for the optional `.[format]` suffix). Pass a `constraints` struct to restrict a placeholder to a regex. ```cfm {test:compile} diff --git a/web/sites/guides/src/content/docs/v4-0-0/core-concepts/how-routing-works.mdx b/web/sites/guides/src/content/docs/v4-0-0/core-concepts/how-routing-works.mdx index 6ac62a0f3e..69d0772193 100644 --- a/web/sites/guides/src/content/docs/v4-0-0/core-concepts/how-routing-works.mdx +++ b/web/sites/guides/src/content/docs/v4-0-0/core-concepts/how-routing-works.mdx @@ -6,7 +6,7 @@ sidebar: order: 7 --- -Routing is the stage between a URL arriving and a controller method running. The algorithm is small enough to keep in your head, the expansion rules are fixed, and the order of declarations is the order of matching — which makes routing failures easy to reason about once you know the shape. +Routing is the stage between a URL arriving and a controller method running. The algorithm is small enough to keep in your head, the expansion rules are fixed, and the precedence rules are mechanical — static paths first, then declaration order — which makes routing failures easy to reason about once you know the shape. **You'll learn:** @@ -18,13 +18,18 @@ Routing is the stage between a URL arriving and a controller method running. The ## The match algorithm -The router walks `config/routes.cfm` top-to-bottom on every request. The first route whose HTTP method and URL pattern both match the request wins — and that's the match, even if a more specific route appears later. There is no scoring, no backtracking, no "best fit." +Routes are compiled at boot into an in-memory table, and the router resolves each request in two steps: -Resource and namespaced declarations are not matched as a unit. They expand at boot time into plain routes that sit in the same ordered list as your hand-written `.get()` and `.post()` calls. A `.resources("posts")` is seven entries in the table, registered in a fixed order. The wildcard route catches anything that falls through and maps `/controller/action` conventionally — a legacy pattern kept for upgrade paths. Named routes are the preferred way to wire URLs. +1. **Static routes first.** Routes whose patterns contain no placeholders (`/posts/featured`, `/about`) live in an exact-path index and are checked with an O(1) lookup before anything else. A literal path always beats a placeholder route, no matter where either was declared. If two routes register the same static pattern, the first one declared wins. +2. **Placeholder routes in declaration order.** Everything that isn't a static match falls through to an ordered top-to-bottom scan of the placeholder routes. The first route whose HTTP method and URL pattern both match wins — even if a more specific placeholder route appears later. There is no scoring, no backtracking, no "best fit." + +The static-first step means declaration order does **not** decide static-vs-placeholder conflicts — only placeholder-vs-placeholder ones. This is a deliberate performance index, though it contradicts the framework's older pure first-match-wins description; [#3073](https://github.com/wheels-dev/wheels/issues/3073) tracks reconciling the two. + +Resource and namespaced declarations are not matched as a unit. They expand at boot time into plain routes that sit in the same table as your hand-written `.get()` and `.post()` calls. A `.resources("posts")` registers an entry per REST action, in a fixed order. The wildcard route catches anything that falls through and maps `/controller/action` conventionally — a legacy pattern kept for upgrade paths. Named routes are the preferred way to wire URLs. ## What `.resources("posts")` expands to -One call generates seven routes. The names in the right column are what you pass to `linkTo`, `redirectTo`, and `urlFor`. +One call generates the seven REST actions below. Each row is also registered with a `.[format]` twin (`/posts.json`, `/posts/:key.xml`, …) for content negotiation, so the actual table holds twice as many rows. The names in the right column are what you pass to `linkTo`, `redirectTo`, and `urlFor`. | HTTP method | Path | Controller#action | Named route | |-------------|------|-------------------|-------------| @@ -42,9 +47,9 @@ Two routes can share a named route because the helpers disambiguate by HTTP verb Route order is almost always the cause when a URL matches the wrong action. Four rules cover every case: -- Specific routes come before generic ones. A literal path beats a pattern with a placeholder. +- A literal path beats a pattern with a placeholder — this is enforced by the router itself (the static-route index resolves first), not a convention you have to maintain by ordering. - `.resources(...)` declarations come before `.root(...)`, which comes before `.wildcard()`. The wildcard is always last. -- A custom `.get(pattern="/posts/featured", to="posts##featured")` must come **before** `.resources("posts")`, or `/posts/featured` matches the `show` route with `params.key = "featured"` instead. +- A custom `.get(pattern="/posts/featured", to="posts##featured")` routes `/posts/featured` to `featured` whether it's declared before or after `.resources("posts")` — the static index wins over the placeholder `show` route in both orders. Declaring it first is still good style: it keeps the intent visible and matches the placeholder-vs-placeholder rule, where order genuinely decides. - Order within a named-route group doesn't matter for URL generation. Named routes are a keyed lookup — the name is the key, not the position. ```cfm title="illustrative — config/routes.cfm"