diff --git a/changelog.d/scaffold-homepage-onboarding.changed.md b/changelog.d/scaffold-homepage-onboarding.changed.md
new file mode 100644
index 0000000000..809dbba29a
--- /dev/null
+++ b/changelog.d/scaffold-homepage-onboarding.changed.md
@@ -0,0 +1 @@
+- `wheels new` generates a richer default home page: a runtime status line (Wheels version, engine, database, environment) plus a Next-steps command guide, replacing the bare two-line placeholder — surfacing the onboarding content from the redesigned framework welcome page where users actually land (#2098)
diff --git a/changelog.d/wheels-welcome-production-gate.security.md b/changelog.d/wheels-welcome-production-gate.security.md
new file mode 100644
index 0000000000..dba3ce0153
--- /dev/null
+++ b/changelog.d/wheels-welcome-production-gate.security.md
@@ -0,0 +1 @@
+- The `/wheels` welcome page now defense-in-depth gates itself with `$blockInProduction()` like every other `Public` handler, so it no longer renders outside `development` when `enablePublicComponent` is manually enabled — closing a version/engine/database/environment disclosure gap (reverses the #2233 exception)
diff --git a/cli/lucli/Module.cfc b/cli/lucli/Module.cfc
index 4667e3f72c..359c329c60 100644
--- a/cli/lucli/Module.cfc
+++ b/cli/lucli/Module.cfc
@@ -6191,7 +6191,27 @@ component extends="modules.BaseModule" {
fileWrite(
targetDir & "/app/views/main/index.cfm",
- '
Welcome to ' & appName & '
' & nl & 'Your Wheels application is running. Edit this file at app/views/main/index.cfm
' & nl
+ (
+ '' & nl &
+ '' & nl &
+ 'Welcome to ' & appName & '
' & nl &
+ 'Your Wheels ##get("version")## application is running on ##application.wheels.serverName## with ##application.wheels.dataSourceName## (##get("environment")##).
' & nl &
+ nl &
+ 'Next steps
' & nl &
+ '' & nl &
+ tab & 'wheels g scaffold Post title content:text — generate a model, controller, and views ' & nl &
+ tab & 'wheels migrate latest — build the database schema ' & nl &
+ tab & 'wheels test — run the test suite ' & nl &
+ '
' & nl &
+ 'This page lives at app/views/main/index.cfm; routing is in config/routes.cfm.
' & nl &
+ '' & nl
+ )
);
printCreated(appName & "/app/views/main/index.cfm");
diff --git a/cli/tests/specs/e2e/ProjectScaffoldTest.cfc b/cli/tests/specs/e2e/ProjectScaffoldTest.cfc
index 3e7a54deb8..675b1c1ad6 100644
--- a/cli/tests/specs/e2e/ProjectScaffoldTest.cfc
+++ b/cli/tests/specs/e2e/ProjectScaffoldTest.cfc
@@ -214,6 +214,16 @@ component extends="testbox.system.BaseSpec" {
var content = fileRead(path);
expect(content).toInclude("Welcome to testapp");
+ // Runtime expressions must survive generation as single-hash
+ // CFML (## -> # in the fileWrite string), not be evaluated at
+ // scaffold time. Locks in the escaping shared with Module.cfc.
+ expect(content).toInclude('##get("version")##');
+ expect(content).toInclude('##application.wheels.serverName##');
+ expect(content).toInclude("");
+ expect(content).toInclude("Next steps");
+ expect(content).toInclude("wheels g scaffold");
+ expect(content).toInclude("wheels migrate latest");
+ expect(content).toInclude("wheels test");
});
it("generates base Controller.cfc in app/controllers/", function() {
@@ -325,7 +335,27 @@ component extends="testbox.system.BaseSpec" {
fileWrite(
arguments.targetDir & "/app/views/main/index.cfm",
- 'Welcome to ' & arguments.appName & '
' & nl & 'Your Wheels application is running. Edit this file at app/views/main/index.cfm
' & nl
+ (
+ '' & nl &
+ '' & nl &
+ 'Welcome to ' & arguments.appName & '
' & nl &
+ 'Your Wheels ##get("version")## application is running on ##application.wheels.serverName## with ##application.wheels.dataSourceName## (##get("environment")##).
' & nl &
+ nl &
+ 'Next steps
' & nl &
+ '' & nl &
+ tab & 'wheels g scaffold Post title content:text — generate a model, controller, and views ' & nl &
+ tab & 'wheels migrate latest — build the database schema ' & nl &
+ tab & 'wheels test — run the test suite ' & nl &
+ '
' & nl &
+ 'This page lives at app/views/main/index.cfm; routing is in config/routes.cfm.
' & nl &
+ '' & nl
+ )
);
}
diff --git a/vendor/wheels/Public.cfc b/vendor/wheels/Public.cfc
index c3d45f7146..512d2be508 100644
--- a/vendor/wheels/Public.cfc
+++ b/vendor/wheels/Public.cfc
@@ -30,7 +30,7 @@ component output="false" displayName="Internal GUI" extends="wheels.Global" {
/**
* Defense-in-depth: unless the current environment is `development`,
* short-circuit the handler with a 404 response before any view is
- * included. Called as the first statement of every non-`index` handler in
+ * included. Called as the first statement of every handler in
* this component.
*/
public void function $blockInProduction() {
@@ -356,6 +356,7 @@ component output="false" displayName="Internal GUI" extends="wheels.Global" {
This is just a proof of concept
*/
function index() {
+ $blockInProduction();
include "/wheels/public/views/congratulations.cfm";
return "";
}
diff --git a/vendor/wheels/tests/specs/dispatch/InvokeMethodSpec.cfc b/vendor/wheels/tests/specs/dispatch/InvokeMethodSpec.cfc
index 06a7b8e0c9..7f848b0253 100644
--- a/vendor/wheels/tests/specs/dispatch/InvokeMethodSpec.cfc
+++ b/vendor/wheels/tests/specs/dispatch/InvokeMethodSpec.cfc
@@ -47,9 +47,10 @@ component extends="wheels.WheelsTest" {
// (since #2903 the gate is a development-only allowlist), so
// the only thing we're testing is "did the receiver survive
// the dispatch?" If it didn't, the call throws before the
- // include statement runs. (This spec invokes the ungated
- // index() handler, so the production-only early-return below
- // is belt-and-suspenders.)
+ // include statement runs. (This spec invokes index(), which now
+ // calls $blockInProduction() too (a development-only no-op per
+ // the allowlist), so the production-only early-return below is
+ // belt-and-suspenders.)
if (
StructKeyExists(application, "wheels")
&& StructKeyExists(application.wheels, "environment")
diff --git a/vendor/wheels/tests/specs/security/PublicComponentProductionSpec.cfc b/vendor/wheels/tests/specs/security/PublicComponentProductionSpec.cfc
index 8e84c856bc..6c219d9ca4 100644
--- a/vendor/wheels/tests/specs/security/PublicComponentProductionSpec.cfc
+++ b/vendor/wheels/tests/specs/security/PublicComponentProductionSpec.cfc
@@ -150,7 +150,8 @@ component extends="wheels.WheelsTest" {
"ai",
"guideImage",
"assets",
- "mcp"
+ "mcp",
+ "index"
];
for (var handler in gatedHandlers) {
@@ -170,17 +171,14 @@ component extends="wheels.WheelsTest" {
})(handler);
}
- it("index() is NOT gated (congratulations page stays discoverable)", () => {
- // The issue explicitly says leave index() reachable in dev/testing.
- // In production enablePublicComponent=false already hides it at
- // the dispatch layer, so no per-handler block is needed.
- var pattern = "function\s+index\s*\([^)]*\)\s*\{\s*\$blockInProduction\s*\(\s*\)\s*;";
- var matched = REFindNoCase(pattern, source) > 0;
- expect(matched).toBeFalse(
- "index() should not call $blockInProduction() — it's the congratulations "
- & "page and the issue (##2233) explicitly keeps it discoverable."
- );
- });
+ // index() (the congratulations/welcome page at the /wheels namespace
+ // root) is now gated like every other handler above. #2233 originally
+ // left it ungated so the welcome page stayed reachable in dev/testing,
+ // relying on enablePublicComponent=false to hide /wheels in production.
+ // Reversed because the redesigned page surfaces version/engine/db/
+ // environment (#2098/#2272), the same class of detail the gated handlers
+ // protect, so it now defense-in-depth gates itself too. Development still
+ // renders (the gate is an allowlist: only "development" passes).
});