diff --git a/CHANGELOG.md b/CHANGELOG.md
index d7bfdb6508..f6cf643b1e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -34,6 +34,7 @@ All historical references to "CFWheels" in this changelog have been preserved fo
### Fixed
+- `paginationLinks()` now emits a one-time per-request `WriteLog(type="warning", ...)` deprecation notice pointing 3.x → 4.x upgraders at `paginationNav()` (the all-in-one helper) and the individual `firstPageLink`/`previousPageLink`/`pageNumberLinks`/`nextPageLink`/`lastPageLink` composables. `wheels upgrade check --to=4.0.0` now also greps `app/views/` for `paginationLinks(` and flags every hit with a remediation pointer, closing the silent-rot gap surfaced by titan Phase 2.4 (#2714)
- `paginationNav()` now throws `Wheels.PaginationNav.InvalidArgument` when passed an argument that none of its sub-helpers (`paginationInfo`, `firstPageLink`, `previousPageLink`, `pageNumberLinks`, `nextPageLink`, `lastPageLink`) accept. Previously, typos such as `prependToList="
"` were silently dropped by CFML's `argumentCollection` dispatch, leaving users to wonder why a styling argument had no effect. The check is gated on `application.wheels.showErrorInformation` so production is unaffected; development environments fail fast and the error names both the rejected arguments and the full allowlist of accepted pass-through keys (#2717)
- `wheels --help` no longer summarises the `packages` command as `Install, update, search Wheels packages` — that phrasing nudged users to type `wheels packages install `, which LuCLI's built-in extension installer intercepts before module dispatch and silently no-ops (`[INFO] No git or extension dependencies to install`, exit 0, nothing under `vendor/`). The summary now leads with the canonical verb (`Add, update, search ...`) and parenthesises the gotcha so the doc surface stops contradicting the runtime. Same trap that earlier renamed `wheels browser install` to `wheels browser setup` (#2706)
- `wheels.middleware.Cors` now emits `Vary: Origin` alongside the reflected `Access-Control-Allow-Origin` header so CDN, reverse-proxy, and browser disk caches key the response on the request Origin instead of serving a cached response with the wrong ACAO to a different origin. Matches the behavior of the legacy 3.x `Global.cfc::$setCORSHeaders` path (vendor/wheels/Global.cfc:3565). The header is only emitted when an origin is actually being reflected — wildcard (`allowOrigins="*"`) responses and disallowed-origin responses are unchanged (#2707)
diff --git a/cli/lucli/Module.cfc b/cli/lucli/Module.cfc
index f36f003da5..48262ee348 100644
--- a/cli/lucli/Module.cfc
+++ b/cli/lucli/Module.cfc
@@ -3733,6 +3733,15 @@ component extends="modules.BaseModule" {
extensions: "cfm,cfc",
fix: "Missing manifest entries throw Wheels.ViteAssetNotFound in production. Rebuild assets during deploy (npm run build) or set(viteStrictManifest=false) to restore 3.x silent fallback."
});
+ // paginationLinks() deprecation grep (#2714, replacement: paginationNav() per #1930).
+ arrayAppend(checks, {
+ description: "Deprecated paginationLinks() helper (renamed to paginationNav() in 4.0)",
+ pattern: "paginationLinks\s*\(",
+ checkType: "grep",
+ scanDir: "app/views",
+ extensions: "cfm,cfc",
+ fix: "Replace paginationLinks() with paginationNav() (the all-in-one nav helper) or compose firstPageLink/previousPageLink/pageNumberLinks/nextPageLink/lastPageLink directly. See https://github.com/wheels-dev/wheels/issues/1930."
+ });
}
// Run checks
diff --git a/vendor/wheels/tests/specs/cli/UpgradeCheckCoverageSpec.cfc b/vendor/wheels/tests/specs/cli/UpgradeCheckCoverageSpec.cfc
index a1f0ebdb68..4c062564d1 100644
--- a/vendor/wheels/tests/specs/cli/UpgradeCheckCoverageSpec.cfc
+++ b/vendor/wheels/tests/specs/cli/UpgradeCheckCoverageSpec.cfc
@@ -88,6 +88,12 @@ component extends="wheels.WheelsTest" {
);
});
+ it("scans for deprecated paginationLinks() helper", () => {
+ expect(findNoCase("paginationLinks", block) > 0).toBeTrue(
+ "3.x -> 4.x checks should grep views for paginationLinks( (renamed to paginationNav(), CHANGELOG ##2714)."
+ );
+ });
+
});
}
diff --git a/vendor/wheels/tests/specs/view/paginationLinksDeprecationSpec.cfc b/vendor/wheels/tests/specs/view/paginationLinksDeprecationSpec.cfc
new file mode 100644
index 0000000000..755fc599e6
--- /dev/null
+++ b/vendor/wheels/tests/specs/view/paginationLinksDeprecationSpec.cfc
@@ -0,0 +1,41 @@
+component extends="wheels.WheelsTest" {
+
+ function run() {
+
+ g = application.wo
+
+ describe("paginationLinks deprecation warning (##2714)", () => {
+
+ beforeEach(() => {
+ _params = {controller = "dummy", action = "dummy"}
+ _controller = g.controller("dummy", _params)
+ g.set(functionName = "paginationLinks", encode = false)
+ structDelete(request.wheels, "$paginationLinksDeprecationLogged")
+ })
+
+ afterEach(() => {
+ g.set(functionName = "paginationLinks", encode = true)
+ structDelete(request.wheels, "$paginationLinksDeprecationLogged")
+ })
+
+ it("sets a request-scoped guard flag on first call", () => {
+ g.model("author").findAll(page = 2, perPage = 3, order = "lastName")
+ expect(structKeyExists(request.wheels, "$paginationLinksDeprecationLogged")).toBeFalse()
+ _controller.paginationLinks()
+ expect(structKeyExists(request.wheels, "$paginationLinksDeprecationLogged")).toBeTrue()
+ expect(request.wheels.$paginationLinksDeprecationLogged).toBeTrue()
+ })
+
+ it("does not re-log when called multiple times in the same request", () => {
+ g.model("author").findAll(page = 2, perPage = 3, order = "lastName")
+ _controller.paginationLinks()
+ request.wheels.$paginationLinksDeprecationLogged = "first"
+ _controller.paginationLinks()
+ expect(request.wheels.$paginationLinksDeprecationLogged).toBe("first")
+ })
+
+ })
+
+ }
+
+}
diff --git a/vendor/wheels/view/links.cfc b/vendor/wheels/view/links.cfc
index a1c98b703c..d3a2a260c7 100644
--- a/vendor/wheels/view/links.cfc
+++ b/vendor/wheels/view/links.cfc
@@ -233,6 +233,15 @@ component {
boolean pageNumberAsParam,
any encode
) {
+ // One-time per-request deprecation warning (#2714) — mirrors $checkPluginsDeprecation() in Plugins.cfc.
+ if (!StructKeyExists(request.wheels, "$paginationLinksDeprecationLogged")) {
+ request.wheels.$paginationLinksDeprecationLogged = true;
+ WriteLog(
+ type = "warning",
+ text = "[Wheels] paginationLinks() is deprecated and will be removed in a future release. Use paginationNav() instead (or compose the individual helpers: firstPageLink/previousPageLink/pageNumberLinks/nextPageLink/lastPageLink). See https://github.com/wheels-dev/wheels/issues/1930"
+ );
+ }
+
/* To fix the bug below:
https://github.com/wheels-dev/wheels/issues/942
diff --git a/web/sites/guides/src/content/docs/v4-0-0/upgrading/3x-to-4x.mdx b/web/sites/guides/src/content/docs/v4-0-0/upgrading/3x-to-4x.mdx
index 557a24cb4e..57e830f9a4 100644
--- a/web/sites/guides/src/content/docs/v4-0-0/upgrading/3x-to-4x.mdx
+++ b/web/sites/guides/src/content/docs/v4-0-0/upgrading/3x-to-4x.mdx
@@ -366,7 +366,7 @@ See [Packages](/v4-0-0/digging-deeper/packages/) for manifest fields, per-method
### Monolithic `paginationLinks()`
-**CHANGELOG:** composable pagination helpers added (#1930). The 3.x `paginationLinks()` is retained. New code should use `paginationNav()` or compose the individual helpers (`paginationInfo`, `firstPageLink`, `previousPageLink`, `pageNumberLinks`, `nextPageLink`, `lastPageLink`).
+**CHANGELOG:** composable pagination helpers added (#1930). The 3.x `paginationLinks()` is retained but deprecated. As of #2727, calling `paginationLinks()` emits a one-time per-request `WriteLog(type="warning")` pointing at the replacement so apps see the signal without flooding the log. `wheels upgrade check --to=4.0.0` also greps `app/views/` for `paginationLinks(` and flags every hit with a remediation pointer. New code should use `paginationNav()` or compose the individual helpers (`paginationInfo`, `firstPageLink`, `previousPageLink`, `pageNumberLinks`, `nextPageLink`, `lastPageLink`).
### RocketUnit test style
diff --git a/web/sites/guides/src/content/docs/v4-0-1-snapshot/upgrading/3x-to-4x.mdx b/web/sites/guides/src/content/docs/v4-0-1-snapshot/upgrading/3x-to-4x.mdx
index 6b3470b986..432d566ed1 100644
--- a/web/sites/guides/src/content/docs/v4-0-1-snapshot/upgrading/3x-to-4x.mdx
+++ b/web/sites/guides/src/content/docs/v4-0-1-snapshot/upgrading/3x-to-4x.mdx
@@ -366,7 +366,7 @@ See [Packages](/v4-0-1-snapshot/digging-deeper/packages/) for manifest fields, p
### Monolithic `paginationLinks()`
-**CHANGELOG:** composable pagination helpers added (#1930). The 3.x `paginationLinks()` is retained. New code should use `paginationNav()` or compose the individual helpers (`paginationInfo`, `firstPageLink`, `previousPageLink`, `pageNumberLinks`, `nextPageLink`, `lastPageLink`).
+**CHANGELOG:** composable pagination helpers added (#1930). The 3.x `paginationLinks()` is retained but deprecated. As of #2727, calling `paginationLinks()` emits a one-time per-request `WriteLog(type="warning")` pointing at the replacement so apps see the signal without flooding the log. `wheels upgrade check --to=4.0.0` also greps `app/views/` for `paginationLinks(` and flags every hit with a remediation pointer. New code should use `paginationNav()` or compose the individual helpers (`paginationInfo`, `firstPageLink`, `previousPageLink`, `pageNumberLinks`, `nextPageLink`, `lastPageLink`).
### RocketUnit test style