From 6a634936561400b5b4e1598da40bc6c345f6b9b1 Mon Sep 17 00:00:00 2001 From: Anto Subash Date: Tue, 12 May 2026 23:28:53 +0200 Subject: [PATCH] fix(cli): scaffold a Landing page at / so fresh apps don't 404 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Quickstart promises `http://localhost:8000` is a landing page, but the CLI host template registered no `/` route — every fresh `smpy new` app 404'd on first visit. Add a host-owned `/` route (decoupled from the `dashboard` module) plus a minimal Tailwind Landing page to the template, and pin the behaviour with a regression test. Closes #137 --- .../host/client_app/pages/Landing.tsx | 78 +++++++++++++++++++ .../simple_module_cli/templates/host/main.py | 3 + .../templates/host/routes.py | 28 +++++++ .../cli/tests/test_cli_new_regressions.py | 24 ++++++ framework/cli/tests/test_scaffolding_host.py | 2 + 5 files changed, 135 insertions(+) create mode 100644 framework/cli/simple_module_cli/templates/host/client_app/pages/Landing.tsx create mode 100644 framework/cli/simple_module_cli/templates/host/routes.py diff --git a/framework/cli/simple_module_cli/templates/host/client_app/pages/Landing.tsx b/framework/cli/simple_module_cli/templates/host/client_app/pages/Landing.tsx new file mode 100644 index 00000000..697bbb2f --- /dev/null +++ b/framework/cli/simple_module_cli/templates/host/client_app/pages/Landing.tsx @@ -0,0 +1,78 @@ +/** + * Landing page for the public root (`/`). + * + * Scaffolded by `smpy new` so a fresh app boots straight to a friendly + * page instead of a 404. Replace or restyle as your app's front door + * takes shape — the only contract is that something renders at "/". + */ + +type LandingProps = { + isAuthenticated: boolean; +}; + +export default function Landing({ isAuthenticated }: LandingProps) { + const primaryHref = isAuthenticated ? '/dashboard' : '/users/login'; + const primaryLabel = isAuthenticated ? 'Open dashboard' : 'Sign in'; + + return ( +
+
+

+ simple_module_py +

+

+ Your app is up. +

+

+ This is the host's landing page — generated by{' '} + smpy new. + Edit{' '} + + client_app/pages/Landing.tsx + {' '} + to make it yours. +

+
+ +
+ + {primaryLabel} + + + Read the docs + +
+ +
+

Next steps

+
    +
  • + Create an admin:{' '} + + uv run smpy users create-admin + +
  • +
  • + Scaffold a module:{' '} + + smpy create-module orders --dest modules/orders + +
  • +
  • + Sign in at{' '} + + /users/login + {' '} + and explore the dashboard. +
  • +
+
+
+ ); +} diff --git a/framework/cli/simple_module_cli/templates/host/main.py b/framework/cli/simple_module_cli/templates/host/main.py index f5384831..b1b9f235 100644 --- a/framework/cli/simple_module_cli/templates/host/main.py +++ b/framework/cli/simple_module_cli/templates/host/main.py @@ -7,6 +7,8 @@ from simple_module_hosting import Settings, create_app from simple_module_hosting.logging import setup_logging +from routes import router as host_router + settings = Settings() setup_logging( @@ -15,6 +17,7 @@ ) app = create_app(settings) +app.include_router(host_router) if __name__ == "__main__": import uvicorn diff --git a/framework/cli/simple_module_cli/templates/host/routes.py b/framework/cli/simple_module_cli/templates/host/routes.py new file mode 100644 index 00000000..e7784329 --- /dev/null +++ b/framework/cli/simple_module_cli/templates/host/routes.py @@ -0,0 +1,28 @@ +"""Host-level routes that don't belong to any module. + +Keeps the public landing at ``/`` decoupled from any plugin module — +``dashboard`` can move, get renamed, or be removed without breaking the +front door. A fresh scaffold lands on this page so the Quickstart's +"visit http://localhost:8000" promise is honoured even before any +module is installed. +""" + +from __future__ import annotations + +from fastapi import APIRouter, Request +from inertia import InertiaResponse +from simple_module_hosting.inertia_deps import InertiaDep + +router = APIRouter() + + +@router.get("/", response_model=None) +async def landing(request: Request, inertia: InertiaDep) -> InertiaResponse: + """Public landing page — no auth required.""" + is_authenticated = getattr(request.state, "user", None) is not None + return await inertia.render( + "Landing", + { + "isAuthenticated": is_authenticated, + }, + ) diff --git a/framework/cli/tests/test_cli_new_regressions.py b/framework/cli/tests/test_cli_new_regressions.py index 03f6e77b..ebc674dc 100644 --- a/framework/cli/tests/test_cli_new_regressions.py +++ b/framework/cli/tests/test_cli_new_regressions.py @@ -59,3 +59,27 @@ def test_sm_new_sample_module_seeds_static_dist_placeholder(tmp_path: Path) -> N ) static_dist = target / "modules" / "hello" / "hello" / "static" / "dist" assert static_dist.is_dir(), "static/dist/ must exist for hatch force-include" + + +def test_sm_new_registers_landing_route_at_root(tmp_path: Path) -> None: + """Issue #137: the Quickstart promises a landing page at ``http://localhost:8000``, + so a fresh scaffold must register a ``/`` route and ship the matching + Inertia page — otherwise users get a branded 404 on first visit.""" + runner = CliRunner() + target = tmp_path / "demo" + result = runner.invoke( + app, + ["new", "demo", "--yes", "--db", "sqlite", "--no-install", "--dest", str(target)], + ) + assert result.exit_code == 0, result.output + + routes_py = (target / "host" / "routes.py").read_text(encoding="utf-8") + assert '@router.get("/"' in routes_py, "host must register GET /" + assert '"Landing"' in routes_py, "host's / route must render the Landing page" + + main_py = (target / "host" / "main.py").read_text(encoding="utf-8") + assert "host_router" in main_py, "main.py must wire host_router into the app" + assert "include_router(host_router)" in main_py + + landing_tsx = target / "host" / "client_app" / "pages" / "Landing.tsx" + assert landing_tsx.is_file(), "Landing.tsx must ship in the host's pages dir" diff --git a/framework/cli/tests/test_scaffolding_host.py b/framework/cli/tests/test_scaffolding_host.py index d76ecaeb..f5562a93 100644 --- a/framework/cli/tests/test_scaffolding_host.py +++ b/framework/cli/tests/test_scaffolding_host.py @@ -83,6 +83,7 @@ async def test_creates_expected_backend_files(self, tmp_path): for relpath in [ "pyproject.toml", "main.py", + "routes.py", "alembic.ini", "migrations/env.py", "migrations/script.py.mako", @@ -99,6 +100,7 @@ async def test_creates_expected_backend_files(self, tmp_path): "client_app/pages.ts", "client_app/styles.css", "client_app/pages/Error.tsx", + "client_app/pages/Landing.tsx", "templates/index.html", ]: assert (dest / relpath).exists(), f"missing: {relpath}"