Same source language, two different runtimes

Kittine's monorepo ships two parallel example applications on purpose. example-app is a real multi-page Leptos client-side rendered (CSR) app — an App.kitty wraps Home/About/User/NotFound pages in a leptos_router <Router>, with client-side <A> navigation, a dynamic /user/:id route, and a 404 fallback. example-ssr is the same idea, but server-side rendered via cargo-leptos + Axum instead of Vite.

The key fact: kittine-compiler needs zero changes to support SSR. The exact same lexer, parser, and codegen that produce example-app's Rust also produce example-ssr's Rust. Everything that differs lives below the compiler — in the toolchain that builds and serves the generated code, not in how the code gets generated.

What example-app actually demonstrates

Home.kitty is the richest single file in the CSR example: a signal-backed counter, an if>/orif>/else> block (including &&/||-combined conditions), a spin loop in both its imperative and list-rendering forms, purr functions, and composed Nav/Card/NavList components. User.kitty reads its dynamic route segment via a method-call chain on use_params_map(). Shapes.kitty goes further into the type system — litter/breed/pounce>/generics/claw, demonstrated through a Point struct, a generic Holder<#t>, a Shape enum pattern-matched to derive a description, a Named claw implemented for Point, and a second generic NamedHolder<#t: Named> bounded by it — all wired up through the Vite plugin end to end.

Why server-rendering is worth having at all

CSR is the right default for most interactive apps — cheap to host, simple deployment story, works anywhere static files can be served. But it has a real cost: the first thing a browser receives is close to an empty HTML shell, with content appearing only once the WebAssembly bundle downloads, initializes, and runs. For content that needs to be crawlable, or needs to render fast on a slow connection, that gap matters.

example-ssr proves Kittine doesn't lock you out of the alternative. It's verified with an actual curl request — no JavaScript involved — returning real HTML content in the very first response. The client bundle then hydrates that HTML for interactivity, rather than being responsible for producing it in the first place.

The two real gotchas — found by actually running it, not assumed

Wiring up cargo-leptos and Axum surfaced two failure modes that would each look identical from the outside: a page that renders correctly and simply never becomes interactive, with no error anywhere.

Gotcha one — the wrong import for HydrationScripts. <HydrationScripts> comes from leptos's own prelude, not from leptos_meta (which is where <MetaTags> actually does come from — an easy mix-up). <HydrationScripts> is the component that injects the <script type="module"> tag responsible for loading the client wasm bundle. Omit it, or import it from the wrong place, and the page renders correctly server-side — but never becomes interactive. Nothing errors. It just looks right and does nothing.

Gotcha two — missing the static-asset fallback. .fallback(leptos_axum::file_and_error_handler(shell)) is required to actually serve the wasm/JS bundle that <HydrationScripts> references. .leptos_routes(..) alone only wires up the page routes themselves — it does not serve static assets. Without the fallback, the hydration script's own import(...) call 404s, and hydration silently never happens. This one doesn't show up as a server-side compile error at all — it's a real page.on('pageerror') you'd only catch by opening a browser and checking the console.

0changes needed in kittine-compiler to support SSR
2silent-failure gotchas found only by actually running it
curlis enough to verify real SSR HTML, no browser needed

The takeaway for choosing between them

Pick example-app's CSR approach by default — it's simpler to deploy and sufficient for most interactive tools. Reach for example-ssr's approach specifically when first-paint content or crawlability genuinely matters for the project, and budget time to actually run the result in a browser once, since both of the gotchas above are invisible from the Rust compiler's point of view. Either way, the .kitty files themselves don't change — the same language, the same compiler, two different places for the output to live.