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.
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:
| | |
|---|
Hero, feature cards, pricing, docs links | | |
Search dialog over generated index | | Static site with search enabled |
Account dashboard with user-specific data | | Server-rendered route or web app |
Cart drawer with local state | | WASM island inside a server page |
Live editor or canvas-heavy UI | | |
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
| |
|---|
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. |