Static site custom pages

Content routes are good for articles. Custom pages are for the designed parts of a site: home pages, product pages, pricing pages, feature overviews, launch pages, and landing pages.
A custom page is not a special HTML template. It is a normal Fission widget tree registered as a static site route. The site shell renders that widget tree at conversion time and lowers it to semantic HTML and generated CSS.

1. Create reusable page components

Use normal Rust modules and Widget implementations. Avoid putting a whole page into one function. The documentation site uses this pattern for its home page components.
use fission::prelude::*;

#[derive(Clone)]
pub struct HomePageHero;

impl From<HomePageHero> for Widget {
    fn from(component: HomePageHero) -> Self {
        Column {
            gap: Some(20.0),
            children: vec![
                Text::new("Build production apps in Rust")
                    .size(56.0)
                    .weight(FontWeight::Bold)
                    .into(),
                Text::new("macOS, Windows, Linux, Web, Android, iOS, Terminal, Static site, and SSR targets share one application model.")
                    .size(20.0)
                    .into(),
            ],
            ..Default::default()
        }
        .into()
    }
}
A component should describe one reusable part of the page. Keep data loading, routing, and product configuration outside the component so it remains easy to test and reuse.

2. Register a route widget

Create the site app in src/main.rs or another module and register the custom route.
use anyhow::Result;
use fission::prelude::*;
use fission::site::{build_from_cli, FissionSite};

#[derive(Default)]
struct SiteState;
impl GlobalState for SiteState {}

fn site_app() -> FissionSite {
    FissionSite::new().route_widget::<SiteState, _>(
        "/",
        "My Product",
        Some("A fast, crawlable product site built with Fission.".to_string()),
        HomePageHero,
    )
}

fn main() -> Result<()> {
    build_from_cli(site_app())
}
The title and description are route metadata. They become part of the generated document metadata and search index.

3. Override content-page chrome

The static-site shell owns a deliberately plain documentation layout. Keep product branding, navigation, and editorial choices in your site app, just as you would for any other Fission application.
Use content_header_widget to replace the default header on every content-backed route. The factory receives a SiteRenderContext, so it can select a different retained component for a route family without placing product-specific conditions in the shell.
#[derive(Clone)]
struct DocsHeader;

#[derive(Clone)]
struct JournalHeader;

fn site_app() -> FissionSite {
    FissionSite::new()
        .content_header_widget::<SiteState, _, _>(|ctx| {
            let header: Widget = if ctx.route_path.starts_with("/journal/") {
                JournalHeader.into()
            } else {
                DocsHeader.into()
            };
            header
        })
        .footer_widget::<SiteState, _>(SiteFooter)
}
Programmatic routes registered with route_widget own their complete page layout, including their header. The content header override only applies to Markdown or MDX content routes. A footer registered with footer_widget is shared by both kinds of route.
Keep each substantial region in its own widget file. This gives custom sites normal retained-widget identity and composition instead of turning page chrome into a string template.

4. Override layout and CSS

Add project CSS through css_files. Files are resolved relative to the project directory and emitted after the shell's base and generated widget CSS, so ordinary cascade rules can replace the generic presentation.
[site]
asset_dirs = ["static"]
css_files = ["site/tokens.css", "site/overrides.css"]
Use semantic identifiers on the retained regions your stylesheet owns:
Column {
    children: vec![page_content],
    semantics: Some(Semantics {
        identifier: Some("product-doc-layout".into()),
        ..Semantics::default()
    }),
    ..Default::default()
}
.into()
[data-fission-semantics="product-doc-layout"] {
  width: min(100% - 3rem, 82rem);
  margin-inline: auto;
}
Prefer these stable identifiers over selectors that depend on generated nesting. Use a custom design system for colours, typography, spacing, and component defaults; reserve project CSS for editorial layout, responsive composition, and visuals specific to the site.
For a completely designed landing page, register the full retained page with route_widget. For articles that should keep the shell's main/sidebar/table-of-contents behavior, use content routes and replace only the header, footer, theme, and CSS.

5. Add route-scoped document enhancements

Static pages should remain useful without JavaScript. When progressive enhancement is appropriate, declare an element in fission.toml and constrain it to the routes that need it:
[[site.elements]]
placement = "body-end"
file = "site/docs-enhancement.html"
route_prefixes = ["/docs/", "/reference/"]
The referenced file may contain a small script or document fragment. Keep brand-specific analytics, navigation behavior, or article tools here rather than adding them to the generic shell. Use head-start, head-end, body-start, or body-end for placement.

6. Select content templates explicitly

The documentation template is the default article layout. The blog template adds post ordering, featured and paginated indexes, adjacent-post navigation, and category/tag routes. It is opt-in and works at the mount path you configure:
[[site.routes]]
kind = "content"
path = "/journal"
source = "content/journal"
template = "fission::site::blog"
Use fission::site::documentation when you want to record the default content-page intent explicitly.

7. Keep the page static by default

A custom static page should be useful before JavaScript loads. Theme switching, sidebar expansion, search, code highlighting, and small document-level scripts are fine. Continuous runtime state, device APIs, and app-like flows belong in a Fission web app, a server route, or a focused island.
Use this rule while designing pages:
Page behavior
Static custom page?
Better target if not
Hero, feature cards, pricing, docs links
Yes
Static site
Search dialog over generated index
Yes
Static site with search enabled
Account dashboard with user-specific data
No
Server-rendered route or web app
Cart drawer with local state
Maybe
WASM island inside a server page
Live editor or canvas-heavy UI
No
Web target

8. Verify the output

Run the full site check before publishing.
fission site check --project-dir . --release
Then build and inspect the output.
fission site build --project-dir . --release
Open the generated index.html and site.css if the page looks wrong. The HTML should contain the real content, not a blank app shell. That is what keeps the site crawlable and shareable.

What you should have working

After this guide, you should have a custom widget route, site-owned content chrome, project CSS loaded after the generic shell styles, and any progressive enhancement limited to the routes that need it. You should also be able to build the site and inspect the generated HTML without changing fission-shell-site.

Common mistakes to check

Symptom
What to check first
The app compiles but nothing changes
Confirm the component is registered with route_widget or content_header_widget in the site passed to build_from_cli.
The override loses to a shell style
Confirm the stylesheet is listed in site.css_files, then target a stable semantics identifier owned by your component.
A script affects unrelated pages
Add route_prefixes or routes to its [[site.elements]] filter.
Blog indexes are not generated
Set template = "fission::site::blog" on that content mount. The mount name alone does not opt in.
The generated page is blank without JavaScript
Keep primary content in retained widgets or Markdown; use document elements only for progressive enhancement.