Build a server-rendered store

This cookbook recreates the structure of the Pokémon Card Store example. The goal is a server-rendered ecommerce page: HTML is produced on the server, product data is loaded through a job, the server cart is keyed by a private session, filters use a progressive worker, and the cart drawer demonstrates a focused Fission WASM island.

1. Configure the server target

targets = ["ssr"]

[app]
name = "pokemon-card-store"
app_id = "rs.fission.examples.pokemon_card_store"

[server]
entry = "pokemon_card_store::pokemon_card_store_server"
default_route_mode = "server_private"
render_pass_limit = 4

[server.cache]
provider = "moka"
max_capacity = 10000

[server.workers]
separate_artifacts = true
bridge = "generated"

[server.islands]
separate_artifacts = true
preload = "route"

Finished state and checks

By the end of Build a server-rendered store, you should have a runnable change, not just a set of snippets. Run the command shown in the page, exercise the feature once manually, and add or update the smallest test that protects the behavior.
Check
Expected result
Build
cargo check or the relevant fission command finishes without target-specific configuration errors.
Behavior
The screen, package, server route, or test flow described by the recipe is visible and responds to input.
Failure path
At least one denied, missing, invalid, or empty case renders a useful message instead of silently doing nothing.
Next link
The reference page linked from the recipe explains the exact fields, commands, or types used here.
The route mode says the storefront is rendered per session, which is the right default for a cart. The worker and island settings say browser artifacts should be generated separately rather than bundled into one large file. Before production, add [server.sessions] with a signed cookie secret as described in Server security.

2. Model the data

Use typed Rust data rather than loosely shaped maps. This mirrors the example's CardProduct idea.
#[derive(Clone, Debug)]
pub struct CardProduct {
    pub id: u32,
    pub name: String,
    pub set: String,
    pub price_pence: u32,
    pub stock: u32,
    pub image_url: String,
}

pub fn catalog_response() -> Vec<CardProduct> {
    vec![
        CardProduct {
            id: 1,
            name: "Pikachu Illustrator".to_string(),
            set: "Promo".to_string(),
            price_pence: 125000,
            stock: 1,
            image_url: "/img/cards/pikachu.svg".to_string(),
        },
    ]
}
In a real store this function would call a database or service. The widget tree should not care where the data came from; it should receive typed data and render it.

3. Register a catalog job

pub const CATALOG_JOB: &str = "catalog";

let jobs = ServerJobRegistry::new().register_job(CATALOG_JOB, |_request, _ctx| {
    Ok(catalog_response())
});
The route can ask for the catalog during rendering. The server shell drains the job before returning HTML, so the user receives a populated page rather than a blank loading shell.

4. Render the page as widgets

#[derive(Clone)]
pub struct StoreHomePage;

impl From<StoreHomePage> for Widget {
    fn from(component: StoreHomePage) -> Self {
        let (_, view) = fission::build::current::<StoreState>();
        Column {
            gap: Some(24.0),
            children: vec![
                Text::new("Pokémon Card Store")
                    .size(44.0)
                    .weight(FontWeight::Bold)
                    .into(),
                CardGrid::new(view.state().products.clone()).into(),
            ],
            ..Default::default()
        }
        .into()
    }
}
Keep the page split into components. A production example should have components/hero.rs, components/card_grid.rs, and components/shell.rs rather than one large file.

5. Add worker and island declarations

.worker(
    "/",
    ProgressiveWorker::new("catalog-filters", "/assets/workers/catalog-filters.wasm")
        .entry("pokemon_card_store::workers::catalog_filters_boot")
        .root_node_id("catalog-grid"),
)
.island(
    "/",
    WasmIsland::new("cart-drawer", "/assets/islands/cart-drawer.wasm", "cart-drawer")
        .entry("pokemon_card_store::islands::cart_drawer_boot"),
)
The page stays server-rendered. The worker adds lightweight DOM behavior. The island owns the cart drawer because cart state and reducer-driven interaction are worth a focused WASM artifact.

6. Check, run, and package

fission server check --project-dir examples/pokemon-card-store
fission server artifacts --project-dir examples/pokemon-card-store
fission server serve --project-dir examples/pokemon-card-store --host 127.0.0.1 --port 8124
Package as a Docker image when you are ready to deploy the server process:
[package.docker]
port = 8080
tags = ["ghcr.io/example/pokemon-card-store:0.1.0"]

[distribution.docker_registry.production]
tags = ["ghcr.io/example/pokemon-card-store:0.1.0"]
fission package --project-dir examples/pokemon-card-store --target ssr --format docker-image --release
fission publish --project-dir examples/pokemon-card-store --provider docker-registry --artifact target/fission/release/ssr/docker-image/artifact-manifest.json --site production
The resulting package manifest is the release record for the server image context and tags.
Fission
A cross-platform, GPU-accelerated user interface framework for Rust. MIT licensed.
Copyright (c) 2026 Fission
Ready to use today. Widget APIs are expected to remain stable; some runtime and shell APIs may change before 1.0.0.
Fission 0.7.0