This version of the documentation is outdated, and features documented here may work differently now. You can see the latest stable version of the docs here.

Build State

The most commonly-used rendering strategy for Perseus is static generation, which renders your pages to static HTML files. These can then be served by the server with almost no additional processing, which makes for an extremely fast experience!

Note that, depending on other strategies used, Perseus may call this strategy at build-time or while the server is running, so you shouldn't depend on anything only present in a build environment (particularly if you're using the incremental generation or revalidation strategies).

Note: if you want to export your app to purely static files, see this section, which will help you use Perseus without any server.

Usage

Without Build Paths or Incremental Generation

On its own, this strategy will simply generate properties for your template to turn it into a page, which would be perfect for something like a list of blog posts (just fetch the list from the filesystem, a database, etc.). Here's an example from here for a simple greeting:

use perseus::{RenderFnResultWithCause, Template};
use sycamore::prelude::{view, Html, View};

#[perseus::make_rx(PageStateRx)]
pub struct PageState {
    pub greeting: String,
}

#[perseus::template_rx]
pub fn build_state_page(state: PageStateRx) -> View<G> {
    view! {
        p { (state.greeting.get()) }
    }
}

pub fn get_template<G: Html>() -> Template<G> {
    Template::new("build_state")
        .build_state_fn(get_build_state)
        .template(build_state_page)
}

// We're told the path we're generating for (useless unless we're using build paths as well) and the locale (which will be `xx-XX` if we're not using i18n)
// Note that this function is asynchronous, so we can do work like fetching from a server or the like here (see the `demo/fetching` example)
#[perseus::autoserde(build_state)]
pub async fn get_build_state(_path: String, _locale: String) -> RenderFnResultWithCause<PageState> {
    Ok(PageState {
        greeting: "Hello World!".to_string(),
    })
}

Note that Perseus passes around properties to pages as Strings, so the function used for this strategy is expected to return a string, but this serialization is done for you with the #[perseus::autoserde(build_state)] macro. Note also the return type RenderFnResultWithCause, a Perseus type that represents the possibility of returning almost any kind of error, with an attached cause declaration that blames either the client or the server for the error. Most of the time, the server will be at fault (e.g. if serializing some obvious properties fails), and this is the default if you use ? or .into() on another error type to run an automatic conversion. However, if you want to explicitly state a different cause (or provide a different HTTP status code), you can construct GenericErrorWithCause or use the more convenient blame_err! macro, as done in the below example (under the next subheading) if the path is post/tests. We set the error (a Box<dyn std::error::Error>) and then set the cause to be the client (they navigated to an illegal page) and tell the server to return a 404, which means our app will display something like Page not found.

With Build Paths or Incremental Generation

You may have noticed in the above example that the build state function takes a path parameter. This becomes useful once you bring the build paths or incremental generation strategies into play, which allow you to render many paths for a single template. In the following example (taken from here), all three strategies are used together to pre-render some blog posts at build-time, and allow the rest to be requested and rendered if they exist (here, any post will exist except one called tests):

// This is exactly the same as the build paths example except for a few lines and some names

use perseus::{blame_err, RenderFnResult, RenderFnResultWithCause, Template};
use sycamore::prelude::{view, Html, View};

#[perseus::make_rx(PageStateRx)]
pub struct PageState {
    title: String,
    content: String,
}

#[perseus::template_rx]
pub fn incremental_generation_page(state: PageStateRx) -> View<G> {
    let title = state.title;
    let content = state.content;
    view! {
        h1 {
            (title.get())
        }
        p {
            (content.get())
        }
    }
}

pub fn get_template<G: Html>() -> Template<G> {
    Template::new("incremental_generation")
        .build_paths_fn(get_build_paths)
        .build_state_fn(get_build_state)
        // This line makes Perseus try to render any given path under the template's root path (`incremental_generation`) by putting it through `get_build_state`
        // If you want to filter the path because some are invalid (e.g. entries that aren't in some database), we can filter them out at the state of the build state function
        .incremental_generation()
        .template(incremental_generation_page)
}

// We'll take in the path here, which will consist of the template name `incremental_generation` followed by the spcific path we're building for (as exported from `get_build_paths`)
#[perseus::autoserde(build_state)]
pub async fn get_build_state(path: String, _locale: String) -> RenderFnResultWithCause<PageState> {
    // This path is illegal, and can't be rendered
    // Because we're using incremental generation, we could gte literally anything as the `path`
    if path == "incremental_generation/tests" {
        // This tells Perseus to return an error that's the client's fault, with the HTTP status code 404 (not found) and the message 'illegal page'
        // You could return this error manually, but this is more convenient
        blame_err!(client, 404, "illegal page");
    }
    let title = path.clone();
    let content = format!(
        "This is a post entitled '{}'. Its original slug was '{}'.",
        &title, &path
    );

    Ok(PageState { title, content })
}

// This just returns a vector of all the paths we want to generate for underneath `incremental_generation` (the template's name and root path)
// Like for build state, this function is asynchronous, so you could fetch these paths from a database or the like
// Note that everything you export from here will be prefixed with `<template-name>/` when it becomes a URL in your app
//
// Note also that there's almost no point in using build paths without build state, as every page would come out exactly the same (unless you differentiated them on the client...)
pub async fn get_build_paths() -> RenderFnResult<Vec<String>> {
    Ok(vec!["test".to_string(), "blah/test/blah".to_string()])
}

When either of these additional strategies are used, build state will be passed the path of the page to be rendered, which allows it to prepare unique properties for that page. In the above example, it just turns the URL into a title and renders that.

For further details on build paths and incremental generation, see the following sections.

Common Pitfalls

When a user goes to your app from another website, Perseus will send all the data they need down straight away (in the initial loads system), which involves setting any state you provide in a JavaScript global variable so that the browser can access it without needing to talk to the server again (which would slow things down). Unfortunately, JavaScript's concept of 'raw strings' (in which you don't need to escape anything) is quite a bit looser than Rust's, and so Perseus internally escapes any instances of backticks or ${ (JS interpolation syntax). This should all work fine, but, when your state is deserialized, it's not considered acceptable for it to contain control characters. In other words, anything like \n, \t or the like that have special meanings in strings must be escaped before being sent through Perseus! Note that this is a requirement imposed by the lower-level module serde_json, not Perseus itself.