Docs

Quickstart

Create a Proa site, run it, and edit your first route in five minutes.

Open Markdown

This quickstart gets you from an empty directory to a hello world Proa. It should take about five minutes once Rust and the Proa CLI are ready.

Building with an AI agent? The complete documentation is served at /llms-full.txt for agent consumption. LLM docs lists the other machine-readable interfaces.

1. Install Proa

Install the latest stable CLI from crates.io using a current Rust toolchain:

cargo install proa-cli --locked

Run the same command to update an existing Cargo installation. Proa is public; no GitHub credential or Cargo publishing token is needed. Troubleshooting covers dependency resolution failures. The Basic app also requires the Tailwind CSS CLI on your PATH; see Tailwind prerequisite before continuing.

2. Create A Site

A minimal Axum-backed application with the core boundaries in place:

proa new site hello-proa --yes
Cargo.toml
proa.config.json
proa.lock.json
build.rs
src/
components/
endpoints/
layouts/
pages/
app.rs
main.rs
routes.rs
migrations/
public/
favicon.svg
styles.css
styles/
input.css
tests/

Checkpoint: Cargo.toml, src/main.rs, src/pages/home.rs, and src/layouts/root.rs should exist.

The kitchen-sink starter, and the fastest way to see a production Proa app fit together:

proa new site origin-atlas --template origin --yes

This scaffolds ORIGIN, a live routing atlas that exercises the whole framework: a custom RIPEstat DataLoader with request-scoped deduplication and a TTL cache, streamed Suspense sections, proa-ui components that each carry a Markdown representation (the same page serves /site/<host>.md for agents), rsjs islands for the interactive surfaces, Tailwind styling with a themed token set, self-hosted fonts, and a golden test that pins the server-rendered document byte for byte. The generated README.md doubles as an editing guide.

Checkpoint: src/lib.rs, src/loader.rs, and src/components/ should exist, and proa dev --open shows the atlas.

You do not have to build the common UI yourself: proa-ui is a shadcn-style catalog of ready-made Proa components, and proa ui add button vendors the source into src/components/ where you own and edit it like any other file. Worth knowing before you write your first card, dialog, or form by hand.

The remaining steps use the Basic app named hello-proa. If you chose ORIGIN, follow its generated README.md and run commands inside origin-atlas.

If styles/starter.css already exists and styles/input.css imports it, continue to the next step.

My starter has no styles/starter.css yet

Inside hello-proa, preserve the starter design before Tailwind rebuilds public/styles.css:

  1. Move public/styles.css to styles/starter.css. If styles/starter.css already exists, keep that file.
  2. Replace the contents of styles/input.css with:
@layer theme, base, proa-compat, components, utilities;
@import "tailwindcss";
@import "./starter.css" layer(components);
@source "../src/**/*.rs";

Tailwind will regenerate public/styles.css with the starter design included.

3. Run The Server

cd hello-proa && proa dev --open

proa dev builds configured assets, runs the linked RSJS pipeline, starts the Axum server on http://localhost:3000, and restarts when Rust, style, or config files change.

The first build downloads dependencies and compiles the Rust crates, which can take several minutes. The terminal reports each build stage; --open waits until the server accepts connections before opening your browser.

Open tabs reload themselves once the rebuilt server is accepting connections again, so you do not have to refresh by hand. Stylesheet edits are applied without navigating, which keeps scroll position and form state. Turn it off or retune it with --no-reload, --no-css-hot-swap, and --debounce-ms; see Live reload.

4. Edit The First Page

Set up your editor for Rust navigation and completion, including expressions inside raw Markdown templates.

Open src/pages/home.rs. Inside impl<L: DataLoader> WebRender<L> for HomePage, find the <section class="hero"> in the existing html! macro. Replace only its <h1> and <p class="lede"> with these two elements:

<h1>"Fast SSR, typed in Rust"</h1>
<p class="lede">"This page renders from a compiled template macro."</p>

Keep the surrounding template, including DatabaseBadge and DatabaseCard. Those children load the database status, so this page uses async WebRender and html!. A static component can use WebRenderSync and html_sync!, as shown by the About page below.

In the same file, find impl<L: DataLoader> MdRender<L> for HomePage. Inside render_md's existing md! { r###"…"### } template, replace the old heading and the paragraph immediately after it with these Markdown lines:

Fast SSR, typed in Rust

This page renders from a compiled template macro.

Keep the other Markdown content and child expressions. Proa's content-coverage lint compares rendered content across the two implementations; changing both keeps browsers and agents informed of the same page edit.

Refresh the browser and open /index.md to see both representations. Edit styles/starter.css to change the starter's design; public/styles.css is the generated output and is rebuilt by Tailwind.

5. Add A Route

Create src/pages/about.rs:

use axum::response::IntoResponse;
use proa_core::{to_async, DataLoader, WebContext, WebRenderSync, WriteError};
use proa_framework_axum::RouteResponse;
use proa_macros::html_sync;

use crate::layouts::RootDocument;

pub async fn handler() -> impl IntoResponse {
    RouteResponse::ssr_async(RootDocument {
        title: "About",
        content: to_async(AboutPage {
            heading: "About",
            body: "This route renders from src/pages/about.rs.",
        }),
    })
}

struct AboutPage {
    heading: &'static str,
    body: &'static str,
}

impl<L: DataLoader> WebRenderSync<L> for AboutPage {
    fn render(self, cx: &mut WebContext<L>) -> Result<(), WriteError> {
        html_sync! {
            <main data-component="about-page" class="mx-auto max-w-3xl px-6 py-16">
                <h1 class="text-4xl font-semibold">{self.heading}</h1>
                <p class="mt-4 text-zinc-600">{self.body}</p>
            </main>
        }
        .render(cx)
    }
}

Export it from src/pages/mod.rs, keeping the generated home module:

pub mod about;
pub mod home;

Register it in src/routes.rs, between the markers the generator maintains:

let (router, metadata) = router_from_spec(vec![
    // proa:route-specs:start
    Route::endpoint("/", get(pages::home::handler)),
    Route::endpoint("/about", get(pages::about::handler)),
    // proa:route-specs:end
]);

That is the whole route flow. RouteResponse renders the page and finalizes the HTTP response, RootDocument supplies the shared page shell (it is async-capable, so a synchronous page goes in through to_async), and the fields passed to AboutPage are compile-time-checked props. Add page caching to the route spec when the response should carry CDN cache headers. Add Axum extractors such as Path, Query, or State to handler when the route needs request data.

Open http://localhost:3000/about. Check the route table:

proa site routes

Checkpoint: /about appears in the route report and renders in the browser.

6. Validate Before Moving On

Run Proa's source-aware checks before the full build:

proa fmt --check --verify src
proa lint src --deny warnings
proa test
proa build

proa fmt --check --verify checks the canonical template layout and verifies that formatting would preserve the rendered output. proa lint understands Proa render paths, so --deny warnings catches performance problems such as intermediate rendered strings, avoidable allocations, and async renderers that never await. Once those focused checks are clean, proa build validates the complete linked application. proa test also checks both page representations and that the two database components share one origin query.

Next steps

Search

Type at least 2 characters