Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 (
<main className="mx-auto flex min-h-screen max-w-2xl flex-col justify-center gap-8 px-6 py-16">
<header className="space-y-3">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">
simple_module_py
</p>
<h1 className="text-4xl font-bold tracking-tight sm:text-5xl">
Your app is up.
</h1>
<p className="text-base text-muted-foreground sm:text-lg">
This is the host's landing page — generated by{' '}
<code className="rounded bg-secondary px-1.5 py-0.5 font-mono text-sm">smpy new</code>.
Edit{' '}
<code className="rounded bg-secondary px-1.5 py-0.5 font-mono text-sm">
client_app/pages/Landing.tsx
</code>{' '}
to make it yours.
</p>
</header>

<div className="flex flex-wrap gap-3">
<a
href={primaryHref}
className="inline-flex items-center justify-center rounded-md bg-primary px-5 py-2.5 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
>
{primaryLabel}
</a>
<a
href="https://github.com/antosubash/simple_module_python"
className="inline-flex items-center justify-center rounded-md border border-border bg-background px-5 py-2.5 text-sm font-medium transition-colors hover:bg-secondary"
>
Read the docs
</a>
</div>

<section className="rounded-xl border border-border bg-card p-5">
<h2 className="mb-3 text-sm font-semibold">Next steps</h2>
<ul className="space-y-2 text-sm text-muted-foreground">
<li>
Create an admin:{' '}
<code className="rounded bg-secondary px-1.5 py-0.5 font-mono text-[13px] text-foreground">
uv run smpy users create-admin
</code>
</li>
<li>
Scaffold a module:{' '}
<code className="rounded bg-secondary px-1.5 py-0.5 font-mono text-[13px] text-foreground">
smpy create-module orders --dest modules/orders
</code>
</li>
<li>
Sign in at{' '}
<a href="/users/login" className="font-mono text-[13px] text-primary hover:underline">
/users/login
</a>{' '}
and explore the dashboard.
</li>
</ul>
</section>
</main>
);
}
3 changes: 3 additions & 0 deletions framework/cli/simple_module_cli/templates/host/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -15,6 +17,7 @@
)

app = create_app(settings)
app.include_router(host_router)

if __name__ == "__main__":
import uvicorn
Expand Down
28 changes: 28 additions & 0 deletions framework/cli/simple_module_cli/templates/host/routes.py
Original file line number Diff line number Diff line change
@@ -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,
},
)
24 changes: 24 additions & 0 deletions framework/cli/tests/test_cli_new_regressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
2 changes: 2 additions & 0 deletions framework/cli/tests/test_scaffolding_host.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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}"
Expand Down
Loading