From 149a521da58d99132735aa9633268e28f3d73dc0 Mon Sep 17 00:00:00 2001 From: arctic-hen7 Date: Tue, 10 Jan 2023 15:08:04 +1100 Subject: [PATCH] docs: wrote first app tutorial A lot of missing links right now! --- docs/next/en-US/SUMMARY.md | 67 ++++++-- docs/next/en-US/features.md | 146 ------------------ docs/next/en-US/first-app/defining.md | 51 ++++++ docs/next/en-US/first-app/deploying.md | 43 ++++++ docs/next/en-US/first-app/dev-cycle.md | 18 +++ docs/next/en-US/first-app/error-handling.md | 29 ++++ docs/next/en-US/first-app/generating-pages.md | 69 +++++++++ docs/next/en-US/first-app/installation.md | 35 +++++ docs/next/en-US/fundamentals/error-views.md | 81 ++++++++++ docs/next/en-US/quickstart.md | 27 ++++ docs/next/en-US/what-is-perseus.md | 123 +++++++++++++++ 11 files changed, 526 insertions(+), 163 deletions(-) delete mode 100644 docs/next/en-US/features.md create mode 100644 docs/next/en-US/first-app/defining.md create mode 100644 docs/next/en-US/first-app/deploying.md create mode 100644 docs/next/en-US/first-app/dev-cycle.md create mode 100644 docs/next/en-US/first-app/error-handling.md create mode 100644 docs/next/en-US/first-app/generating-pages.md create mode 100644 docs/next/en-US/first-app/installation.md create mode 100644 docs/next/en-US/fundamentals/error-views.md create mode 100644 docs/next/en-US/quickstart.md diff --git a/docs/next/en-US/SUMMARY.md b/docs/next/en-US/SUMMARY.md index 02012326e8..8e638a3602 100644 --- a/docs/next/en-US/SUMMARY.md +++ b/docs/next/en-US/SUMMARY.md @@ -1,23 +1,56 @@ # Introduction - [Introduction](/docs/intro) +- [Quickstart](/docs/quickstart) - [What is Perseus?](/docs/what-is-perseus) - [Core Principles](/docs/core-principles) -# Reference - -- [Feature Discovery Terminal](/docs/features) -- [Improving Compilation Times](/docs/reference/compilation-times) -- [State Platform](/docs/reference/state-platform) -- [State Generation](/docs/reference/state-generation) -- [Live Reloading and HSR](/docs/reference/live-reloading-and-hsr) -- [Internationalization](/docs/reference/i18n) -- [Hydration](/docs/reference/hydration) -- [Static Exporting](/docs/reference/exporting) -- [Plugins](/docs/reference/plugins) -- [Deploying](/docs/reference/deploying) -- [Architecture Details](/docs/reference/architecture) -- [Router](/docs/reference/router) -- [Initial vs. Subsequent Loads](/docs/reference/initial_subsequent_loads) -- [Migrating from v0.3.x](/docs/reference/migrating) -- [Common Pitfalls and Known Bugs](/docs/reference/faq) +# Your First App + +- [Installing Perseus](/docs/first-app/installation) +- [Defining your app](/docs/first-app/defining) +- [Generating pages](/docs/first-app/generating-pages) +- [Development cycle](/docs/first-app/dev-cycle) +- [Error handling](/docs/first-app/error-handling) +- [Deploying your app](/docs/first-app/deploying) + +# Fundamentals + +- [`PerseusApp`](/docs/fundamentals/perseus-app) +- [Routing and navigation](/docs/fundamentals/routing) + - [Preloading](/docs/fundamentals/preloading) +- [Internationalization](/docs/fundamentals/i18n) +- [Error views](/docs/fundamentals/error-views) +- [Hydration](/docs/fundamentals/hydration) +- [Static content](/docs/fundamentals/static-content) +- [Styling](/docs/fundamentals/styling) +- [Working with JS](/docs/fundamentals/js-interop) +- [Servers and exporting](/docs/fundamentals/serving-exporting) +- [Debugging](/docs/fundamentals/debugging) +- [Writing tests](/docs/fundamentals/testing) +- [Plugins](/docs/fundamentals/plugins) +- [Improving Compilation Times](/docs/fundamentals/compilation-times) + +# The State Platform + +- [Understanding state](/docs/state/intro) +- [Build-time state](/docs/state/build) +- [Request-time state](/docs/state/request) +- [Revalidation](/docs/state/revalidation) +- [Incremental generation](/docs/state/incremental) +- [Using state](/docs/state/browser) +- [Global state](/docs/state/global) +- [Helper state](/docs/state/helper) +- [Suspended state](/docs/state/suspense) +- [Freezing and thawing](/docs/state/freezing-thawing) + +# Capsules + +- [Introduction](/docs/capsules/intro) +- [Capsules vs. templates](/docs/capsules/capsules-vs-templates) +- [Delayed widgets](/docs/capsules/delayed) + +# Miscellaneous + +- [Migrating from v0.3.x](/docs/migrating) +- [Common pitfalls and FAQs](/docs/faq) diff --git a/docs/next/en-US/features.md b/docs/next/en-US/features.md deleted file mode 100644 index a6cf5bd85d..0000000000 --- a/docs/next/en-US/features.md +++ /dev/null @@ -1,146 +0,0 @@ -# Feature Discovery Terminal - -The rest of these docs are written in a bit of an experimental way. Perseus is *really big*, and there are a lot of features that you can use. This is a place where you can learn about them all, and easily find code examples for a particular pattern you want to use. Perseus is written with a ton of a examples, and these are all great sources of information, but they're a bit hard to find sometimes. This page is a list of usage patterns of Perseus that you can use to find exactly what you're looking for! - -*Note: this is a bit of an experimental documentation design. Let us know what you think and add your suggestions [here](https://github.com/arctic-hen7/perseus/discussions/new)!* - -If you've got a usage pattern that isn't in here, [let us know](https://github.com/arctic-hen7/perseus/issues/new), and we'll happily add it! - -
-Hooking into the router to know when page transitions are occurring - - - -
-
-Custom index view - - - -
-
-Debugging - - - -
-
-Modifying the head - - - -
-
-Modifying HTTP headers - - - -
-
-Adding static content - - - -
-
-Using internationalization - - - -
-
-Custom translations manager - - - -
-
-Global state - - - -
-
-State freezing/thawing - - - -
-
-Freezing to browser storage (IndexedDB) - - - -
-
-Writing tests - - - -
-
-Styling - - - -
-
-Communicating with a server - - - -
-
-Custom server with API routes - - - -
-
-Plugins - - - -
-
-Deploying to Docker - - - -
-
-Deploying to a relative path - - - -
-
-Route announcer - - - -
-
-Authentication - - - -
-
-Unreactive state - - - -
-
-Imperative router control - - - -
-
-JS interop - - - -
diff --git a/docs/next/en-US/first-app/defining.md b/docs/next/en-US/first-app/defining.md new file mode 100644 index 0000000000..c498845dcc --- /dev/null +++ b/docs/next/en-US/first-app/defining.md @@ -0,0 +1,51 @@ +# Defining a Perseus App + +Once you've got all your dependencies installed, it's time to create the entrypoint to your Perseus app. In most Rust programs. you'll have a `main.rs` file that contains some `fn main() { .. }` that executes yuor code, and Perseus is no exception. However, remember that Perseus has two parts: the engine-side and the client-side, so you actually need *two* `main()` functions, one for each. Now, don't put anything in `src/main.rs` just yet, because, as we'll see later, there's actually a much more convenient way of handling all this. + +Remember, you can tell Rust to only compile some code on the engine-side by putting `#[cfg(engine)]` over it, and you can use `#[cfg(client)]` to do the same for the browser. So, our code in `main.rs` should logically look something like this: + +``` +#[cfg(engine)] +fn main() { + // Engine code here +} + +#[cfg(client)] +fn main() { + // Browser code here +} +``` + +Now, this actually isn't too far off, except that running WebAssembly is a little different than you might think. Currently, there isn't really a good concept of a 'binary' Wasm program, you'll always be coding a library that some JavaScript imports and runs. In the case of Perseus apps, we use a `main.rs` file because it makes more logical sense, since Perseus handles all that nasty JS stuff behind the scenes. From your point of view, you're just writing a normal binary. However, there is something special that the client-side function has to do: it has to return a `Result<(), JsValue>`, where `JsValue` is a special type that represents *stuff* in JS-land. You can use Perseus' [`ClientReturn`](=type.ClientReturn@perseus) type alias for this, but note that Perseus actually *can't* return an error from its invocation: all errors are gracefully handled, even panics (although they will eventually propagate up as an unhandled exception in the calling JS, which is why any panics in Perseus will appear as two messages in your browser console rather than one). + +Further, Perseus makes the engine and client code pretty convenient with two features (which are enabled by default): `dflt-engine`, and `client-helpers`. The first of these gives us the [`run_dftl_engine()`](=engine/fn.run_dflt_engine@perseus) fucntion, which takes an [`EngineOperation`](=engine/enum.EngineOperation@perseus) derived from the [`get_op()`](=engine/fn.get_op@perseus) function (which just parses environment variables passed through by the CLI), a function that returns a [`PerseusApp`](=prelude/struct.PerseusAppBase) (whcih we'll get to), and some function to run your server. + +As for the client-side, Perseus provides `run_client()`, which just takes a function that returns a `PerseusApp`. + +So what is this `PerseusApp`, you might ask? This `struct` forms the bridge between Perseus' internals, and your own code, because it's how you tell Perseus what your app looks like. In fact, because the vast majority of engine and client `main()` functions are so formulaic, Perseus provides a convenient macro, [`#[perseus::main(..)]`](=attr.main@perseus), which you can use to annotate a *single* `main()` function that returns a `PerseusApp`, and that macro will then do the rest automatically. Most of time, this is what you want, but you can always take a look at [the source code]() of that macro if you want to drill deeper into customizing your app (again, you will probably *never* need to do this, even if you're creating an insanely advanced app). + +So, our actual `src/main.rs` file would look something like this (theory over, *now* we start coding): + +``` +{{#include ../../../examples/core/basic/src/main.rs}} +``` + +First off, we declare a module called `templates`, which will correspond to the `src/templates/` folder, which we'll use to store the code for all our templates. Go ahead and create that folder now, with an empty `mod.rs` file inside. The next thing is to import the Perseus `prelude` module, which just collates everything you'll need to run a Perseus app, which helps to avoid having to manually import a million different things. Most of your Perseus files will begin with `use perseus::prelude::*;`, and then `use sycamore::prelude::*;` + +Then we get to that special `main()` function. As you can see, it returns a `PerseusApp`, which takes a generic `G`: this is a special part of Sycamore that lets is say "let this function work with any rendering backend that implements `Html`", because Sycamore can actually go way beyond the web! This generic restricts us to using `SsrNode` (for prerendering), `DomNode` (for rendering to the Document Object Model in the browser), or `HydrateNode` (the same as `DomNode`, but for when we're [hydrating]). + +You'll also notice that we've provided an argument to the `#[perseus::main(..)]` attribute macro: that's the function that will start up our server! If you want to add things like custom API routes, etc., then you can set this function manually, and then use one of the Perseus server integrations to work with the code you've written (see [this example] for more), but here we're just using the default server from the `perseus-warp` package. If you fancy [Axum], you can use `perseus-axum`, and [Actix Web] fans can use the `perseus-actix-web` package! + +## Your `PerseusApp` + +Now we get to the fun stuff: actually defining your app! The first step is to invoke `PerseusApp::new()`, which is what you'll nearly always want, unless you're in an environment with very special characteristics (e.g. a serverless function with a read-only filesystem), or if you want to manage your translations in a non-standard way for internationalization. Again, 99% of the time, `PerseusApp::new()` is fine. + +The next thing we do is declare our templates, which we'll create in a moment. Generally, in Perseus, you'll have an `src/templates/` folder that contains all your templates, and each template will export a `get_template()` function that you call from here. However, if you're from JS-land, where you might be used to something called *filesystem routing* (in which the nesting of a file implies the route it will be hosted at), Perseus has no such thing. If you want to store the about page at `index.rs` and the index page at `about.rs`, have fun! + +The next thing we do is specify some [`ErrorViews`](=error_views/struct.ErrorViews@perseus), which are responsible for doing all the error handling in our app. We'll cover this in more detail in [the error handling section](:first-app/error-handling), but just know for now that Perseus has a very strict error handling system, and, unlike a lot of other frameworks, there is no such thing as an unhandled error in Perseus: *everything* is handled (even panics, though they're a bit special). + +Of course, you usually just want to dive straight into your app, so you can leave the `.error_views()` bit out if you like, and Perseus will provide some sensible defaults while you're still in development. However, if you try to deploy your app with those defaults, you'll get errors. + +(Note that you might see `ErrorViews::unlocalized_development_defaults()` hanging around a lot in the examples, which basically tells Perseus to force-use those 'sensible defaults' in production as well. This is very convenient for examples about how to use Perseus, but it's almost certainly a bad idea in your own code, especially if you want your app available in multiple languages!) + +With all that explained, it's time to create some pages! diff --git a/docs/next/en-US/first-app/deploying.md b/docs/next/en-US/first-app/deploying.md new file mode 100644 index 0000000000..eb3cf5e833 --- /dev/null +++ b/docs/next/en-US/first-app/deploying.md @@ -0,0 +1,43 @@ +# Deploying! + +Congratulations on making it through the tutorial, it's time to deploy your app! First, though, we haven't actually run it yet, so we may as well make sure it all compiles. Remember, you an always do this quickly with `perseus check`, which should give all ticks if everything's working. If not, you've probably just mistyped a variable name or something, which happens to us all. If you're having trouble, let us know [in a GitHub discussion], or [on Discord], and we'll be happy to help! (And remember, there are no stupid questions or dumb bugs.) + +When you're ready to actually run your app, you can run `perseus serve`, which will prepare everything to be run for development. When it's ready, you'll be able to see your brand-new app at , in all its *Hello World!* glory! If you try clicking on the link to the *About* page, you should find that the page doesn't seem to change from the browser's perspective, it just instantly updates: this is Perseus' router in action. Press the back button in your browser to pop back to the landing page, and, again, it should be near-instant (Perseus has *cached* the index page, and can return to it with no network requests needed). + +
+ +I'm throttling my network connection, and Perseus seems extremely slow... + +A lot of DevTools in browsers have the option to throttle your network connection, to emulate how long it would take to load a real app. If you do this with Perseus, however, it will probably take around a full minute to even load your app. You'll see content very quickly because of Perseus' preloading system, but the `bundle.wasm` file will take forever. This is because, in development, Wasm bundles are *huge*. What will optimize and compress down to the size of a small cat photo can start as a muilti-megabyte behemoth, and this is why it's usually not a good idea to throttle Perseus apps to test their load-speed. If you wait for the Wasm bundle to load though, and *then* throttle, you'll get a better idea of real-world performance (if your browser supports this). + +
+ +If that's all working, you might want to try going to , which is a non-existent page. You should see a lovely *Page not found* message, and that's error handling in action! + +## Deploying + +But enough development shenanigans, we want to deploy this thing! To deploy a Perseus app, you'll need to make sure you've defined your [error views](:first-app/error-handling), because Perseus won't let you use the default implied error views in production. + +When you're ready, just run this command: + +``` +perseus deploy +``` + +It's literally that easy. And, because Rust is a really nice programming language, something that works in development is basically guaranteed to work in production. + +Note that this command will take a rather long time, especially on older machines, because it's applying aggressive optimizations to everything to keep bundle sizes down and page loads speedy, while also trying to keep your app as fast as possible. All these optimizations are configurable, but the defaults are tuned to be sensible, and the `deploy` command does pretty much everything automatically. Usually, there's no post-processing to be done at all. + +When it's done, this command wil produce a `pkg/` folder in the root of your project that you can send to any server under the sun. Just tell it to run the `pkg/server` binary, and your app will run beautifully! (But make sure you don't tamper with the contents of this folder, because Perseus needs everything to be in just the right place, otherwise we get one of those crash-and-burn-in-production scenarios.) In fact, try running that binary right now, and you should see your app on once more! + +Obviously, you probably want to host your app in production on a different address, like `0.0.0.0` (network-speak for "host this everywhere so everyone who comes to my server can find it"), and perhaps on port `80`. Note that Perseus doesn't handle HTTPS at all, and you'll need to do this with a reverse proxy or the like (which comes built-in to most servers these days). You can set the host and port with the `PERSEUS_HOST` and `PERSEUS_PORT` environment variables. + +## Export deployment + +However, there's actually a simpler way of deploying this app in particular. Because we aren't using any features that need a server (e.g. we're generating state at build-time, not request-time, so all the server is doing is just passing over files that it generated when we built the app), we can *export* our app. You can try this for development with `perseus export -s` (the `-s` tells Perseus to spin up a file server automatically to serve your app for you). In production, use `perseus deploy -e` to make `pkg/` contain a series of static files. If you have `python` installed on your computer, you can serve this with `python -m http.server -d pkg/`. The nice thing about exported apps is that they can be sent to places like [GitHub Pages], which will host your app for free. In fact, this whole website is exported (because it's all static documentation), and hosted on exactly that service! + +## Closing words + +With all that, you've successfully built and deployed your first ever Perseus app: well done! If you're liking Perseus so far, you can check out the rest of the documentation to learn about its features in greater detail, and [the examples] will be your friends in writing real-world Perseus code: they have examples of every single Perseus feature. If you think you've found a bug, please let us know by [opening an issue], or, if you'd like to contribute a feature, you can [open a pull request]. If you're having trouble, you can [open a GitHub discussion] or [as on our Discord], and our friendly community will be happy to help yout out! Also, make sure to take a look at [the Sycamore docs] to learn more about the library that underlies all of Perseus. + +Best of luck in your journey, and, if you [defeat Medusa](https://en.wikipedia.org/wiki/Perseus), let us know! diff --git a/docs/next/en-US/first-app/dev-cycle.md b/docs/next/en-US/first-app/dev-cycle.md new file mode 100644 index 0000000000..167660c7c7 --- /dev/null +++ b/docs/next/en-US/first-app/dev-cycle.md @@ -0,0 +1,18 @@ +# Development Cycle + +When you're developing a Perseus app, you'll generally have two "modes": coding, and fine-tuning. In the *coding* stage, you're building features of your app, which will typically involve quite a lot of working on business logic, etc. If you're familiar with Rust programming, this is the stage when you'd be using `cargo check` instead of `cargo run`. Conveniently, Perseus provides `perseus check -w` for this, which will not only `cargo check` your app's engine-side, but also the browser-side, because each one is built for a different target. This command is *much* faster than `perseus serve`, because it just checks your code, rather than actually compiling it. If you want to test your build logic as well, you can run `perseus check -gw`, which will also test this (but that will take a bit longer). + +When you're using an IDE, like VS Code, you'll usually want proper syntax highlighting, and you may find that Perseus causea few problems. This is because Perseus distinguishes between the engine and the browser by using a custom feature, so you'll need to create a `.cargo/config.toml` file in the root of your project with the following contents: + +``` +[build] +rustflags = [ "--cfg", "engine" ] +``` + +That will set up your IDE to only check your app's engine-side code, which, somewhat counterintuitively, *does* include things like `view!`, because, remember, Perseus renders everything ahead of time, so it still needs access to all that on the engine-side. Usually, this will be enough, but, when you're working on some browser-only logic, you can change that `engine` to be `browser` instead, and your IDE will automatically update. These settings won't affect commands like `perseus check` or `perseus serve`, which provide these flags automatically. + +Importantly, any time you don't need to be actually seeing the views your app is producing, you should use `perseus check` instead of one of the other commands, because it will be *much* faster (especially if you follow [these tips](:fundamentals/compilation-times)). + +Then, when you need to see what your app looks like in a browser, for example when you're styling it, or testing a particular feature, you can use `perseus serve -w`. If you're updating static content (like a `.css` file), rebuilds will be pretty much instant, but updating the Rust code of your app will be a fair bit slower. This is unfortunately a downside of working with Rust web development, but, in return, you get an *extremely* performant site that eliminates whole classes of bugs that run rampant in JS code. + +*Note: there is currently ongoing development on the Sycamore side for a system to remove the need for recompilation when you change things in the `view! { .. }` macro, which will dramatically improve performance.* diff --git a/docs/next/en-US/first-app/error-handling.md b/docs/next/en-US/first-app/error-handling.md new file mode 100644 index 0000000000..c356d70e55 --- /dev/null +++ b/docs/next/en-US/first-app/error-handling.md @@ -0,0 +1,29 @@ +# Error Handling + +Now we come to the error handling of our app, which is an important part of Perseus. Basically, you've got to explain to Perseus how it should cope with errors that might occur, not in your code, but in its own. For example, let's say the user's internet connection fails: whose responsibility is that? Well, your code isn't manually fetching the next page, so it will probably be Perseus' problem. However, there's a famous story about the Australian parliament that applies quite nicely here: the chambers of parliament there are color-coded, with the House of Representatives being green and the Senate red. However, all emergency exit signs in Australia must, by law, be green. This would be a bit of an antipattern in the red Senate, so a law was specially passed to allow red exit signs in the Senate only. (Yes, this really happened.) + +In the same manner, Perseus doesn't want to produce bright red error messages in Times New Roman if your website is bright orange in Comic Sans, so you're given full control over how to display errors. You provide `View`s to Perseus, and it renders them appropriately. For now though, we'll just do some pretty simple error handling to cover the basics: to learn more about how error handling works, and how advanced apps should handle it, see [this page](:fundamentals/error-views). + +First, put the following in `src/error_views.rs`: + +``` +{{#include ../../../examples/core/basic/src/error_views.rs}} +``` + +This code might look intimidating, but it's actually very basic. All we're doing is defining a function `get_error_views()` that's responsible for generating our [`ErrorViews`](=prelude/struct.ErrorViews@perseus), which is the type that handles errors in Perseus. We provide a closure to `ErrorViews::new()` that takes four arguments: a Sycamore scope, the error itself, an [`ErrorContext`](=error_views/enum.ErrorContext@perseus), and an [`ErrorPosition`](=error_views/enum.ErrorPosition@perseus). Those last two are more complex, and you can read [this page](:fundamentals/error-views) to learn more about them, but the first two are what we'll concentrate on here. + +The error type will always be [`ClientError`](=errors/enum.ClientError@perseus), which has a number of variants for all the different kinds of errors that can occur in Perseus. For now, all you need to know is that the main three are: `ClientError::ServerError`, which is used for errors that the server picked up on (e.g. a *404 Not Found*); `ClientError::Panic`, which is called just before the app terminates due to a panic; and `ClientError::FetchError`, which indicates either an internal server error or a failed network connection (usually the latter). There are several more variants, but we handle those here with a wildcard, labelling them all internal errors. With those variants explained, things are pretty self-explanatory, except perhaps for the fact that we return a tuple of two `Views`. The first one is for the document ``, and the second is for the body. + +The other thing to keep in mind with error views in Perseus is that they won't always take up the whole page (and this is what `ErrorPosition` is for telling you): sometimes the content can be prerendered fine, but the client can't be initialized for whatever reason, so the user can still see content, it's just not interactive. Because it would be a bit pointless to replace perfectly good, albeit uninteractive, content with an error message, Perseus renders a less intrusive popup error, which you can style with the `#__perseus_popup_error` CSS selector. In popup errors, whatever head you render for the error will be ignored, and the original head will be kept (because the page is still perfectly good, just, again, uninteractive). + +
+ +What does uninteractive actually mean? + +Great question! You can learn more about this in [the section on hydration], but it basically means that the user can see the content, because it was *prerendered* on the server-side, but they can't interact with it: e.g. if they press a button, it won't do anything. Clicking links will still work, but they'll be handled by the browser, not by Perseus. + +
+ +Finally, we handle different types of `ClientError::ServerError`s differently by their [HTTP status code], which is the language HTTP (the protocol used for communicating between clients and servers) uses to describe errors. Anything starting with a 4 is a client error, and anything starting with a 5 is a server error (1 is informational, 2 is ok, and 3 indicates a redirect; you won't need to handle those). We also separately handle 404, just because it's so common. + +With error handling done, it's about time to run this app! diff --git a/docs/next/en-US/first-app/generating-pages.md b/docs/next/en-US/first-app/generating-pages.md new file mode 100644 index 0000000000..0147f83976 --- /dev/null +++ b/docs/next/en-US/first-app/generating-pages.md @@ -0,0 +1,69 @@ +# Generating Pages + +Before we start generating pages for your app, it's important to understand how Perseus handles pages, because it's quite different to other frameworks. Let's use an example: imagine you have a personal website with a blog, and there are some posts that should each be hosted at `/post/`, where 'slug' refers to a machine-friendly version (i.e. no spaces, all lowercase, etc.). You might be used to declaring some kind of `enum Router`, or creating a `post/` folder in your code, but Perseus goes a different route (pun intended). + +We use *templates* to generate *pages*. + +Re-read that a couple of times, because it's the core idea that underlies Perseus' design. We use *templates* to generate *pages*. + +A template is like a page with some holes. A `post` template might have all the styling, the header, the footer, etc., with a gap for the title, a gap for the content, maybe a gap for some tags, etc. Think of them like stencils. You can then generate *state*, which is a term Perseus uses for data that can fill in a template. Generally speaking, if you list the gaps in your template, and make a `struct` with a field for each of those gaps, that's what your state should look like. So, if we were making a blog, we would have a struct that perhaps looks something like this: + +``` +struct PostState { + title: String, + content: String, + tags: Vec, +} +``` + +When you plug *state* into a matching *template*, you can create a *page*. **Template + state = page**, in other words. Perseus has some convenient ways to do this: you'll usually declare an `async` function that produces a `Vec` of all the paths you want to generate (e.g. for a blog it might enumerate all the `.md` files in a directory), and then another function that goes through each of those paths one-by-one and generates their state (e.g. fora blog it might read the contents of each file). Once you've generated the state, Perseus does the boring work of fitting it all together, and it prerenders your pages to HTML at build-time so they can be served to clients as quickly as possible. + +## A simple greeting + +But for this tutorial, we're just getting started, so we'll use *build-time state* to produce a greeting that we can fit into our index template. In this case, our template will just produce one page: the landing page. + +To do this, first add `pub mod index;` to your `src/templates/mod.rs` file, and then out the following in `src/templates/index.rs`: + +``` +{{#include ../../../examples/core/basic/src/templates/index.rs}} +``` + +This is much more complex than you might have been expecting! First, we import those `prelude` modules, as usual, and we also grab `serde`'s `Serialize` and `Deserialize` derive macros, because, when you think about it, Perseus needs to send whatever state you generate over the network to a user's browser, so it has to be able to turn your state into a string and back again. + +The first major part of this file is the state definition: here we're creating a `struct IndexPageState` with one field `greeting: String`, and we've annotated that with what look like some pretty scary macros! + +In fact, though, they're actually pretty simple. First, we want `Serialize` and `Deserialize`, as explained earlier, and then we want our state to be `Clone`able, mostly because Perseus sometimes needs to do this internally (but it doesn't happen regularly by any means). We also derive `ReactiveState`, which is a special Perseus macro that you can read more about [here](=derive.ReactiveState@perseus). Basically, it wraps all your fields in [`RcSignal`](=prelude/struct.RcSignal@sycamore)s, which make them *reactive*. Internally, Perseus will maintain a copy of this reactive state, so any changes made to it will be automatically reflected in the core, meaning you don't have to rely on the browser to keep things like forms filled in the way they were when the user last visited a particular page! (Of course, though, you can turn this off if you don't like it.) + +One of the things about `ReactiveState` is that it needs to create a whole new `struct` for the reactive version of your code, and it needs a name for that: this is what the `#[rx(alias = "IndexPageStateRx")]` part does: it tells Perseus to call that thing `IndexPageStateRx` (or, more accurately, to create a type alias to it with that name). + +Now we get to the view function, called `index_page`. Like our `main()` function, this takes a generic `G: Html`, for the same purpose, because Perseus will prerender it on the engine-side, and then hydrate it on the client-side. This function takes two arguments: the first is a Sycamore [`Scope`](prelude/struct.Scope@sycamore), and then second is a reference to our reactive state type. For those familiar with Sycamore, you might be wondering how the heck this works: shouldn't that reference have the same lifetime as the scope? Yes, it should! And that's what [`#[auto_scope]`](=attr.auto_scope@perseus) is for. In reality, the lifetimes on this function are much more complex, but, because you basically don't need to care about them 95% of the time, you can elide them with this macro for convenience. If you dislike macros though, you can write it out manually yourself (see [the macro documentation](=attr.auto_scope@perseus) for how to do this). + +Returning to our view function, it returns a `View`, somewhat unsurprisingly given its name, which is just Sycamore's way of representing some *stuff* that can be rendered for the user to see. To create this *stuff*, we use the `view!` macro, which takes a special syntax for creating HTML. First is a `p` element, which is HTML for *paragraph*, and, inside, we use parentheses within our curly brackets to tell Sycamore that we're going to interpolate a variable of some kind. That variable is `state.greeting`, but remember that we've got the reactive version here, so `state.greeting` is an `RcSignal`, which means we need to `.get()` it to actually get the value. Similarly, we could `.set()` it if we wanted to change it, and any `.get()`s would update automatically! + +The other element is just an `a` (HTML for *anchor*, which is HTML-specification-writer-speak for link). There's actually something quite important about this though: the link's `href`. Perseus is quite special when it comes to `href`s, because it throws a `` element into the metadata of all your pages that declares where the root is. This means Perseus can be deployed easily to `framesurge.sh`, `framesurge.sh/perseus`, or even `framesurge.sh/some/arbitrarily/nested/url`, and it will work fine. The tradeoff of this is that, unlike what you might initially expect, you can't just omit the `/` to get something relative to the current page. If you need to know what path you're currently at (which you'll find, with Perseus' template-based model, is quite rare), you can use `Reactor::::from_cx(cx).router_state.get_path()`. Again, you probably won't need this. + +Now, we get to the `head()` function, which, you might notice, is suspiciously similar to the `index_page` function, except that it takes the *unreactive* version of your state, and that it always has its render backend set to `SsrNode`. Why is that? Because this is responsible for rendering the `` of your pages, which is like the metadata. None of this is visible to users, so it isn't reactive, so Perseus just renders it ahead of time to make things easier. Usually, you'll use this `view!` for things like `title`s, CSS imports, etc. If you want metadata that applies to every page in your app, rather than just every page in one template, check out [the index view example]. + +Then we have the `get_build_state` function, which is responsible for generating the state that will fill out our template. Sure, it's a little pointless here, but this function can do *literally anything*. It can read files, it can request from APIs, it can index databases, *anything*. And, of course, it's `async`, and Perseus does everything in parallel wherever possible, so you won't slow down the rest of your build. (But, if you do, we've got [a fix for that](=utils/fn.cache_res@perseus).) This function returns an instance of the unreactive version of your state: if you're feeling a bit confused about where it's supposed to be reactive and where it's supposed to be unreactive, we understand! But, there's actually only one place where your state will ever be reactive: your view function (e.g. `index_page`). Everywhere else, it's unreactive. + +That build state function takes a type called [`StateGeneratorInfo`](=prelude/struct.StateGeneratorInfo@perseus), which contains three things: the path that we're generating state for within the template, the locale we're generating state for, and any [helper state] you might have created. Here though, we don't actually need any of it. + +Those [`#[engine_only_fn]`](=prelude/attr.engine_only_fn@perseus) macros are very simple too, and, if you don't like macros, you can easily replicate their functionality manually. All they do is wrap the function they annotate in `#[cfg(engine)]`, and then create a function of the same name, but that takes no arguments, returns nothing, and does nothing, and annotate that with `#[cfg(client)]`. Basically, these will make sure that your function still *exists* on the client-side, but that it's just a dummy. This is very useful for `.build_state_fn()`, which we'll get to, which expects a fully featured `async` function on the engine-side, and a dummy on the client-side. This strategy keeps your bundle sizes low, and your pages fast, while keeping the target-gating to a minimum. + +You might be wondering about error handling on the engine-side: surely, if you're connecting to a database, you would need to return errors sometimes? What if the server building your app loses its internet connection? Well, you actually can return errors. In fact, try changing the return types of both `head` and `get_build_state` to return `Result`, where `T` was what they returned before. If you then wrap what they're returning in `Ok(..)`, there will be no errors. Perseus is designed to accept either fallible or infallible functions, and the error type can be whatever you like, as long as it implements `std::error::Error`. For `get_build_state` though, it's actually a tiny bit more complicated than this, as you'll need to wrap your error type in something called [`BlamedError`](prelude/strcut.BlamedError@perseus), which you can learn more about in [the section on build-time state generation]. + +And finally, we come to that famous `get_template` function, which we call from `PerseusApp` to get this whole template. This is responsible for producing a [`Template`](prelude/struct.Template@perseus) that strings everything together. This too takes a `G: Html` bound, and the `Template::build("index")` call is setting up a new template whose pages will all fal under `/index`, but `index` is a special name, and it resolves to an empty string. In other words, you're creating the template for the root of your site. Then we declare our build state function, out view function, and our head function. Since we're not actually using our state in the head, we could have used `.head()` instead of `.head_with_state()`, but we showed the state for demonstration purposes. Finally, we call `.build()` to create the full `Template`, which we return. + +This is called the *functional definition* pattern in Perseus: you define your `Template`s inside functions (usually called `get_template()`), which you then call in `PerseusApp`. + +## An about page + +With all that out of the way, let's create an even simpler page to demonstrate Perseus routing, an about page. Add `pub mod about;` to `src/templates/mod.rs`, and then put this into `src/templates/about.rs`: + +``` +{{#include ../../../examples/core/basic/src/templates/about.rs}} +``` + +This is very similar to our index templateL it also generates only one page, but it doesn't have any state at all. We've used `.view()` rather than `.view_with_state()`, and, because there's no state, we don't have to worry about those finicky lifetimes: we can omit the `#[auto_scope]` entirely. The head is similar, except we're also using `.head()` to declare it on the `Template`. Note the different string in `Template::build()`, which is `about` here, the name of the template (and page) that we'll be creating. Because we're rendering one single page here, with no state generation at all, Perseus will put that page at the root of our template, `/about/`, which is the same as `/about`. So, when we link from the index page to `about`, we'll end up here! (It seems simple, but it's worth understanding that whole template generates page thing.) + +Now it's time for error handling. diff --git a/docs/next/en-US/first-app/installation.md b/docs/next/en-US/first-app/installation.md new file mode 100644 index 0000000000..04141cc428 --- /dev/null +++ b/docs/next/en-US/first-app/installation.md @@ -0,0 +1,35 @@ +# Installation + +Before you get to coding your first Perseus app, you'll need to install the Perseus command-line interface (CLI) first, which you'll use to manage your app. The reason for this is that Perseus is a *framework*, not a library: you don't import Perseus into your code and use it, Perseus imports your code into itself. In fact, in the old days, you used to write a library that another crate would literally import! + +To install the Perseus CLI, first make sure you have Rust installed (preferably with [`rustup`]), and then run this command: + +``` +cargo install perseus-cli --version 0.4.0-beta.14 +``` + +Once that's done, you can go ahead and create your first app! Although this would usually be done with the `perseus new` command, which spins up a scaffold for you, in this tutorial we'll do things manually so we can go through each line of code step by step. First, create a new Rust project: + +``` +cargo new my-app +cd my-app +``` + +This will create a new directory called `my-app/` that's equipped for a binary project (i.e. something you can run, rather than a library, which other code uses). First of all, create `.cargo/config.toml` in the root of your project, with the following contents: + +``` +[build] +rustflags = [ "--cfg", "engine" ] +``` + +This will make sure your IDE builds your app correctly. Without this, you'll have red squiggly lines all over the place, because Perseus needs to be explicitly told if it's working on the engine-side (e.g. a server) or the browser-side, which are very different environments! Also, setting things up explicitly like this lets you change `engine` to `client` in that file when you want your IDE to help you out with working on browser-only code. + +Next, put the following in your app's `Cargo.toml`: + +```toml +{{#include ../../../examples/core/basic/Cargo.toml.example}} +``` + +The main things to pay attention to here are the dependencies, which are laid out differently from most Rust apps. Perseus is built in two parts: the *engine-side*, which is responsible for prerendering your pages, serving content, exporting your app, etc.; and the *client-side*, which runs inside a user's browser to make Perseus interactive, handling routing, interactivity, etc. The engine-side of your app will build to whatever target you compile it for, like `x86_64-unknown-linux-gnu`, which you would have on an OS like Ubuntu. This means Rust will translate your code into machine code that computers with that kind of processor and OS can understand (if you were running on an M1 Mac, the target would be quite different). The browser has its own sepaarate target, which ensures that you don't have to compile your code for every possible device that a user might view it on --- the browser takes care of all that, and runs Wasm, which is its own special language that Rust can translate itself into. + +That all means that there are some features that don't belong in the browser (like building your app), and others that don't belong in the engine (like managing routing), so Perseus *target-gates* these, using Rust's `#[cfg(..)]` macro to make sure that certain things are only compiled at the right time. This reduces compilation times, and also slims down the bundles for both the engine and the browser (because they contain no unnecessary code). Sometimes, you'll want to do this in your own code as well, like if you have some function that should only run on the browser-side. Remember how we set up that `rustflags` key in `.cargo/config.toml`? Well, that's so you can use it just like this! If you want code to only be compiled for the browser, you put `#[cfg(client)]` on top of it, and you can use `#[cfg(engine)]` to do the same for the engine. You'll usually see this in Rust code, but your `Cargo.toml` can use it too for declaring dependencies that will only be used on one particular target. Here, we're making sure to bring in `perseus` everywhere, but `perseus-warp` (our server integration) should only be used on the engine-side. When you bring in a new dependency, think about whether it has to be available on the browser-side, because it often doesn't. For example, you could bring in the `regex` crate to automatically highlight any technical terms in a documentation site, but you can actually do that solely on the engine-side if you handle all that in the state generation process (which we'll get to). This avoids bringing the `regex` crate into the browser, which keeps your `.wasm` bundle nice and slim. A smaller Wasm bundle means it can be transferred over the network more quickly, which means faster page loads. diff --git a/docs/next/en-US/fundamentals/error-views.md b/docs/next/en-US/fundamentals/error-views.md new file mode 100644 index 0000000000..5c57c11454 --- /dev/null +++ b/docs/next/en-US/fundamentals/error-views.md @@ -0,0 +1,81 @@ +# Error Views + +If there's one thing that's guaranteed to happen in all software, it would be errors, and, when they do happen, your app needs to be prepared for them. Internally, there are quite a few failure scenarios that Perseus might encounter: for example, if the user loses their internet connection while Perseus is trying to fetch a page, it won't be able to complete that. This would be a failure in the *framework*, not your app. In these cases, as well as in some cases of failures caused by your code (e.g. a mistyped link address, a deliberate authentication failure, etc.), Perseus will render what we call *error views*. These are basically special Sycamore `View`s that your app holds onto until an error occurs, at which time it will automatically display them appropriately. + +One thing to make clear about error views is that *you don't invoke these, Perseus does*. What you are writing are a series of instructions to Perseus on what to do in each type of failure: you are **not** writing error handling for your own logic. If, for example, a user enters a password that's too short and submits a form, you would not display a Perseus error view, you would handle errors manually there. The reason for this is that there are simply too many cases of error handling in real-world apps for Perseus to be able to reasonably and flexibly handle them all. + +By convention, error views are usually placed in an `src/error_views.rs` file in your project, though this could also be a folder if your error views are particularly elaborate. + +## `ClientError` + +All errors in Perseus, even ones that occur on the server, fall under the [`ClientError`](=errors/enum.ClientError@perseus) `enum`, which you'll typically `match` against to determine what to render. In terms of semantic versioning, this `enum` is considered stable, and new variants will not be added except in a breaking change. Let's go through those variants one by one. + +### `ServerError` + +A `ServerError` is probably the error type you'll come into contact with most frequently, because it denotes errors that have been propagated down by the server explicitly. This usually means something failed on its end, which could be anything from a *404 Not Found* error to a *500 Internal Server Error*. This variant has two properties: one for its [HTTP status code], and another for an actual error message, which will be in English (for internationalized apps). + +Because of the frequency of this error, it is very common to handle status variants independently. For example, the [error views] example in the Perseus repository handles a 404 error independently (because people mistype URLs far more frequently than servers fail), and then handles anything starting with a 4 (client error) separately to anything starting with a 5 (server error). + +One important difference between `ServerError` and all the other error types is that it is the only one that occurs *before* the client. This means it is the only error type that will be reflected in what the user sees, before hydration. For example, let's say the user mistyped a URL and ends up at a 404 error page. Because this error 'occurred' on the server (in that that was where Perseus first noticed that something had gone wrong), the HTML that goes to the client will show that error. If, however, something like a hydration error had occurred (which would *not* fall under `ServerError`), the server might have handled everything fine, and the user may be able to still see all the content they want, it just won't be interactive. In these cases, it's better for user experience not to completely delete all the content that has been served statically, just because it isn't interactive. In these cases, Perseus will display a popup error. + +### `FetchError` + +Fetch errors are fairly self-explanatory, and occur when there has been a failure in communicating with the server. There are a few sub-variants of this, governed by the [`FetchError`](=errors/enum.FetchError@perseus) type. + +You may notice in the API documentation that there's a `FetchError::NotOk` variant, which you might expect to fire when the user hits a 404 in what's called a *subsequent load* (i.e. any load of another page in the app where the user has come from somewhere else in the app, rather than an outside website, like a search engine). However, Perseus actually performs route matching on the client-side, before it makes any requests, which is why broken links in Perseus will not lead to any network requests. For subsequent loads, anything other than a *200 OK* response indicates a server error. + +### `PluginError` + +Plugin errors are exactly what it says on the tin: any error that a plugin can produce. Because all plugin functions are implicitly fallible, any plugin could, at any time, return any error. Since there are a number of opportunities for plugins to run just before a Perseus app starts, they can produce errors, which will often be critical (if they occur before your app starts, Perseus will have to perform a full abort). + +### `ThawError` + +Thaw errors only apply to you if you're using the [state freezing] system, and they usually arise from corrupted state. Very importantly, thaw errors are *sleeper errors*, meaning that you might thaw some frozen state that has an error in it, and then that error might only become apparent much later. This is because Perseus' thawing mechanism is gradual: state is only deserialized when a page needs it. So, if only one page has invalid state, the thaw error will occur much later. + +However, in the vast majority of cases of corruption, the whole frozen app will be garbled, and Perseus will detect this error and return it immediately, avoiding sleepers. + +### `PlatformError` + +Platform errors are currently not really used in Perseus, but, in future, they will be used to make the render backend of Perseus generic so that it could be used beyond the browser. If you're running in a browser though, and you get this error, you can be fairly confident of a critical failure, and you shouldn't expect your error message to even display. An example of a cause for this kind of error would be the `window` object not being defined. To see examples of this error, try running a Perseus app in NodeJS, and see what happens... + +### `PreloadError` + +Preload errors occur when you try to preload something that's invalid, and they will nearly always be the result of failures in your own code (usually mistyping a template name or the like). + +### `InvariantError` + +Invariant errors are the most insidious kind of error in Perseus: they arise from internal invariants not being upheld within Perseus. For example, all Perseus apps are expected to define, within the document metadata, a global state. For apps that don't have a global state, they should explicitly declare the fact that they don't have one. However, if a hypothetical overzealous minifcation system decided to strip out the empty global state declaration, deciding it was useless, Perseus would be unable to function. The reason these sorts of things don't resolve to panics is because, unlike most Rust programs, where invariants are simply logical properties that we can't prove to the compiler, in Perseus there's a divide between the client and the server: the network. That means that we might be 100% certain that all invariants are upheld on the server-side, but, by the time we get to the client-side, we might be looking at a completely different file type. + +In short, these errors are rare, but catastrophic, and usually cannot be recovered from. + +However, there are some cases in which these errors *might* be caused by your code. The most obvious is if you try to fetch the wrong global state type. Let's say you registered `MyGlobalState`, but then you try to get `MyPageState` by accident. Because Perseus uses downcasting to manage state, this would lead to a runtime invariant error. However, in this case, it would be perfectly reasonable for a confused user to go to a different page instead, so we don't panic and fail the whole system. + +### `Panic` + +Finally, we come to panics, which Perseus takes a unique approach to. Rather than having you set a panic handler, Perseus does it for you, because there are several additional things it needs to do. First, it will print messages to the console telling the user what has happened, just in case everything goes pear-shaped after that (remember, we might be panicking because the `window` doesn't exist, in which case we sure aren't going to be able to display error messages). Then, it will display a popup error that you set the contents of through this handler. Generally, a panic handler should be quite apologetic, because there's literally no way to proceed from here, and the app is practically guaranteed to fail completely. You can also set custom panic handling logic, which will be executed once the message has been displayed, and this is usually a good time to do things like report the error to a crash analytics server. (Note that this is distinct from the `crash` plugin opportunity, which would occur in the case of a critical startup invariant error.) + +If you're coming from non-web Rust, you might be thinking you can just set a `catch_unwind` and restart your app nicely, but Wasm uses `panic = "abort"` by default, meaning such handlers are meaningless. Also, while you could re-instantiate Perseus manually, it's not recommended at all, since panics usually indicate that something has gone critically wrong, and that's likely to repeat itself. In general, you should ask the user to restart the app manually by reloading the page, which should fix most spontaneous errors. + +## Error positioning + +One very unique part of Perseus' error handling system that's very important is its concept of *error positioning*, i.e. how an error appears. There are are three options for this, which correspond to the variants of the [`ErrorPostion`](=error_views/enum.ErrorPosition@perseus) `enum`. The first is that the error will take up the whole page, the second is that it will take up the whole of a widget (such errors will of course only be triggered by widgets), and the third is that the error will be confined to a popup. + +It's important to check what kind of error position you have for two reasons: the first is so that you don't display a full-page view with `100vh` styling all over the place if it ends up being displayed in a tiny popup, and the second is because, in a popup error view, you won't have access to a router. This means that any links placed into a popup error will be handled with the browser's default behavior. While this is fine, it may not be what you expect, so be aware of this. + +Perseus decides how to position an error based on some simple rules: if it's an *initial load* where the error occurred (i.e. the user has come to your app from the outside internet, e.g. from a search engine), then check if it's a `ClientError::ServerError`. If so, `Page`, if not, `Popup`. If it's a *subsequent load* (i.e. where the user has gone from one page to another inside your app), then this is delegated to you through the `subsequent_load_determinant_fn` function, which you can set on your [`ErrorViews`](=prelude/struct.ErrorViews@perseus). If you don't set this though, the same rules are applied automatically. + +The reason behind these rules is based on user experience. Any error that is not a `ServerError` will have occurred on the client-side, right? So that means the server was fine, which in turn means that the server-side rendering was okay. That means there's prerendered content that's perfectly valid sitting in front of the user, and they can read it. They might not be able to interact with it, but, in nearly all cases, it's better that they can't interact with it than that they can't see it at all. There is nothing more infuriating than going to a news website, seeing the article prerendered in front of you, and then having that lovely content be replaced with an error message. + +However, if you have some cases where you know for certain that the error should take up the whole page, because the prerendered content is bad in some way, then you can always style the popup error to be absolutely positioned and take up the whole page. + +Popup errors will be rendered in a `
` with the HTML `id` `__perseus_popup_error`, which you can use to style it arbitrarily. + +## Error context + +Because Perseus is, for the millionth time, a very complex system, errors can occur in different places. For example, an error that occurs in a plugin before the app is started will mean that you can't have access to something like a translator, because it literally doesn't exist yet. Such errors are rare, but they can definitely happen, and this is why Perseus provides an [`ErrorContext`](=error_views/enum.ErrorContext@perseus). This has four variants, each corresponding to a different level of interactivity that Perseus has, which you can read about in detail [here](=error_views/enum.ErrorContext@perseus). + +One important thing to understand about error contexts is how they interact with internationalized apps. Perseus will always make a best effort to give your error views a translator, to the point that, if the user goes to, say, `/bad-page`, Perseus will first resolve it to a localized version (e.g. `/en-US/bad-page`), and only *then* handle the routing error. However, in exported apps, the situation is very different, because error views can be managed beautifully on the client-side, but 404 errors in particular are handled by exporting the error page to a static file, usually called `404.html`, which your serving infrastructure is responsible for providing. Unfortunately, there are very few providers who support localized error views, and, in such cases, error views will always be non-internationalized. However, Perseus will do all it can to, on the client-side, provide a translator. In some cases, however, this may be simply impossible. If you're using internationalization, it's generally recommended to avoid exporting for this reason. + +## Writing `ErrorViews` + +When you actually write your error views, it is surprisingly simple. Just call `ErrorViews::new()` and provide a closure that takes four arguments: a Sycamore scope, the `ClientError` that occurred, an `ErrorContext`, and an `ErrorPosition`. Then, match them as you like and return a tuple of two `View`s: the first for the document metadata, and the second for the body of the error. Note that popup errors will have their head views ignored, as will widget errors. (In such cases, you can use `View::empty()` to just produce an empty view.) You can see a full example of using error views [here]. diff --git a/docs/next/en-US/quickstart.md b/docs/next/en-US/quickstart.md new file mode 100644 index 0000000000..f47e0a5970 --- /dev/null +++ b/docs/next/en-US/quickstart.md @@ -0,0 +1,27 @@ +# Quickstart + +To get started with Perseus, you should first make sure you have the Rust language installed, and it's recommended that you do this through [`rustup`], which will let you manage the different parts of Rust very easily. + +Once you have Rust installed, you can run the following command to install Perseus: + +```sh +cargo install perseus-cli --version 0.4.0-beta.14 +``` + +(While v0.4.x is still in beta, that `--version` flag is needed to make sure you get the latest beta version.) + +Now, pop over to some directory where you keep your projects, and run `perseus new my-app`. That will create a new directory called `my-app/`, which you can easily `cd` into, and, once you're there, you can run this command to start your app: + +``` +perseus serve -w +``` + +The `serve` command tells Perseus to spin up a proper server for your app, rather than just exporting it to a series of static files (doing this lets you use some of the more advanced features of Perseus, if you want), and `-w` tells it to watch the files in the current directory for changes, so that your app will automatically be rebuilt if you change any code. After this command is done, go and take a look at , and you should see a welcome screen! + +This command has a few stages. First, there's *Generating your app...*, which will compile your app's *engine-side* (often called the *server-side*, but Perseus has exporting, tinkering, and a million other things that happen there, so we just call the not-browser the *engine* for simplicity) and build all your app's pages. Then, there's *Building your app to Wasm...*, which compiles your app's *browser-side* into WebAssembly, allowing it to be run in the browser. Finally, there's *Building server...*, which just compiles and prepares the server that Perseus will use to run your code. If it's the first time you're running Perseus, there'll also be a stage for *installing external tools...*, in which Perseus downloads some external dependencies, like `wasm-opt`, which helps to supercharge your Wasm in production. Here, Perseus will also use `rustup` to install the `wasm32-unknown-unknown` target, which is Rust's way of saying 'the browser'. If you're not using `rustup`, you'll need to install this target manually. + +*In fact, all these stages run in parallel, which is why Perseus builds take up a fair bit of memory. If you're running on an older device, you might want to add the `--sequential` flag to the above command, which will run these steps one-by-one. This can also be useful if you're running multiple Perseus builds at once.* + +Now, if you change some code in, say, `my-app/src/templates/index.rs` (where, by convention, you store the code for your app's landing page), you should see the build process automatically restart, and, once it's done, your browser will automatically reload with your changes! + +Unfortunately, one downside of compiled languages like Rust is that building them takes a while, so don't expect Perseus builds to be as snappy as JS builds. However, this usually isn't actually too much of a problem, and the builds will only get faster in future, as the Rust compiler improves, as Sycamore improves, and as Perseus improves! For tips on reducing compilation time, take a look at [this page](:fundamentals/compilation-times). diff --git a/docs/next/en-US/what-is-perseus.md b/docs/next/en-US/what-is-perseus.md index 5446ca01d0..b22627685a 100644 --- a/docs/next/en-US/what-is-perseus.md +++ b/docs/next/en-US/what-is-perseus.md @@ -1,4 +1,127 @@ # What is Perseus? + +Perseus is a **web development framework** for the **Rust** programming language that focuses on the **state** of your app. Since there are three main ways you might approach Perseus, we'll break down each one individually here. + +## You're familiar with Rust + +We can obviously agree that Rust is much better than JavaScript: it's way faster, strongly-typed, has a great compiler, and a fantastic package management system. In the browser, it runs *amazingly*. This is because of [WebAssembly] (abbreviated *Wasm*), which is basically an assembly language for programs like Chrome, Firefox, etc. With it, you can compile your Rust code to run in the browser, and even access browser APIs, allowing you to display content to the user. In the past, Rust has been used with Wasm to perform things like heavy cryptography, but Perseus lets you exile JS completely, and run your whole site with Rust only. + +Now, you might have come across other web development libraries and frameworks for Rust before, but there's a big difference between those two terms, so let's sort that out first. A *library* is a piece of code that you use to help you build your site. A *framework* is a mammoth of code that uses your code to build your site. Think of it like the difference between `futures::executor::block_on` and `#[tokio::main]`: one is being used by you to handle a bit of `async`, and the other is using your code to handle *all* the `async`. In the same way, a library is a great choice for when you want to build a small site, or when you want to replace just part of a site with Rust. For these kinds of things, we absolutely recommend [Sycamore], on which Perseus is based. + +However, sometimes you'll need to break out the big guns. Sometimes, you'll need to render content in advance so that your users see it straight away, rather than a blank page while your Wasm boots up. Sometimes, you'll want to have a*stateful* app. This doesn't just mean you've got buttons and forms, etc., but that you're building your app in a special kind of pattern, which Perseus is built around. Let's say you have a simple static blog: you might have a `/post` URL, under which all your posts can be found. Fundamentally, all these posts have the same structure, just with different titles, dates, tags, and contents, so you might choose to create some kind of *template* for them, and then maybe build a Markdown parser or the like to push all that into your app to create *pages*. Essentially, **template + state = page**. In Perseus, this is all handled for you, and you can't actually create pages, you can just create templates, like `/post`, and ways to render their state. + +For example, for a blog, you might create a new post template with `Template::build("post")`, and then create a function that takes in some state and plugs it into a Sycamore `view! { .. }` to render some content. You might take in a `struct` containining contents, titles, tags, etc. If you then specify a function that can list the pages that this template should create (e.g. by getting all the Markdown files in a certain directory), and then another one that takes each path and generates state for it, Perseus will string it all together and give a lightning-fast app. + +Beyond this, Perseus has all sorts of extra features, like inbuilt error handling systems that allow you to gracefully display error messages if state generation fails, or if your app panics, or something else like that. All you do is match an `enum ClientError`, and Perseus shows your errors to the client. Beyond that, if you want to build an app in multiple languages, Perseus will let you do it straight away: just replace the text in your code with identifiers inside the `t!()` macro, and define a map of translation IDs to text for each language you want to support. Variable interpolation is supported out of the box, and you can unleash the full power of [Fluent] for handling pluralization rules, genders, etc. + +Going even further, Perseus' state generation platform is built for even the most advanced use-cases: let's say you have not a blog, but an ecommerce site selling a thousand products. Well, a thousand would actually build very quickly, so perhaps a million. Still probably looking at less than a second, but we'll go with it. Maybe you don't want to build all that at build time. Simple! Just add `.incremental_generation()` to your template definition and then...you're done. If a user goes to a produce page that doesn't exist yet, it will passed to your state generation functions, and, if it's a page that exists, they can produce the page. For any future users, that page will be cached and returned immediately. It's like building your whole app over time, on-demand. And, if you have an index of all your products, you could automatically *revalidate* that every, say, 24 hours, to make sure users have a fairly up to date listing. Or you could logic-based revalidation that checks each time whether or not there are actually any new products, before rebuilding. You could even combine the two: only check ever few hours whether or not there are new products, and, if there are, rebuild that page. + +To be clear, and this is important if you aren't familiar with web development, Perseus is not a library, it's a framework. It's a giant engine into which you plug your code that will connect everything together and optimize it, producing a super-fast site that outperforms every JS framework under the sun. It might well seem like you don't need a lot of these features, and, if you don't, you can just run `perseus export` to get a series of static HTML files that you can serve to users however you like, with a simple Wasm bundle making sure whatever interactivity you have works as smoothly as possible (and it will still be unreasonably fast). If you're used to systems programming, the whole idea of a framework might seem a bit absurd, but it's very often required in web development, simply because the best experiences come from complex features, like rendering your site to HTML in advance, or caching transations, or delayable capsules that can be infinitely nested to create lazy-loaded pages, etc. Some of these are easy to implement, others are not. The point of Perseus is to let you get on with what you want to write: your app. + +If Perseus doesn't sound like your cup of tea, there are several other Rust frameworks you might like to check out: [Sycamore] is the library on which Perseus is based, if you want to keep the same sort of style; [Yew] is a very popular library/framework; and [Seed] is another. There's also [Sauron], [MoonZoon], and [Leptos], just to name a few. If you'd like to see some more in-depth comparisons between these projects, check out [the comparisons page]. + +## You're familiar with JavaScript, and you've know what NextJS, ReactJS, etc. mean + +Alright, you're pretty familiar with what web development is, and why we tend to need frameworks to make things simple and to remove the need to write hundreds of lines of boilerplate code for features we use in every app. But you've probably got plenty of questions about Perseus. + +### Why Rust? + +Put simply, JS is [a bit of a mess]. It's dynamically-typed, and executed at runtime, meaning you can't really catch bugs while you're coding. Sure, an IDE helps with this by showing you squiggly red lines, but it still won't stop you from forgetting about passing a certain argument to a function. TypeScript helps with this by introducing stricter typing rules, but it's really an addition on top of already existing JavaScript, and, let's be honest, how many times have you had to search up solutions for getting your `tsconfig` to work? + +[Rust], on the other hand, is generally thought of as a systems programming language, meaning it's much lower-level and closer to the hardware, letting you do things like memory management more manually. It's certainly got a much steeper learning curve, but, let's walk through a quick example. Imagine you have a variable `data` that contains a very large amount of information. Obviously, copying this is going to slow your program down, so we want to avoid that if possible. In JS, you could do something like this: + +```javascript +const data = "..."; +let valid = isDataValid(data); +let useful = isDataUseful(data); +``` + +You might not realize it, but this code could copy the whole of `data` under certain conditions, because, when you think about it, both `isDataValid()` and `isDataUseful()` need it. In fact, depending on your code's structure, JS might even implicitly copy this whole variable *twice*! This is an oversimplification, and there's a lot more going on here, but, in Rust, you have total control over this: + +```rust +let data = get_data(); +let valid = is_data_valid(&data); +let useful = is_data_useful(&data); +``` + +Here, we're passing *references* to `data` to those functions, which are like telling them where `data` can be found in memory, rather than giving them it's actual value. Again, we're oversimplifying, but the point is that Rust allows you much lower-level control over your data, and it's a compiled language, meaning you have to build your code into an executable, rather than just running it. In this stage, the compiler goes over your code with a fine-toothed comb, finding whole classes of bugs and making them impossible at runtime. And, to make things even better, *undefined behavior*, a special type of bug in C/C++/etc. (which often leads to `Segmentation fault` messages, which you might have seen before), is literally impossible in Rust, because the whole language is built on a clear boundary between *safe* code, and *unsafe* code. The latter might cause UB, and should explicitly clarify what has to be upheld for it to all work properly. Then, if code can be certain that it's upholding the necessary invariants, it can call itself safe. Basically, where the compiler can't prove that your code won't crash and burn, you explicitly have to, and there's no getting around it. + +To illustrate just how powerful this model of programming is, let's take a bit of a meta-example. When we were building Perseus v0.4.0, we had to rewrite the entire Perseus core, over 12,000 lines of code. After innumerable cycles of changing some code and seeing errors pop up in the terminal, when we got all the errors fixed and the code actually compiled, the first time we ran `perseus build`, *it worked*. No logic bugs, no syntax errors, it just worked. *That* is the kind of power you get from working with Rust. + +Usefully, the Rust compiler supports compiling for different *targets*, which are basically formats of machine code. Your Rust code can go into code that will run on Linux, macOS, Windows, etc. Or, it could run in the browser, through a revolutionary new technology called [WebAssembly](), abbreviated as *Wasm*. Technically, any language, like C or C++, could compile into this format, but Rust has the added guarantees of *safety*. + +Oh, and did we mention that Rust is [insanely fast]? + +When you combine that with Wasm, a Rust site is usually >30% faster than the equivalent site built in JavaScript, in terms of runtime performance. And, when we say >30%, we mean >90% on anything modern that's not running Safari (Apple being a bastion of implementing web standards, as usual). + +With all this, Rust is the perfect language to implement a next-generation web framework in, and that's exactly what Perseus is. + +### Okay, but what *is* it? + +As NextJS is to ReactJS, Perseus is to [Sycamore]. Sycamore is a low-level reactive library for building websites in Rust that uses *no virtual DOM*, making it [faster than Svelte] in some cases (with improvements on the horizon to get *even faster*), and Perseus builds on these foundations to create a framework designed to make your life easier by minimizing boilerplate. + +Assuming you're familiar with a few terms from the usual JS jargon about frameworks, let's run through Perseus' features. It supports static site generation (building your app to HTML before it's even running), server-side rendering (building pages at request-time based on user details, like cookies), client-side rendering (fetching data in the browser to render components), using SSG and SSR *on the same page* (which, to our knowledge, no other framework in the world supports), revalidation (allowing you to rebuild a page that was built originally at build-time), incremental generation (rendering a page at request-time the first time it's requested, and then caching it for future use so it can be returned instantly next time), and *capsules* (we'll come to those). + +This is all based around *state*, because that's the focus of Perseus. Unashamedly, Perseus focuses on supporting highly complex apps with many moving parts and interconnected components. Of course, if you want to build a static blog, that's a piece of cake. + +Fundamentally, Perseus boils down to a state framework, and, really, the whole idea of actually displaying content to a user is secondary. As far as Perseus is concerned, your state is generated in almost any way conceivable, it gets to the user, it's made *reactive* of its own accord (meaning, if you're coming from React, that any state you generate on the server comes to you already in a `useState()` hook), and then you can work with it however you like to display it to users. If your site isn't interactive (like a static blog), you can use unreactive state instead, no problem. + +Based on this, Perseus' rendering model comes down to *templates*, which are like stencils for creating pages. For example, you might have a blog post template at the `post` URL, which would have the basic structure that all blog posts share. When you plug in the data of an individual blog post called `foo`, you get out that template, filled in with that state, to produce `post/foo`, a page. + +In essence, **template + state = page**, that's the fundamental equation of Perseus. + +But, we went further than this. If you're familiar with [Astro], then you'll have heard of the *islands architecture*, where you split your app into components that can individually render, hydrate, etc. Now, things are a bit different over here in Wasm-world, because things are so fast here that we don't really have to care about delaying hydration, or things like that, because it all happens in literally milliseconds. Instead, our main concern is minimizing the amount of *stuff* (i.e. HTML and Wasm) that needs to be sent to the user's browser, because that's the real bottleneck for us. So, if you split out a complex ecommerce page into, say, a *widget* (Perseus' term for islands) for each product on your home page, then your home page can load as a simple skeleton waiting for some content. It's kind of like a template waiting for state, but the pieces that need to be filled in are actual mini-pages themselves. In fact, unlike any other framework ever created, Perseus has the unique concept that **capsule + state = widget**. That's right, as a template creates pages, a capsule creates widgets, meaning you can have a `product` capsule that incrementally generates product widgets as they're requested. You can use every single rendering strategy that works for pages on widgets, and you can control exactly when they're rendered too. If you want, say, the first row of products on your website's landing page to be instantly rendered, and then the rest to be lazy-loaded in parallel, you can do that by chaning `.widget()` to `.delayed_widget()`. It's that simple. + +Naturally, Perseus also comes with the usual stew of extra framework features, like internationalization out of the box that just works (translator APIs etc. are all available for you, and you can pick a really powerful one using [Fluent] or a really tiny one using JSON, with more to come), and one-command deployment to a `pkg/` folder that you put literally anywhere that runs executables. And if you want a static site, you just run `perseus export`, and you're set. + +As for the Lighthouse scores, Perseus achieves 100 on desktop without even trying, and consistently about 90 on mobile. The reason for the dropoff in mobile performance is mostly because of the way mobile browsers still have to go in optimizing Wasm, but this wil improve with time, and any user on a modern smartphone will see a snappy and responsive site practically instantly. That whole idea of render-then-hydrate is baked into Perseus: your users see content straight away, and it becomes reactive a moment later. + +Unfortunately, the idea of *resumability*, as pioneered by [Qwik], isn't really possible with Wasm yet, because you actually can't split a Wasm bundle into smaller pieces, you just send the whole thing to the user. While that does mean that Perseus apps are *insanely* fast when going between pages, it can mean slightly slower load times when a user first comes to your site. That said, it's still 100 on Lighthouse, so it can't be *that* bad. Even so, we're sure you've had that bad experience of loading a site and trying to press buttons that don't work, and knowing (as a developer) that it's because the site hasn't hydrated yet. Now, with Perseus, your users really won't be waiting too long for those buttons to be working, but you can enable a feature flag that holds user interactions in stasis until your app is hydrated, before automatically re-sending them, leading to a much better overall user experience. And, if you don't like it, as with most things in Perseus, you can just turn it off. + +The other really cool thing about Perseus is *error handling*. A lot of JS frameworks have this concept of *error boundaries*, but still more leave all the error management to you. If JS blow up (as it frequently does), you're left to clean up on your own. In Rust, errors have to be propagated explicitly with a type called `Result`, which can either be `Ok` or `Err`. Unless a function `panic!`s, it can't rip the floor out from under you and cause everything to fail. That means Perseus can handle nearly all errors gracefully: for example, if a single widget can't render its contents properly, it will automatically render an error instead. If Perseus can'tt start up your app, but it knows the user can already see some content, it will show a popup error message instead of replacing the perfectly good static content. And, if your whole app panics, crashing and burning to the ground, Perseus gives you the opportunity to run arbitrary code (like crash analytics) as well as display a nice error message to the user. And, because Rust is strongly-typed, if you forget to explicitly handle (or not handle) a particular type of error, your app just won't compile, and you'll get a lovely error message from the compiler. Basically, it would take an alignment of cosmic rays flipping dozens of bits in your computer simultaneously, or a total browser crash, to make Perseus fail without producing an error message of some kind. We don't crash and burn a lot, but when we do, we do it in style. + +## You're new to web development and Rust, welcome! + +Usually, people build websites with three languages: HTML (HyperText Markup Language), CSS (Cascading Style Sheets), and JS (JavaScript). If you imagine building a bed in real life with these languages, HTML would be responsible for declaring that what you're building is a ``, while you would CSS to set how rounded the corners are, what color the whole thing is, what shape, what size, etc. Finally, you would use JS to make the bed, perhaps, start playing music at a certain time in the morning to wake you up. + +However, these languages are all *interpreted*, meaning the browser tries to figure out what your code does as it gets it. So, if you were to, say, make a typo in some code that you put on your website, you wouldn't know until the code just doesn't run for your users, and some part of your site breaks. Although there are ways of working around these types of errors, usually with extensions to JS like [TypeScript], they effectively bring the power of *compiled* and *typed* languages (like [Rust]) to the web, except they're just extensions, which means they don't solve a lot the underlying problems. + +For example, let's say we have a variable `x` in JavaScript, which we set to be `5`. If we then change this to say the string `foo`, that's perfectly fine according to JS, but think about it: how many units of memory does it take to represent `5`? And how many to represent `foo`? The fact that these are different, and that this sort of thing is permissible in the language, means that JS has to do a whole lot of overhead work making everything work out. Sure, it can be nice to be able to set any variable to anything, and that sort of freedom can certainly be useful for rapid prototyping (one of the great appeals of conceptually similar languages, like Python), but it doesn't make for very fast code. + +If, instead, you were to build your site in another programming language that's *typed* (meaning, once you set `x = 5`, it can't be anything other than a number, because the language knows exactly how much memory to allocate) and *compiled* (meaning there's a stage before code execution where your code is parsed, checked for errors, and automatically optimized, being translated from human-readable code to machine-readable instructions), it could be, at a minimum, over 30% faster than one built with JS. Also, you get much more performant continuity between platforms. For example, you can happily build your site in Rust, and your server. If you were to do that with JavaScript, then both would be *quite slow*. And, when we're talking about corporate applications, even a second slower loads can do [meaningful harm] to customer conversion. + +Perseus is a framework for building complex websites and webapps in Rust, which consistently outperforms every other JS framework under the sun in benchmarks. It's based on [Sycamore], which provides underlying *reactivity* (which lets you do cool things like say "show the value of variable `x` here and update the view whenever that variable updates"), and is [faster than Svelte], one of the fastest JS frameworks, in several benchmarks. On its own, Perseus will take your code, compile it, and then add an extra stage of *building* your app, in which it looks at your code, figures out the earliest pages can be prepared for users, and prepares them. So, if you have an *about us* page that's the same for every user, and that doesn't depend on users, say, being logged in, then Perseus will automatically render that page when you build your app, meaning your users will see it more quickly when they want it. + +If you're completely new to web development and Rust, explaining the rest of Perseus' features will probably not be the best thing, so we'd recommend taking a look at the [MDN] documentation for information about web dev generally, and you should read [the Rust book] (it's not too long) to get a feel for Rust. Once you've got the basics down, you should be ready to dive straight into Perseus! And, if you need some help, don't hesitate to ask on [our Discord]! Best of luck! + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Perseus is a framework for building extremely fast web apps in Rust, with a focus on the state of your app, enabling dynamic server-side state generation, request-time state alteration, time or logic-based state revalidation, and even freezing your entire app's state and thawing it later!