diff --git a/docs/next/src/SUMMARY.md b/docs/next/src/SUMMARY.md index 24f5d43709..7fa1390319 100644 --- a/docs/next/src/SUMMARY.md +++ b/docs/next/src/SUMMARY.md @@ -1,23 +1,6 @@ # Summary - [Introduction](./intro.md) -- [Setup](./setup.md) -- [Architecture](./arch.md) -- [Building Your First App](./tutorials/first_app/intro.md) - - [Installation and Setup](./tutorials/first_app/setup.md) - - [Writing Your First Template](./tutorials/first_app/template.md) - - [Setting up the App Itself](./tutorials/first_app/app.md) -- [CLI](./cli.md) -- [Templates](./templates.md) -- [Routing](./routing.md) -- [Error Pages](./error_pages.md) -- [Rendering Strategies](./strategies/intro.md) - - [Build Paths](./strategies/build_paths.md) - - [Build State](./strategies/build_state.md) - - [Request State](./strategies/request_state.md) - - [Revalidation](./strategies/revalidation.md) - - [Incremental generation](./strategies/incremental.md) -- [Building](./building.md) -- [Serving](./serving.md) - - [Actix Web Integration](./integrations/actix-web.md) -- [Config Managers](./config_managers.md) + - [What is Perseus?](./what-is-perseus.md) + - [Hello World!](./hello-world.md) +- [Your Second App](./second-app.md) diff --git a/docs/next/src/arch.md b/docs/next/src/arch.md deleted file mode 100644 index 0627f273d1..0000000000 --- a/docs/next/src/arch.md +++ /dev/null @@ -1,39 +0,0 @@ -# Architecture - -Perseus is a complex system, and this page will aim to explain the basics in a beginner-friendly way. If you've already used similar frameworks from the JS world like NextJS, then some of this may be familiar to you. If you're having trouble following along, please [open an issue](https://github.com/arctic-hen7/perseus/issues/new/choose) and ask us to clarify some sections, because this page in particular should be accessible to everyone. If you'd like more specific help, [ask on Gitter](TODO)! - -## Templates and Pages - -The core of Perseus is the idea of templates. When you create a Perseus app, what you're basically doing is telling Perseus how to compile your code into a series of *pages*. **Each page has a unique URL on your final website.** If you have a blog, and every post is stored as something like `post/title`, then each post would be a unique page. - -But this doesn't mean you have to write the code for every page individually! Perseus does this for you, and only asks you to write *templates*. A template can generate one page or many, and a great example of one would be a `post` template. Each template has a *root path*, which is essentially like the space on your website that that template controls. For example, a post template might control `/post`, meaning it can render pages at `/post`, `/post/test`, `/post/longer/path`, etc. In theory, a template could render pages outside its domain, but this would be a bad idea for structure, and makes your code difficult to understand. - -### State - -What differentiates pages from templates is *state*, which tells a page how to fill out its template to give unique content. For example, our post template would probably have a `content` field in its state, and its pages would use that to render their unique content! - -In terms of writing code, a page's state is just a `struct` that can be serialized and deserialized with [Serde](https://serde.rs). - -## Rendering Strategies - -Each template has a rendering strategy, which it uses to create its pages. There are a number of rendering strategies in Perseus, each of which is documented in detail in its own section. What's important to understand for now is that there are two main ways a template can render pages, at *build time*, or at *request time*. If a template renders at build time, it generates the code for your pages when you build your app, which means you end up serving static pages. This is *really fast*. However, sometimes you need information specific to each request to render a page (e.g. an authentication token), and you can't render at build. Instead, you'd render at request time, which gives you access to information about the HTTP request a user sent for your page. - -Here's a list of Perseus' currently supported rendering strategies. These can all be combined, but some combinations make more sense than others. - -| Strategy | Description | Type | -| ---------------------- | ------------------------------------------ | ------- | -| Build paths | Generates a series of pages for a template | Build | -| Build state | Generates page state | Build | -| Request state | Generates page state | Request | -| Revalidation | Rebuilds pages conditionally | Hybrid | -| Incremental generation | Builds pages on-demand | Hybrid | - -There are two *hybrid* strategies listed above. They're a little more complicated, and out of the scope of this page, but they operate at both build *and* request-time, allowing you to reap the benefits of both worlds! - -## Routing - -*This section describes how Perseus works under the hood. Skip it if you want.* - -Perseus doesn't just host your pages at their URLs though. In fact, Perseus has a generic handler for *any URL*, which returns what we call the *app shell*. That's a concept from the single-page app (e.g. ReactJS), where your app always has a constant shell around it, and each page is loaded into that shell, making page transitions more seamless. Perseus adopts this as well, but with the added benefits of super-fast static rendering strategies and a more lightweight shell. - -The shell includes a router (courtesy of [Sycamore](https://github.com/sycamore-rs/sycamore)), which determines what page the user wants, and then sends a request to a special endpoint behind `/.perseus`. That then renders the page and returns some static HTML and the page's state. diff --git a/docs/next/src/building.md b/docs/next/src/building.md deleted file mode 100644 index 728566ea86..0000000000 --- a/docs/next/src/building.md +++ /dev/null @@ -1,52 +0,0 @@ -# Building - -*You only need this page if you're not using the Perseus CLI, which performs this process for you!* - -After you've defined all your templates and the like, you'll of course need to build them into pages! Perseus tries to make this process as simple as possible. - -## Usage - -You'll want to be able to execute this code as part of an executable, so defining a new binary in your `Cargo.toml` is advised like so: - -```toml -[[bin]] -name = "ssg" -path = "src/bin/build.rs" -``` - -Then put this code in `bin/build.rs` (or wherever else you put your binary) - -```rust -use futures::executor::block_on; -use perseus::{build::build_templates, config_manager::FsConfigManager}; -use perseus_showcase_app::pages; -use sycamore::prelude::SsrNode; - -fn main() { - let config_manager = FsConfigManager::new(); - - let fut = build_templates( - vec![ - pages::index::get_page::(), - pages::about::get_page::(), - pages::post::get_page::(), - pages::new_post::get_page::(), - pages::ip::get_page::(), - pages::time::get_page::(), - pages::time_root::get_page::(), - ], - &config_manager, - ); - block_on(fut).expect("Static generation failed!"); - - println!("Static generation successfully completed!"); -} -``` - -This code defines a synchronous `main` function that blocks to call `build_templates`, which, unsurprisingly, builds your templates! Each entry in the vector you give this function should be a template, and note that we specify they should be `SsrNode`s, which is Sycamore's way of saying they should be prepared to be rendered on the server rather than in the browser, which makes sense given that we're building them! - -The reason we don't just make this whole function asynchronous is so we don't have to include a runtime like `tokio`, which would be unnecessary. - -## File Storage - -It may have crossed your mind as to where all these static files are stored in production, and Perseus provides an excellent solution to this problem with custom read/write systems, documented in-depth [here](./config_managers.md). diff --git a/docs/next/src/cli.md b/docs/next/src/cli.md deleted file mode 100644 index b27a83f6ad..0000000000 --- a/docs/next/src/cli.md +++ /dev/null @@ -1,100 +0,0 @@ -# CLI - -Perseus has a CLI (command line interface) designed to make your life significantly easier when developing Perseus apps. Its primary functions are to build and serve your apps for you, meaning you can focus pretty much entirely on your application code and ignore all the boilerplate! - -## Installation - -You can install the Perseus CLI by running `cargo install perseus-cli`, it should then be available as `perseus` on your system! - -We currently don't provide independent executables installable without `cargo` because you'll need `cargo` and Rust generally to be able to write a Perseus app, and Perseus depends on the `cargo` commands being available, so there's really no point. That said, if you have a use-case for this, please [open an issue](https://github.com/arctic-hen7/perseus/issues/new/choose)! - -## Setup - -Set up a library project with `cargo`, and add the following to the `[dependencies]` section in your `Cargo.toml`: - -```toml -perseus = { path = "../../packages/perseus" } -sycamore = { version = "0.5", features = ["ssr"] } -sycamore-router = "0.5" -# You only need these for pages that take properties -serde = { version = "1", features = ["derive"] } -serde_json = "1" -``` - -Then, add a `lib.rs` file to your project under `src/` that contains the following: - -```rust -mod pages; -mod error_pages; - -use perseus::define_app; - -#[derive(perseus::Route)] -pub enum Route { - #[to("/")] - Index, - #[to("/about")] - About, - #[not_found] - NotFound, -} -define_app!{ - root: "#root", - route: Route, - router: { - Route::Index => [ - "index".to_string(), - pages::index::template_fn() - ], - Route::About => [ - "about".to_string(), - pages::about::template_fn() - ] - } - error_pages: crate::error_pages::get_error_pages(), - templates: [ - crate::pages::index::get_template::(), - crate::pages::about::get_template::() - ] - // config_manager: perseus::FsConfigManager::new() -} -``` - -This assumes you've defined a function to get your error pages elsewhere (you can read more about that [here](error_pages.md)), and that it's in a module called `error_pages`, you can customize that as needed. - -The way the rest of this works is pretty simple. First off, you define a router with [Sycamore](https://sycamore-rs.netlify.app/docs/advanced/routing), which defines each of your templates and the paths on your site that it will accept. This **must** have a variant explicitly named `NotFound`, that's handled for you. Then, you define your app itself, which takes the following properties (which need to be in order right now!): - -- `root` – the CSS selector for the element to render Perseus to, which should be unique, like an HTML `id` -- `route` – the `enum` for your app's routes that you just defined -- `router` – a match-like input that handles each of the variants of your `route`, except `NotFound` (handled for you); each one gets mapped to the corresponding page's path (e.g. `Post` with slug `test` might be mapped to `format!("post/{}", slug)`), which shouldn't include a leading or trailing `/` -- `error_pages` – your [error pages](error_pages.md) -- `templates` – each of your templates, taking the `G` parameter (which will be used at runtime to render them for the server or the client) -- `config_manager` (optional) – the [config manager](config_manager.md) your app should use, default is the inbuilt `FsConfigManager::new()` - -## Usage - -Once you've got that out of the way, go ahead and define your templates as usual, and then run the following command in your project's directory: - -``` -perseus serve -``` - -That will automatically prepare the CLI to work with your app, then it will build your app and statically generate everything as appropriate (using any custom config manager your specified), and then it will serve your app on by default! - -If you want to change the host/port your app is served on, just set the `HOST`/`PORT` environment variables as you'd like. - -## Other Commands - -If you just want to build your app, you can run `perseus build`. If you only want to prepare the CLI to interface with your app (which creates a `.perseus/` directory), you can run `perseus prep`. - -If you want to serve pre-built files (which you'll have to generate with `perseus build`), you can run `perseus serve --no-build`. - -## Watching - -All these commands act statically, they don't watch your code for any changes. This feature will be added _very_ soon to the CLI, but until it is, we advise you to use a tool like [`entr`](https://github.com/eradman/entr), which you can make work with Perseus like so (on Linux): - -``` -find . -not -path "./.perseus/*" -not -path "./target/*" | entr -s "perseus serve" -``` - -This just lists all files except those in `.perseus/` and `target/` and runs `perseus serve` on any changes. You should exclude anything else as necessary. diff --git a/docs/next/src/config_managers.md b/docs/next/src/config_managers.md deleted file mode 100644 index a9a65a8106..0000000000 --- a/docs/next/src/config_managers.md +++ /dev/null @@ -1,34 +0,0 @@ -# Config Managers - -As you may have noticed, Perseus generates a considerable volume of static files to speed up serving your app and to cache various data. Storing these is trivial in development, they can just be put in a `dist` folder or the like. However, when in production, we don't always have access to such luxuries as a stateful filesystem, and we may need to access files from elsewhere, like a database or a CDN. - -## Default - -In development, you'll still need to specify a config manager, which allows you to test out your production config manager even in local development! The easiest one to use for typical development though is the inbuilt `FsConfigManager`. - -## Writing a Config Manager - -Any custom config managers have to implement the `ConfigManager` trait, which only has two functions: `read` and `write`. Here's the trait definition: - -```rust,no_run,no_playground -pub trait ConfigManager { - /// Reads data from the named asset. - async fn read(&self, name: &str) -> Result; - /// Writes data to the named asset. This will create a new asset if one doesn't exist already. - async fn write(&self, name: &str, content: &str) -> Result<()>; -} -``` - -### Errors - -It's easily possible for CDNs of filesystems to throw errors when we try to interact with them, and Perseus provides a custom set of errors with [`error_chain!`]() to deal with this. Note that your implementation *must* use these, or it will not implement the trait and thus not be compatible with Perseus. The errors available to you are: - -- `NotFound`, takes a `String` asset name -- `ReadFailed`, takes a `String` asset name and a `String` error (not chained because it might come back by carrier pigeon for all we know) -- `WriteFailed`, takes a `String` asset name and a `String` error (not chained because it might come back by carrier pigeon for all we know) - -## Best Practices - -Some storage solutions will be significantly faster in production than others, and a CDN is recommended over a database or the like. Generally speaking, go with something lightning-fast, or a local filesystem if you can. Unfortunately, Perseus must run its own logic to know what files to fetch, so direct communication between the app shell and the CDN is not possible, so speed of connection with your storage provider is essential. - -We're currently working on framework support on providers like Netlify so this process is fully seamless for you, but for now it will be very inconvenient. Hence, **Perseus is presently not recommended for production use, but soon will be**! Once integrations are up and running, the project has matured some more, and setup on platforms like Vercel is done, have at it! diff --git a/docs/next/src/error_pages.md b/docs/next/src/error_pages.md deleted file mode 100644 index 07784355a7..0000000000 --- a/docs/next/src/error_pages.md +++ /dev/null @@ -1,54 +0,0 @@ -# Error Pages - -As in any app, errors may well happen in Perseus, and you should be prepared for them. However, because we're building a user-facing app, we need to be able to display pretty error pages to the user to explain what went wrong and what they can do to fix it (if applicable). - -In Perseus, error pages are defined by HTTP status codes, and you can define others as necessary for your own internal errors, Perseus is only concerned with those that may occur in communication with the backend. - -## Defining Error Pages - -The easiest way of defining your error pages is to put them into a function like `get_error_pages`, like so: - -```rust -use sycamore::prelude::template; -use perseus::ErrorPages; - -pub fn get_error_pages() -> ErrorPages { - let mut error_pages = ErrorPages::new(Box::new(|_, _, _| { - template! { - p { "Another error occurred." } - } - })); - error_pages.add_page( - 404, - Box::new(|_, _, _| { - template! { - p { "Page not found." } - } - }), - ); - error_pages.add_page( - 400, - Box::new(|_, _, _| { - template! { - p { "Client error occurred..." } - } - }), - ); - - error_pages -} -``` - -This example creates three error pages, the first of which is mandatory: a fallback page. There are [a lot](https://httpstatuses.com) of HTTP status codes, and it would be ridiculous for you to handle them all. However, occasionally you may get one that you didn't anticipate, and you should be ready for it. - -The rest of the error pages are defined specifically for error codes, like `404` and `400` above. - -Every error page is a function that takes three arguments: the URL that caused the problem, the HTTP status code, and the error message that was the payload of the request. - -## Best Practices - -When designing error pages, remember that they should be intelligible to everyone, regardless of technical ability! To that end, the error message provided by the serve should probably only be accessible through a dropdown or the like that provides technical details so the user can report it. - -## Security Concerns - -Error messages can leak sensitive data, so Perseus tries to make sure that they don't! No Perseus error message will ever provide details more than the rendering strategy a page uses (which shouldn't be sensitive...), however they will include error messages generated by your custom rendering functions. Essentially, assume any error message generated and returned in the rendering process may at some point be visible to the client, so **don't put sensitive information in your error messages**! diff --git a/docs/next/src/hello-world.md b/docs/next/src/hello-world.md new file mode 100644 index 0000000000..c68cb4a0cb --- /dev/null +++ b/docs/next/src/hello-world.md @@ -0,0 +1,99 @@ +# Hello World! + +Let's get started with Perseus! + +*To follow along here, you'll want to be familiar with Rust, which you can learn more about [here](https://rust-lang.org). You should also have it and `cargo` installed.* + +To begin, create a new folder for your project, let's call it `my-perseus-app`. Now, create a `Cargo.toml` file in that folder. This tells Rust which packages you want to use in your project and some other metadata. Put the following inside: + +```toml +{{#include ../../../examples/tiny/Cargo.toml}} +``` + +
+What are those dependencies doing? + +- `perseus` -- the core module for Perseus +- [`sycamore`](https://github.com/sycamore-rs/sycamore) -- the amazing system on which Perseus is built, this allows you to write reactive web apps in Rust + +Note that we've set these dependencies up so that they'll automatically update *patch versions*, which means we'll get bug fixes automatically, but we won't get any updates that will break our app! + +
+ +Now, create an `index.html` file at the root of your project and put the following inside: + +```html +{{#include ../../../examples/tiny/index.html}} +``` + +
+Why do I need an HTML file? + +Perseus aims to be as versatile as possible, and so it allows you to include your own `index.html` file, in which you can import things like fonts, analytics, etc. + +This file MUST contain at least the following: + +- `
`, which is where your app will be rendered, this must be a `
` with no other attributes except the `id`, and that spacing (that way parsing is lightweight and fast) +- A ``, which is where HTML metadata goes (even if you don't have any metadata, Perseus still needs it) + +Note also that we don't have to import anything to make Perseus run here, the server will do that automatically for us! + +
+ +Now, create a new directory called `src` and add a new file inside called `lib.rs`. Put the following inside: + +```rust,no_run,no_playground +{{#include ../../../examples/tiny/src/lib.rs}} +``` + +
+How does that work? + +First, we import some things that'll be useful: + +- `perseus::{define_app, ErrorPages, Template}` -- the -`define_app!` macro, which tells Perseus how your app works; the `ErrorPages` `struct`, which lets you tell Perseus how to handle errors (like *404 Not Found* if the user goes to a nonexistent page); and the `Template` `struct`, which is how Perseus manages pages in your app +- `std::rc::Rc` -- a [reference-counted smart pointer](https://doc.rust-lang.org/std/rc/struct.Rc.html) (you don't *have* to understand these to use Perseus, but reading that link would be helpful) +- `sycamore::template` -- Sycamore's [`template!` macro], which lets you write HTML-like code in Rust + +Then, we use the `define_app!` macro to declare the different aspects of the app, starting with the *templates*. We only have one template, which we've called `index` (a special name that makes it render at the root of your app), and then we define how that should look, creating a paragraph (`p`) containing the text `Hello World!`. Perseus does all kinds of clever stuff with this under the hood, and we put it in an `Rc` to enable that. + +Finally, we tell Perseus what to do if something in your app fails, like if the user goes to a page that doesn't exist. This requires creating a new instance of `ErrorPages`, which is a `struct` that lets you define a separate error page for every [HTTP status code](https://httpstatuses.com), as well as a fallback. Here, we've just defined the fallback. That page is given the URL that caused the error, the HTTP status code, and the actual error message, all of which we display with a Sycamore `template!`, with seamless interpolation. + +
+ +Now install the Perseus CLI with `cargo install perseus-cli` (you'll need `wasm-pack` to let Perseus build your app, use `cargo install wasm-pack` to install it) to make your life way easier, and deploy your app to by running `perseus serve` inside the root of your project! This will take a while the first time, because it's got to fetch all your dependencies and build your app. + +
+Why do I need a CLI? + +Perseus is a *very* complex system, and, if you had to write all that complexity yourself, that *Hello World!* example would be more like 1700 lines of code than 17! The CLI lets you abstract away all that complexity into a directory that you might have noticed appear called `.perseus/`. If you take a look inside, you'll actually find two crates (Rust packages): one for your app, and another for the server that serves your app. These are what actually run your app, and they import the code you've written. The `define_app!` macro defines a series of functions and constants at compile-time that make this possible. + +When you run `perseus serve`, the `.perseus/` directory is created and added to your `.gitignore`, and then three stages occur in parallel (they're shown in your terminal): + +- *🔨 Generating your app* -- here, your app is built to a series of static files in `.perseus/dist/static`, which makes your app lightning-fast (your app's pages are ready before it's even been deployed, which is called *static site generation*, or SSG) +- *🏗️ Building your app to Wasm* -- here, your app is built to [WebAssembly](), which is what lets a low-level programming language like Rust run in the browser +- *📡 Building server* -- here, Perseus builds its internal server based on your code, and prepares to serve your app + +The first time you run this command, it can take quite a while to get everything ready, but after that it'll be really fast. And, if you haven't changed any code (*at all*) since you last ran it, you can run `perseus serve --no-build` to run the server basically instantaneously. + +
+ +Once that's done, hop over to in any modern browser (not Internet Explorer...), and you should see *Hello World!* printed on the screen! If you try going to or any other page, you should see a message that tells you the page wasn't found. + +Congratulations! You've just created your first ever Perseus app! You can see the source code for this section [here](https://github.com/arctic-hen7/perseus/tree/main/examples/tiny). + +## Moving Forward + +The next section creates a slightly more realistic app with more than just one file, which will show you how a Perseus app is usually structured. + +After that, you'll learn how different features of Perseus work, like *incremental generation* (which lets you build pages on-demand at runtime)! + +### Alternatives + +If you've gone through this and you aren't that chuffed with Perseus, here are some similar projects in Rust: + +- [Sycamore](https://github.com/sycamore-rs/sycamore) (without Perseus) -- *A reactive library for creating web apps in Rust and WebAssembly.* +- [Yew](https://github.com/yewstack/yew) -- *Rust/Wasm framework for building client web apps.* +- [Seed](https://github.com/seed-rs/seed) -- *A Rust framework for creating web apps.* +- [Percy](https://github.com/chinedufn/percy) -- *Build frontend browser apps with Rust + WebAssembly. Supports server side rendering.* +- [MoonZoon](https://github.com/MoonZoon/MoonZoon) -- *Rust Fullstack Framework.* diff --git a/docs/next/src/integrations/actix-web.md b/docs/next/src/integrations/actix-web.md deleted file mode 100644 index e7d3e62617..0000000000 --- a/docs/next/src/integrations/actix-web.md +++ /dev/null @@ -1,59 +0,0 @@ -# Actix Web Integration - -If you're using [Actix Web](https://actix.rs), then Perseus can automate nearly all the boilerplate of serving your app for you! - -This integration provides a configuration function, which you can use to configure an existing web server to support Perseus, so you could even run something like [Diana](https://github.com/arctic-hen7/diana) on the same server! - -This integration should support almost every use case of Perseus, but there may be some extremely advanced things that you'll need to go back to basics for. If that's the case, please let us know by [opening an issue]() (we want these integrations to be as powerful as possible), and in the meantime you can use the guide [here](./serving.md) to see how to set up a server without using the integrations. If you need implementation details, check out the actual source code for the integration in the [repository](https://github.com/arctic-hen7/perseus). - -## Installation - -You can install the Actix Web integration by adding the following to your `Cargo.toml` under the `dependencies` section: - -```toml -perseus-actix-web = "0.1" -``` - -Note that you will still need `actix-web`, `futures`, and `perseus` itself, even in a server repository. - -All Perseus integrations follow the same version format as the core library, meaning they're all updated simultaneously. This makes version management much easier, and it means that you can just install the same version of every Perseus package and not have to worry about compatibility issues. - -## Usage - -This is an example of a web server that only uses Perseus, but you can call `.configure()` on any existing web server. Note though that **Perseus must be configured after all other logic**, because it adds a generic handler for all remaining pages, which will break other more specific logic that comes after it. - -```rust,no_run -use perseus::{FsConfigManager, SsrNode}; -use perseus_actix_web::{configurer, Options}; -use perseus_showcase_app::pages; -use actix_web::{HttpServer, App}; -use futures::executor::block_on; - -#[actix_web::main] -async fn main() -> std::io::Result<()> { - HttpServer::new(|| { - App::new() - // Other server logic here - .configure( - block_on(configurer( - Options { - index: "../app/index.html".to_string(), - js_bundle: "../app/pkg/bundle.js".to_string(), - wasm_bundle: "../app/pkg/perseus_showcase_app_bg.wasm".to_string(), - templates_map: pages::get_templates_map::() - }, - FsConfigManager::new() - )) - ) - }) - .bind(("localhost", 8080))? - .run() - .await -} -``` - -When you use the integration, you'll have to define a few options to tell it what exactly to serve. Specifically, you'll need to tell it where your `index.html` file, your JS bundle, and your Wasm bundle all are. In addition, you'll need to a provide it with a template map (which you'll often define a getter function for as above). - -Also, because this plugs into an existing server, you have full control over hosting options, like the port to be used! - -It's worth mentioning the blocking component of this design. The function that returns the closure that actually configures your server for Perseus is asynchronous because it needs to get your render configuration and add it as data to the server (this improves performance by reducing reads), which unfortunately is an asynchronous operation. We also can't `.await` that without causing ownership errors due to Actix Web's closure structure, which means the best solution for now is to `block_on` that configuration (which won't impact performance other than in your startup times, and all that's happening is a read from a file). If you have a better solution, [PRs are welcome](https://github.com/arctic-hen7/pulls)! diff --git a/docs/next/src/intro.md b/docs/next/src/intro.md index 89e192443a..06ea9a289d 100644 --- a/docs/next/src/intro.md +++ b/docs/next/src/intro.md @@ -4,94 +4,7 @@ Welcome to the Perseus documentation! Here, you'll find guides on how to use Perseus, as well as documentation for specific features and plenty of examples! -## What does it do? - -If you're familiar with [NextJS](https://nextjs.org), Perseus is that for Wasm. If you're familiar with [SvelteKit](https://kit.svelte.dev), it's that for [Sycamore](https://github.com/sycamore-rs/sycamore). - -If none of that makes any sense, this is the section for you! If you're not in the mood for a lecture, [here's a TL;DR](#summary)! - -### Rust web development - -[Rust](https://www.rust-lang.org/) is an extremely powerful programming language, but I'll leave the introduction of it [to its developers](https://www.rust-lang.org/). - -[WebAssembly](https://webassembly.org) (abbreviated Wasm) is like a low-level programming language for your browser. This is revolutionary, because it allows websites and web apps to built in programming languages other than JavaScript. Also, it's [really fast](https://medium.com/@torch2424/webassembly-is-fast-a-real-world-benchmark-of-webassembly-vs-es6-d85a23f8e193) (usually >30% faster than JS). - -But developing directly for the web with Rust using something like [`web-sys`](https://docs.rs/web-sys) isn't a great experience, it's generally agreed in the web development community that developer experience and productivity is vastly improved by having a *reactive* framework. Let's approach this from a traditional JavaScript and HTML perspective first. - -Imagine you want to create a simple counter. Here's how you might do it in a non-reactive framework (again, JS and HTML here, no Rust yet): - -```html -

0


- -``` - -If you're unfamiliar with HTML and JS, don't worry. All this does is create a paragraph with a number inside and then increment it. But the problem is clear in terms of expression: why can't we just put a variable in the paragraph and have that re-render when we increment that variable? Well, that's reactivity! - -In JS, there are frameworks like [Svelte](https://svelte.dev) and [ReactJS](https://reactjs.org) that solve this problem, but they're all bound significantly by the language itself. JavaScript is slow, dynamically typed, and [a bit of a mess](https://medium.com/netscape/javascript-is-kinda-shit-im-sorry-2e973e36fec4). Like all things to do with the web, changing things is really difficult because people have already started using them, and there will always be *someone* still using Internet Explorer, which supports almost no modern web standards at all. - -[Wasm](https://webassembly.org) solves all these problems by creating a unified format that other programming languages, like Rust, can compile into for the browser environment. This makes websites safer, faster, and development more productive. The equivalent of these reactive frameworks for Rust in particular would be projects like [Sycamore](https://sycamore-rs.netlify.app), [Seed](https://seed-rs.org), and [Yew](https://yew.rs). Sycamore is the most extensible and low-level of those options, and it's more performant because it doesn't use a [virtual DOM](https://svelte.dev/blog/virtual-dom-is-pure-overhead) (link about JS rather than Rust), and so it was chosen to be the backbone of Perseus. Here's what that counter might look like in [Sycamore](https://sycamore-rs.netlify.app) (the incrementation has been moved into a new closure for convenience): - -```rust,no_run,no_playground -use sycamore::prelude::*; - -let counter = Signal::new(0); -let increment = cloned!((counter) => move |_| counter.set(*counter.get() + 1)); - -template! { - p {(props.greeting)} - a(href = "/about") { "About!" } - - p { (counter.get()) } - button(on:click = increment) { "Increment" } -} -``` - -You can learn more about Sycamore's amazing systems [here](https://sycamore-rs.netlify.app). - -### This sounds good... - -But there's a catch to all this: rendering. With all these approaches in Rust so far (except for a few mentioned later), all your pages are rendered *in the user's browser*. That means your users have to download you Wasm code and run it before they see anything at all on their screens. Not only does that increase your loading time ([which can drive away users](https://medium.com/@vikigreen/impact-of-slow-page-load-time-on-website-performance-40d5c9ce568a)), it reduces your search engine rankings as well. - -This can be solved through *server-side rendering* (SSR), which means that we render pages on the server and send them to the client, which means your users see something very quickly, and then it becomes *interactive* (usable) a moment later. This is better for user retention (shorter loading times) and SEO (search engine optimization). - -The traditional approach to SSR is to wait for a request for a particular page (say `/about`), and then render it on the server and send that to the client. This is what [Seed](https://seed-rs.org) (an alternative to Perseus) does. However, this means that your website's *time to first byte* (TTFB) is slower, because the user won't even get *anything* from the server until it has finished rendering. In times of high load, that can drive loading times up worryingly. - -The solution to this is *static site generation* (SSG), whereby your pages are rendered *at build time*, and they can be served almost instantly on any request. This approach is fantastic, and thus far widely unimplemented in Rust. The downside to this is that you don't get as much flexibility, because you have to render everything at build time. That means you don't have access to any user credentials or anything else like that. Every page you render statically has to be the same for every user. - -Perseus supports SSR *and* SSG out of the box, along with the ability to use both on the same page, rebuild pages after a certain period of time (e.g. to update a list of blog posts every 24 hours) or based on certain conditions (e.g. if the hash of a file has changed), or even to statically build pages on demand (the first request is SSR, all the rest are SSG), meaning you can get the best of every world and faster build times. - -To our knowledge, the only other framework in the world right now that supports this feature set is [NextJS](https://nextjs.org) (with growing competition from [GatsbyJS](https://www.gatsbyjs.com)), which only works with JavaScript. Perseus goes above and beyond this for Wasm by supporting whole new combinations of rendering options not previously available, allowing you to create optimized websites and web apps extremely efficiently. - -## How fast is it? - -[Benchmarks show](https://rawgit.com/krausest/js-framework-benchmark/master/webdriver-ts-results/table.html) that [Sycamore](https://sycamore-rs.netlify.app) is slightly faster than [Svelte](https://svelte.dev) in places, one of the fastest JS frameworks ever. Perseus uses it and [Actix Web](https://actix.rs), one of the fastest web servers in the world. Essentially, Perseus is built on the fastest tech and is itself made to be fast. - -Right now, Perseus is undergoing major improvements to make it even faster and to introduce new features, like support for internationalization (making your app available in many languages) out of the box, which involves significant changes to the code. Once these are ready, benchmarks for Perseus itself will be written to show how fast Perseus really is, but right now none exist. - -## How convenient is it? - -Perseus aims to be more convenient than any other Rust web framework by taking an approach similar to that of [ReactJS](https://reactjs.org). Perseus itself is an extremely complex system consisting of many moving parts that can all be brought together to create something amazing, but the vast majority of apps don't need all that customizability, so we built a command-line interface (CLI) that handles all that complexity for you, allowing you to focus entirely on your app's code. - -Basically, here's your workflow: - -1. Create a new project. -2. Define your app in under 30 lines of code (coming down to 15 with v0.2.0!) -3. Code your amazing app. -4. Run `perseus serve`. - -## How stable is it? - -Okay, there had to be one caveat! Perseus is *very* new, and as such can't be recommended for *production* usage yet. However, we're aiming to get it ready for that really soon, which will hopefully include even being able to deploy Perseus with [serverless functions](https://en.wikipedia.org/wiki/Serverless_computing), the step beyond a server! - -For now though, Perseus is perfect for anything that doesn't face the wider internet, like internal tools, personal projects, or the like. Just don't use it to run a nuclear power plant, okay? - -## Summary - -If all that was way too long, here's a quick summary of what Perseus does and why it's useful! - -- JS is slow and bad, [Wasm](https://webassembly.org) lets you run most programing languages, like Rust, in the browser, and is really fast -- Doing web development without reactivity is really annoying, so [Sycamore](https://sycamore-rs.netlify.app) is great -- Perseus lets you render your app on the server, making the client's experience *really* fast, and adds a ton of features to make that possible, convenient, and productive (even for really complicated apps) +If you like Perseus, please consider giving us a star [on GitHub](https://github.com/arctic-hen7/perseus)! [repo]: https://github.com/arctic-hen7/perseus [crate]: https://crates.io/crates/perseus diff --git a/docs/next/src/routing.md b/docs/next/src/routing.md deleted file mode 100644 index 0a9c91664f..0000000000 --- a/docs/next/src/routing.md +++ /dev/null @@ -1,81 +0,0 @@ -# Routing - -*You only need this page if you're not using the Perseus CLI, which performs this process for you! It does provide some useful background even so though.* - -Perseus will serve your pages on the backend, rendered however you like, but it depends on [Sycamore](https://github.com/sycamore-rs/sycamore) for front-end rendering and routing, so you'll need to provide a router for your pages. You can see more information about Sycamore routing in their official documentation [here](https://sycamore-rs.netlify.app/docs/advanced/routing). - -## Usage - -You'll need to define a rout `enum` at the root of your app like so to define your app's routes: - -```rust -use sycamore::prelude::*; - -#[derive(Route)] -enum AppRoute { - #[to("/")] - Index, - #[to("/about")] - About, - #[to("/post/new")] - NewPost, - #[to("/post/")] - Post { slug: Vec }, - #[to("/ip")] - Ip, - #[to("/time")] - TimeRoot, - #[to("/timeisr/")] - Time { slug: String }, - #[not_found] - NotFound, -} -``` - -Note in the above example the usage of the `NewPost` template to override a section of the domain of the `Post` template, specifically the `/post/new` path, where a post writing page is hosted. Notably, such intrusive routes must be placed before. In general, **order your routes by specificity**. If you're not having troubles though, put them in any order you like (but `NotFound` must come last). - -You can then match each of your routes and render it like so (subset of the previous example): - -```rust,no_run,no_plyaground -let root = web_sys::window() - .unwrap() - .document() - .unwrap() - .query_selector("#_perseus_root") - .unwrap() - .unwrap(); - -sycamore::render_to( - || { - template! { - BrowserRouter(|route: AppRoute| { - match route { - AppRoute::Index => app_shell( - "index".to_string(), - pages::index::template_fn(), - get_error_pages() - ), - AppRoute::About => app_shell( - "about".to_string(), - pages::about::template_fn(), - get_error_pages() - ), - AppRoute::Post { slug } => app_shell( - format!("post/{}", slug.join("/")), - pages::post::template_fn(), - get_error_pages() - ), - AppRoute::NotFound => template! { - p {"Not Found."} - } - } - }) - } - }, - &root, - ); -``` - -Note that you pass your error pages to the app shell, allowing it to conditionally render them if need be. Also note the template function being reused for the router as well as in the template itself. - -The router is the core of your app, and should be rendered to a location from which you'll use Perseus. Perseus is a full framework for rendering, so if you want incremental adoption of reactivity, you should check out the underlying [Sycamore](https://github.com/sycamore-rs/sycamore) library. diff --git a/docs/next/src/second-app.md b/docs/next/src/second-app.md new file mode 100644 index 0000000000..e571c64d12 --- /dev/null +++ b/docs/next/src/second-app.md @@ -0,0 +1,172 @@ +# Your Second App + +This section will cover building a more realistic app than the *Hello World!* section, with proper structuring and multiple templates. + +If learning by reading isn't really your thing, or you'd like a reference, you can see all the code in [this repository](https://github.com/arctic-hen7/perseus/tree/main/examples/basic)! + +## Setup + +Much like the *Hello World!* app, we'll start off by creating a new directory for the project, maybe `my-second-perseus-app` (or you could exercise imagination...). Then, we'll create a new `Cargo.toml` file and fill it with the following: + +```toml +{{#include ../../../examples/basic/Cargo.toml}} +``` + +The only difference between this and the last `Cargo.toml` we created is two new dependencies: + +- [`serde`](https://serde.rs) -- a really useful Rust library for serializing/deserializing data +- [`serde_json`](https://github.com/serde-rs/json) -- Serde's integration for JSON, which lets us pass around properties for more advanced pages in Perseus + +The next thing to do is to create `index.html`, which is pretty much the same as last time: + +```html +{{#include ../../../examples/basic/index.html}} +``` + +The only notable difference here is the absence of a ``, which is because we'll be creating it inside Perseus! Any Perseus template can modify the `<head>` of the document, but anything you put into `index.html` will persist across all pages. We don't want to have conflicting titles, so we leave that property out of `index.html`. + +## `lib.rs` + +As in every Perseus app, `lib.rs` is how we communicate with the CLI and tell it how our app works. Put the following content in `src/lib.rs`: + +```rust,no_playground,no_run +{{#include ../../../examples/basic/src/lib.rs}} +``` + +This code is quite different from your first app, so let's go through how it works. + +First, we define two other modules in our code: `error_pages` (at `src/error_pages.rs`) and `templates` (at `src/templates`). Don't worry, we'll create those in a moment. The rest of the code creates a new app with two templates, which are expected to be in the `src/templates` directory. Note the use of `<G>` here, which is a Rust *type parameter* (the `get_template` function can work for the browser or the server, so Rust needs to know which one it is). This parameter is *ambient* to the `templates` key, which means you can use it without declaring it as long as you're inside `templates: {...}`. This will be set to `DomNode` for the browser and `SsrNode` for the server, but that all happens behind the scenes. + +Also note that we're pulling in our error pages from another file as well (in a larger app you may even want to have a different file for each error page). + +The last thing we do is new, we define `static_aliases` to map the URL `/test.txt` in our app to the file `static/test.txt`. This feature is detailed in more depth later, but it can be extremely useful, for example for defining your site's logo (or favicon), which browsers expect to be available at `/favicon.ico`. Create the `static/test.txt` file now (`static/` should NOT be inside `src/`!) and fill it with whatever you want. + +## Error Handling + +Before we get to the cool part of building the actual pages of the app, we should set up error pages again, which we'll do in `src/error_pages.rs`: + +```rust,no_run,no_playground +{{#include ../../../examples/basic/src/error_pages.rs}} +``` + +This is a little more advanced than the last time we did this, and there are a few things we should note. + +The first is the import of `GenericNode`, which we define as a type parameter on the `get_error_pages` function. As we said before, this means your error pages will work on the client or the server, and they're needed in both environments. If you're interested, this separation of browser and server elements is done by Sycamore, and you can learn more about it [here](https://docs.rs/sycamore/0.6/sycamore/generic_node/trait.GenericNode.html). + +In this function, we also define a different error page for a 404 error, which will occur when a user tries to go to a page that doesn't exist. The fallback page (which we initialize `ErrorPages` with) is the same as last time, and will be called for any errors other than a *404 Not Found*. + +## `index.rs` + +It's time to create the first page for this app! But first, we need to make sure that import in `src/lib.rs` of `mod templates;` works, which requires us to create a new file `src/templates/mod.rs`, which declares `src/templates` as a module with its own code. Add the following to that file: + +```rust,no_run,no_playground +{{#include ../../../examples/basic/src/templates/mod.rs}} +``` + +It's common practice to have a file for each *template*, which is slightly different to a page (explained in more detail later), and this app has two pages: a landing page (index) and an about page. + +Let's begin with the landing page. Create a new file `src/templates/index.rs` and put the following inside: + +```rust,no_run,no_playground +{{#include ../../../examples/basic/src/templates/index.rs}} +``` + +This code is *much* more complex than the *Hello World!* example, so let's go through it carefully. + +First, we import a whole ton of stuff: + +- `perseus` + - `StringResultWithCause` -- see below for an explanation of this + - `Template` -- as before + - `GenericNode` -- as before +- `serde` + - `Serialize` -- a trait for `struct`s that can be turned into a string (like JSON) + - `Deserialize` -- a trait for `struct`s that can be *de*serialized from a string (like JSON) +- `std::rc::Rc` -- same as before, you can read more about `Rc`s [here](https://doc.rust-lang.org/std/rc/struct.Rc.html) +- `sycamore` + - `component` -- a macro that turns a function into a Sycamore component + - `template` -- the `template!` macro, same as before + - `Template as SycamoreTemplate` -- the output of the `template!` macro, aliased as `SycamoreTemplate` so it doesn't conflict with `perseus::Template`, which is very different + +Then we define a number of different functions and a `struct`, each of which gets a section now. + +### `IndexPageProps` + +This `struct` represents the properties that the index page will take. In this case, we're building an index page that will display a greeting defined in this, specifically in the `greeting` property. + +Any template can take arguments in Perseus, which should always be given inside a `struct`. For simplicity and performance, Perseus only ever passes your properties around as a `String`, so you'll need to serialize/deserialize them yourself (as in the functions below). + +### `index_page()` + +This is the actual component that your page is. Technically, you could just put this under `template_fn()`, but it's conventional to break it out independently. By annotating it with `#[component(IndexPage<G>)]`, we tell Sycamore to turn it into a complex `struct` that can be called inside `template!` (which we do in `template_fn()`). + +Note that this takes `IndexPageProps` as an argument, which it can then access in the `template!`. This is Sycamore's interpolation system, which you can read about [here](https://sycamore-rs.netlify.app/docs/basics/template), but all you need to know is that it's basically seamless and works exactly as you'd expect. + +The only other thing we do here is define an `<a>` (an HTML link) to `/about`. This link, and any others you define, will automatically be detected by Sycamore's systems, which will pass them to Perseus' routing logic, which means your users **never leave the page**. In this way, Perseus only pulls in the content that needs to change, and gives your users the feeling of a lightning-fast and weightless app. + +*Note: external links will automatically be excluded from this, and you can exclude manually by adding `rel="external"` if you need.* + +### `get_template()` + +This function is what we call in `lib.rs`, and it combines everything else in this file to produce an actual Perseus `Template` to be used. Note the name of the template as `index`, which Perseus interprets as special, which causes this template to be rendered at `/` (the landing page). + +Perseus' templating system is extremely versatile, and here we're using it to define our page itself through `.template()`, and to define a function that will modify the document `<head>` (which allows us to add a title) with `.head()`. Notably, we also use the *build state* rendering strategy, which tells Perseus to call the `get_build_props()` function when your app builds to get some state. More on that now. + +### `get_build_props()` + +This function is part of Perseus' secret sauce (actually *open* sauce), and it will be called when the CLI builds your app to create properties that the template will take (it expects a string, hence the serialization). Here, we just hard-code a greeting in to be used, but the real power of this comes when you start using the fact that this function is `async`. You might query a database to get a list of blog posts, or pull in a Markdown documentation page and parse it, the possibilities are endless! + +### `template_fn()` + +The result of this function is what Perseus will call when it wants to render your template (which it does more than you might think), and it passes it the props that your template takes as an `Option<String>`. This might seem a bit weird, but there are reasons under the hood. All you need to know here is that if your template takes any properties, they **will** be here, and it's safe to `.unwrap()` them for deserialization. + +### `head_fn()` + +This is very similar to `template_fn`, except it can't be reactive. In other words, anything you put in here is like a picture, it can't move (so no buttons, counters, etc.). This is because this modifies the document `<head>`, so you should put metadata, titles, etc. in here. Note that the function we return from here does take an argument (ignored with `_`), that's a string of the properties to your app, but we don't need it in this example. If this page was a generic template for blog posts, you might use this capability to render a different title for each blog post. + +All this does though is set the `<title>`. If you inspect the source code of the HTML in your browser, you'll find a big comment in the `<head>` that says `<!--PERSEUS_INTERPOLATED_HEAD_BEGINS-->`, that separates the stuff that should remain the same on every page from the stuff that should update for each page. + +## `about.rs` + +Okay! We're past the hump, and now it's time to define the (much simpler) `/about` page. Create `src/templates/about.rs` and put the following inside: + +```rust,no_run,no_playground +{{#include ../../../examples/basic/src/templates/about.rs}} +``` + +This is basically exactly the same as `index.rs`, except we don't have any properties to deal with, and we don't need to generate anything special at build time (but Perseus will still render this page to static HTML, ready to be served to your users). + +## Running It + +`perseus serve` + +That's all. Every time you build a Perseus app, that's all you need to do. + +Once this is finished, your app will be live at <http://localhost:8080>! Note that if you don't like that, you can change the host/port with the `HOST`/`PORT` environment variables (e.g. you'd want to set the host to `0.0.0.0` if you want other people on your network to be able to access your site). + +Hop over to <http://localhost:8080> in any modern browser and you should see your greeting `Hello World!` above a link to the about page! if you click that link, you'll be taken to a page that just says `About.`, but notice how your browser seemingly never navigates to a new page (the tab doesn't show a loading icon)? That's Perseus' *app shell* in action, which intercepts navigation to other pages and makes it occur seamlessly, only fetching the bare minimum to make the new page load. The same behavior will occur if you use your browser's forward/backward buttons. + +<details> +<summary>Why a 'modern browser'?</summary> + +### Browser Compatibility + +Perseus is compatible with any browser that supports Wasm, which is most modern browsers like Firefox and Chrome. However, legacy browsers like Internet Explorer will not work with any Perseus app, unless you *polyfill* support for WebAssembly. + +</details> + +By the way, remember this little bit of code in `src/lib.rs`? + +```rust,no_run,no_playground +{{#include ../../../examples/basic/src/lib.rs:12:14}} +``` + +If you navigate to <http://localhost:8080/test.txt>, you should see the contents on `static/test.txt`! You can also access them at <http://localhost:8080/.perseus/static/test.txt> + + + +## Moving Forward + +Congratulations! You're now well on your way to building highly performant web apps in Rust! The remaining sections of this book are more reference-style, and won't guide you through building an app, but they'll focus instead on specific features of Perseus that can be used to make extremely powerful systems. + +So go forth, and build! diff --git a/docs/next/src/serving.md b/docs/next/src/serving.md deleted file mode 100644 index 2d332f3936..0000000000 --- a/docs/next/src/serving.md +++ /dev/null @@ -1,102 +0,0 @@ -# Serving - -_You only need this page if you're not using the Perseus CLI, which performs this process for you!_ - -Having generated a large number of static files, you'll need a system to host your files for you! Due to the dynamic nature of some rendering strategies, Perseus needs to be involved in this process (for executing request-time logic), and so it provides a simple API interface for serving pages. - -Perseus aims to be agnostic as to what framework you use to host your files, and any framework that gives you access to request headers and wildcard paths should work (in other words, any framework worth its salt). - -If you're using one of our supported integrations, you don't have to bother with this page, nearly all of it can be done for you! - -- [Actix Web](./integrations/actix-web.md) -- _More coming soon..._ - -## Endpoints - -Here are the endpoints that a server for Perseus must serve: - -- `/.perseus/page/*` – used to serve the JSON data that the app shell needs to render a page (`*` should be extractable as a filename, e.g. `{filename:.*}` in Actix Web) -- `/.perseus/bundle.js` – the JavaScript bundle file that calls your Wasm code (see [tutorial on building your first app](./tutorials/first_app/intro.md)) -- `/.perseus/bundle.wasm` – the Wasm bundle file that contains your code (see [tutorial on building your first app](./tutorials/first_app/intro.md)) -- `*` (anything else) – any page that the user actually requests, which will return the app shell to do the heavy lifting (or more accurately an HTML file that includes the bundle) - -## Usage - -This example shows what would be done to acquire a page for any framework. You'll need to have access to these data to get a page: - -- The page path the user requested, e.g. `/post/test` for a request to `/.perseus/page/post/test` -- Data about the HTTP request the user sent (see below) -- A map of templates produced with [`get_templates_map!`]() (API docs WIP) -- A [config manager](./config_managers.md) - -```rust,no_run,no_playground -use perseus::{get_page}; - -// See below for details on this line -let http_req = convert_req(&req).unwrap(); - -let page_data = get_page(path, http_req, &render_cfg, &templates, config_manager.get_ref()).await; - -match page_data { - Ok(page_data) => // Return a 200 with the stringified `page_data` - // We parse the error to return an appropriate status code - Err(err) => // Return the error dictated by `err_to_status_code(&err)` with the body of the stringified `err` -} -``` - -## Request Data - -Perseus needs access to information about HTTP requests so it can perform tasks related to the _request state_ strategy, which provides access to headers and the like. Internally, Perseus uses [`http::Request`](https://docs.rs/http/0.2.4/http/request/struct.Request.html) for this, with the body type `()` (payloads are irrelevant in requests that ask for a page at a URL). - -Unfortunately, different web server frameworks represent request data differently, and so you'll need to convert from your framework's system to `http`'s. When integrations are ready, this will be done for you! - -### Writing a Converter - -This is a simplified version of an Actix Web converter: - -```rust -use perseus::{HttpRequest, Request}; - -/// Converts an Actix Web request into an `http::request`. -pub fn convert_req(raw: &actix_web::HttpRequest) -> Result<Request, String> { - let mut builder = HttpRequest::builder(); - // Add headers one by one - for (name, val) in raw.headers() { - // Each method call consumes and returns `self`, so we re-self-assign - builder = builder.header(name, val); - } - // The URI to which the request was sent - builder = builder.uri(raw.uri()); - // The method (e.g. GET, POST, etc.) - builder = builder.method(raw.method()); - // The HTTP version used - builder = builder.version(raw.version()); - - let req = builder - // We always use an empty body because, in a Perseus request, only the URI matters - // Any custom data should therefore be sent in headers (if you're doing that, consider a dedicated API) - .body(()) - .map_err(|err| format!("converting actix web request to perseus-compliant request failed: '{}'", err))?; - - Ok(req) -} -``` - -Notably, the data that need to be converted are: - -- Headers -- URI to which the request was sent -- HTTP method (subject to change in future Perseus versions, currently `GET`) -- HTTP version used - -Note that mis-converting any of these data will not affect Perseus (which doesn't require any of them to function), only your own code. So if you have no intention of using the _request state_ strategy in your app, you could theoretically just parse an empty request to Perseus like so: - -```rust -use perseus::HttpRequest - -HttpRequest::new(()); -``` - -## File Storage - -Perseus' systems of storing files in production are documented in-depth [here](./config_managers.md). diff --git a/docs/next/src/setup.md b/docs/next/src/setup.md deleted file mode 100644 index e741cd6edd..0000000000 --- a/docs/next/src/setup.md +++ /dev/null @@ -1,15 +0,0 @@ -# Setup - -Perseus is designed to be server-agnostic, and can be hosted functionally anywhere you can run Rust (or at least execute a binary), for example in a serverless function. - -## Installation - -You can install the Perseus crate by adding the following to your `Cargo.toml` under `[dependencies]`: - -```toml -perseus = "0.1" -``` - -## Project Structure - -The structure of a Perseus project is described in detail in the [architecture section](./arch.md), but you'll need two crates, an app and a server. A great example of this is in the showcase example, which you can find on GitHub [here](). We advise setting up a Cargo workspace with an `app` and a `server` crate for development. Soon, Perseus will support a CLI to run your server for you so you can focus more on your app. diff --git a/docs/next/src/strategies/build_paths.md b/docs/next/src/strategies/build_paths.md deleted file mode 100644 index 90cda7d212..0000000000 --- a/docs/next/src/strategies/build_paths.md +++ /dev/null @@ -1,28 +0,0 @@ -# Build Paths - -This strategy allows you to define the pages a template will render. For example, you might use this for a blog posts system to get all the posts from a database and return a `Vec<String>` of them. This strategy is roughly equivalent to NextJS's `get_static_paths` function. - -Every element returned here will have the opportunity to create its state with the *build state* strategy, being passed the path defined here. Note that every element returned here will be built, so if you need to return more than about 10 elements, it's a better idea to only return the most used ones and leave the rest to the *incremental generation* strategy to reduce your build time. - -## Usage - -You can define a function for this strategy like so: - -```rust -pub async fn get_build_paths() -> Result<Vec<String>, String> { - Ok(vec![ - "test".to_string(), - "blah/test/blah".to_string() - ]) -} -``` - -Paths returned from this function will be rendered under `[template-path]/[returned-path]`, and they should not have a leading or trailing `/`. If you want to return a nested path, simply do so (but make sure to handle it properly in your router). Note that any errors must be returned as `String`s, and the function must be asynchronous. - -You can add this strategy to a template like so: - -```rust,no_run,no_playground -template - // ... - .build_paths_fn(Box::new(get_static_paths)) -``` diff --git a/docs/next/src/strategies/build_state.md b/docs/next/src/strategies/build_state.md deleted file mode 100644 index 9fd97375f2..0000000000 --- a/docs/next/src/strategies/build_state.md +++ /dev/null @@ -1,48 +0,0 @@ -# Build State - -This strategy allows you to define the state for a page. The function you give will be provided with the path of the page being rendered from the template on which this strategy is defined. - -If used without the *build paths* strategy, this will simply render one state for the only page that the template will render. If used with it, this strategy will be invoked for every page that the template renders. - -Note also that this strategy will be invoked on every on-demand build if used with the *incremental* strategy. - -## Usage - -You can define a function for this strategy like so: - -```rust -use serde::{Serialize, Deserialize}; -use perseus::ErrorCause; - -#[derive(Serialize, Deserialize)] -pub struct PostPageProps { - title: String, - content: String, -} -// ... -pub async fn get_build_state(path: String) -> Result<String, (String, ErrorCause)> { - let title = urlencoding::decode(&path).unwrap(); - let content = format!( - "This is a post entitled '{}'. Its original slug was '{}'.", - title, path - ); - - Ok(serde_json::to_string(&PostPageProps { - title: title.to_string(), - content, - }) - .unwrap()) -} -``` - -This function can produce two kinds of errors, broadly: those caused by the server, and those caused by the client (if this is called for a page that doesn't exist from the *incremental generation* strategy). For that reason, you need to return a `(String, ErrorCause)` tuple, the second part of which specifies who's responsible for the error. This allows Perseus to figure out whether it should send a 400 (client error) or 500 (server error) HTTP status code in the event of an error. While returning `String` errors may seem annoying, it prevents unnecessary internal heap allocation, and does overall make things faster (if you have a better way, please [open a PR](https://github.com/arctic-hen7/perseus/pulls)!). This function must also be asynchronous and the state must be returned in a stringified format. - -The path provided to the function will be provided as **whatever will end up being rendered**. For example, if you returned the element `test` from the build paths strategy (intending it to be rendered as `/post/test`), it will be passed to this function as `post/test`. - -You can add this strategy to a template like so: - -```rust,no_run,no_playground -template - // ... - .build_state_fn(Box::new(get_build_state)) -``` diff --git a/docs/next/src/strategies/incremental.md b/docs/next/src/strategies/incremental.md deleted file mode 100644 index 9f50dc6170..0000000000 --- a/docs/next/src/strategies/incremental.md +++ /dev/null @@ -1,27 +0,0 @@ -# Incremental Generation - -This strategy is both the most complex and most simple that Perseus offers. It's simple in that you don't have to write any extra code except telling Perseus to use it (see below), but it's complex in what it does. - -Let's say you have an online store, where every item you sell is rendered by the template `item`. If you have 5 items, you might fetch them from a database at build-time and tell Perseus to render each one with the *build paths* strategy. But what if you have 5 million items? If you were to render every single one of these at build time, your builds would take a very long time, especially if you're fetching data for every single item! Enter *incremental generation*, which allows you to return a subset of items with *build paths*, like only the 5 most commonly accessed items, and then the rest are left unspoken. - -When a user requests a pre-rendered item, it's served as usual, but if they request something under `/item`, Perseus will detect that that page may well exist, but hasn't been rendered yet, and so it will invoke the *build state* strategy as it would've if this page were being built at build-time, it just does it at request-time! And here's the magic of it, **after the first request, Perseus will cache the page**! So basically, pages are built on demand, and then cached for everyone else! Only the first user to access a page will see the slightest delay. - -The one caveat with this strategy is that you need to handle the possibility in the *build state* strategy that the given path may not actually exist, and you'll need to return a 404 (page not found error) in that case. You can do that like so: - -```rust,no_run,no_playground -use perseus::ErrorCause - -return Err(("custom error message".to_string(), ErrorCause::Client(Some(404)))) -``` - -Note that this tells Perseus that the client caused an error, particularly a 404, which should be handled in your app to return something like 'Page not found'. - -## Usage - -This strategy can be added to a template like so: - -```rust,no_run,no_playground -template - // ... - .incremental(true) -``` diff --git a/docs/next/src/strategies/intro.md b/docs/next/src/strategies/intro.md deleted file mode 100644 index 6eb39d392a..0000000000 --- a/docs/next/src/strategies/intro.md +++ /dev/null @@ -1,3 +0,0 @@ -# Rendering Strategies - -This section details each rendering strategy offered by Perseus in detail. Note that one of the aims of the rendering mechanism is that any rendering strategy can be used with other, however some combinations are more sensible than others! diff --git a/docs/next/src/strategies/request_state.md b/docs/next/src/strategies/request_state.md deleted file mode 100644 index 40d4746348..0000000000 --- a/docs/next/src/strategies/request_state.md +++ /dev/null @@ -1,48 +0,0 @@ -# Request State - -This strategy allows you to define the state for a page at request-time, which gives you access to information like the headers of the user's HTTP request (including any authorization tokens) and real-time factors. This can be useful if you want to render something like a user dashboard on the server, which you wouldn't be able to do with the *build state* strategy, as it only has access to information at build-time. - -It should be noted that this strategy is much slower than build-time strategies, as it requires extra computations on the server every time a user requests a page. However, this strategy is superior to client-side rendering (rendering a page at build time with fillers for unique content that you then fill in in the browser) in some ways. This strategy is essentially server-side rendering, and you can read more about its performance [here](https://medium.com/walmartglobaltech/the-benefits-of-server-side-rendering-over-client-side-rendering-5d07ff2cefe8). - -## Using with Build State - -Perseus supports using both build and request state simultaneously, though it's not advised unless absolutely necessary. This will result in the generation of two competing states, one from build and one from request, which you can then amalgamate by using the `amalgamate_states` strategy. Due to the phenomenally niche nature of this approach, it's not covered in depth in the documentation, but you can check out the `showcase` example if you want to see it in action (specifically the `amalgamate` page). - -## Usage - -You can define a function for this strategy like so (this will tell the user their own IP address): - -```rust -use serde::{Deserialize, Serialize}; -use perseus::ErrorCause; - -#[derive(Serialize, Deserialize)] -pub struct IpPageProps { - ip: String, -} -pub async fn get_request_state(_path: String, req: Request) -> Result<String, (String, ErrorCause)> { - Ok(serde_json::to_string(&IpPageProps { - // Gets the client's IP address - ip: format!( - "{:?}", - req - .headers() - .get("X-Forwarded-For") - .unwrap_or(&perseus::http::HeaderValue::from_str("hidden from view!").unwrap()) - ), - }) - .unwrap()) -} -``` - -This function can produce two kinds of errors, broadly: those caused by the server, and those caused by the client. For that reason, you need to return a `(String, ErrorCause)` tuple, the second part of which specifies who's responsible for the error. This allows Perseus to figure out whether it should send a 400 (client error) or 500 (server error) HTTP status code in the event of an error. This function must also be asynchronous. - -As with the *build state* strategy, you must return state from this function as a string, and the path provided to this function is the same as the final path at which the page will be rendered. - -You can add this strategy to a template like so: - -```rust,no_run,no_playground -template - // ... - .request_state_fn(Box::new(get_request_state)) -``` diff --git a/docs/next/src/strategies/revalidation.md b/docs/next/src/strategies/revalidation.md deleted file mode 100644 index 8dba5c0251..0000000000 --- a/docs/next/src/strategies/revalidation.md +++ /dev/null @@ -1,44 +0,0 @@ -# Revalidation - -This strategy allows you to rebuild pages built with the *build state* strategy on a later request. A common reason for this might be to update a statically rendered list of blog posts every 24 hours so it's up-to-date relatively regularly. Perseus' revalidation strategy allows you re-render a page on two conditions: time-based and logic-based. The time-based variant lets you provide a string like `1w`, and then your page will be re-rendered every week. The logic-based variant lets you provide a function that returns a boolean as to whether or not to re-render, which will be run on every request to the page. Notably, the variants can be combined so that you run a logic check only after some length of time. - -The time-based strategy adds very little server overhead, as it simply performs a time check, though it does involve another read from your data cache, which may be computationally expensive. The logic-based check is as expensive as you make it. - -## Time-Based Variant - -The time based variant does have some slightly weird behaviour to watch out for though, which is best explained by explaining how it works. - -1. Evaluates your time string (e.g. `1w` for 1 week) to a number of seconds after January 1 1970 (how computers represent time). This provides a timestamp in the future, past which your page should be revalidated. -2. On every request, Perseus checks if this timestamp has been passed yet. If it has, it re-renders your page. This means that **your page will only be revalidated after the time has elapsed *and* a user has queried it**. -3. After revalidation, Perseus repeats from step 1. However, this may not be 2 weeks after the original build (in our example of `1w`), but 1 week after the revalidation, whcih may have been later than a week after the original setting. - -To put it simply, Perseus will only revalidate when requested, so don't expect different pages to be synchronised in their revalidations, even if they all have the same timestamp. - -This logic is a bit weird, so you may need to think about it for a bit. Don't worry though, it shouldn't impact your app negatively in any way, it's just something to take note of! - -## Time Syntax - -Perseus lets you define revalidation intervals as strings, the syntax for which is as follows: `xXyYzZ...`, where lower-case letters are numbers meaning the number of the interval X/Y/Z (e.g. 1m4d -- one month four days). - -The available intervals are: - -- s: second, -- m: minute, -- h: hour, -- d: day, -- w: week, -- M: month (30 days used here, 12M ≠ 1y!), -- y: year (365 days always, leap years ignored, if you want them add them as days) - -## Usage - -You can add this strategy to a template like so: - -```rust,no_run,no_playground -template - // ... - .revalidate_after("5s".to_string()) - .should_revalidate_fn(Box::new(|| async { Ok(true) })) -``` - -That example uses both variants of revalidation, but you can use one or both as necessary. Note that the logic-based variant must be asynchronous, and errors must be returned as `String`s. diff --git a/docs/next/src/templates.md b/docs/next/src/templates.md deleted file mode 100644 index 5f722987a4..0000000000 --- a/docs/next/src/templates.md +++ /dev/null @@ -1,39 +0,0 @@ -# Templates - -The most central part of Perseus is the definition of templates, which control how pages are built. - -## Usage - -An extremely simple template would look like this: - -```rust -use perseus::Template; -use sycamore::prelude::{component, template, GenericNode, Template as SycamoreTemplate}; - -#[component(AboutPage<G>)] -pub fn about_page() -> SycamoreTemplate<G> { - template! { - p { "About." } - } -} - -pub fn template_fn<G: GenericNode>() -> perseus::template::TemplateFn<G> { - Box::new(|_| { - template! { - AboutPage() - } - }) -} - -pub fn get_template<G: GenericNode>() -> Template<G> { - Template::new("about").template(template_fn()) -} -``` - -First, we define the *component function*, which is done with Sycamore. This is the page itself, and it contains its actual markup, in Sycamore's templating syntax (you can read more about that [here]()). Next is the *template function*, which simply defines a function that will actually render the page. We break this simple closure out into a function to get it because we'll reuse it in the routing process later. If your page takes a state, it will be passed to this closure **as a string**. You must then deserialize it, and it is safe to `.unwrap()` here (barring a horrific logic failure). The final function we define is the *page function*, which just creates the actual template for the page. - -## Template Definition - -You can define a template with the `Template::new()` method, which takes the template's path as an argument (with no leading or trailing slashes). In the above example, `about` renders only one page, which would be hosted at `/about`. - -The only mandatory builder function after that is `.template()`, which defines your template function (the closure inside `template_fn()` in the above example). There are a number of other functions available to customize how the template renders, all of which are documented [here](./strategies/intro.md). diff --git a/docs/next/src/tutorials/first_app/app.md b/docs/next/src/tutorials/first_app/app.md deleted file mode 100644 index b79ec9a9bf..0000000000 --- a/docs/next/src/tutorials/first_app/app.md +++ /dev/null @@ -1,147 +0,0 @@ -# Setting up the App Itself - -Okay, you're over the hump! Now it's time to put your template into a functional app and get stuff happening in your browser! - -## Defining a Router - -Perseus needs to know where to put your final pages, which can be defined with a router, which is defined through [Sycamore](https://github.com/sycamore-rs/sycamore) (which handles that side of things). - -In `src/lib.rs`, replace everything other than `mod templates;` with the following: - -```rust -use perseus::define_app; - -#[derive(perseus::Route)] -pub enum Route { - #[to("/")] - Index, - #[not_found] - NotFound, -} -``` - -This imports a macro we'll use in a moment to define your app, and then it sets up an `enum` for each of your pages. Notice that the `NotFound` page is special, and note that Perseus will pretty much handle it for you. - -All we've done for this simple app is defined an `Index` variant that will be served at the `/` path, the root of your app. Thus, it will be your landing page! But Perseus still needs you to connect that variant and the template we created in the last section. - -## Error Pages - -But first, let's define some custom error pages for if your users go to a page that doesn't exist. To keep everything clean, we'll do this in a new file. Create `src/error_pages.rs` and put the following inside (making sure to add `mod error_pages;` to the top of `lib.rs`): - -```rust -use perseus::ErrorPages; -use sycamore::template; - -pub fn get_error_pages() -> ErrorPages { - let mut error_pages = ErrorPages::new(Box::new(|_, _, _| { - template! { - p { "Another error occurred." } - } - })); - error_pages.add_page( - 404, - Box::new(|_, _, _| { - template! { - p { "Page not found." } - } - }), - ); - error_pages.add_page( - 400, - Box::new(|_, _, _| { - template! { - p { "Client error occurred..." } - } - }), - ); - - error_pages -} -``` - -Here's what this code does: - -1. Import the Perseus [`ErrorPages`](https://docs.rs/perseus/0.1.2/perseus/shell/struct.ErrorPages.html) `struct`, and the Sycamore templating macro for writing pseudo-HTML. -2. Define a single function that will get all your error pages (you'll call this in `lib.rs`). -3. Create a new instance of `ErrorPages` with the required fallback page. Error pages in Perseus are based on HTTP status codes (but you can create your own beyond this system if you need), and there are *a lot* of them, so the fallback page is used for all the status codes that you don't explicitly handle. -4. Add two new error pages, one for 404 (page not found) and another for 400 (generic client error). Note that the functions we provide have to be `Box`ed (so Rust can allocate the memory properly), and they'll also be provided three arguments, which you'll want to use in a production app. They are: the URL that caused the problem, the HTTP status code, and the error message that was the payload of the request. - -You can read more about error pages [here](https://arctic-hen7.github.io/perseus/error_pages.html). - -## Setting up Some HTML - -Perseus is just a web framework, and it needs some good old HTML to cling to, so you'll need to create an `index.html` file in the root of your project (*next* to `src/`). Then put the following inside: - -```html -<!DOCTYPE html> -<html lang="en"> - <head> - <meta charset="UTF-8" /> - <meta http-equiv="X-UA-Compatible" content="IE=edge" /> - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> - <title>Perseus Starter App - - - - -
- - -``` - -This is all pretty typical, and you shouldn't have to worry about it. If you want to include analytics or some other script in your app, you can do it here, this HTML will be on every page of your app (including errors). - -Note the single `