diff --git a/docs/0.4.x/en-US/SUMMARY.md b/docs/0.4.x/en-US/SUMMARY.md index 1a7f5c526f..878bba369c 100644 --- a/docs/0.4.x/en-US/SUMMARY.md +++ b/docs/0.4.x/en-US/SUMMARY.md @@ -1,87 +1,22 @@ # Introduction - [Introduction](/docs/intro) - - [What is Perseus?](/docs/what-is-perseus) - - [Core Principles](/docs/core-principles) - - [Hello World!](/docs/tutorials/hello-world) +- [What is Perseus?](/docs/what-is-perseus) +- [Getting Started](/docs/getting-started/intro) + - [Installation](/docs/getting-started/installation) + - [Your First App](/docs/getting-started/first-app) +- [Core Principles](/docs/core-principles) - [Your Second App](/docs/tutorials/second-app) ---- - # Reference -- [`PerseusApp`](/docs/reference/perseus-app) - - [`define_app!`](/docs/reference/define-app) -- [Writing Views](/docs/reference/views) - - [The Index View](/docs/reference/index-view) -- [Debugging](/docs/reference/debugging) -- [Live Reloading](/docs/reference/live-reloading) -- [Templates and Routing](/docs/reference/templates/intro) - - [Modifying the ``](/docs/reference/templates/metadata-modification) - - [Modifying HTTP Headers](/docs/reference/templates/setting-headers) - - [Listening to the Router](/docs/reference/templates/router-state) -- [Error Pages](/docs/reference/error-pages) -- [Static Content](/docs/reference/static-content) -- [Internationalization](/docs/reference/i18n/intro) - - [Defining Translations](/docs/reference/i18n/defining) - - [Using Translations](/docs/reference/i18n/using) - - [Translations Managers](/docs/reference/i18n/translations-managers) - - [Other Translation Engines](/docs/reference/i18n/other-engines) -- [Rendering Strategies](/docs/reference/strategies/intro) - - [Build State](/docs/reference/strategies/build-state) - - [Build Paths](/docs/reference/strategies/build-paths) - - [Request State](/docs/reference/strategies/request-state) - - [Revalidation](/docs/reference/strategies/revalidation) - - [Incremental Generation](/docs/reference/strategies/incremental) - - [State Amalgamation](/docs/reference/strategies/amalgamation) +- [Feature Discovery Terminal](/docs/features) +- [Live Reloading and HSR](/docs/reference/live-reloading-and-hsr) +- [Internationalization](/docs/reference/i18n) - [Hydration](/docs/reference/hydration) -- [Reactive State](/docs/reference/state/rx) - - [Global State](/docs/reference/state/global) - - [State Freezing](/docs/reference/state/freezing) - - [Freezing to IndexedDB](/docs/reference/state/idb-freezing) - - [Hot State Reloading (HSR)](/docs/reference/state/hsr) -- [CLI](/docs/reference/cli) - - [Ejecting](/docs/reference/ejecting) - - [Snooping](/docs/reference/snooping) -- [Testing](/docs/reference/testing/intro) - - [Checkpoints](/docs/reference/testing/checkpoints) - - [Fantoccini Basics](/docs/reference/testing/fantoccini-basics) - - [Manual Testing](/docs/reference/testing/manual) -- [Styling](/docs/reference/styling) -- [Communicating with a Server](/docs/reference/server-communication) -- [Stores](/docs/reference/stores) - [Static Exporting](/docs/reference/exporting) -- [Plugins](/docs/reference/plugins/intro) - - [Functional Actions](/docs/reference/plugins/functional) - - [Control Actions](/docs/reference/plugins/control) - - [Using Plugins](/docs/reference/plugins/using) - - [The `tinker` Action](/docs/reference/plugins/tinker) - - [Writing Plugins](/docs/reference/plugins/writing) - - [Security Considerations](/docs/reference/plugins/security) - - [Publishing Plugins](/docs/reference/plugins/publishing) -- [Engines](/docs/reference/engines) -- [Deploying](/docs/reference/deploying/intro) - - [Server Deployment](/docs/reference/deploying/serverful) - - [Serverless Deployment](/docs/reference/deploying/serverless) - - [Optimizing Code Size](/docs/reference/deploying/size) - - [Relative Paths](/docs/reference/deploying/relative-paths) - - [Docker Deployment](/docs/reference/deploying/docker) -- [Migrating from v0.3.x](/docs/reference/updating) -- [Common Pitfalls and Known Bugs](/docs/reference/pitfalls-and-bugs) - ---- - -# Advanced - -- [Under the Hood](/docs/advanced/intro) - - [Architecture](/docs/advanced/arch) - - [Initial Loads](/docs/advanced/initial-loads) - - [Subsequent Loads](/docs/advanced/subsequent-loads) - - [Routing](/docs/advanced/routing) -- [Route Announcer](/docs/advanced/route-announcer) - ---- - -# Further Tutorials - -- [Authentication](docs/tutorials/auth) +- [Plugins](/docs/reference/plugins) +- [Deploying](/docs/reference/deploying) +- [Architecture Details](/docs/reference/architecture) +- [Migrating from v0.3.x](/docs/reference/migrating) +- [Common Pitfalls and Known Bugs](/docs/reference/faq) diff --git a/docs/0.4.x/en-US/advanced/arch.md b/docs/0.4.x/en-US/advanced/arch.md deleted file mode 100644 index 689ccaf219..0000000000 --- a/docs/0.4.x/en-US/advanced/arch.md +++ /dev/null @@ -1,49 +0,0 @@ -# Architecture - -Perseus has several main components: - -- `perseus` -- the core module that defines everything necessary to build a Perseus app if you try hard enough -- `perseus-actix-web` -- an integration that makes it easy to run Perseus on the [Actix Web](https://actix.rs) framework -- `perseus-warp` -- an integration that makes it easy to run Perseus on the [Warp](https://github.com/seanmonstar/warp) framework -- `perseus-cli` -- the command-line interface used to run Perseus apps conveniently -- `perseus-engine` -- an internal crate created by the CLI responsible for building an app -- `perseus-engine-server` -- an internal crate created by the CLI responsible for serving an app and performing runtime logic - -## Core - -At the core of Perseus is the [`perseus`](https://docs.rs/perseus) module, which is used for nearly everything in Perseus. In theory, you could build a fully-functional app based on this crate alone, but you'd be reinventing the wheel at least three times. This crate exposes types for the i18n systems, configuration management, routing, and asset fetching, most of which aren't intended to be used directly by the user. - -What is intended to be used directly is the `Template` `struct`, which is integral to Perseus. This stores closures for every rendering strategy, which are executed as provided and necessary at build and runtime. Note that these are all stored in `Rc`s, and `Template`s are cloned. - -The other commonly used system from this crate is the `Translator` system, explained in detail in [the i18n section](:i18n/intro). `Translator`s are passed around in `Rc`s, and `TranslationsManager` on the server caches all translations by default in memory on the server. - -## Server Integrations - -The core of Perseus provides very few systems to set up a functional Perseus server though, which requires a significant amount of additional work. To this end, server integration crates are used to make this process easy. If you've ejected, you'll be working with these directly, which should be relatively simple, as they just accept configuration options and then should simply work. - -## CLI - -As documented in [this section](:cli), the CLI simply runs commands to execute the last two components of the Perseus system, acting as a convenience. It also contains these two components inside its binary (using [`include_dir!`](https://github.com/Michael-F-Bryan/include_dir)). - -### CLI Builder - -This system can be further broken down into two parts. - -#### Static Generator - -This is a single binary that just imports the user's templates and some other information (like locales) and then calls `build_app`. This will result in generating a number of files to `.perseus/dist/`, which will be served by the server to any clients, which will then hydrate those static pages into fully-fledged Sycamore templates. - -#### App Shell - -This is encapsulated in `.perseus/src/lib.rs`, and it performs a number of integral functions: - -- Ensures that any `panic!`s or the like ar printed properly in the browser console -- Creates and manages the internal router -- Renders your actual app -- Handles locale detection -- Invokes the core app shell to manage initial/subsequent loads and translations -- Handles error page displaying - -### CLI Server - -This is just an invocation of the the appropriate server integration's systems with the data provided by the user through `PerseusApp`. diff --git a/docs/0.4.x/en-US/advanced/initial-loads.md b/docs/0.4.x/en-US/advanced/initial-loads.md deleted file mode 100644 index 108ceeacc2..0000000000 --- a/docs/0.4.x/en-US/advanced/initial-loads.md +++ /dev/null @@ -1,23 +0,0 @@ -# Initial Loads - -Perseus handles _initial loads_ very differently from _subsequent loads_. The former refers to what's done when a user visits a page on a Perseus app from an external source (e.g. visiting from a search engine, redirected from another site), and this requires a full HTMl page to be sent that can be interpreted by the browser. By contrast, subsequent loads are loads between pages within the same Perseus app, which can be performed by the app shell (described in the next section). - -The process of initial loads is slightly complex, and occurs like so (this example is for a page called `/posts/test`, rendered with incremental generation): - -1. Browser requests `/posts/test` from the server. -2. Server matches requested URL to wildcard (`*`) and handles it with the server-side inferred router, determining which `Template` to use. -3. Server calls internal core methods to render the page (using incremental generation strategy, but it doesn't need to know that), producing an HTML snippet and a set of JSON properties. -4. Server calls `template.render_head_str()` and injects the result into the document's `` (avoiding `` flashes and improving SEO) after a delimiter comment that separates it from the metadata on every page (which is hardcoded into `index.html`). -5. Server interpolates JSON state into `index.html` as a global variable in a `<script>`. -6. Server interpolates HTML snippet directly into the user's `index.html` file. -7. Server sends final HTML package to client, including Wasm (injected at build-time). -8. Browser renders HTML package, user sees content immediately. -9. Browser invokes Wasm, hands control to the app shell. -10. App shell checks if initial state declaration global variable is present, finds that it is and unsets it (so that it doesn't interfere with subsequent loads). -11. App shell moves server-rendered content out of `__perseus_content_initial` and into `__perseus_content_rx`, which Sycamore's router had control over (allowing it to catch links and use the subsequent loads system). -12. App shell gets a translator if the app uses i18n. -13. App shell hydrates content at `__perseus_content_rx` with Sycamore and returns, the page is now interactive and has a translator context. - -Note: if this app had used i18n, the server would've returned the app shell with no content, and the app shell, when invoked, would've immediately redirected the user to their preferred locale (or the closest equivalent). - -The two files integral to this process are [`initial_load.rs`](https://github.com/arctic-hen7/perseus/blob/main/packages/perseus-actix-web/src/initial_load.rs) and [`shell.rs`](https://github.com/arctic-hen7/perseus/blob/main/packages/perseus/src/shell.rs). diff --git a/docs/0.4.x/en-US/advanced/intro.md b/docs/0.4.x/en-US/advanced/intro.md deleted file mode 100644 index ed92658732..0000000000 --- a/docs/0.4.x/en-US/advanced/intro.md +++ /dev/null @@ -1,3 +0,0 @@ -# Under the Hood - -This section of the documentation is devoted to explaining the inner workings of Perseus, which will be particularly useful if you choose to eject from the CLI's harness or if you want to contribute to Perseus! diff --git a/docs/0.4.x/en-US/advanced/route-announcer.md b/docs/0.4.x/en-US/advanced/route-announcer.md deleted file mode 100644 index dd63ef2bce..0000000000 --- a/docs/0.4.x/en-US/advanced/route-announcer.md +++ /dev/null @@ -1,13 +0,0 @@ -# Route Announcer - -Perseus uses a routing system separate to the browser's, as is typical of SPA and hybrid frameworks. However, while this means we download fewer resources for each page transition, this does mean that screen readers often won't know when a page change occurs. Of course, this is catastrophic for accessibility for vision-impaired users, so Perseus follows the example set by other frameworks and uses a *route announcer*. This is essentially just a glorified `<p>` element with the ID `__perseus_route_announcer` (so that you can make modifications to it imperatively if necessary) that's updated to tell the user the title of the current page. - -When a user enters a session in your app (i.e. when they open your app from a link that's not inside your app), the browser can announce the page to any screen readers as usual, so the route announcer will start empty. However, on every subsequent page load in your app (all of which will use Perseus' custom router), the route announcer will be updated to declare the title of the page as it can figure it out. It does so in this order: - -1. The `<title>` element of the page. -2. The first `<h1>` element on the page. -3. The page's URL (e.g. `/en-US/about`). - -This prioritization is the same as used by NextJS, and Perseus' route announcer is heavily based on NextJS'. - -Notably, the route announcer is invisible to the naked eye, and it will only 'appear' through a screen reader. This is achieved through some special styling optimized for displaying this kind of text, again inspired by NextJS' router announcer (which has been proven to work very well in long-term production). diff --git a/docs/0.4.x/en-US/advanced/routing.md b/docs/0.4.x/en-US/advanced/routing.md deleted file mode 100644 index baef062410..0000000000 --- a/docs/0.4.x/en-US/advanced/routing.md +++ /dev/null @@ -1,38 +0,0 @@ -# Routing - -Perseus' routing system is quite unique in that it's almost entirely _inferred_, meaning that you don't ever have to define a router or explain to the system which paths go where. Instead, they're inferred from templates in a system that's explained in detail in the [templates section](:templates/intro). - -## Template Selection Algorithm - -Perseus has a very specific algorithm that it uses to determine which template to use for a given route, which is greatly dependent on `.perseus/dist/render_conf.json`. This is executed on the client-side for _subsequent loads_ and on the server-side for _initial loads_. - -Here's an example render configuration (for the [showcase example](https://github.com/arctic-hen7/perseus/blob/main/examples/showcase)), which maps path to template root path. - -```json -{ - "about": "about", - "index": "index", - "post/new": "post/new", - "ip": "ip", - "post/*": "post", - "timeisr/test": "timeisr", - "timeisr/*": "timeisr", - "time": "time", - "amalgamation": "amalgamation", - "post/blah/test/blah": "post", - "post/test": "post" -} -``` - -Here are the algorithm's steps (see [`router.rs`](https://github.com/arctic-hen7/perseus/blob/main/packages/perseus/src/router.rs)): - -1. If the path is empty, set it to `index` (which is used for the landing page). -2. Try to directly get the template name by trying the path as a key. This would work for anything not using incremental generation (in the above example, anything other than `post/*`). -3. Split the path into sections by `/` and iterate through them, performing the following on each section (iterating forwards from the beginning of the path, becoming more and more specific): - 1. Make a path out of all segments up to the current point, adding `/*` at the end (indicative of incremental generation in the render configuration). - 2. Try that as a key, return if it works. - 3. Even if we have something, continue iterating until we have nothing. This way, we get the most specific path possible (and we can have incremental generation in incremental generation). - -## Relationship with Sycamore's Router - -Sycamore has its own [routing system](https://sycamore-rs.netlify.app/docs/v0.6/advanced/routing), which Perseus depends on extensively under the hood. This is evident in `.perseus/src/lib.rs`, which invokes the router. However, rather than using the traditional Sycamore approach of having an `enum` with variants for each possible route (which was the approach in Perseus v0.1.x), Perseus provides the router with a `struct` that performs routing logic and returns either `RouteVerdict::Found`, `RouteVerdict::LocaleDetection`, or `RouteVerdict::NotFound`. The render configuration is accessed through a global variable implanted in the user's HTML shell when the server initializes. diff --git a/docs/0.4.x/en-US/advanced/subsequent-loads.md b/docs/0.4.x/en-US/advanced/subsequent-loads.md deleted file mode 100644 index ca5cb214e4..0000000000 --- a/docs/0.4.x/en-US/advanced/subsequent-loads.md +++ /dev/null @@ -1,20 +0,0 @@ -# Subsequent Loads - -if the user follows a link inside a Perseus app to another page within that same app, the Sycamore router will catch it and prevent the browser from requesting the new file from the server. The following will then occur (for an `/about` page rendered simply): - -1. Sycamore router calls Perseus inferred router logic. -2. Perseus inferred router determines from new URL that template `about` should be used, returns to Sycamore router. -3. Sycamore router passes that to closure in `perseus-cli-builder` shell, which executes core app shell. -4. App shell checks if an initial load declaration global variable is present and finds none, hence it will proceed with the subsequent load system. -5. App shell fetches page data from `/.perseus/page/<locale>/about?template_name=about` (if the app isn't using i18n, `<locale>` will verbatim be `xx-XX`). -6. Server checks to ensure that locale is supported. -7. Server renders page using internal systems (in this case that will just return the static HTML file from `.perseus/dist/static/`). -8. Server renders document `<head>`. -9. Server returns JSON of HTML snippet (not complete file), stringified properties, and head. -10. App shell deserializes page data into state and HTML snippet. -11. App shell interpolates HTML snippet directly into `__perseus_content_rx` (which Sycamore router controls), user can now see new page. -12. App shell interpolates new document `<head>`. -13. App shell initializes translator if the app is using i18n. -14. App shell hydrates content at `__perseus_content_rx`, page is now interactive. - -The two files integral to this process are [`page_data.rs`](https://github.com/arctic-hen7/perseus/blob/main/packages/perseus-actix-web/src/page_data.rs) and [`shell.rs`](https://github.com/arctic-hen7/perseus/blob/main/packages/perseus/src/shell.rs). diff --git a/docs/0.4.x/en-US/core-principles.md b/docs/0.4.x/en-US/core-principles.md index f3bcf89541..fe07cd5e5f 100644 --- a/docs/0.4.x/en-US/core-principles.md +++ b/docs/0.4.x/en-US/core-principles.md @@ -1,24 +1,66 @@ # Core Principles -Perseus has two fundamental building blocks that make up its entire approach to app building, and if you understand them, you'll be able build extremely complex interfaces in Perseus that come out as fantastic apps. +Now that you've built your first app, it's important to understand some of the building blocks behind Perseus that make the code you write turn into an actual web app! -1. An app is split into templates, which are split into pages. -2. A page is generated from a template and some state. +The main key idea that underpins Perseus is about *templates*, and the primary architectural matter to understand is how Perseus apps actually work in terms of their components. -The first of these is based on the crucial idea of routing on the web, that everything is split into pages. This will be pretty familiar to most people: websites have different pages that display different content. Perseus extends on this with the idea of *templates*, which can produce pages. +## Templates -The second fundamental tells us how templates work: they take in state, and they put out pages. **Template + state = page.** +Templates are the key to understanding Perseus code. Once you do, you should be able to confidently write clear code for apps that do exactly what you want them to. Nicely, this core concept also correlates with the file of code that defines the majority of the inner workings of Perseus (which is 600 lines long...). -Let's take an example to understand how this works in practice. Let's say you're building a music player app that has a vast library of songs (we'll ignore playlists, artists, etc. to keep things simple). The first step in designing your app is thinking about its structure. It comes fairly quickly that you'll need an index page to show the top songs, an about page to tell people about the platform, and one page for each song. Now, the index and about pages have different structures, but every song page has the same structure, just with different content. This is where templates come in. You would have one template for the index page and another for the about page, and then you'd have a third template for the songs pages. Unlike the other two, this template takes in *state*, which it can use to create many different pages with the same structure. +There are two things you need to know about templates: -How we generate and manage that stage is where Perseus really shines. Are all those songs listed in a database available at build-time? Use the [*build state*](:reference/strategies/build-state) strategy. Are there too many to build all at once? Use [*incremental generation*](:reference/strategies/incremental-generation) to build only the most commonly used songs first, and then build the rest on-demand when they're first accessed, caching to make them fast for subsequent users. +1. An app is split into templates, and each template is split into pages. +2. A page is generated from a template and state. **Template + state = page** -Once that state is generated, Perseus will go right ahead and proactively prerender your pages, meaning your users see content the second they load your site. +Anyone who's ever used a website before will be at least passingly familiar with the idea of *pages* --- they're things that display different content, each at a different route. For example, you might have a landing page at `https://example.com` and an about page at `https://example.com/about`. -These ideas are built into Perseus at the core, and generating state for templates to generate pages is the fundamental idea behind Perseus. You'll find similar concepts in popular JavaScript frameworks like NextJS and GatsbyJS. It's Perseus' speed, ergonomics, and what it does from there that sets it apart. Once you've generated some state and you've got all the pages ready for your app, there's still a log of work to be done on this music player app. A given song might be paused or playing, the user might have manually turned off dark mode, autoplaying related songs might be on or off. This is all state, but it's not state that we can handle when we build your app. Traditionally, frameworks would leave you on your own here to work this all out, but Perseus tries to be a little more helpful by automatically making your state reactive. Let's say the state for a single song page includes the properties `name`, `artist`, `album`, `year`, and `paused` (there'd probably be a lot more in reality though!). The first four can be set at build time and forgotten about, but `paused` could be changed at any time. No problem, you can change it once the page is loaded. Just call `.set()` on it and Perseus will not only update it locally, but it will update it in a store global to your app so that, if a user goes back to that song later, it will be preserved (or not, your choice). And what about things like `dark_mode`, state that's relevant to the whole app? Well, Perseus provides inbuilt support for reactive global state that can be interacted with from any page. +In Perseus, pages are never coded directly, they're generated by the engine from templates. Templates can be thought of as mathematical functions if you like: (crudely) a template `T` can be defined such that `T(x) = P`, where `x` is some state, and `P` is a page. + +Let's take an example to understand how this works in practice. Let's say you're building a music player app that has a vast library of songs (we'll ignore playlists, artists, etc. to keep things simple). The first step in designing your app is thinking about its structure. It comes fairly quickly that you'll need an index page to show the top songs, an about page to tell people about the platform, and one page for each song. Now, the index and about pages have different structures, but every song page has the same structure, just with different content. This is where templates come in. You would have one template for the index page and another for the about page, and then you'd have a third template for the songs pages. + +That third template can take in some state, and produce a different page for every single song, but all with the same structure. You can see this kind of concept in action on this very website. Every page in the docs has the same heading up the top, footer down the bottom, and sidebar on the left (or in a menu if you're on mobile), but they all have different content. There's just one template involved for all this, which generates hundreds of pages (here, that same template generates pages for every version of Perseus ever). + +So what about those first two? Well, they're very simple templates that don't take any state at all --- they can only produce one page. To take our crude mathematical definition, `T() = P` for these, and, since `T` takes no arguments, it can only produce the same page every time. + +This illustrates nicely that the determining factor that differentiates pages from each other is state, and that's what Perseus is built around. + +Let's return to our music player app. Are all those songs listed in a database available at build-time? Use the [*build state*](:reference/strategies/build-state) strategy. Are there too many to build all at once? Use [*incremental generation*](:reference/strategies/incremental-generation) to build only the most commonly used songs first, and then build the rest on-demand when they're first accessed, caching to make them fast for subsequent users. + +Once that state is generated, Perseus will go right ahead and proactively prerender your pages to HTML, meaning your users see content the second they load your site. (This is called server-side rendering, except the actual rendering has happened ahead of time, whenever you built your app.) + +These ideas are built into Perseus at the core, and generating state for templates to generate pages is the fundamental idea behind Perseus. You'll find similar concepts in popular JavaScript frameworks like NextJS and GatsbyJS. It's Perseus' speed, ergonomics, and some things we'll explain in a moment that set it apart. + +Once you've generated some state and you've got all the pages ready, there's still a log of work to be done on this music player app. A given song might be paused or playing, the user might have manually turned off dark mode, autoplaying related songs might be on or off. This is all state, but it's not state that we can handle when we build your app. Traditionally, frameworks would leave you on your own here to work this all out, but Perseus tries to be a little more helpful by *automatically making your state reactive*. Let's say the state for a single song page includes the properties `name`, `artist`, `album`, `year`, and `paused` (there'd probably be a lot more in reality though!). The first four can be set at build time and forgotten about, but `paused` could be changed at any time. No problem, you can change it once the page is loaded. Just call `.set()` on it and Perseus will not only update it locally, but it will update it in a store global to your app so that, if a user goes back to that song later, it will be preserved (or not, your choice). And what about things like `dark_mode`, state that's relevant to the whole app? Well, Perseus provides inbuilt support for reactive global state that can be interacted with from any page. Now, if you're familiar with user interface (UI) development, this might all sound familiar to you, it's very similar to the *MVC*, or *model, view, controller* pattern. If you've never heard of this, it's just a way of developing apps in which you hold all the states that your app can possibly be in in a *model* and use that to build a *view*. Then you handle user interaction with a *controller*, which modifies the state, and the *view* updates accordingly. Perseus doesn't force this structure on you, and in fact you can opt out entirely from all this reactive state business if it's not your cup of tea with no problems, because Perseus doesn't use MVC as a *pattern* that you develop in, it uses it as an *architecture* that your code works in. You can use development patterns from 1960 or 2060 if you want, Perseus doesn't mind, it'll just work away in the background and make sure your app's state *just works*. -Perseus also adds a little twist to the usual ideas of app state. If your entire app is represented in its state, then wouldn't it be cool if you could *freeze* that state, save it somewhere, and then boot it back up later to bring your app to exactly where it was before. This is inbuilt into Perseus, and it's still insanely fast. But, if you don't want it, you can turn it off, no problem. +Perseus also adds a little twist to the usual ideas of app state. If your entire app is represented in its state, then wouldn't it be cool if you could *freeze* that state, save it somewhere, and then boot it back up later to bring your app to exactly where it was before? This is inbuilt into Perseus, and it's still insanely fast. But, if you don't want it, you can turn it off, no problem. + +THis does let you do some really cool stuff though, like bringing a user back to exactly where they left off when they log back into your app, down to the last character they typed into an input, with only a few lines of code. (You store a string, Perseus handles the freezing and thawing.) + +## Architecture + +When you write a Perseus app, you'll usually just define a `main()` function annotated with `#[perseus::main(...)]`, but this does some important things in the background. Specifically, it actually creates three functions: one that returns your `PerseusApp`, and then two new `main()` functions: one for the engine, and another for the browser. That distinction is one you should get used to, because it pervades Perseus. Unfortunately, most other frameworks try to shove this away behind some abstractions, which leads to confusing dynamics about where a function should actually be run. Perseus tries to make this as clear as possible. + +Before we can go any further into this though, we'll need to define the *engine*, because it's a Perseus-specific term. Usually, people would refer to the server-side, but this term was avoided for Perseus to make clear that the server is just a single part of the engine. The engine is made up of these components: + +- Builder --- builds your app, generating some stuff in `dist/` +- Exporter --- goes a few steps further than the builder, structuring your app for serving as a flat file structure, with no explicit server +- Server --- serves the built artifacts in `dist/`, executing certain server-side logic as necessary +- Error page exporter --- exports a single error page to a static file (e.g. you'll need this if you want your custom error pages to work on GitHub Pages or similar hosts) +- Tinker --- runs a certain type of plugin (more on this later) + +So, when we talk about *engine-side*, we mean this! The reason these are all lumped together is because they're all actually one binary, which is told what exact action to perform by a special environment variable automatically set by the CLI. So, when you run `perseus export` and `perseus serve`, those are actually *basically* both doing the exact same thing, just with a different environment variable setting! + +As for the browser-side, this is just the code that runs on the `wasm32-unknown-unknown` target (yes, those `unknown`s are supposed to be there!), which is Rust's way of talking about the browser. + +So, when we use the `#[perseus::main(...)]` macro, that's creating a function that returns your `PerseusApp`, and another called `main()` for the server (which is annotated with `#[tokio::main]` to make it asynchronous), and another called `main()` for the client (annotated with `#[wasm_bindgen::prelude::wasm_bindgen]` to make it discoverable by the browser). + +What's nice about this architecture is that you can do it yourself without the macro! In fact, if you want to do more advanced things, like setting up custom API routes, this is the best way to go. Then, you would use the `#[perseus::engine_main]` and `#[perseus::browser_main]` annotations to make your life easier. (Or, you could avoid them and do their work yourself, which is very straightforward.) + +The key thing here is that you can easily use this more advanced structure to gain greater control over your app, without sacrificing any performance. From here, you can also gain greater control over any part of your app's build process, which makes Perseus practically infinitely customizable to do exactly what you want! + +The upshot of all this is that Perseus is actually creating two separate entrypoints, one for the engine-side, and another for the browser-side. Crucially, both use the same `PerseusApp`, which is a universal way of defining your app's components, like templates. (You don't need to know this, but it actually does slightly different things on the browser and engine-sides itself to optimize your app.) -If you like the sound of all that, keep reading, and you'll learn how to build your first Perseus app! +Why do you need to know all this? Because it makes it much easier to understand how to expand your app's capabilities, and it demystifies those macros a bit. Also, it shows that you can actually avoid them entirely if you want to! (Sycamore also has a [builder API](https://sycamore-rs.netlify.app/docs/basics/view#builder-syntax) that you can use to avoid their `view! { .. }` macro too, if you really want.) diff --git a/docs/0.4.x/en-US/features.md b/docs/0.4.x/en-US/features.md new file mode 100644 index 0000000000..a74f6e19aa --- /dev/null +++ b/docs/0.4.x/en-US/features.md @@ -0,0 +1,8 @@ +# 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 fidn 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! + diff --git a/docs/0.4.x/en-US/getting-started/first-app.md b/docs/0.4.x/en-US/getting-started/first-app.md new file mode 100644 index 0000000000..f59d562e2f --- /dev/null +++ b/docs/0.4.x/en-US/getting-started/first-app.md @@ -0,0 +1,83 @@ +# Your First App + +With a basic directory scaffold all set up, it's time to get into the nitty-gritty of building a Perseus app. Somewhat humorously, the hardest part to wrap your head around as a beginner is probably going to be the `Cargo.toml` we're about to set up. For now, make it look like the following. + +```toml +{{#include ../../../examples/comprehensive/tiny/Cargo.toml.example}} +``` + +<details> +<summary>Excuse me??</summary> + +The first section is still pretty straightforward, just defining the name and version of your app's package. The line after that, `edition = "2021"`, tells Rust to use a specific version of itself. There's also a 2018 version and a 2015 version, though Perseus won't work with either of those, as it needs some features only introduced in 2021. That version also includes some comfort features that will make your life easier at times. + +Now we'll address the interesting dependencies setup. Essentially, we've got three dependency sections. The reason for this is because Perseus runs partly on a server, and partially in a browser. The former is called the *engine*, which is responsible for building and serving your app. The latter is just called the browser, which is where Perseus makes your app work for your users. + +These two environments couldn't really be more different, and, while Perseus tries to minimize the complexities of managing both from your perspective, there are *many* Rust packages that won't run in the browser yet. By having separate dependency sections for each environment, we can decide explicitly which packages we want to be available where. + +The first section is the usual one, pulling in dependencies that we want everywhere. Both `perseus` and `sycamore` are needed in the browser and on the server-side, so we put them here. Most of the packages you bring in yourself will go here too. As a general rule of thumb, put stuff here unless you're getting errors or trying to optimize your app (which we have a whole section on). + +The second is `[target.'cfg(not(target_arch = "wasm32"))'.dependencies]`, which looks scary, but is actually pretty simple when you think about it. It defines the `dependencies` section only on the `target` (operating system) that matches the condition `cfg(not(target_arch = "wasm32"))` --- the target that's not `wasm32`, which is Rust's way of talking about the browser. This section contains engine-only dependencies. In other words, the code that runs in the browser will have no clude these even exist. We put two things in here: `tokio` and `perseus-warp`. The first is an asynchronous runtime that Perseus uses in the background (this means we can do stuff like compile three parts of your app at the same time, which speeds up builds). The second is one of those integration crates we were talking about earlier, with the `dflt-server` feature enabled, which makes it expose an extra function that just makes us a server that we don't have to think about. Unless you're writing custom API routes, this is all you need. + +The third section is exactly the same as the previous, just without that `not(...)`, so this one defines dependencies that we use in the browser only. We've put `wasm-bindgen` here, which we could compile on the server, but it would be a little pointless, since Perseus only uses it behind the scenes in making your app work in the browser. (This is needed for a macro that a Perseus macro defines, which is why you have to import it yourself.) + +Now we've got some really weird stuff, that you may not have seen before even if you've been working with Rust for a while. The next two sections, `[lib]` and `[bin]`, declare that `src/lib.rs` is both a binary *and* a library. What does that mean? Well, a library is something that exposes some functions, and that's what Wasm expects. But the engine needs a binary to not just expose code, but to run it as well. Conveniently, you can define these in the same place, and you can then annoy Cargo a little by telling it to treat `lib.rs` as both a binary and a library. This will print a little warning at the top of every `cargo` command you run in this directory, but there's no actual problem here. + +*Note: this solution isn't ideal, and the situation of this will be improved by the time v0.4.0 goes stable. The aim is to get Wasm to work with a binary rather than a library, which requires changing several things in the CLI.* + +The only other thing of note here is `crate-type` under the `[lib]` section, which makes your library compatible with Wasm. Unfortunately, Perseus has basically no control over the fact that that's required, and we'd like it to not be necessary too! + +So, to summarize, there's a bunch of weird stuff in `Cargo.toml` for every Perseus app, but you usually don't have to care about it too much! + +</details> + +Next, we can get on with the app's actual code! Head over to `src/lib.rs` and put the following inside: + +```rust +{{#include ../../../examples/comprehensive/tiny/src/lib.rs}} +``` + +This is your entire first app! Note that most Perseus app's won't actually look like this, we've condensed everything into 17 lines of code for simplicity. + +<details> +<summary>So that means something, does it?</summary> + +We've started off with some simple imports that we need, which we'll talk about as we get to them. The really important thing here is the `main()` function, which is annotated with the `#[perseus::main()]` *proc macro* (these are nifty things in Rust that let you define something, like a function, and then let the macro modify it). This macro isn't necessary, but it's very good for small apps, because there's actually fair bit of stuff happening behind the scenes here. + +We also give that macro an argument, `perseus_integration::dflt_server`. You should change this to whatever integration you're using (we set up `perseus_warp` earlier). Every integration has a feature called `dflt-server` (which we enabled earlier in `Cargo.toml`) that exposes a function called `dflt_server` (notice how the packages use hyphens and the code uses underscores --- this is a Rust convention). + +As you might have inferred, the argument we provide to the `#[perseus::main()]` macro is the function it will use to create a server for our app! You can provide something like `dflt_server` here if you don't want to think about that much more, or you can define an expansive API and use that here instead! (Note that there's actually a much better way to do this, which is addressed much later on.) + +This function also takes a *generic*, or *type parameter*, called `G`. We use this to return a [`PerseusApp`](=type.PerseusApp@perseus) (which is the construct that contains all the information about our app) that uses `G`. This is essentially saying that we want to return a `PerseusApp` for something that implements the `Html` trait, which we imported earlier. This is Sycamore's way of expressing that this function can either return something designed for the browser, or for the engine. Specifically, the engine uses `SsrNode` (server-side rendering), and the browser uses `DomNode`/`HydrateNode`. Don't worry though, you don't need to understand these distinctions just yet. + +The body of the function is where the magic happens: we define a new `PerseusApp` with our one template and some error pages. The template is called `index`, which is a special name that means it will be hosted at the index of our site --- it will be the landing page. The code for that template is a `view! { .. }`, which comes from Sycamore, and it's how we write things that the user can see. If you've used HTML before, this is the Rust version of that. It might look a bit daunting at first, but most people tend to warm to it fairly well after using it a little. + +All this `view! { .. }` defines is a `p`, which is equivalent to the HTML `<p></p>`, a paragraph element. This is where we can put text for our site. The contents are the universal phrase, `Hello World!`. + +You might be scratching your head about that `cx` though. Understandable. This is the *reactive scope* of the view, which is something complicated that you would need to understand much more about if you were using normal Sycamore. In Perseus, all you really need to know for the basics is that this is a thing that you need to give to every `view! { .. }`, and that your templates always take it as an argument. If you want to know what this actually does, you can read more about it [here](https://sycamore-rs.netlify.app/docs/basics/reactivity). + +The last thing to note is the `ErrorPages`, which are an innovation of Perseus that force you to write fallback pages for situations like the user going to a page that doesn't exist (the infamous 404 error). You could leave these out in development, but when you go to production, Perseus will scream at you. The error pages we've defined here are dead simple: we're just using the universal fallback provided to `ErrorPages::new()`, which is used for everything, unless you provide specific error pages for errors like 404, 500, etc. This fallback page is told the URL the error occurred on, the HTTP status code, and the error itself. + +</details> + +With that all out of the way, add the following to `.gitignore`: + +``` +dist/ +``` + +That just tells Git not to pay any attention to the build artifacts that Perseus is about to create. Now run this command: + +```sh +perseus serve -w +``` + +Because this is the first time building your app, Cargo has to pull in a whole lot of stuff behind the scenes, so now would be a good time to fix yourself a beverage. Once it's done, you can see your app at <http://localhost:8080>, and you should be greeted pleasantly by your app! If you want to check out the error pages, go to <http://localhost:8080/blah>, or any other page that doesn't exist. + +Now, try updating that `Hello World!` message to be a little more like the first of its kind: `Hello, world!` Once you save the file, the CLI will immediately get to work rebuilding your app, and your browser will reload automatically when it's done! + +*Note: if you're finding the build process really slow, or if you're on older hardware, you should try switching to Rust's [nightly channel](https://doc.rust-lang.org/book/appendix-07-nightly-rust.html#rustup-and-the-role-of-rust-nightly) for a faster compiler experience.* + +Now stop that command with `Ctrl+C` and run `perseus deploy` instead. This will take a very long time, but it will produce a `pkg/` directory that you could put on a real-world server, and it would be completely ready to serve your brand-new app! Because this app is so simple, you could even use `perseus deploy -e` instead to just produce a bunch of flat files that you could host from anywhere without needing a proper server. + +All this has just scratched the surface of what's possible with Perseus, and there's so much more to learn! The next big things are about understanding some of the core principles behind Perseus, which should help you to understand why any of what you just did actually worked. diff --git a/docs/0.4.x/en-US/getting-started/installation.md b/docs/0.4.x/en-US/getting-started/installation.md new file mode 100644 index 0000000000..0b60cc61a6 --- /dev/null +++ b/docs/0.4.x/en-US/getting-started/installation.md @@ -0,0 +1,43 @@ +# Installing Perseus + +Perseus comes in a few parts: there's the core `perseus` crate, there's a server integration crate (like `perseus-warp` or `perseus-actix-web`), and then there's the Perseus CLI. + +If you're unfamiliar with Rust's package management system, no problem, *crates* are packages that other people create so you can use their code easily. For example, the `perseus` crate exposes all the functions you need to build a Perseus app. + +You also might be wondering why there are separate server integration crates. We could've bundled everything together in the `perseus` crate, but we wanted to give you a choice of which server integration to use. There are quite a few in the Russt ecosystem at the moment, and, especially if you're migrating an existing app from another system, you might already have a whole API defined in an Actix Web server, or an Axum one, or a Warp one. So, there's a Perseus integration crate for each of those, which you can easily plug an existing API into! Note that there's basically no difference between the APIs of integration crates, and that they're all fairly similar in speed (though Actix Web is usually the fastest). + +Finally, the Perseus CLI is just a tool to make your life exceedingly easy when building web apps. You just run `perseus serve -w` to run your app and `perseus deploy` to output a folder of stuff to send to production! While you *could* use Perseus without the CLI, that approach isn't well-documented, and you'll probably end up in a tangle. The CLI makes things much easier, performing parallel builds and moving files around so you don't have to. + +## Get on with it! + +Alright, that's enough theory! Assuming you've already got `cargo` (Rust's package manager installed), you can install the Perseus CLI like so: + +```sh +cargo install perseus-cli +``` + +This will take a few minutes to download and compile everything. (Note: if you don't have Rust or Cargo yet, see [here](https://rust-lang.org/tools/install) for installation instructions.) + +Next up, you should create a new directory for your app and set it up like so: + +```sh +cargo new --lib my-app +cd my-app +``` + +This will create a new directory called `my-app/` in your current directory, set it up for a new Rust project, and then move into that directory. If you want to move this directory somewhere else, you can do that as usual, everything's self-contained. + +You'll notice in there a file called `Cargo.toml`, which is the manifest of any Rust app; it defines dependencies, the package name, the author, etc. + +In that file, add the following underneath the `[dependencies]` line: + +``` +perseus = { version = "=0.4.0-beta.2", features = [ "hydrate" ] } +sycamore = "=0.8.0-beta.7" +``` + +*Note: we install Sycamore as well because that's how you write views in Perseus, it's useless without it! We've also used the `=[version]` syntax here to pin our app to a specific beta version of Sycamore, otherwise Cargo will politely update it automatically when a new version comes out. Normally, that's very nice of it, but, when we're working with beta versions (which won't be for much longer, hopefully!), a new version could break your code, so it's best to deliberately update when you decide to.* + +Now you can run `cargo build`, and that will fetch the `perseus` crate and get everything ready for you! Note that we haven't defined the integration as a dependency yet, and that's deliberate, because this `Cargo.toml` is going to get *much* more complicated! + +But, for now, you're all good to move onto the next section, in which we'll build our first app with Perseus! diff --git a/docs/0.4.x/en-US/getting-started/intro.md b/docs/0.4.x/en-US/getting-started/intro.md new file mode 100644 index 0000000000..4841c0239a --- /dev/null +++ b/docs/0.4.x/en-US/getting-started/intro.md @@ -0,0 +1,3 @@ +# Getting Started + +This section will walk you through building your first app with Perseus. Even if you don't follow along yourself, this tutorial is still a great way to get to know the basics of Perseus, and how its ergonomics compare to other frameworks. diff --git a/docs/0.4.x/en-US/intro.md b/docs/0.4.x/en-US/intro.md index eeda301c58..a65c311649 100644 --- a/docs/0.4.x/en-US/intro.md +++ b/docs/0.4.x/en-US/intro.md @@ -2,7 +2,7 @@ [Home][repo] • [Crate Page][crate] • [API Documentation][docs] • [Contributing][contrib] -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! Note that every code snippet in this book comes from something in the [examples](https://github.com/arctic-hen7/perseus/tree/main/examples), where you can get context from real-world code. +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! Note that every code snippet in these docs comes from something in the [examples](https://github.com/arctic-hen7/perseus/tree/main/examples), where you can get context from real-world code. If you like Perseus, please consider giving us a star [on GitHub](https://github.com/arctic-hen7/perseus)! diff --git a/docs/0.4.x/en-US/reference/architecture.md b/docs/0.4.x/en-US/reference/architecture.md new file mode 100644 index 0000000000..8d9527295c --- /dev/null +++ b/docs/0.4.x/en-US/reference/architecture.md @@ -0,0 +1 @@ +# Architecture Details diff --git a/docs/0.4.x/en-US/reference/cli.md b/docs/0.4.x/en-US/reference/cli.md deleted file mode 100644 index 7ba8cb7efc..0000000000 --- a/docs/0.4.x/en-US/reference/cli.md +++ /dev/null @@ -1,51 +0,0 @@ -# CLI - -One of the things that makes Perseus so different from most Rust frameworks is that it has its own CLI for development. The reason for this is to make using Perseus as simple as possible, and also because, if you have a look at what's in `.perseus/`, building without the CLI is really hard! - -## Commands - -### `build` - -Builds your app, performing static generation and preparing a Wasm package in `.perseus/dist/`. - -### `serve` - -Builds your app in the same way as `build`, and then builds the Perseus server (which has dependencies on your code, and so needs to rebuilt on any changes just like the stuff in `.perseus/dist/`), finally serving your app at <http://localhost:8080>. You can change the default host and port this serves on with the `HOST` and `PORT` environment variables. - -You can also provide `--no-build` to this command to make it skip building your app to Wasm and performing static generation. In this case, it will just build the serve rand run it (ideal for restarting the server if you've made no changes). - -### `test` - -Exactly the same as `serve`, but runs your app in testing mode, which you can read more about [here](:reference/testing/intro). - -### `export` - -Builds and exports your app to a series of purely static files at `.perseus/dist/exported/`. This will only work if your app doesn't use any strategies that can't be run at build time, but if that's the case, then you can easily use Perseus without a server after running this command! You can read more about static exporting [here](:reference/exporting). - -### `deploy` - -Builds your app for production and places it in `pkg/`. You can then upload that folder to a server of your choosing to deploy your app live! You can (and really should) read more about deployment and the potential problems you may encounter [here](:reference/deploying/intro). - -### `clean` - -This command is the solution to just about any problem in your app that doesn't make sense, it deletes the `.perseus/` directory entirely, which should remove any corruptions! If this doesn't work, then the problem is in your code (unless you just updated to a new version and now something doesn't work, then it's probably on us, please [open an issue](https://github.com/arctic-hen7/perseus)!). - -Note that this command will force Perseus to rebuild `.perseus/` the next time you run `perseus build` or `perseus serve`, which can be annoying in terms of build times. It's almost always sufficient to run this command with the `--dist` flag, which will only delete some content in `.perseus/dist/` that's likely to be problematic. - -### `eject` - -See the next section for the details of this command. - -## Watching - -The Perseus CLI supports watching your local directory for changes when running `perseus serve` or `perseus export` through the `-w/--watch` flag. Adding this will make the CLI spawn another version of itself responsible for running the actual builds, and the original process acts as a supervisor. This approach was chosen due to the complexity of the CLI's multithreaded build system, which makes terminating unfinished builds _extremely_ difficult. - -Notably, the CLI spawns another version of itself as a process group (or `JobObject` on Windows) using the [`command-group`](https://github.com/watchexec/command-group) crate, which allows terminations signals to go to all builder child processes. However, this means that the CLI needs to manually handle termination signals to it to terminate the processes in thr group. This means that, if the CLI terminates improperly (e.g. if you `kill` it), you will very likely end up with build jobs running in the background. Those shouldn't be too problematic, and you probably won't even notice them, but a server process could also be orphaned, which would leave a port occupied. If this happens, use `ps aux | grep perseus` to find the process ID, and then `kill` it by that (e.g. `kill 60850`) on Linux. If possible though, avoiding force-terminating the Perseus CLI. - -Right now, the CLI's watching systems will ignore `.perseus/`, `target/`, and `.git/`. If you have any other directories that you'd like to ignore, you should use an alternative watching system, like [`entr`](https://github.com/eradman/entr). However, we're willing to add support for this if it's a widely-requested feature, so please feel free to [open an issue](https://github.com/arctic-hen7/perseus/issues/new/choose) if this affects you! - -Here's an example of watching files with `entr` on Linux: - -``` -find . -not -path "./.perseus/*" -not -path "./target/*" | entr -rs "perseus serve" -``` diff --git a/docs/0.4.x/en-US/reference/debugging.md b/docs/0.4.x/en-US/reference/debugging.md deleted file mode 100644 index 5739b9272f..0000000000 --- a/docs/0.4.x/en-US/reference/debugging.md +++ /dev/null @@ -1,9 +0,0 @@ -# Debugging - -If you're used to Rust, you might be expecting to be able to call `println!` or `dbg!` to easily print a value to the browser console while working on an app, however this is unfortunately not yet the case (this is an issue in the lower-level libraries that Perseus depends on). - -However, Perseus exports a macro called `web_log!` that can be used to print to the console. It accepts syntax identical to `format!`, `println!`, and the like and behaves in the same way, but it will print to the browser console instead of the terminal. Because Perseus builds your templates on the server as well as in the browser though, some of the calls to this macro may run both on a 'normal' architecture and in Wasm. `web_log!` is designed for this, and will print to `stdout` (as `println!` does) if it's used on the server-side (though you'll need to run something like `perseus snoop build` to see the output). - -## Debugging the build process - -If you have a problem in your build process, you'll probably notice quite quickly that you can't see any `dbg!`, `println!`, or `web_log!` calls in your terminal when you run `perseus serve` (or `export`, `build`, etc.). This is because the CLI hides the output of the commands that it runs behind the scenes (if you've ever had the CLI spout an error at you and show you everything it's done behind the scenes, you'll probably understand why!). As useful as this is for simple usability, it can be extremely annoying for loggin, so the CLI provides a separate command `perseus snoop <process>`, which allows you to run one of the commands that the CLI does, directly. Usually, this will be `perseus snoop build`, though you can also use `perseus snoop wasm-build` if you're having an issue in your Wasm building (usually caused by a crate that can't work in the browser), or `perseus snoop serve` if you're getting errors on the server (which shouldn't happen unless you've modified it). To learn more, see [this page](:reference/snooping). diff --git a/docs/0.4.x/en-US/reference/define-app.md b/docs/0.4.x/en-US/reference/define-app.md deleted file mode 100644 index 59de8958d9..0000000000 --- a/docs/0.4.x/en-US/reference/define-app.md +++ /dev/null @@ -1,43 +0,0 @@ -# `define_app!` - -Perseus used to be configured through a macro rather than through `PerseusApp`: `define_app!`. For now, this is still supported, but it will be removed in the next major release. If you're still using `define_app!`, you should switch to `PerseusApp` when possible. Note also that `define_app!` is now simply a wrapper for `PerseusApp`. - -The smallest this can reasonably get is a fully self-contained app (taken from [here](https://github.com/arctic-hen7/perseus/tree/main/examples/comprehensive/tiny/src/lib.rs)): - -```rust -{{#include ../../../examples/comprehensive/tiny/src/lib.rs}} -``` - -In a more complex app though, this macro still remains very manageable (taken from [here](https://github.com/arctic-hen7/perseus/tree/main/examples/core/state_generation/src/lib.rs)): - -```rust -{{#include ../../../examples/core/state_generation/src/lib.rs}} -``` - -## Parameters - -Here's a list of everything you can provide to the macro and what each one does (note that the order of these matters): - -- `root` (optional) -- the HTML `id` to which your app will be rendered, the default is `root`; this MUST be reflected in your `index.html` file as an exact replication (spacing and all) of `<div id="root-id-here"></div>` (replacing `root-id-here` with the value of this property) -- `templates` -- defines a list of your templates in which order is irrelevant -- `error_pages` -- defines an instance of `ErrorPages`, which tells Perseus what to do on an error (like a _404 Not Found_) -- `locales` (optional) -- defines options for i18n (internationalization), this shouldn't be specified for apps not using i18n - - `default` -- the default locale of your app (e.g. `en-US`) - - `other` -- a list of the other locales your app supports -- `static_aliases` (optional) -- a list of aliases to static files in your project (e.g. for a favicon) -- `plugins` (optional) -- a list of plugins to add to extend Perseus (see [here](:reference/plugins/intro)) -- `dist_path` (optional) -- a custom path to distribution artifacts (this is relative to `.perseus/`!) -- `mutable_store` (optional) -- a custom mutable store -- `translations_manager` (optional) -- a custom translations manager - -**WARNING:** if you try to include something from outside the current directory in `static_aliases`, **no part of your app will load**! If you could include such content, you might end up serving `/etc/passwd`, which would be a major security risk. - -## Other Files - -There's only one other file that the `define_app!` macro expects to exist: `index.html`. Note that any content in the `<head>` of this will be on every page, above anything inserted by the template. - -Here's an example of this file (taken from [here](https://github.com/arctic-hen7/perseus/blob/main/examples/core/basic/index.html)): - -```html -{{#include ../../../examples/core/basic/index.html}} -``` diff --git a/docs/0.4.x/en-US/reference/deploying.md b/docs/0.4.x/en-US/reference/deploying.md new file mode 100644 index 0000000000..5fcd385799 --- /dev/null +++ b/docs/0.4.x/en-US/reference/deploying.md @@ -0,0 +1,37 @@ +# Deploying + +When you've built your app, and you're ready to go to production with it, Perseus provides some nifty tools to make your life easy. First off, you'll notice that all your files are sequestered away in `dist/`, which is all very well for keeping a ton of cached stuff out of your way, but not very useful for getting production binaries! + +When you're ready for production, you should run `perseus deploy`, which will build your entire app in release mode (optimizing for size in the browser and speed on the server, which we'll return to), which will take quite a while. This is a good time to make yourself a beverage of some form. When it's done, you'll get a `pkg/` folder with some stuff inside. The main thing is a file `pkg/server`, which is a binary that will run your app's server, using the rest of the stuff in there for all sorts of purposes. Unless you really know what you're doing, you shouldn't add files here or rearrange things, because that can send the production server a little crazy (it's very particular). + +If you don't need a server for your app, you can use `perseus deploy -e`, which will produce a set of static files to be uploaded to your file host of choice. + +## Optimizations + +Of course, when you're deploying your app, you want it to be as fast as possible. On the engine-side, this is handled automatically by Rust, which will naturally produce super-fast binaries. On the browser-side, there are problems though. This is because of the way the internet works --- before your users can run your super-fast code, they need to download it first. That download process is what's involved in loading your app, which is generally the indicator of speed on the web. That means we actually improve the speed of your app by optimizing more aggreassively for the *size* of your app, thus minimizing download times and making your app load faster. + +With JavaScript, you can 'chunk' your app into many different files that are loaded at the appropriate times, but no such mechanisms exists yet for Wasm of any kind, which means your final `bundle.wasm` will be big. This is often used as a criticism of Wasm: the Perseus basic example produces a bundle that's over 200kb, where a JavaScript equivalent would be a tenth of the size. However, this comparison is flawed, since JavaScript is actually slower to execute. It's an oversimplification, but you can think of it like this: JS needs to be 'compiled' in the browser, whereas Wasm is already compiled. For that reason, it's better to compare Wasm file sizes to image file sizes (another type of file that doesn't need as much browser processing). In fact, that over 200kb bundle is probably faster than the tenth-of-the-size JS. + +If you're getting into real strife with your bundle sizes though, you can, theoretically, split out your app into multiple components by literally building different parts of your website as different apps. This should be an absolute last resort though, and we have never come across an app that was big enough to need this. (Remember that Perseus will still give your users a page very quickly, it's just the interactivity that might take a little longer --- as in a few milliseconds longer.) + +Very usefully, the Perseus CLI automatically applies several optimizations when you build in release mode. Specifically, Cargo's optimization level is set to `z`, which means it will aggressively optimize for size at the expense of speed, which actually means a faster site, due to faster load times for the Wasm bundle. Additionally, `codegen-units` is set to `1`, which slows down compilation with `perseus deploy`, but both speeds up, and reduces the size of, the final bundle. + +Notably, these optimizations are enabled through the `RUSTFLAGS` environment variable on the Wasm build, and only in release-mode (e.g. `perseus deploy`). If you want to tweak these changes, you can directly override the value of that environment variable in this context (i.e. you can apply your own optimization settings) by setting the `PERSEUS_WASM_RELEASE_RUSTFLAGS` environment variable. This takes the same format as `RUSTFLAGS`, and its default value is `-C opt-level=z -C codegen-units=1`. + +*Note: the reason these optimizations are applied through `RUSTFLAGS` rather than `Cargo.toml` is because Cargo doesn't yet support target-specific release profiles, and we only want to optimize for size on the browser-side. Applying the same optimizations to the server would slow things down greatly!* + +The next thing you can do is switch to `wee_alloc`, an alternative allocator designed for the web that produces less efficient, but smaller bundles. Again though, that lower efficiency is barely noticeable, while every kilobyte you can shave off the bundle's size leads to a notably faster load speed. Importantly, you still want to retain that efficiency on the server, so it's very important to only use `wee_alloc` on the browser-side, which you can do by adding the following to the very top of your `lib.rs`: + +```rust +#[cfg(target_arch = "wasm32")] +#[global_allocator] +static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; +``` + +To make this work, you should also add the following to your `Cargo.toml` under the `[target.'cfg(target_arch = "wasm32")'.dependencies]` section (for browser-only dependencies): + +```toml +wee_alloc = "0.4" +``` + +You can find more information about optimizing Wasm bundle sizes [here](https://rustwasm.github.io/book/reference/code-size.html#optimizing-builds-for-code-size). diff --git a/docs/0.4.x/en-US/reference/deploying/docker.md b/docs/0.4.x/en-US/reference/deploying/docker.md deleted file mode 100644 index 6a1ad1faec..0000000000 --- a/docs/0.4.x/en-US/reference/deploying/docker.md +++ /dev/null @@ -1,267 +0,0 @@ -# Docker Deployment - -For situations where [serverful deployment](:reference/deploying/serverful) is required, or in case there is a need to deploy one of the examples found on GitHub without prior setup of all necessary dependencies, below are `Dockerfile` examples meant to serve for different deployment scenarios. These steps can also serve as guidelines for production deployments. - -Note that the following examples should be modified for your particular use-case rather than being used as-is. Also, these `Dockerfile`s are standalone because they use `curl` to download examples directly from the Perseus repository (of course, you'll probably want to use your own code in production). - -Before proceeding with this section, you should be familiar with Docker's [multi-stage builds system](https://docs.docker.com/develop/develop-images/multistage-build) and Perseus' [code size optimizations](:reference/deploying/size). - -<details> -<summary>Production example using the size optimizations plugin</summary> - -```dockerfile -# get the base image -FROM rust:1.57-slim AS build - -# install build dependencies -RUN apt update \ - && apt install -y --no-install-recommends lsb-release apt-transport-https \ - build-essential curl wget - -# vars -ENV PERSEUS_VERSION=0.4.0-beta.2 \ - PERSEUS_SIZE_OPT_VERSION=0.1.7 \ - ESBUILD_VERSION=0.14.7 \ - BINARYEN_VERSION=104 - -# prepare root project dir -WORKDIR /app - -# download the target for wasm -RUN rustup target add wasm32-unknown-unknown - -# install wasm-pack -RUN cargo install wasm-pack - -# retrieve the src dir -RUN curl https://codeload.github.com/arctic-hen7/perseus-size-opt/tar.gz/main | tar -xz --strip=2 perseus-size-opt-main/examples/simple - -# go to src dir -WORKDIR /app/simple - -# install perseus-cli -RUN cargo install perseus-cli --version $PERSEUS_VERSION - -# clean and prep app -RUN perseus clean && perseus prep - -# specify deps in app config -RUN sed -i s"/perseus = .*/perseus = \"${PERSEUS_VERSION}\"/" ./Cargo.toml \ - && sed -i s"/perseus-size-opt = .*/perseus-size-opt = \"${PERSEUS_SIZE_OPT_VERSION}\"/" ./Cargo.toml \ - && cat ./Cargo.toml - -# modify lib.rs -RUN sed -i s'/SizeOpts::default()/SizeOpts { wee_alloc: true, lto: true, opt_level: "s".to_string(), codegen_units: 1, enable_fluent_bundle_patch: false, }/' ./src/lib.rs \ - && cat ./src/lib.rs - -# run plugin(s) to adjust app -RUN perseus tinker \ - && cat .perseus/Cargo.toml \ - && cat ./src/lib.rs - -# single-threaded perseus CLI mode required for low memory environments -#ENV PERSEUS_CLI_SEQUENTIAL=true - -# deploy app -RUN perseus deploy - -# go back to app dir -WORKDIR /app - -# download and unpack esbuild -RUN curl -O https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-${ESBUILD_VERSION}.tgz \ - && tar xf esbuild-linux-64-${ESBUILD_VERSION}.tgz \ - && ./package/bin/esbuild --version - -# run esbuild against bundle.js -RUN ./package/bin/esbuild ./simple/pkg/dist/pkg/perseus_engine.js --minify --target=es6 --outfile=./simple/pkg/dist/pkg/perseus_engine.js --allow-overwrite \ - && ls -lha ./simple/pkg/dist/pkg - -# download and unpack binaryen -RUN wget -nv https://github.com/WebAssembly/binaryen/releases/download/version_${BINARYEN_VERSION}/binaryen-version_${BINARYEN_VERSION}-x86_64-linux.tar.gz \ - && tar xf binaryen-version_${BINARYEN_VERSION}-x86_64-linux.tar.gz \ - && ./binaryen-version_${BINARYEN_VERSION}/bin/wasm-opt --version - -# run wasm-opt against bundle.wasm -RUN ./binaryen-version_${BINARYEN_VERSION}/bin/wasm-opt -Os ./simple/pkg/dist/pkg/perseus_engine_bg.wasm -o ./simple/pkg/dist/pkg/perseus_engine_bg.wasm \ - && ls -lha ./simple/pkg/dist/pkg - -# prepare deployment image -FROM debian:stable-slim - -WORKDIR /app - -COPY --from=build /app/simple/pkg /app/ - -ENV HOST=0.0.0.0 - -CMD ["./server"] -``` - -</details> - -<details> -<summary>Production examples using `wee_alloc` manually</summary> - -```dockerfile -# get the base image -FROM rust:1.57-slim AS build - -# install build dependencies -RUN apt update \ - && apt install -y --no-install-recommends lsb-release apt-transport-https \ - build-essential curl wget - -# vars -ENV PERSEUS_VERSION=0.4.0-beta.2 \ - WEE_ALLOC_VERSION=0.4 \ - ESBUILD_VERSION=0.14.7 \ - BINARYEN_VERSION=104 - -# prepare root project dir -WORKDIR /app - -# download the target for wasm -RUN rustup target add wasm32-unknown-unknown - -# install wasm-pack -RUN cargo install wasm-pack - -# retrieve the src dir -RUN curl https://codeload.github.com/arctic-hen7/perseus/tar.gz/v${PERSEUS_VERSION} | tar -xz --strip=2 perseus-${PERSEUS_VERSION}/examples/tiny - -# go to src dir -WORKDIR /app/tiny - -# install perseus-cli -RUN cargo install perseus-cli --version $PERSEUS_VERSION - -# specify deps in app config -RUN sed -i s"/perseus = .*/perseus = \"${PERSEUS_VERSION}\"/" ./Cargo.toml \ - && sed -i "/\[dependencies\]/a wee_alloc = \"${WEE_ALLOC_VERSION}\"" ./Cargo.toml \ - && cat ./Cargo.toml - -# modify and prepend lib.rs -RUN echo '#[global_allocator] \n\ -static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; \n\ -' | cat - ./src/lib.rs > ./src/lib.rs.tmp \ - && mv ./src/lib.rs.tmp ./src/lib.rs \ - && cat ./src/lib.rs - -# clean, prep and eject app -RUN perseus clean && perseus prep && perseus eject - -# adjust and append perseus config -RUN sed -i s"/perseus = .*/perseus = \"${PERSEUS_VERSION}\"/" .perseus/Cargo.toml \ - && echo ' \n\n\ -[profile.release] \n\ -codegen-units = 1 \n\ -opt-level = "s" \n\ -lto = true ' >> .perseus/Cargo.toml \ - && cat .perseus/Cargo.toml - -# single-threaded perseus CLI mode required for low memory environments -#ENV PERSEUS_CLI_SEQUENTIAL=true - -# deploy app -RUN perseus deploy - -# go back to app dir -WORKDIR /app - -# download and unpack esbuild -RUN curl -O https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-${ESBUILD_VERSION}.tgz \ - && tar xf esbuild-linux-64-${ESBUILD_VERSION}.tgz \ - && ./package/bin/esbuild --version - -# run esbuild against bundle.js -RUN ./package/bin/esbuild ./tiny/pkg/dist/pkg/perseus_engine.js --minify --target=es6 --outfile=./tiny/pkg/dist/pkg/perseus_engine.js --allow-overwrite \ - && ls -lha ./tiny/pkg/dist/pkg - -# download and unpack binaryen -RUN wget -nv https://github.com/WebAssembly/binaryen/releases/download/version_${BINARYEN_VERSION}/binaryen-version_${BINARYEN_VERSION}-x86_64-linux.tar.gz \ - && tar xf binaryen-version_${BINARYEN_VERSION}-x86_64-linux.tar.gz \ - && ./binaryen-version_${BINARYEN_VERSION}/bin/wasm-opt --version - -# run wasm-opt against bundle.wasm -RUN ./binaryen-version_${BINARYEN_VERSION}/bin/wasm-opt -Os ./tiny/pkg/dist/pkg/perseus_engine_bg.wasm -o ./tiny/pkg/dist/pkg/perseus_engine_bg.wasm \ - && ls -lha ./tiny/pkg/dist/pkg - -# prepare deployment image -FROM debian:stable-slim - -WORKDIR /app - -COPY --from=build /app/tiny/pkg /app/ - -ENV HOST=0.0.0.0 - -CMD ["./server"] -``` - -</details> - -<details> -<summary>Test example for deploying a specific branch from the Perseus repository</summary> - -```dockerfile -# get the base image -FROM rust:1.57-slim AS build - -# install build dependencies -RUN apt update \ - && apt install -y --no-install-recommends lsb-release apt-transport-https \ - build-essential curl - -# vars -ENV PERSEUS_BRANCH=main - -# prepare root project dir -WORKDIR /app - -# download the target for wasm -RUN rustup target add wasm32-unknown-unknown - -# install wasm-pack -RUN cargo install wasm-pack - -# install bonnie -RUN cargo install bonnie - -# retrieve the branch dir -RUN curl https://codeload.github.com/arctic-hen7/perseus/tar.gz/${PERSEUS_BRANCH} | tar -xz - -# go to branch dir -WORKDIR /app/perseus-${PERSEUS_BRANCH} - -# install perseus-cli from branch -RUN bonnie setup - -# clean app -RUN bonnie dev example tiny clean - -# go to the branch dir -WORKDIR /app/perseus-${PERSEUS_BRANCH} - -# single-threaded perseus CLI mode required for low memory environments -#ENV PERSEUS_CLI_SEQUENTIAL=true - -# deploy app -RUN bonnie dev example tiny deploy - -# move branch dir -RUN mv /app/perseus-${PERSEUS_BRANCH} /app/perseus-branch - -# prepare deployment image -FROM debian:stable-slim - -WORKDIR /app - -COPY --from=build /app/perseus-branch/examples/tiny/pkg /app/ - -ENV HOST=0.0.0.0 - -CMD ["./server"] -``` - -</details> diff --git a/docs/0.4.x/en-US/reference/deploying/intro.md b/docs/0.4.x/en-US/reference/deploying/intro.md deleted file mode 100644 index 084c89d5e4..0000000000 --- a/docs/0.4.x/en-US/reference/deploying/intro.md +++ /dev/null @@ -1,25 +0,0 @@ -# Deploying - -> **WARNING:** although Perseus is technically ready for deployment, the system is not yet recommended for production! See [here](:reference/what-is-perseus.md#how-stable-is-it) for more details. - -Perseus is a complex system, but we aim to make deploying it as easy as possible. This section will describe a few different types of Perseus deployments, and how they can be managed. - -## Release Mode - -The Perseus CLI supports the `--release` flag on the `build`, `serve`, and `export` commands. When you're preparing a production release of your app, be sure to use this flag! - -## `perseus deploy` - -If you haven't [ejected](:reference/ejecting), then you can prepare your app for deployment with a single command: `perseus deploy`. If you can use [static exporting](:reference/exporting), then you should run `perseus deploy -e`, otherwise you should just use `perseus deploy`. - -This will create a new directory `pkg/` for you (you can change that by specifying `--output`) which will contain everything you need to deploy your app. That directory is entirely self-contained, and can be copied to an appropriate hosting provider for production deployment! - -Note that this command will run a number of optimizations in the background, including using the `--release` flag, but it won't try to aggressively minimize your Wasm code size. For tips on how to do that, see [here](:reference/deploying/size). - -### Static Exporting - -If you use `perseus deploy -e`, the contents of `pkg/` can be served by any file host that can handle the [slight hiccup](:reference/exporting#file-extensions) of file extensions. Locally, you can test this out with [`serve`](https://github.com/vercel/serve), a JavaScript package designed for this purpose. - -### Fully-Fledged Server - -If you just use `perseus deploy`, the `pkg/` directory will contain a binary called `server` for you to run, which will serve your app on its own, without the need for any of the development infrastructure (e.g. the `.perseus/` directory). Running this used to require setting the `PERSEUS_STANDALONE` environment variable, though after [this](https://github.com/arctic-hen7/perseus/issues/87) that's no longer required. diff --git a/docs/0.4.x/en-US/reference/deploying/relative-paths.md b/docs/0.4.x/en-US/reference/deploying/relative-paths.md deleted file mode 100644 index 6d4a4698c5..0000000000 --- a/docs/0.4.x/en-US/reference/deploying/relative-paths.md +++ /dev/null @@ -1,9 +0,0 @@ -# Deploying to Relative Paths - -There are many instances where you'll want to deploy a Perseus website not to the root of a domain (e.g. <https://arctic-hen7.github.io>) but to a relative path under it (e.g. <https://arctic-hen7.github.io/perseus>). This is difficult because Perseus expects all its internal assets (under the URL `/.perseus`) to be at the root of the domain. However, this is easily solved with the `PERSEUS_BASE_PATH` environment variable, which you should set to be the full URL you intend to deploy your app at. - -For example, if we wanted to deploy an existing app to the URL <https://arctic-hen7.github.io/perseus> (where you're reading this right now), we'd set `PERSEUS_BASE_PATH=https://arctic-hen7.github.io/perseus` before running `perseus export` (note that relative path prefixing is designed for exported apps, though it could be used for apps run with `perseus serve` as well in theory). This will tell Perseus where to expect things to be, and it will also automatically set your app's _base URI_ with the HTML `<base>` tag (if you're familiar with this, don't worry about trailing slashes, Perseus does this for you). - -## Code Changes - -If you want to deploy a Perseus app to a relative path, the only code changes you need to make are to your links, which should be made _relative_ rather than _absolute_. For example, you linked to `/about` before, now you would link to `about`. Don't worry about doing this, because the HTML `<base>` tag is designed to prepend your base path to this automatically, effectively turning your relative path into an absolute one. You can read more about this [on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base). diff --git a/docs/0.4.x/en-US/reference/deploying/serverful.md b/docs/0.4.x/en-US/reference/deploying/serverful.md deleted file mode 100644 index 6093bb92c8..0000000000 --- a/docs/0.4.x/en-US/reference/deploying/serverful.md +++ /dev/null @@ -1,31 +0,0 @@ -# Server Deployment - -If your app uses rendering strategies that need a server, you won't be able to export your app to purely static files, and so you'll need to host the Perseus server itself. - -You can prepare your production server by running `perseus deploy`, which will create a new directory called `pkg/`, which will contain the standalone binary and everything needed to run it. - -## Hosting Providers - -As you may recall from [this section](:reference/stores) on immutable and mutable stores, Perseus modifies some data at runtime, which is problematic if your hosting provider imposes the restriction that you can't write to the filesystem (as Netlify does). Perseus automatically handles this as well as it can by separating out mutable from immutable data, and storing as much as it can on the filesystem without causing problems. However, data for pages that use the _revalidation_ or _incremental generation_ strategies must be placed in a location where it can be changed while Perseus is running. - -If you're only using _build state_ and/or _build paths_ (or neither), you should export your app to purely static files instead, which you can read more about doing [here](:reference/exporting). That will avoid this entire category of problems, and you can deploy basically wherever you want. - -If you're bringing _request state_ into the mix, you can't export to static files, but you can run on a read-only filesystem, because only the _revalidation_ and _incremental generation_ strategies require mutability. Perseus will use a mutable store on the filesystem in the background, but won't ever need it. - -If you're using _revalidation_ and _incremental generation_, you have two options, detailed below. - -### Writable Filesystems - -The first of these is to use an old-school provider that gives you a filesystem that you can write to. This may be more expensive for hosting, but it will allow you to take full advantage of all Perseus' features in a highly performant way. - -You can deploy to one of these providers without any further changes to your code, as they mimic your local system almost entirely (with a writable filesystem). Just run `perseus deploy` and copy the resulting `pkg/` folder to the server! - -### Alternative Mutable Stores - -The other option you have is deploying to a modern provider that has a read-only filesystem and then using an alternative mutable store. That is, you store your mutable data in a database or the like rather than on the filesystem. This requires you to implement the `MutableStore` `trait` for your storage system (see the [API docs](https://docs.rs/perseus)), which should be relatively easy. - -You can then provide this to `PerseusApp` with the `.new_with_mutable_store()` function, which must be run on `PerseusAppWithMutableStore`, which takes a second parameter for the type of the mutable store. - -Make sure to test this on your local system to ensure that your connections all work as expected before deploying to the server, which you can do with `perseus deploy` and by then copying the `pkg/` directory to the server. - -This approach may seem more resilient and modern, but it comes with a severe downside: speed. Every request that involves mutable data (so any request for a revalidating page or an incrementally generated one) must go through four trips (an extra one to and from the database) rather than two, which is twice as many as usual! This will bring down your site's time to first byte (TTFB) radically, so you should ensure that your mutable store is as close to your server as possible so that the latency between them is negligible. If this performance pitfall is not acceptable, you should use an old-school hosting provider instead. diff --git a/docs/0.4.x/en-US/reference/deploying/serverless.md b/docs/0.4.x/en-US/reference/deploying/serverless.md deleted file mode 100644 index 923dca4156..0000000000 --- a/docs/0.4.x/en-US/reference/deploying/serverless.md +++ /dev/null @@ -1,3 +0,0 @@ -# Serverless Deployment - -> This strategy of Perseus deployment will be possible eventually, but right now more work needs to be done on support for read-only filesystems before work on this can even be considered. diff --git a/docs/0.4.x/en-US/reference/deploying/size.md b/docs/0.4.x/en-US/reference/deploying/size.md deleted file mode 100644 index 2cc7532831..0000000000 --- a/docs/0.4.x/en-US/reference/deploying/size.md +++ /dev/null @@ -1,13 +0,0 @@ -# Optimizing Code Size - -If you're used to working with Rust, you're probably used to two things: performance is everything, and Rust produces big binaries. With Wasm, these actually become problems because of the way the web works. If you think about it, your Wasm files (big because Rust optimizes for speed instead of size by default) need to be sent to browsers. So, the larger they are, the slower your site will be (because it'll take longer to load). Fortunately, Perseus only makes this relevant when a user first navigates to your site with its [subsequent loads](:advanced/subsequent-loads) system. However, it's still worth optimizing code size in places. - -Before we go any further though, it's worth addressing a common misconception about Wasm. The size of the final Wasm bianry for the Perseus basic example is usually around 200-300kB (this of course changes over time), which would be utterly unacceptable with JavaScript, but this would be considered about normal for an image. It's common to compare Wasm file sizes to JS file sizes, but Wasm is actually much similar to an image, because, unlike with JS, the browser can instantiate it much more quickly because it's already been compiled. This is a complex topic, but the general idea is that Wasm file sizes are better compared to image file sizes than to JS file sizes. You can read more about this [here](https://hacks.mozilla.org/2018/01/making-webassembly-even-faster-firefoxs-new-streaming-and-tiering-compiler/). - -Now onto the optimizations. If you've worked with Rust and Wasm before, you may be familiar with `wasm-opt`, which performs a ton of optimizations for you. Perseus does this automatically with `wasm-pack`. But we can do better. - -The easiest way to apply size optimizations for Perseus is the [`perseus-size-opt` plugin](https://github.com/arctic-hen7/perseus-size-opt), which prevents the need for ejecting to apply optimizations in `.perseus/`, and requires only a single line of code to use. Check it out [here](https://github.com/arctic-hen7/perseus-size-opt) for more details! It's recommended that all Perseus apps use this plugin before they deploy to production, because it can result in size decreases of upwards of 100kb, which translates into real increases in loading time for your users. - -*Note: every size optimization has a trade-off with either speed or compile-time, and you should test the performance of your app after applying these optimizations to make sure you're happy with the results, because the balance between speed and size will be different for every app. That said, using even all these optimizations usually has a negligible performance impact.* - -Usually, this will be enough for most apps, though, if you really want to cut down a few kilobytes, you can use `default-features = false` on the `perseus` dependency in your `Cargo.toml` to deactivate [live reloading](:reference/live-reloading) and [HSR](:reference/state/hsr). These don't *do* anything in production, though they do have a very slight code footprint, which can be elimimated by entirely disabling their features (`live-reloading` and `hsr`, which are enabled by default). Note that you'll probably only want to do this in production, as using this setting will disable these features in development as well. Note though that this will usually only remove fewer than 5kB from your final bundle (sometimes less). diff --git a/docs/0.4.x/en-US/reference/ejecting.md b/docs/0.4.x/en-US/reference/ejecting.md deleted file mode 100644 index 8721b84c5d..0000000000 --- a/docs/0.4.x/en-US/reference/ejecting.md +++ /dev/null @@ -1,32 +0,0 @@ -# Ejecting - -The Perseus CLI is fantastic at enabling rapid and efficient development, but sometimes it can be overly restrictive. If there's a use-case that the CLI doesn't seem to support, please [open an issue](https://github.com/arctic-hen7/perseus/issues/new/choose) on GitHub, and we'll look into supporting it out-of-the-box. - -However, there are some things that are too advanced for the CLI to support, and, in those cases, you'll need to eject. Don't worry, you'll still be able to use the CLI itself for running your app, but you'll be given access to the engine that underlies it, and you'll be able to tweak basically anything you want. - -Before you proceed though, you should know that Perseus supports modularizing the functionality of ejected code through [plugins](:reference/plugins/intro), which let you modify the `.perseus/` directory in all sorts of ways (including arbitrary file modification), without needing to eject in the first place. In nearly all cases (even for smaller apps), plugins are a better way to go than ejecting. In future, you'll even be able to replace the entire `.perseus/` directory with a custom engine (planned for v0.4.0)! - -_Note: ejecting from Perseus exposes the bones of the system, and you should be quite familiar with Rust before doing this. That said, if you're just doing it for fun, go right ahead!_ - -## Ejecting - -`perseus eject` - -This command does two things: it removes `.perseus/` from your `.gitignore` file, and it adds a new file called `.perseus/.ejected`. - -After ejecting, there are a few things that change. - -- You can no longer run `perseus clean` unless you provide the `--dist` flag (otherwise it would delete the engine you're tweaking!) -- A ton of files appear in Git that you should commit, all from `.perseus/` - -## Architecture - -Under the hood, Perseus' CLI is only responsible for running commands like `cargo run` and `wasm-pack build`. All the logic is done in `.perseus/`, which provides two crates, one for your app itself (which also contains a binary for running static generation) and another for the server that will run your app. That means that you can still use the CLI! - -One of the first things you'll probably want to do if you choose to eject is to remove the `[workspace]` declaration from `.perseus/Cargo.toml` and instead add both crates inside to your project's workspace. This will make sure that linters like RLS will check your modifications to `.perseus/` for any problems, and you won't be flying blind. - -The rest of the documentation on how Perseus works under the hood can be found in the _Advanced_ section of the book, which you'll want to peruse if you choose to eject. - -## Reversing Ejection - -If, after taking a look at the innards, you decide that you'd like to find a solution for your problem that works without having to perform what can easily seem like the programming equivalent of brain surgery, you can easily reverse ejection by deleting the `.perseus/.ejected` file and running `perseus clean`, which will permanently delete your modifications and allow you to start again with a clean slate. Note that the reversal of ejection is irreversible, so it pays to have a backup of your changes in case you want to check something later! diff --git a/docs/0.4.x/en-US/reference/engines.md b/docs/0.4.x/en-US/reference/engines.md deleted file mode 100644 index 8c40b24bf0..0000000000 --- a/docs/0.4.x/en-US/reference/engines.md +++ /dev/null @@ -1,31 +0,0 @@ -# Engines - -Perseus' plugins system aims to enable extrreme extensibility of Perseus to support niche use-cases, but it can't do everything. Occasionally, a problem is best solved by rewriting significant parts of the contents of `.perseus/`. These contents constitute Perseus' *engine*, the thing that ties together all the code exposed by the various Perseus libraries into an app. As mentioned before, the engine actually takes in your app's code as a library and produces a final app based on it (so technically, the engine is your *app* in the strictest sense, you provide the details). - -The Perseus CLI comes bundled with the default Perseus engine, but, be it for experimentation of necessary workarounds, it also supports using custom engines by providing the `-e` option at the top level (that is, `perseus -e <engine> serve` or similar). The value of this should be a URL to a Git repository available online, and a branch name can be optionally supplied after an `@` at the end (e.g. `https://github.com/user/repo@v3.0.1`). The URL provided should be one that can be put into the `git clone` command, and the branch (if provided) must be available on the repository. If you're routinely using an alternative engine, it's best for convenience to alias the `perseus` command to `perseus -e <custom-engine-url>` on your system. - -Note that if a custom branch is not supplied, the CLI will fetch from the `stable` branch by default, which MUST correspond to the latest stable version of the engine. - -## Developing Engines - -Developing a custom engine can be quite difficult, because Perseus expects a lot of things to be true. For starters, you'll need to follow a folder structure *identical* to the default Perseus engine (which you can find [here](https://github.com/arctic-hen7/perseus/tree/main/examples/core/basic/.perseus)). There are three modules here, the root one (responsible for exposing a library that `wasm-pack` will interpret as the app and exposing the user's app's code to the other modules), the server, and the builder (responsible for building and exporting). - -The process of actually coding your engine should best start by copying the code of the default engine, and then tweaking it piece by piece. For convenience, you may wish to do this in the context of the entire Perseus repository (which provides internal tools optimized for using an engine that's being actively developed). Beyond this, documentation is best provided by the actual code itself. However, any problems you have can be raised on [the Perseus Discord channel on Sycamore's server](https://discord.com/invite/GNqWYWNTdp). - -### Working with the CLI - -The CLI works with the binaries in `builder` in particular, and you should be careful to keep the same file structure as the default engine. Further, the CLI expects certain dependencies in certain places. Specifically, the root and builder crates are expected to import `perseus`, and the server crate is expected to import `perseus` and all of its integrations (though you don't need to use all of them). In the branch of your repository used by users to download from, you'll need to use the tokens `PERSEUS_VERSION`, `PERSEUS_ACTIX_WEB_VERSION`, and `PERSEUS_WARP_VERSION` to replace the versions of these packages in `Cargo.toml` files. The reason for this is that the CLI will replace them with the appropriate version directly (and relative paths to the packages will be used when working inside the Perseus repository). - -### Versioning - -You may version your engine however you'd like to, though, for simplicity, it's generally recommended that you keep your engine on the same version number as the Perseus packages. The reason for this is because the CLI will impose its own version onto your engine (e.g. if you were using `v0.2.2` but the user's copy of the CLI was at `v0.3.1`, the latter would be used). This means that your engine can break with changes in the CLI, hence why it's recommended to keep versions in lockstep. - -### Semantic Versioning - -Perseus is strict with semantic versioning and not introducing breaking changes to end users, but the same policy does **not** apply to engines. Internal code used by engines and not end users could experience breaking changes at any time, which is why it's recommended to explicitly tell your users to stay on one version of Perseus until you've prepared for an upgrade. Note also that, as Perseus becomes more mature, these changes will become much less frequent. - -*Once Perseus reaches v1.0.0, the policy of introducing breaking changes without warning to code used only by engines will be reviewed.* - -## A Final Note - -Engines are suitable for *extreme* customizations of Perseus. As a general rule, if you can do it with a plugin, even if it's inconvenient, you should, because maintaining a custom engine will likely be very difficult! diff --git a/docs/0.4.x/en-US/reference/error-pages.md b/docs/0.4.x/en-US/reference/error-pages.md deleted file mode 100644 index 7349cca951..0000000000 --- a/docs/0.4.x/en-US/reference/error-pages.md +++ /dev/null @@ -1,34 +0,0 @@ -# Error Pages - -When developing for the web, you'll need to be familiar with the idea of an _HTTP status code_, which is a numerical indication in HTTP (HyperText Transfer Protocol) of how the server reacted to a client's request. The most well-known of these is the infamous _404 Not Found_ error, but there are dozens of these in total. Don't worry, you certainly don't need to know all of them by heart! - -## Handling HTTP Status Codes in Perseus - -Perseus has an _app shell_ that manages fetching pages for the user (it's a little more complicated than the traditional design of that kind of a system, but that's all you need to know for now), and this is where HTTP errors will occur as it communicates with the Perseus server. If the status code is an error, this shell will fail and render an error page instead of the page the user visited. This way, an error page can be displayed at any route, without having to navigate to a special route. - -You can define one error page for each HTTP status code in Perseus, and you can see a list of those [here](https://httpstatuses.com). Here's an example of doing so for _404 Not Found_ and _400_ (a generic error caused by the client) (taken from [here](https://github.com/arctic-hen7/perseus/tree/main/examples/core/state_generation/src/error_pages.rs)): - -```rust -{{#include ../../../examples/core/state_generation/src/error_pages.rs}} -``` - -It's conventional in Perseus to define a file called `src/error_pages.rs` and put your error pages in here for small apps, but for larger apps where your error pages are customized with beautiful logos and animations, you'll almost certainly want this to be a folder, and to have a separate file for each error page. - -When defining an instance of `ErrorPages`, you'll need to provide a fallback page, which will be used for all the status codes that you haven't specified unique pages for. In the above example, this fallback would be used for, say, a _500_ error, which indicates an internal server error. - -The most important thing to note about these error pages is the arguments they each take, which have all been ignored in the above example with `_`s. There are four of these: - -- URL that caused the error -- HTTP status code (`u16`) -- Error message -- Translator (inside an `Option<T>`) - -## Translations in Error Pages - -Error pages are also available for you to use yourself (see the [API docs](https://docs.rs/perseus) on the functions to call for that) if an error occurs in one of your own pages, and in that case, if you're using i18n, you'll have a `Translator` available. However, there are _many_ cases in Perseus in which translators are not available to error pages (e.g. the error page might have been rendered because the translator couldn't be initialized for some reason), and in these cases, while it may be tempting to fall back to the default locale, you should optimally make your page as easy to decipher for speakers of other languages as possible. This means emoji, pictures, icons, etc. Bottom line: if the fourth parameter to an error page is `None`, then communicate as universally as possible. - -An alternative is just to display an error message in every language that your app supports, which may in some cases be easier and more practical. - -## Error Pages in Development - -When you start building a new app, you want to spend as little time on boilerplate as possible, and Perseus enables this by having a default set of error pages that you can use in development so you can get straight to work on your app, but you'll need to make them eventually for production, otherwise you'll get an error when you try to deploy your app. The reason for not allowing the default error pages in production is to avoid people not noticing that they're using the defaults, which may not be what they want, or even in the language they want (even if you're building an i18n site, the default development error pages are still in English for simplicity). diff --git a/docs/0.4.x/en-US/reference/exporting.md b/docs/0.4.x/en-US/reference/exporting.md index acafe381f1..24a9614353 100644 --- a/docs/0.4.x/en-US/reference/exporting.md +++ b/docs/0.4.x/en-US/reference/exporting.md @@ -1,11 +1,13 @@ # Static Exporting -Thus far, we've used `perseus serve` to build and serve Perseus apps, but there is an alternative way that offers better performance in some cases. Namely, if your app doesn't need any rendering strategies that can't be run at build time (so if you're only using _build state_ and/or _build paths_ or neither), you can export your app to a set of purely static files that can be served by almost any hosting provider. You can do this by running `perseus export`, which will create a new directory `.perseus/dist/exported/`, the contents of which can be served on a system like [GitHub Pages](https:://pages.github.com). Your app should behave in the exact same way with exporting as with normal serving. If this isn't the case, please [open an issue](https://github.com/arctic-hen7/perseus/issues/new/choose). +There are two ways to run a Perseus app: with a server, and without a server. This might seem odd, how could you run an app without a server? Well, you can't really. But, you can *export* your app to static files and then use a stock server to just host those for you. For example, [GitHub Pages]() is a hosting service free for open-source projects that can host static files, and that's what this website runs on! There's no Perseus server behind this, just static files that you could serve with a Python script if you wanted to. -There is only one known difference between the behavior of your exported site and your normally served site, and that's regarding [static aliases](:reference/static-content). In a normal serving scenario, any static aliases that conflicted with a Perseus page or internal asset would be ignored, but, in an exporting context, **any static aliases that conflict with Perseus pages will override them**! If you suspect this might be happening to you, try exporting without those aliases and make sure the URL of your alias file doesn't already exist (in which case it would be a Perseus component). +Most of Perseus' features don't actually need a server, though some do. Specifically, if your app uses incremental generation, revalidation, or request state, attempting to export your app will fail. -## File Extensions +But, if your app doesn't use any of those, you can run `perseus export` and that will export your app to a series of static files! For convenience, `perseus export -s` will spin up a server built into the CLI so you can see what everything looks like. When you're ready to go to production, you can run `perseus deploy -e` to get a `pkg/` directory with your static files, which can be sent off to any file hosting platform! -One slight hiccup with Perseus' static exporting system comes with regards to the `.html` file extension. Perseus' server expects that pages shouldn't have such extensions (hence `/about` rather than `/about.html`), but, when statically generated, they must have these extensions in the filesystem. So, if you don't want these extensions for your users (and if you want consistent behavior between exporting and serving), it's up to whatever system you're hosting your files with to strip these extensions. Many systems do this automatically, though some (like Python's `http.server`) do not. +The one big caveat with this approach is error pages. When you're using Perseus's server, it's smart enough to use your error pages when it undergoes a 404, 500, 418, etc. error, but a run-of-the-mill static file server will be clueless (this goes for the development server from `perseus export -s` as well). Usually, you'll need to explicitly provide something like `404.html` as a file to tell these servers what to provide to users. However, confusingly, your error pages will work in some circumstances. Specifically, if the user clicks a link inside your app that goes to a page that doesn't exist, this involves asking for a page from the server in the background, and the app will automatically return your error page from within itself in this case. If you go to a nonexistent page from *outside* your app though, you'll get some stock error page that's got nothing to do with Perseus. This might be hard to understand, which is why it's best to do the following. -Note that, in development, you can easily serve your app with `perseus export -s`, which will spin up a local server automatically! +You can solve this problem by exporting your error pages to static files with the `perseus export-error-page --code <http-code> --output <output>` command, replacing `<http-code>` with the code you want to export for (e.g. `404`) and `<output>` with where you want to put the file. These won't work at all with the development server, which isn't designed to handle error pages (yet), but a production file server should manage this fine. (Your mileage may vary depending on the hosting provider, so it's best to check first!) + +*Note: apps using exporting only should see [these examples]() for how to avoid having to import a server in `Cargo.toml`.* diff --git a/docs/0.4.x/en-US/reference/pitfalls-and-bugs.md b/docs/0.4.x/en-US/reference/faq.md similarity index 100% rename from docs/0.4.x/en-US/reference/pitfalls-and-bugs.md rename to docs/0.4.x/en-US/reference/faq.md diff --git a/docs/0.4.x/en-US/reference/hydration.md b/docs/0.4.x/en-US/reference/hydration.md index 4a4928ea32..14f013995a 100644 --- a/docs/0.4.x/en-US/reference/hydration.md +++ b/docs/0.4.x/en-US/reference/hydration.md @@ -1,17 +1 @@ # Hydration - -In the examples of `Cargo.toml` files shown thus far, we've enabled a feature called `hydrate` on Perseus. This feature controls _hydration_, which is a way of making your app more performant. To explain it, we'll need to go a little in-depth. - -Perseus uses server-side rendering of some kind for almost everything. In fact, unless you explicitly make something only run in the browser, Perseus will try to prerender it on the server first, either at build-time (faster, so Perseus does this for everything it can) or at request-time. This prerendering process yields a series of HTML and JSON fiels that make up the markup and state of your app. When a page is requested by a user, Perseus can serve them these files, and then the app shell (the Wasm code that runs everything in a Perseus app in the browser) will bring everything to life. - -Those prerendered files can be imagined as solid iron, but, to make your app work, we need molten iron. In the real world, you need a lot of heat to turn iron into a liquid, and you need a lot of code to turn simple markup into interactive buttons and text in a browser! So, let's go through the metaphor a bit more: the build process and server are the miners that fetch all the iron out of the mines of the tempalte code you write. Then, that iron is sent to the user's browser, and the app shell does _something_ to get molten iron that can be used to run your app. - -Without hydration, the app shell will kindly thank the server for sending it the solid iron, and will then proceed to mine more of its own. In other words, the app shell will completely ignore the prerendered files that the server has sent (displaying them only until it's ready, which is why Perseus apps still work without JavaScript!). - -But with hydration, the app shell can intelligently melt the iron that it's been given, it can _hydrate_ the simple markdown. Using hydration is generally much faster than not using hydration, though it's also very hard to implement! Hydration is done by Sycamore, and it's still experimental right now, so it's opt-in with Perseus. You can use the `hydrate` feature flag to enable it in any Perseus app, though you should be aware that there's a chance that things may break in very strange ways! If this happens, try disabling hydration. - -## Performance Costs of Disabling Hydration - -Not using hydration will impact your site's performance when the user first loads a page (moving around within the app is no problem), because the browser has to do a little more work, but it also has to completely re-display your site. The difference shouldn't be visible to users at all unless they try to scroll as soon as your site loads (as in within less than half a second on modern machines), in which case they'll be thrown back to the top of the site. However, Lighthouse doesn't seem to notice any differences, so your scores there won't change! - -Notably, to make hydration better for the community, you should file any bugs about hydration on [the Sycamore repository](https://github.com/sycamore-rs/sycamore). diff --git a/docs/0.4.x/en-US/reference/i18n.md b/docs/0.4.x/en-US/reference/i18n.md new file mode 100644 index 0000000000..798fcef772 --- /dev/null +++ b/docs/0.4.x/en-US/reference/i18n.md @@ -0,0 +1,15 @@ +# Internationalization + +Internationalization, or *i18n* for short, is the process of making your app available in many languages, something Perseus supports out of the box! + +Usually, i18n is done in one of two ways: by having subdomains for each different locale (e.g. `en.example.com`, `de.example.com`), or by having each locale have a separate top-level route (e.g. `example.com/en`, `example.com/de`). Perseus favors the latter approach, which requires less routing overhead (you don't have to manage subdomains), and is generally easier to set up. + +The process of i18n is mostly behind-the-scenes in Perseus, but what it involves at heart is this: each template is built once for each locale, with a different language parameter provided. That parameter allows you to, in your code, detect the locale being used, and provide the right translation of your page's content. Perseus also takes this one step further by providing an inbuilt `TranslationsManager` system, which is used to find translations in the `translations/` folder at the root of your project, which can then be interpolated into your page with the `t!` macro. All examples are available [here](). + +Specifically, Perseus uses the [Fluent](https://projectfluent.org) translation system, which provides a file format with full support for managing variable interpolation, plurals, genders, etc. The `t!` macro allows working with most of these features, though for some more advanced use-cases you'll need to drill down into the `Translator` instance itself, an example of which can be found [here](). + +Not everyone appreciates Fluent though, and there are plenty of other translations systems that exist today. Perseus manages translators on a feature-flag system (so you enable `translator-fluent` to use the default Fluent system), which means more translators can be built into Perseus without any cost to bundle sizes. Currently, only Fluent is supported, though we're happy to accept [PRs]() or [issues]() implementing or proposing more systems! + +The last thing to understand about Perseus' approach to i18n is how we manage translations. You'll store your translations for each locale somewhere like `translations/en-US.ftl` (from the root of your project), but this isn't always the ideal system. Sometimes, for example, you'll want to fetch translations from a database instead, if they're being regularly updated. This can be done by using an alternative to `FsTranslationsManager`, as long as it implements `TranslationsManager`. An example for this can be found [here](). Note that translations will be fetched extremely regularly, so it's generally not recommended to use high-latency managers in server-based applications. If you use `perseus export`, then all translations are automatically hardcoded, though `perseus serve` will fetch them all as it starts up, caching them. (You should never update translations without rebuilding your app, as this could lead to unexpected results.) The translations that are cached immediately can be changed as per [this example](). + +*Note for contributors: there is a `struct ClientTranslationsManager` also present in the codebase, which is responsible for caching translations in the browser. It is not customizable, and has no relation to the `trait TranslationsManager` used on the engine-side.* diff --git a/docs/0.4.x/en-US/reference/i18n/defining.md b/docs/0.4.x/en-US/reference/i18n/defining.md deleted file mode 100644 index 6f2eeb3390..0000000000 --- a/docs/0.4.x/en-US/reference/i18n/defining.md +++ /dev/null @@ -1,27 +0,0 @@ -# Defining Translations - -The first part of setting up i18n in Perseus is to state that your app uses it, which is done in `PerseusApp` like so (taken from [the i18n example](https://github.com/arctic-hen7/perseus/tree/main/examples/core/i18n)): - -```rust -{{#include ../../../../examples/core/i18n/src/lib.rs}} -``` - -There are two paremeters to the `.locales()` function: the default locale for your app, and then any other locales it supports (other than the default). Each of these locales should be specified in the form `xx-XX`, where `xx` is the language code (e.g. `en` for English, `fr` for French, `la` for Latin) and `XX` is the region code (e.g. `US` for United States, `GB` for Great Britain, `CN` for China). - -## Routing - -After you've enabled i18n like so, every page on your app will be rendered behind a locale. For example, `/about` will become `/en-US/about`, `/fr-FR/about`, and`/es-ES/about` in the above example. These are automatically rendered by Perseus at build-time, and they behave exactly the same as every other feature of Perseus. - -Of course, it's hardly optimal to direct users to a pre-translated page if they may prefer it in another language, which is why Perseus supports _locale detection_ automatically. In other words, you can direct users to `/about`, and they'll automatically be redirected to `/<locale>/about`, where `<locale>` is their preferred locale according to `navigator.languages`. This matching is done based on [RFC 4647](https://www.rfc-editor.org/rfc/rfc4647.txt), which defines how locale detection should be done. - -## Adding Translations - -After you've added those definitions to `PerseusApp`, if you try to run your app, you'll find that ever page throws an error because it can't find any of the translations files. These must be defined under `translations/` (which should be NEXT to `/src`, not in it!), though this can be customized (explained later). They must also adhere to the naming format `xx-XX.ftl` (e.g. `en-US.ftl`). `.ftl` is the file extension that [Fluent](https://projectfluent.org) files use, which is the default translations system of Perseus. If you'd like to use a different system, this will be explained later. - -Here's an example of a translations file (taken from [here](https://github.com/arctic-hen7/perseus/blob/main/examples/core/i18n/translations/en-US.ftl)): - -```fluent -{{#include ../../../../examples/core/i18n/translations/en-US.ftl}} -``` - -You can read more about Fluent's syntax [here](https://projectfluent.org) (it's _very_ powerful). diff --git a/docs/0.4.x/en-US/reference/i18n/intro.md b/docs/0.4.x/en-US/reference/i18n/intro.md deleted file mode 100644 index 115ebf0220..0000000000 --- a/docs/0.4.x/en-US/reference/i18n/intro.md +++ /dev/null @@ -1,7 +0,0 @@ -# Internationalization - -Internationalization (abbreviated *i18n*) is making an app available in many languages. Perseus supports this out-of-the-box with [Fluent](https://projectfluent.org). - -The approach usually taken to i18n is to use translation IDs in your code instead of natural language. For example, instead of writing `format!("Hello, {}!", name.get())`, you'd write something like `t!("greeting", {"name" => name.get()})`. This ensures that your app works well for people across the world, and is crucial for any large apps. - -This section will explain how i18n works in Perseus and how to use it to make lightning-fast apps that work for people across the planet. diff --git a/docs/0.4.x/en-US/reference/i18n/other-engines.md b/docs/0.4.x/en-US/reference/i18n/other-engines.md deleted file mode 100644 index 1f654ed429..0000000000 --- a/docs/0.4.x/en-US/reference/i18n/other-engines.md +++ /dev/null @@ -1,13 +0,0 @@ -# Other Translation Engines - -Perseus uses [Fluent](https://projectfluent.org) for i18n by default, but this isn't set in stone. Rather than providing only one instance of `Translator`, Perseus can support many through Cargo's features system. By default, Perseus will enable the `translator-fluent` feature to build a `Translator` `struct` that uses Fluent. The `translator-dflt-fluent` feature will also be enabled, which sets `perseus::Translator` to be an alias for `FluentTranslator`. - -If you want to create a translator for a different system, this will need to be integrated into Perseus as a pull request, but we're more than happy to help with these efforts. Optimally, Perseus will in future support multiple translations systems, and developers will be able to pick the one they like the most - -## Why Not a Trait? - -It may seem like this problem could simply be solved with a `Translator` trait, as is done with translations managers, but unfortunately this isn't so simple because of the way translators are transported through the app. The feature-gating solution was chosen as the best compromise between convenience and performance. - -## How Do I Make One? - -If you want to make your own alternative translation engine, please [open an issue](https://github.com/arctic-hen7/perseus/issues/new/choose) about it, explaining the system you want to support. Provided the system is compatible with Perseus' i18n design (which it certainly should be if we've done our job correctly!), we'll be happy to help you get it into Perseus! diff --git a/docs/0.4.x/en-US/reference/i18n/translations-managers.md b/docs/0.4.x/en-US/reference/i18n/translations-managers.md deleted file mode 100644 index a91354b37a..0000000000 --- a/docs/0.4.x/en-US/reference/i18n/translations-managers.md +++ /dev/null @@ -1,17 +0,0 @@ -# Translations Managers - -As mentioned earlier, Perseus expects your translations to be in the very specific location of `translations/<locale>.ftl`, which may not be feasible or preferable in all cases. In fact, there may indeed be cases where translations might be stored in an external database (not recommended for performance as translations are regularly requested, filesystem storage with caching is far faster). - -If you'd like to change this default behavior, this section is for you! Perseus manages the locations of translations with a `TranslationsManager`, which defines a number of methods for accessing translations, and should implement caching internally. Perseus has two inbuilt managers: `FsTranslationsManager` and `DummyTranslationsManager`. The former is used by default, and the latter if i18n is disabled. - -## Using a Custom Translations Manager - -`PerseusApp` can be used with a custom translations manager through the `.translations_manager()` function. Note that this must be used with `PerseusAppWithTranslationsManager` rather than the usual `PerseusApp` (there's also `PerseusAppBase` if you want this and a custom mutable store). Further, translations managers all instantiate asynchronously, but we can't have asynchronous code in `PerseusApp` because of how it's called in the browser, so you should provide a future here (just don't add the `.await`), and Perseus will evaluate this when needed. - -## Using a Custom Directory - -If you just want to change the directory in which translations are stored, you can still use `FsTranslationsmanager`, just initialize it with a different directory, and make sure to set up caching properly. - -## Building a Custom Translations Manager - -This is more complex, and you'll need to consult [this file](https://github.com/arctic-hen7/perseus/blob/main/packages/perseus/src/i18n/translations_manager.rs) (note: the client translations manager is very different) in the Perseus source code for guidance. If you're stuck, don't hesitate to ask a question under [discussions](https://github.com/arctic-hen7/perseus/discussions/new) on GitHub! diff --git a/docs/0.4.x/en-US/reference/i18n/using.md b/docs/0.4.x/en-US/reference/i18n/using.md deleted file mode 100644 index 329b49387b..0000000000 --- a/docs/0.4.x/en-US/reference/i18n/using.md +++ /dev/null @@ -1,27 +0,0 @@ -# Using Translations - -Perseus tries to make it as easy as possible to use translations in your app by exposing the low-level Fluent primitives necessary to work with very complex translations, as well as a `t!` macro that does the basics. Note that, to use i18n, you'll need to enable a translator, the usual one is for [Fluent](https://projectfluent.org). Change your Perseus import in your `Cargo.toml` to look like this: - -```toml -perseus = { version = "<version of Perseus that you're using>", features = [ "translator-fluent" ] } -``` - -If you don't do this, your app won't build. - -All translations in Perseus are done with an instance of `Translator`, which is provided through Sycamore's [context system](https://sycamore-rs.netlify.app/docs/v0.6/advanced/contexts). Here's an example taken from [here](https://github.com/arctic-hen7/perseus/blob/main/examples/core/i18n/src/templates/index.rs): - -```rust -{{#include ../../../../examples/core/i18n/src/templates/index.rs}} -``` - -In that example, we've imported `perseus::t`, and we use it to translate the `hello` ID, which takes an argument for the username. Notice that we don't provide a locale, Perseus handles all this in the background for us. - -## Getting the `Translator` - -That said, there are some cases in which you'll want access to the underlying `Translator` so you can do more complex things. You can get it like so: - -```rust -perseus::get_render_ctx!().translator; -``` - -To see all the methods available on `Translator`, see [the API docs](https://docs.rs/perseus). diff --git a/docs/0.4.x/en-US/reference/index-view.md b/docs/0.4.x/en-US/reference/index-view.md deleted file mode 100644 index 3e0015b9ca..0000000000 --- a/docs/0.4.x/en-US/reference/index-view.md +++ /dev/null @@ -1,23 +0,0 @@ -# The Index View - -In most Perseus apps, you can just ofcus on building your app's templates, and leave the boilerplate entirely to the Perseus engine, but sometimes that isn't quite sufficient, like if you want to use one stylesheet across your entire app. In traditional architectures, these are the kinds of modifications you might make to an `index.html` file that a framework inserts itself into, and you can do exactly this with Perseus! If you provide an `index.html` file in the root of your porject (not inside `src/`), Perseus will insert itself into that! - -However, if you're using Perseus, you probably don't want to be writing HTML right? You're supposed to be using Sycamore! Well, that's completely true, and so Perseus supports creating an index view with Sycamore code! You can do this like so: - -```rust -{{#include ../../../examples/core/index_view/src/lib.rs}} -``` - -Note that you can also use `.index_view_str()` to provide an arbitrary HTML string to use instead of Sycamore code. - -It's also important to remember that whatever you put in your index view will persist across *all* the pages of your app! There is no way to change this, as Perseus literally injects itself into this, using it as a super-template for all your other templates! - -## Requirements for the Index View - -Perseus' index view is very versatile, but there are a few things you HAVE to include, or Perseus moves into undefined behavior, and almost anything could happen! This mostly translates to your app just spitting out several hundred errors when it tries to build though, because none of the tactics Perseus uses to insert itself into your app will work anymore. - -1. You need a `<head>`. This can be empty, but it needs to be present in the form `<head></head>` (no self-closing tags allowed). The reason for this is that Perseus uses these tags as markers for inserting components of the magical metadata that makes your app work. -2. You need a `<body>`. This needs to be defined as `<body></body>`, for similar reasons to the `<head>`. -3. You need a `<div id="root"></div>`. Literally, you need that *exact* string in your index view, or Perseus won't be able to find your app at all! Now, yes we could parse the HTML fully and find this by ID, or we could just use string replacement and reduce dependencies and build time. Importantly, you can't use this directly is you use `.index_view()` and provide Sycamore code, as Sycamore will add some extra information that stuffs things up. Instead, you should use `perseus::PerseusRoot`, which is specially designed to be a drop-in entrypoint for Perseus. It should go without saying that you need to put this in the `<body>` of your app. - -*Note: you don't need the typical `<!DOCTYPE html>`in your index view, since that's all Perseus targets, so it's added automatically. If, for some magical reason, you need to override this, you can do so with a [control plugin](:reference/plugins/control).* diff --git a/docs/0.4.x/en-US/reference/live-reloading-and-hsr.md b/docs/0.4.x/en-US/reference/live-reloading-and-hsr.md new file mode 100644 index 0000000000..fffd00dc08 --- /dev/null +++ b/docs/0.4.x/en-US/reference/live-reloading-and-hsr.md @@ -0,0 +1,17 @@ +# Live Reloading and HSR + +When you develop with Perseus, you can add the `-w` flag to either `perseus serve` or `perseus export` to automatically rebuild your app whenever you change any code in your project. When you do, any browsers connected to the development version of your app will also be automatically reloaded, which allows for a more rapid development cycle. (If you want faster compile times, use the nightly channel of Rust.) + +This also involves using *hot state reloading* (HSR), a world first in the non-JavaScript world pioneered by Perseus. This is very similar to *hot module reloading* (HMR) in JavaScript frameworks, which only changes the bare minimum amount of code necessary to let you preview your changes, meaning the state of your app is kept. + +But what does that actually mean? Well, let's take a simple example. Imagine you're working on a form page that has twelve inputs that all need to be filled out. With HMR, most changes to your code will lead to small substitutions in the browser because of the way JS can be chunked into many small files --- your inputs into the form are preserved even across code changes, which is extremely helpful! + +As you may know, Perseus has the concept of state freezing and thawing inbuilt, which allows you to turn the entire state of your app into a string and then restore your app to a single point of interaction from that, which would allow you to take a user back to exactly where they were after they logged back into your app, for example. + +In development, this system is applied automatically to save your app's state to a string in your browser's storage automatically just after it's rebuilt, and this is then restored after the reload, meaning you're taken back to exactly where you were before you made the code change! + +Of course, there are some cases in which this isn't possible --- namely when you change the data model of your app. So, if you add new parameters to the current page's state, Perseus won't be able to process it, and the previous state will be dumped. If you change the data model for another page though, things will still work, until you go to that page, because of the incremental nature of thawing (something you almost never need to care about). Very occasionally, this can lead to odd behavior, which is immediately fixed by simply reloading the page. + +So, in summary, because Wasm can't be chunked, HMR can't be implemented for Wasm projects, including Perseus ones, so we invented a new way of achieving the same results grounded in the state-based architecture of Perseus, meaning you can easily develop complex flows in your app without losing state every time you change some code. + +*Note: if you're a developer using another Wasm framework, and you'd like to implement HSR yourself, hop over to our [Discord channel on the Sycamore server](https://discord.com/invite/GNqWYWNTdp) if you want to discuss implementation details. All of this is open-source, and we'd be thrilled if HSR were more widely adopted in the Wasm community, to improve the developer experience of all!* diff --git a/docs/0.4.x/en-US/reference/live-reloading.md b/docs/0.4.x/en-US/reference/live-reloading.md deleted file mode 100644 index 065bfb7937..0000000000 --- a/docs/0.4.x/en-US/reference/live-reloading.md +++ /dev/null @@ -1,9 +0,0 @@ -# Live Reloading - -When you develop a Perseus app, you'll usually be using `-w` on the command you're running (e.g. `perseus serve -w`, `perseus export -sw`), which will make the CLI watch your code and rebuild your app when it changes. In addition to that, Perseus will automatically reload any browser sessions that are connected to your app, meaning you can just change your code and save the file, and then your updated app will be ready for you! - -In production of course, the code of your app won't change, so Perseus disables live reloading automatically when you build for production (e.g. with `perseus deploy`). - -If you find that live reloading isn't to your liking, you can disable it by adding `default-features = false` to the `perseus` dependency in your `Cargo.toml`, which will disable all default features, including live reloading. Currently, it's the only default feature (along with [HSR](:reference/state/hsr), which depends on it), so you don't need to enable any other features after doing this. - -To achieve live reloading, Perseus runs a server at <http://localhost:3100>, though, if you have something else on this port, this would be problematic. You can change the port by setting the `PERSEUS_RELOAD_SERVER_PORT` environment variable (and `PERSEUS_RELOAD_SERVER_HOST` also exists if you need to change the host). diff --git a/docs/0.4.x/en-US/reference/updating.md b/docs/0.4.x/en-US/reference/migrating.md similarity index 98% rename from docs/0.4.x/en-US/reference/updating.md rename to docs/0.4.x/en-US/reference/migrating.md index 3beb9e33d0..1dafaada47 100644 --- a/docs/0.4.x/en-US/reference/updating.md +++ b/docs/0.4.x/en-US/reference/migrating.md @@ -1,4 +1,4 @@ -# Migrating from v0.4.x +# Migrating from v0.3.x Perseus v0.4.x added several breaking changes, along with a full migration to Sycamore v0.8.x, which requires some rewriting of your view code, most of which is covered on the [Sycamore website](https://sycamore-rs.netlify.app). diff --git a/docs/0.4.x/en-US/reference/perseus-app.md b/docs/0.4.x/en-US/reference/perseus-app.md deleted file mode 100644 index 406d55edc2..0000000000 --- a/docs/0.4.x/en-US/reference/perseus-app.md +++ /dev/null @@ -1,32 +0,0 @@ -# `PerseusApp` - -The core of Perseus is how it interacts with the code in `.perseus/`, which defines the engine that actually runs your app. Of course, to perform this interaction, you need to be able to tell the engine details about your app, like the templates you've written, the error pages you want to use, etc. All this is done through the `PerseusApp` `struct`, which acts as a bridge between your code and the engine, so this is essentially the core of Perseus, from the perspective of building apps with it. - -The way you define `PerseusApp` in any Perseus app is by creating a function that returns an instance of it, with a type parameter (usually called `G`) of type `Html`, which gives Perseus the flexibility to render your app on both the server-side (as it needs to do for prerendering) and in a browser. You need to export this from the root of your app (`lib.rs`), and it's conventional to define it there too as a function called `main` or something similar. Notably, what you call this function is completely irrelevant, provided it has the `#[perseus::main]` attribute macro annotating it, which automatically tells Perseus to use it as your main function. - -<details> -<summary>What does that attribute macro do?</summary> - -Currently, `#[perseus::main]` just wraps your function in another one with the name `__perseus_entrypoint`, but this behavior could change at any time, so using this macro isn't optional! For example, in future it might modify your code in some crucial way, and such a modification to the macro would be considered a non-breaking change, which means your code could break in production. To be safe, use the macro (or pin Perseus to a specific minor version if you *really* hate it). - -</details> - -The smallest this can reasonably get is a fully self-contained app (taken from [here](https://github.com/arctic-hen7/perseus/tree/main/examples/comprehensive/tiny/src/lib.rs)): - -```rust -{{#include ../../../examples/comprehensive/tiny/src/lib.rs}} -``` - -In a more complex app though, this macro still remains very manageable (taken from [here](https://github.com/arctic-hen7/perseus/tree/main/examples/core/state_generation/src/lib.rs)): - -```rust -{{#include ../../../examples/core/state_generation/src/lib.rs}} -``` - -Note that, in theory, you could just define your entire app with `PerseusApp::new()`, and this would work, but you would have no pages and no functionality whatsoever. This might be a good way of benchmarking a server's performance with Perseus, perhaps?? - -## Configuration - -To learn about all the functions this macro supports, see [here](https://docs.rs/perseus/latest/perseus/struct.PerseusApp.html). - -Again, `PerseusApp` is just a bridge, so all the features you can use through it are documented elsewhere on this site. diff --git a/docs/0.4.x/en-US/reference/plugins.md b/docs/0.4.x/en-US/reference/plugins.md new file mode 100644 index 0000000000..070bebaebf --- /dev/null +++ b/docs/0.4.x/en-US/reference/plugins.md @@ -0,0 +1,15 @@ +# Plugins + +Like many fullstack frameworks, Perseus supports *plugins*, which allow you to extend the basic functionality of Perseus in sometimes extreme ways! However, unlike other frameworks, Perseus is already extremely customizabel with usual usage, due to the way it exposes all operations directly to the user. For example, if you wanted to restructure your server, all that code is open to you directly. + +In earlier version of Perseus, there was a folder called `.perseus/` that stored a large amount of internal code, and plugins were mostly used to modify that. Today, that code simply doesn't exist anymore, and everything is bundled into the main app! (With the consequence of a slightly wild `Cargo.toml`...) This means that most things aren't done with a plugin anymore. + +However, for tasks that involve injecting into Perseus' build system, a plugin is usually the right tool for the job. Let's say for example that you wanted to, before building the app, identify all Sass files that were being imported in the index view and compile them to CSS. You could do this by first parsing the index view, and then compiling everything at build-time. This is doable with plugins, though the mechanics of this particular example are fairly complex. + +In the general case, you can do most things in Perseus by playing around with the code that's exposed to you. If you just want to add something extra inside Perseus' internal processes though (e.g. adding a copyright header to all exported files), this is done with a plugin. If you're unsure, you can always ask in [a discussion]() or on [our Discord channel on the Sycamore server](). + +Perseus' plugins are based on *actions*, which you can make your plugin use to execute arbitrary code, as per [these examples](). There are three types of actions: functional, control, and tinker. Functional actions can have many plugins connected to them (e.g. adding more templates). Control actions can have just one plugin connected to them (e.g. modifying the index view). Tinker plugins are weird, they're executed on the special command `perseus tinker`, and they were originally designed to let people modify the code inside `.perseus/` (that legacy hidden folder), but now they're just...a thing. We haven't thought of any particular use-case for them yet, but there's not really any downside in having them, and, who knows, you might one day discover that a very particular niche application requires that extra step of explicitly executing `perseus tinker` to modify stuff. As for what those tinker plugins can do: literally anything. They're given the entire filesystem and they can roam free. Heck, you could use a tinker plugin to install an application on your computer, if you really wanted to! (And that's why you should only ever use trusted plugins!) + +On the note of security, plugins are extremely powerful. They can execute arbitrary code, and so can do basically whatever they want to your system. We have a list of publicly available plugins [here]() that are accompanied by little badges that indicate review by the Perseus dev team. Usually, those ones at least will be safe to use, though we strongly recommend reviewing the code of the plugins you use yourself, as we do NOT review each new version, and we do NOT keep track of changes to plugin maintainership. In other words, we take no responsibility whatsoever for anything that goes wrong when using a plugin --- make sure you trust the plugins you use! + +All examples of plugin usage are available [here](). diff --git a/docs/0.4.x/en-US/reference/plugins/control.md b/docs/0.4.x/en-US/reference/plugins/control.md deleted file mode 100644 index 27f3c801f7..0000000000 --- a/docs/0.4.x/en-US/reference/plugins/control.md +++ /dev/null @@ -1,22 +0,0 @@ -# Control Actions - -Control actions in Perseus can only be taken by one plugin, unlike [functional actions](:reference/plugins/functional), because, if multiple plugins took them, Perseus wouldn't know what to do. For example, if more than one plugin tried to replace the [immutable store](:reference/stores), Perseus wouldn't know which alternative to use. - -Control actions can be considered more powerful than functional actions because they allow a plugin to not only extend, but to replace engine functionality. - -## List of Control Actions - -Here's a list of all the control actions currently supported by Perseus, which will likely grow over time. You can see these in [this file](https://github.com/arctic-hen7/perseus/blob/main/packages/perseus/src/plugins/control.rs) in the Perseus repository. - -If you'd like to request that a new action, functional or control, be added, please [open an issue](https://github.com/arctic-hen7/perseus/issues/new/choose). - -_Note: there are currently very few control actions, and this list will be expanded over time._ - -- `settings_actions` -- actions that can alter the settings provided by the user with [`PerseusApp`](:reference/perseus-app) - - `set_immutable_store` -- sets an alternative [immutable store](:reference/stores) (e.g. to store data somewhere other than the filesystem for some reason) - - `set_locales` -- sets the app's locales (e.g. to fetch locales from a database in a more convenient way) - - `set_app_root` -- sets the HTML `id` of the `div` in which to render Perseus (e.g. to fetch the app root from some other service) -- `build_actions` -- actions that'll be run when the user runs `perseus build` or `perseus serve` as part of the build process (these will not be run in [static exporting](:reference/exporting)) -- `export_actions` -- actions that'll be run when the user runs `perseus export` -- `server_actions` -- actions that'll be run as part of the Perseus server when the user runs `perseus serve` (or when a [serverful production deployment](:reference/deploying/serverful) runs) -- `client_actions` -- actions that'll run in the browser when the user's app is accessed diff --git a/docs/0.4.x/en-US/reference/plugins/functional.md b/docs/0.4.x/en-US/reference/plugins/functional.md deleted file mode 100644 index 05aec7d572..0000000000 --- a/docs/0.4.x/en-US/reference/plugins/functional.md +++ /dev/null @@ -1,37 +0,0 @@ -# Functional Actions - -The first type of action that a Perseus plugin can take is a functional action, and a single functional action can be taken by many plugins. These are the more common type of Perseus action, and are extremely versatile in extending the capabilities of the Perseus engine. However, they don't have the ability to replace critical functionality on their own. - -## List of Functional Actions - -Here's a list of all the functional actions currently supported by Perseus, which will likely grow over time. You can see these in [this file](https://github.com/arctic-hen7/perseus/blob/main/packages/perseus/src/plugins/functional.rs) in the Perseus repository. - -If you'd like to request that a new action, functional or control, be added, please [open an issue](https://github.com/arctic-hen7/perseus/issues/new/choose). - -- `tinker` -- see [this section](:reference/plugins/tinker) -- `settings_actions` -- actions that can alter the settings provided by the user with [`PerseusApp`](:reference/perseus-app) - - `add_static_aliases` -- adds extra static aliases to the user's app (e.g. a [TailwindCSS](https://tailwindcss.com) stylesheet) - - `add_templates` -- adds extra templates to the user's app (e.g. a prebuilt documentation system) - - `add_error_pages` -- adds extra [error pages](:reference/error-pages) to the user's app (e.g. a prebuilt 404 page) -- `build_actions` -- actions that'll be run when the user runs `perseus build` or `perseus serve` as part of the build process (these will not be run in [static exporting](:reference/exporting)) - - `before_build` -- runs arbitrary code just before the build process starts (e.g. to run a CSS preprocessor) - - `after_successful_build` -- runs arbitrary code after the build process has completed, if it was successful (e.g. copying custom files into `.perseus/dist/`) - - `after_failed_build` -- runs arbitrary code after the build process has completed, if it failed (e.g. to report the failed build to a server crash management system) - - `after_failed_global_state_creation` -- runs arbitrary code after if the build process failed to generate global state -- `export_actions` -- actions that'll be run when the user runs `perseus export` - - `before_export` -- runs arbitrary code just before the export process starts (e.g. to run a CSS preprocessor) - - `after_successful_build` -- runs arbitrary code after the build process has completed (inside the export process), if it was successful (e.g. copying custom files into `.perseus/dist/`) - - `after_failed_build` -- runs arbitrary code after the build process has completed (inside the export process), if it failed (e.g. to report the failed export to a server crash management system) - - `after_failed_export` -- runs arbitrary code after the export process has completed, if it failed (e.g. to report the failed export to a server crash management system) - - `after_failed_static_copy` -- runs arbitrary code if the export process fails to copy the `static` directory (e.g. to report the failed export to a server crash management system) - - `after_failed_static_alias_dir_copy` -- runs arbitrary code if the export process fails to copy a static alias that was a directory (e.g. to report the failed export to a server crash management system) - - `after_failed_static_alias_file_copy` -- runs arbitrary code if the export process fails to copy a static alias that was a file (e.g. to report the failed export to a server crash management system) - - `after_successful_export` -- runs arbitrary code after the export process has completed, if it was successful (e.g. copying custom files into `.perseus/dist/`) - - `after_failed_global_state_creation` -- runs arbitrary code if the export process failed to generate global state -- `export_error_page_actions` --- actions that'll be run when exporting an error page - `before_export_error_page` --- runs arbitrary code before this process has started (providing the error code to be exported for and the output file) - - `after_successful_export_error_page` -- runs arbitrary code after this process has completed, if it was successful - - `after_failed_write` -- runs arbitrary code after this process has completed, if it couldn't write to the target output file -- `server_actions` -- actions that'll be run as part of the Perseus server when the user runs `perseus serve` (or when a [serverful production deployment](:reference/deploying/serverful) runs) - - `before_serve` -- runs arbitrary code before the server starts (e.g. to spawn an API server) -- `client_actions` -- actions that'll run in the browser when the user's app is accessed - - `start` -- runs arbitrary code when the Wasm delivered to the browser is executed (e.g. to ping an analytics service) diff --git a/docs/0.4.x/en-US/reference/plugins/intro.md b/docs/0.4.x/en-US/reference/plugins/intro.md deleted file mode 100644 index 637be83b34..0000000000 --- a/docs/0.4.x/en-US/reference/plugins/intro.md +++ /dev/null @@ -1,9 +0,0 @@ -# Plugins - -Perseus is extremely versatile, but there are some cases where is needs to be modified a little under the hood to do something very advanced. For example, as you'll learn [here](:reference/deploying/size), the common need for applying size optimizations requires modifying a file in the `.perseus/` directory, which requires [ejecting](:reference/ejecting). This is a laborious process, and makes updating difficult, so Perseus support a system of _plugins_ to automatically apply common modifications under the hood! - -First, a little bit of background. The `.perseus/` directory contains what's called the Perseus engine, which is basically the core of your app. The code you write is actually imported by this and used to invoke various methods from the `perseus` crate. If you had to build all this yourself, it would take a very long time! Because this directory can be automatically generated though, there's no need to check it into version control (like Git). However, this becomes problematic if you then want to change even a single file inside, because you'll then need to commit the whole directory, which can be unwieldy. More importantly, when updates come along that involve changes to that directory, you'll either have to delete it and re-apply your modifications to the updated directory, or apply the updates manually, either of which is overly tedious for simple cases. - -Perseus has plugins to help with this. At various points in the engine, plugins have what are called _actions_ that they can take. Those actions are then executed by the engine at the appropriate time. For example, if a plugin needed to run some code before a Perseus app initialized, it could do that by taking a particular action, and then the engine would execute that action just before the app initialized. - -There are two types of actions a plugin can take: _functional actions_, and _control actions_. A single functional action can be taken by many plugins, and they (usually) won't interfere with each other. For example, many plugins can add additional [static aliases](:reference/static-content) to an app. A single control action can only be taken by one plugin, because otherwise Perseus would have conflicting data. For example, if multiple plugins all set their own custom [immutable stores](:reference/stores), Perseus wouldn't know which one to use. Both types of actions are explained in detail in the following sections. diff --git a/docs/0.4.x/en-US/reference/plugins/publishing.md b/docs/0.4.x/en-US/reference/plugins/publishing.md deleted file mode 100644 index 54eb11ea1b..0000000000 --- a/docs/0.4.x/en-US/reference/plugins/publishing.md +++ /dev/null @@ -1,13 +0,0 @@ -# Publishing Plugins - -After you've written a plugin, you can either use it locally, or you can publish it to the world on <https://crates.io>, Rust's package registry. That will mean anyone in the world can use it in their own code, and you'll be contributing to the Perseus community! It's usual to name plugins beginning with `perseus-` (e.g. `perseus-size-opt`), but this isn't required. - -Perseus also maintains a registry of all plugins that have been published, but we rely on users to let us know about their plugins. You can do this by [opening an issue](https://github.com/arctic-hen7/perseus/issues/new/choose) on the Perseus repository, and we'll be happy to include your project! - -## Trusted Plugins - -You may have noticed that some plugins in the Perseus registry have ticks next to them. These plugins are _trusted_, meaning they've been reviewed by the Perseus team and are considered to be high quality and safe to use. Note however that this is in no way a guarantee of quality, and that a trusted plugin may still contain malware or bugs, and that the Perseus team is in no way responsible for any plugin on the registry. - -If you'd like to apply for your plugin to be trusted after it's been listed on the registry, reach out to the Perseus maintainer [by email](mailto:arctic_hen7@pm.me), and a code review will be happily undertaken. - -By the same token though, an untrusted plugin is not in any way an indication that a plugin is low quality or malicious, it just means it hasn't been reviewed by the Perseus team. If you don't want to have your plugin reviewed, no problem! diff --git a/docs/0.4.x/en-US/reference/plugins/security.md b/docs/0.4.x/en-US/reference/plugins/security.md deleted file mode 100644 index 67a518c979..0000000000 --- a/docs/0.4.x/en-US/reference/plugins/security.md +++ /dev/null @@ -1,14 +0,0 @@ -# Security Considerations of Plugins - -Perseus' plugins system makes it phenomenally versatile, and allows you to reshape default behavior in ways that are possible in very few other frameworks (especially frameworks built in compiled languages like Rust). However, this comes with a major security risk to your system, because plugins have the power to execute arbitrary code. - -## The Risks - -If you enable a plugin in your app, it will have the opportunity to run arbitrary code. The actions that plugins take are just functions that they provide, so a plugin could easily be saying that it's adding an extra [static alias](:reference/static-content) while simultaneously installing malware on your computer. - -## Precautions - -1. **Only ever use plugins that you trust!** Anyone can create a Perseus plugin, and some people may create plugins designed to install malware on your system. Optimally, you should review the code of every plugin that you install. -2. **Never run Perseus as root!** If you run Perseus and any plugins as the root user, a plugin can do literally anything on your computer, which could include installing privileged malware (by which point your computer would be owned by an attacker). - -**TL;DR:** don't use shady code, and don't run things with unnecessary privileges in general. diff --git a/docs/0.4.x/en-US/reference/plugins/tinker.md b/docs/0.4.x/en-US/reference/plugins/tinker.md deleted file mode 100644 index 7d308dc89c..0000000000 --- a/docs/0.4.x/en-US/reference/plugins/tinker.md +++ /dev/null @@ -1,11 +0,0 @@ -# The `tinker` Action - -There's one [functional action](:reference/plugins/functional) that's quite special in Perseus: the `tinker` action. This action doesn't run as part of any of the usual processes, and it actually has its own command in the CLI: `perseus tinker`. That's because this action allows plugins to modify the code of the Perseus engine. For example, applying [size optimizations](:reference/deploying/size) is a common requirement in Perseus apps, which means modifying `.perseus/Cargo.toml`. This is the perfect job for a plugin, but if it were done by any other action, you'd be modifying the `Cargo.toml` _after_ the code had been compiled, which means the modifications would have no effect until the next run. - -The `tinker` action solves this problem by creating its own process that's specifically designed for engine modification and tweaking. Until [#59](https://github.com/arctic-hen7/perseus/issues/59) is resolved, this is how you'd make major modifications to the `.perseus/` engine efficiently. - -## `perseus tinker` - -The `tinker` subcommand in the CLI has one simple function: to execute the tinkers of all the plugins an app uses. By default, it will delete and re-create the `.perseus/` directory to remove any corruptions (which are common with plugins that arbitrarily modify Perseus' code, as you can probably imagine). You can disable that behavior with the `--no-clean` flag. - -If you've ejected, running this command will lead to an error, because running tinkers after you've ejected may delete some of your modifications. Most plugins expect to start with the default engines, and your modifications may cause all sorts of problems. If you're certain your modifications won't interfere with things, you can add the `--force` flag to push on. Note that if you don't provide `--no-clean` as well, the entire `.perseus/` directory will be deleted irrecoverably! diff --git a/docs/0.4.x/en-US/reference/plugins/using.md b/docs/0.4.x/en-US/reference/plugins/using.md deleted file mode 100644 index 309000cc96..0000000000 --- a/docs/0.4.x/en-US/reference/plugins/using.md +++ /dev/null @@ -1,13 +0,0 @@ -# Using Plugins - -The plugins system is designed to be as easy as possible to use, and you can import plugins into your app like so (taken from [here](https://github.com/arctic-hen7/perseus/blob/main/examples/core/plugins/src/lib.rs)): - -```rust -{{#include ../../../../examples/core/plugins/src/lib.rs}} -``` - -In addition to the usual `PerseusApp` setup, this also uses the `.plugins()` function, passing to it an instance of `perseus::plugins::Plugins`, which manages all the intricacies of the plugins system. If this parameter isn't provided, it'll default to `Plugins::new()`, which creates the configuration for plugins without registering any. - -To register a plugin, we use the `.plugin()`/`.plugin_with_client_privilege()` function on `Plugins`, which takes two parameters: the plugin's definition (a `perseus::plugins::Plugin`) and any data that should be provided to the plugin. The former should be exported from the plugin's crate, and the latter you'll need to provide based on the plugin's documentation. Note that plugins can accept almost anything as data (specifically, anything that can be expressed as `dyn Any`). - -Plugins in Perseus are fantastic, but they're also a great way to increase your Wasm bundle size, which will make your website slower to laod when users first visit it. To mitigate this, Perseus lets plugin authors define where their plugins should run: in the browser (`PluginEnv::Client`), on the server-side (`PluginEnv::Server`), or on both (`PluginEnv::Both`). Plugins that only run on the server-side should be registered with `.plugin()`, and they will not be included in your final Wasm binary, which keeps your website nimble. If a plugin does need to run on the client though, it can be registered with `.plugin_with_client_privilege()` instead, which is named separately for conditional compilation reasons as well as to create a clear separation. But don't worry, if you accidentally register a plugin with the wrong function, your app won'y build, and Perseus will tell you that you've used the wrong function. diff --git a/docs/0.4.x/en-US/reference/plugins/writing.md b/docs/0.4.x/en-US/reference/plugins/writing.md deleted file mode 100644 index 54a40cd82f..0000000000 --- a/docs/0.4.x/en-US/reference/plugins/writing.md +++ /dev/null @@ -1,48 +0,0 @@ -# Writing Plugins - -Writing Perseus plugins is a relatively seamless process once you get the hang of the structure, and this section will guide you through the process. If you just want to use plugins, you can skip this section. - -## Structure - -A plugin will usually occupy its own crate, but it may also be part of a larger app that just uses plugins for convenience and to avoid [ejection](:reference/ejecting). The only thing you'll need in a plugin is the `perseus` crate, though you'll probably want to bring other libraries in (like `sycamore` if you're adding templates or error pages). - -## Defining a Plugin - -To define a plugin, you'll call `perseus::plugins::Plugin::new()`, which takes four parameters: - -- The name of the plugin as a `&str`, which should be the name of the crate the plugin is in (or the name of a larger app with some extension) (**all plugins MUST have unique names**) -- A [functional actions](:reference/plugins/functional) registrar function, which is given some functional actions and then extends them -- A [control actions](:reference/plugins/control) registrar, which is given some control actions and then extends them -- The environment for the plugin to run in (see below) - -Here's an example of a very simple plugin that adds a static alias for the project's `Cargo.toml`, creates an about page, and prints the working directory at [tinker](:reference/plugins/tinker)-time (taken from [here](https://github.com/arctic-hen7/perseus/blob/main/examples/core/plugins/src/plugin.rs)): - -```rust -{{#include ../../../../examples/core/plugins/src/plugin.rs}} -``` - -One particularly important thing to note here is the absence of any control actions in this plugin. Because you still have to provide a registrar, this function is using the `empty_control_actions_registrar` convenience function, which does exactly what its name implies. - -Another notable thing is the presence of `GenericNode` as a type parameter, because some plugin actions take this, so you'll need to pass it through. We also tell Perseus what type of data out plugin will take in the second type parameter, which enables type checking in the `.plugin()` call when the user imports the plugin. - -The rest of the code is the functional actions registrar, which just registers the plugin on the `functional_actions.settings_actions.add_static_aliases`, `functional_actions.settings_actions.add_templates`, and `functional_actions.tinker` actions. The functions provided to the `.register_plugin()` function are _runners_, which will be executed at the appropriate time by the Perseus engine. Runners take two parameters, _action data_, and _plugin data_. Action data are data provided to every runner for the given action (e.g. an action that runs after a failed build will be told what the error was). You should refer to [the API docs](https://docs.rs/perseus) to learn more about these for different actions. The second parameter is plugin data, covered below. - -## Plugin Data - -Quite often, plugins should accept user configuration, and this is supported through the second runner parameter, which will be given any data that the user defined for your plugin. You can define the type of this with the second type parameter to `Plugin`. - -However, because Perseus is juggling all the data for all the plugins the user has installed, across all their different runners, it can't store the type of the data that the user gives (but don't worry, whatever they provide will be type-checked). This means that your runner ends up being given what Rust considers to be _something_. Basically, **we know that it's your plugin data, but Rust doesn't**. Specifically, you'll be given `&dyn Any`, which means you'll need to _downcast_ this to a concrete type (the type of your plugin data). As in the above example, we can do this with `plugin_data.downcast_ref::<YourPluginDataTypeHere>()`, which will return an `Option<T>`. **This will always be `Some`**, which is why it's perfectly safe to label the `None` branch as `unreachable!()`. If this ever does result in `None`, then either you've tried to downcast to something that's not your plugin's data type, or there's a critical bug in Perseus' plugins system, which you should [report to us](https://github.com/arctic-hen7/perseus/issues/new/choose). - -## Caveats - -Right now, there are few things that you can't do with Perseus plugins, which can be quite weird. - -- You can't extend the engine's server (due to a limitation of Actix Web types), you'll need to manually run a `tinker` on it (add your code into the file by writing it in using [the `tinker` action](:reference/plugins/tinker)) -- You can't set the [mutable store](:reference/stores) from a plugin due to a traits issue, so you'll need to provide something for the user to set as a custom mutable store (see [here](:reference/stores/)) -- Similarly, you can't set the translations manager from a plugin - -## Plugin Environments - -As explained [here](:reference/plugins/using), plugins can either run on the client (`PluginEnv::Client`), the server-side (`PluginEnv::Server`), or on both (`PluginEnv::Both`). Note that the server-side includes `tinker`-time and during the build process. If your plugin does not absolutely need to run on the client, use `PluginEnv::Server`! Your users will thank you for their much smaller bundles! If you don't do this, every single dependency of your plugin will end up in the user's final Wasm bundle, which has to be sent to browsers, and bundle sizes can end up doubling or more as a result! If this is the case though, make sure to tell your users to register your plugin using `.plugin_with_client_privilege()` rather than just `.plugin()` (but don't stress, they'll get an explanatory error if they use the wrong one accidentally). - -You can set the environment your plugin runs on by changing the fourth argument to a variant of `PluginEnv`. diff --git a/docs/0.4.x/en-US/reference/server-communication.md b/docs/0.4.x/en-US/reference/server-communication.md deleted file mode 100644 index a364adfec4..0000000000 --- a/docs/0.4.x/en-US/reference/server-communication.md +++ /dev/null @@ -1,42 +0,0 @@ -# Communicating with a Server - -So far, we've described how to use Perseus to build powerful and performant frontend apps, but we've mostly left out the backend. If you want to fetch data from a database, authenticate users, perform server-side calculations or the like, you'll almost certainly want a backend of some kind. - -<details> -<summary>Frontend? Backend?</summary> - -In web development, we typically refer to a project as having a _frontend_, which is the thing users see (i.e. your web app, with all its styling and the like), and a _backend_, which is a server or serverless function (see below) that performs server-side work. A classic example would be a server that communicates with a database to fetch some data, but it needs to authenticate against the database. If you're new to web dev, you might well be thinking we could just query the database from the web app, but that would mean we'd have to store the access token in our frontend code, which can be easily inspected by the user (albeit less easily with Wasm, but still definitely doable). For that reason, we communicate with a server and ask it to get the data from the database for us. - -Of course, a much simpler way of doing the above would be to make the database not need authentication in the first place, but the point stands. - -</details> - -Perseus has an inbuilt server that serves your app and its data, and this can be extended by your own code. However, this requires [ejecting](:reference/ejecting), which can be brittle, because you'll have to redo everything every time there's a major update. This is NOT the recommended approach for setting up your backend! - -Instead, it's recommended that you create a server separate from Perseus that you control completely. You might do this with [Actix Web](https://actix.rs) or similar software. You could even set up serverless functions on a platform like [AWS Lambda](https://aws.amazon.com/lambda), which can reduce operation costs. - -## Querying a Server - -Querying a server in Perseus is fairly simple, thoguh there are two different environments in which you'll want to do it, which are quite different from each other: on the server and in the browser. The main reason for this difference is because, in the browser, we're limited to the Web APIs, which are restricted by [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS), meaning the browser will ask APIs you query if they'r eexpecting your app, which they won't be unless they've been configured to. For this reason, it's nearly always best to proxy requests to third-party APIs through your own server, which you can configure CORS on as necessary. In many cases, you can even perform third-party queries entirely at build-time and then pass through the results as state to pages. - -Here's an example of both approaches (taken from [here](https://github.com/arctic-hen7/perseus/tree/main/examples/demos/fetching)): - -```rust -{{#include ../../../examples/demos/fetching/src/templates/index.rs}} -``` - -### Build-Time - -In the above example, we fetch the server's IP address at build-time from <https://api.ipify.org> using [`reqwest`](https://docs.rs/reqwest), a simple HTTP client. Note that Perseus gives you access to a full Tokio `1.x` runtime at build time, meaning asynchronous clients like this can easily be used. - -One problem of fetching data at build-time though, in any framework, is that you have to fetch it again every time you rebuild your app, which slows down the build process and thus slows down your development cycle. To alleviate this, Perseus provides two helper functions, `cache_res` and `cache_fallible_res` (used for functions that return a `Result`) that can be used to wrap any asynchronous code that runs on the server-side (e.g. at build-time, request-time, etc.). The first time they run, these will just run your code, but then they'll cache the result to a file in `.perseus/`, which can be used in all subsequent requests, making your long-running code (typically network request code, but you could even put machine learning stuff in them in theory...) run almost instantaneously. Of course, sometimes you'll need to re-run that asynchronous code if you change something, which yo ucan do trivially by changing the second argument from `false` to `true`, which will override the cache and always re-run the given code. - -Incidentally, you can also use those functions to work in an offline environment, even if your app includes calls to external APIs at build time. As long as you've called your app's build process once so that Perseus can cache all the requests, it won't make any more network requests in development unless you tell it to explicitly or delete `.perseus/cache/`. - -Note also that those functions don't have to be removed for production, they'll automatically be disabled. - -### In the Browser - -In the above example's `index_page()` function, we perform some request logic that we want to do in the browser. It's important to remember here that Perseus will run your template's code on the server as well when it prerenders (which happens more often than you may think!), so if we want to only run something in the browser, we have to check with `G::IS_BROWSER` (usefully provided by Sycamore). From there, the comments in the code should mostly explain what we're doing, but the broad idea is to spawn a `Future` in the browser (which we do with a function that Perseus re-exports from another library called [`wasm-bindgen-futures`](https://docs.rs/wasm-bindgen-futures)) that uses a library like [`reqwasm`](https://docs.rs/reqwasm) (a wrapper over the browser's Fetch API) to get some data. In this example, we fetch that data from some static content on the same site, which avoids issues with [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) (something you will very much want to understand, because it can generate some very confusing errors, especially for those new to web development). - -As for what we do with the data we fetch, we just modify a Sycamore `Signal` to hold it, and our `view! {...}` will update accordingly! diff --git a/docs/0.4.x/en-US/reference/snooping.md b/docs/0.4.x/en-US/reference/snooping.md deleted file mode 100644 index 10d35a5262..0000000000 --- a/docs/0.4.x/en-US/reference/snooping.md +++ /dev/null @@ -1,15 +0,0 @@ -# Snooping on the CLI - -Most of the time, it's fine to run `perseus serve` and enjoy the results, but sometimes you'll need to delve a little deeper and see some more detailed logs. Then you need `perseus snoop`. This command will run one of the lower-level steps the Perseus CLI runs, but in such a way that you can see everything it does. The time you'll use this the most is when you have a `dbg!()` call in the static generation process (e.g. in the *build state* strategy) and you want to actually see its output, which neither `perseus build` nor `perseus serve` will let you do. - -## `perseus snoop build` - -This snoops on the static generation process, which is half of what `perseus build` does. You can use this to see the outputs of `dbg!()` calls in your build-time code. - -## `perseus snoop wasm-build` - -This snoops on the `wasm-pack` call that compiles your app to Wasm. There aren't really any use cases for this outside debugging strange errors, because Perseus calls out to this process without augmenting it in any way, so your code shouldn't impact this really at all (unless you're using some package that can't be compiled to Wasm). - -## `perseus snoop serve` - -This snoops on the server, which is useful if you're hacking on it, or if you're getting errors from it (e.g. panics in the server will only appear if you run this). Crucially though, this expects to be working with a correct build state, which means **you must run `perseus build` before running this command**, otherwise all sorts of things could happen. If such things do happen, you should run `perseus clean --dist`, and that should solve things. diff --git a/docs/0.4.x/en-US/reference/state/freezing.md b/docs/0.4.x/en-US/reference/state/freezing.md deleted file mode 100644 index bad1bc5230..0000000000 --- a/docs/0.4.x/en-US/reference/state/freezing.md +++ /dev/null @@ -1,37 +0,0 @@ -# State Freezing - -If you use the reactive and global state systems to their full potential, your entire app can be represented as its state. So what if you could make all that state unreactive again, serialize it to a string, and keep it for later? Well, you'd be able to let your users pick up at the *exact* same place they were when they come back later. Imagine you're in the middle of filling out some forms and then your computer crashes. You boot back up and go to the website you were on. If it's built with Perseus and state freezing occurred just before the crash, you're right back to where you were. Same page, same inputs, same everything. - -Specifically, Perseus achieves this by serializing the global state and the page state store, along with the route that the user's currently on. You can invoke this easily by running `.freeze()` on the render context, which you can access with `perseus::get_render_ctx!()`. Best of all, if state hasn't been used yet (e.g. a page hasn't been visited), it won't be cached, because it doesn't need to be. That also applies to global state, meaning the size of your frozen output is minimized (note that this isn't property-level granular yet, but that *might* be investigated in future). - -## Example - -You can easily imperatively instruct your app to freeze itself like so (see [here](https://github.com/arctic-hen7/perseus/tree/main/examples/core/freezing_and_thawing/src/templates/index.rs)): - -```rust -{{#include ../../../../examples/core/freezing_and_thawing/src/templates/index.rs}} -``` - -## Thawing - -Recovering your app's state from a frozen state is called *thawing* in Perseus (basically like hydration for state, but remember that hydration is for views and thawing is for state!), and it can occur gradually and automatically once you provide Perseus a frozen state to use, which you can do by calling `.thaw()` on the render context (which you can get with `perseus::get_render_ctx!()`). How you store and retrieve frozen state is entirely up to you. For example, you could store the user's last state in a database and then fetch that when the user logs in, or you could store it in IndexedDB and have even logging in be covered by it (if authentication tokens are part of your global state). Note that thawing will also return the user to the page they were on when the state was thawed. - -One important thing to understand about thawing though is how Perseus decided what state to use for a template, because there can be up to three options. Every template that accepts state will have generated state that's provided to it from the generation proceses on the server, but there could also be a frozen state and an active state (some state that's already been made reactive). The server-generated state is always the lowest priority, and it will be used if no active or frozen state is available. However, deciding between frozen and active state is more complicated. If only one is available, it will of course be used, but it both are available, the choice is yours. You can represent this choice through the `ThawPrefs` `struct`, which must be provided to a call to `.thaw()` as the second argument. This has two fields, one for page state, and another for global state. For global state, you can set the `global_prefers_frozen` field to `true` if you want to override active global state with a frozen one. For page state, you'll use `PageThawPrefs`, which can be set to `IncludeAll` (all pages will prefer frozen state), `Include(Vec<String>)` (the listed pages will prefer frozen state, all others will prefer active state), or `Exclude(Vec<String>)` (the listed pages will prefer active state, all others will prefer frozen state). There's no `ExcludeAll` option because that would defeat the entire purpose of thawing. - -It may at first be tempting to use `IncludeAll`, but this is an important UX decision that you should consider carefully. Using frozen state when active state isn't available is automatic, but preferring frozen state *over* active state translates to something like this: a user does some stuff, then state is thawed, everything they did at the start is gone and replaced with whatever they did in the previous session. This might be entirely reasonable in pages that can only be accessed after thawing is complete, but in pages that are accessible at all times, this could be extremely irritating to your users! - -<details> -<summary>Thawing isn't working...</summary> - -It may seem sometimes like thawing has completely failed, and this is usually for one of two reasons. - -1. You're extracting the state of another page. -2. You're getting the global state without having it as the second argument to your template function. (In other words, you're getting it manually through `perseus::get_render_ctx!().global_state.borrow()`). - -In the first case, the reasoning is simple. Statw thawing is a gradual process, so the state for a page won't be thawed until the user actually visits that page. This is why it's much better to use global state for state that needs to be shared between pages, and you should generally avoid extracting state from other pages. - -In the second case, the reason is similar. When you get the global state directly in this way, you bypass the thawing process altogether, meaning thawed state won't show up. If you need to access the global state, you should do it by making it the second argument to your template function (as documented [here](:reference/state/global)). - -*Note: in a future version of Perseus, thawing logic may be altered so that direct access does become possible, but it's currently not.* - -</details> diff --git a/docs/0.4.x/en-US/reference/state/global.md b/docs/0.4.x/en-US/reference/state/global.md deleted file mode 100644 index a95aa6187b..0000000000 --- a/docs/0.4.x/en-US/reference/state/global.md +++ /dev/null @@ -1,40 +0,0 @@ -# Global State - -As you've seen, Perseus has full support for reactive state in templates, but what about state that's not associated with any template? The usual example is something like dark mode, which the user might manually disable. In most JavaScript frameworks, you'd bring in some bloated state management system to handle this, but Perseus has global state built in. To declare it, you create a `GlobalStateCreator`, which will be used to generate some state, and then that'll be made reactive and passed to your templates as their second argument (if they have one, and you'll have to use the `#[template_rx]` macro). - -The essence of global state is that you can generate it at build-time (though with something like setting dark mode, you'll probably want to ignore whatever was set at build time until you know the browser's preferences) and access it seamlessly from any template. Just like usual [reactive state](:reference/state/rx), you can make it reactive with `#[make_rx(...)]`, and you essentially get app-wide MVC with just a few lines of code (and no extra dependencies, all this is completely built into Perseus). - -<details> -<summary>How would I actually implement dark mode like this?</summary> - -Above is a contrived example. In reality, dark mode is set in two ways: the preference that the browser exposes and the user's own manual setting (usually through a toggle switch in the header or similar). You obviously don't want to start in light mode and then flicker to dark mode once you know the user's preference, that would be awful, so it's far better to rely on a class in the HTML (e.g. if `dark` is set on the `<body>`, certain things should be styled in different ways) that you set based on a cookie that you've stored, falling back to the `prefers-color-scheme` if there's no cookie yet. Perseus is designed to load content and then make it interactive, so if you do this cookie-checking in your Wasm ode, you'll be too late to avoid that flicker, which is why it's better to either do this with a separate Wasm bundle, or with a quick bit of JS written directly into your `index.html` file. There are plenty of guides on how to achieve this online. - -The role of dark mode comes in in styling that toggle switch mostly, and whenever it changes, you should toggle the `dark` class on the `<body>` or similar and update the global state. YOu could to the class toggling with a `create_effect` that listens for changes in the global state. So, in this case, global state actually isn't crucial, it just makes things cleaner and easier to set up. Unfortunately though, dark mode is irritating with any prerendering because you want to avoid that flicker. (That said, if you don't mind temporarily blinding your 3am users, do whatever you like!) - -</details> - -## Example - -All the following examples are taken from [here](https://github.com/arctic-hen7/perseus/blob/main/examples/global_state). - -To being with, you'll need to set up a `GlobalStateCreator`, which will look something like this (it's supposed to be fairly similar to the process of generating state for a `Template`, but it currently only supports build-time state generation): - -```rust -{{#include ../../../../examples/core/global_state/src/global_state.rs}} -``` - -Then, you can tell Perseus about that by adding it to `PerseusApp` like so: - -```rust -{{#include ../../../../examples/core/global_state/src/lib.rs}} -``` - -Finally, you can use it like so (note the second argument to `index_page`): - -```rust -{{#include ../../../../examples/core/global_state/src/templates/index.rs}} -``` - -## Potential Issues - -Global state has a quirk that shouldn't be an issue for most, but that can be very helpful to know about if you start to dig into the internals of Perseus. Global state is passed down from the server as a window-level JS variable (as with template state), but it doesn't immediately get deserialized and registered, it's loaded lazily. So, if the user loads fifty templates that don't access global state, your app won't initialize the global state. But, the moment you take it as an argument to a template, it will be set up. This means that, while you can access the global state through the render context (with `perseus::get_render_ctx!()`), you shouldn't do this except in templates that already take the global state as an argument. It may seem tempting to assume that the user has already gone to another page which has set up global state, but no matter how the flow of your app works, you mustn't assume this because of [state freezing](:reference/state/freezing), which can break such flows. Basically, don't access the global state through the render context, you almost never need to and it may be wrong. Trust in `#[template_rx]`. diff --git a/docs/0.4.x/en-US/reference/state/hsr.md b/docs/0.4.x/en-US/reference/state/hsr.md deleted file mode 100644 index 49e0647aa3..0000000000 --- a/docs/0.4.x/en-US/reference/state/hsr.md +++ /dev/null @@ -1,27 +0,0 @@ -# Hot State Reloading - -If you've started using Perseus with reactive state, you may have already noticed something pretty cool. If you're using `-w` in the CLI to rebuild your app when you change your code, you'll find that the state of your app is maintained! This is called *hot state reloading*, and it's currently a feature entirely unique to Perseus. - -If you've come from the JavaScript world, you might have heard of *hot module reloading*, or HMR, which is used by many JS frameworks to figure out which parts of your code you changed in development so that they only need to change the smallest part, meaning most of your app's state is retained. This approach requires being able to break your code up into many chunks, which is easy with JS, but currently extremely difficult with Wasm, so Perseus takes a different approach. - -Perseus supports [state freezing](:reference/state/freezing) automatically, which allows you to store the entire state of your app in a string and reload from that at any time. Perseus also supports [freezing that state to IndexedDB](:reference/state/idb-freezing). When you combine that with Perseus' [live reloading](:reference/live-reloading) system, why not freeze the state before every live reload and thaw it afterward? This is exactly what Perseus does, and it means that you can change your code and pick up from *exactly* where you were in your app without missing a beat. - -If you ever want to ditch the current state completely, just manually reload the page in your browser (usually by pressing `Ctrl+R` or the reload button) and Perseus will start your app afresh! - -HSR is inbuilt into Perseus, and it automatically enabled for all development builds. When you build for production (e.g. with `perseus deploy`), HSR will automatically be turned off, and you won't have to worry about it anymore. If you feel HSR gets in your way, you can easily disable it by disabling Perseus' default features (by adding `default-features = false` to the properties of the dependency `perseus` in your `Cargo.toml`). Note though that this will also disable [live reloading](:reference/live-reloading), and you'll need to manually the `live-reload` feature to get that back. - -## Problems - -HSR is far less buggy than most implementations of HMR because it takes advantage of features built into Perseus' core, though there are some cases in which it won't work. If you're finding that you make a modification to your code and your last state isn't being restored, you'll probably find the reason here. - -### Incorrect Data Model - -This is the most common case. If you're finding that most state properties are being restored except one or two, then you'll probably find that that those properties aren't in your template's data model. In other words, they aren't part of the state for your template that you're providing to Perseus, which usually means you're setting them up as `Signal`s separately. Moving them into your data model should solve your problems. - -### New Data Model - -If you've changed the structure of your template's data model, for example by adding a new property that it includes, then you'll find that Perseus can't deserialize the state it kept from before properly (it saved your old data model, which is different to the new one), so it'll abort attempting to thaw the old state and regenerate from scratch. Unfortunately, due to the strictness of Rust's type system, this is unavoidable. - -### Corrupt Entry - -If your state isn't restored and the above reasons don't fit, then it's possible that the state may have somehow been corrupted. That said, this is very unlikely, and really shouldn't happen outside contrived scenarios. That said, Perseus should automatically resolve this by clearing the stale state and the next reload should work properly. If it doesn't, you should manually reload the page to get out of any strange logic issues. If that still doesn't work, try going into your browser's developer tools and making the console logs persist across reloads, there could be some helpful error messages in there (if they occur just before the CLi-induced reload, they'll be wiped away by the browser). diff --git a/docs/0.4.x/en-US/reference/state/idb-freezing.md b/docs/0.4.x/en-US/reference/state/idb-freezing.md deleted file mode 100644 index c0a35f8a24..0000000000 --- a/docs/0.4.x/en-US/reference/state/idb-freezing.md +++ /dev/null @@ -1,23 +0,0 @@ -# Freezing to IndexedDB - -One of the most common places to store frozen state is inside the browser, which can be done with Perseus' inbuilt `IdbFrozenStateStore` system, which uses [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) to store as many frozen states as you want, allowing you to revert to not just the previous state, but the one before that, the one before that, etc. - -To use this system, you'll need to enable the `idb-freezing` feature flag, and then you can use the system as per the below example. - -# Example - -The following code is taken from [here](https://github.com/arctic-hen7/perseus/tree/main/examples/idb_freezing/src/idb.rs). - -```rust -{{#include ../../../../examples/core/idb_freezing/src/templates/index.rs}} -``` - -This example is very contrived, but it illustrates the fundamentals of freezing and thawing to IndexedDB. You'll need to perform most of this logic in a `wasm_bindgen_futures::spawn_local()`, a function that spawns a future in the browser, because the IndexedDB API is asynchronous (so that costly DB operations don't block the main UI thread). The first button we have in this example has its `on:click` handler set to one of these futures, and it then freezes the state, initializes the database (which will either create it or open it if it already exists), and then calls `.set()` to set the new frozen state (which will remove previously stored frozen states in the background). The rest of the code here is just boilerplate for reporting successes or failures to the user. - -Notably, the operations you'll perform through `IdbFrozenStateStore` are all fallible, they can all return an `Err`. These cases should be handled carefully, because there are a myriad number of causes (filesystem errors in the browser, invalid data, etc.). Perseus tries to shield you from these as much as possible, but you should be wary of potentially extremely strange errors when working with IndexedDB (they should be very rare though). If your app experiences an error, it's often worth retrying the operation once to see if it works the second time. If you're having trouble in local development, you should use your browser's developer tools to delete the `perseus` database. - -As for thawing, the process is essentially the same, except in reverse, and it should be noted that the `.thaw()` method is fallible, while the `.freeze()` method is not. This is due to the potential issues of accepting a frozen state of unknown origin. - -One thing that may seem strange here is that we get the render context outside the click handlers. The reason for this is that the render context is composed almost entirely of `Signal`s and the like, so once you have one instance, it will update. Further, we actually couldn't get the render context in the futures even if we tried, since once we go into the future, we decouple from Sycamore's rendering system, so the context no longer exists as far as it's concerned. We can work around this, but for simplicity it's best to just get the render context at the beginning and use it later. - -It's also important to understand that we don't freeze straight away, but only when the user presses the button, since the result of `.freeze()` is an unreactive `String`, which won't update with changes to our app's state. diff --git a/docs/0.4.x/en-US/reference/state/rx.md b/docs/0.4.x/en-US/reference/state/rx.md deleted file mode 100644 index 238420a16e..0000000000 --- a/docs/0.4.x/en-US/reference/state/rx.md +++ /dev/null @@ -1,35 +0,0 @@ -# Reactive State - -In v0.3.4, Perseus added support for *reactive state*, which we talked about a bit in the tutorials at the beginning of the documentation. If you've come from a Perseus version before v0.3.4, this system will be quite new to you, as it adds a whole new platform on which templates can interact with their state. Originally, you could generate state, and then it would be done, and the template would receive it as is. Now, that state can be made *reactive* by wrapping all its fields inside `Signal`s, and it will then be added to a global store of page state. The platform this is built on allows a whole new level of state mechanics in Perseus, including [global state](:reference/state/global) and even [hot state reloading](:reference/state/hsr) (a world first to our knowledge)! - -In essence, Perseus now provides a way to make your state automatically reactive, which enables some *really* cool new features! - -To use this new platform, just annotate a state `struct` with `#[perseus::make_rx(RxName)]`, where `RxName` is the name of the new reactive `struct` (e.g. `IndexState` might become `IndexStateRx`). This macro wraps every single property in your `struct` in a `Signal` and produces a new reactive version that way, implementing `perseus::state::MakeRx` on the original to provide a method `.make_rx()` that can be used to convert from the unreactive version to the reactive one (there's also the reverse through `perseus::state::MakeUnrx`, which is implemented on the new, reactive version). If you have fields on your `struct` that are themselves `struct`s, you'll need to nest that reactivity, which you can do by adding `#[rx::nested("field", FieldRxName)]` just underneath the `#[make_rx(...)]` macro, providing it the name of the field and the type of the reactive version (which you'd generated with `#[make_rx(...)]`). Notably, `#[make_rx(...)]` automatically derives `Serialize`, `Deserialize`, and `Clone` on your `struct` (so don't derive them yourself!). - -*Note: Sycamore has a proposal to support fine-grained reactivity like this through observers, which will supersede this when they're released, and they'll make all this even faster! Right now, everything has to be cloned unfortunately.* - -Once you've got some reactive versions of your state `struct`s ready, you should generate the unreactive versions as usual in functions like `get_build_state()`, but then set the first argument on your template function to the reactive version (e.g. `IndexStateRx` rather than `IndexState`). This requires Perseus to convert between the unreactive and reactive versions in the background, which you can enable by changing the old `#[template(...)]` (used in the old documentation/tutorials) to `#[template_rx]` and removing the Sycamore `#[component]` annotation (this is added automatically by `#[template_rx]`). Behind the scenes, you've just enabled the world's most powerful state platform, and not only will your state be made reactive for you, it will be added to the *page state store*, a global store that enables Perseus to cache the state of a page. So, if your users start filling out forms on page 1 and then go to page 2, and then come back to page 1, their forms will be just how they left them. (Not sure about you, but it feels to us like it's about time this was the default on the web!) - -*Side note: if you think this behavior is horrific, you can still use the old `#[template(...)] macro, and we have no plans to deprecate it. Perseus' original unreactive state system worked very well, and there are still plenty of use cases where you may not want all this newfangled reactive state nonsense (like completely static blogs).* - -You may be wondering what the benefits of having a reactive state are. Well, the intention is this: every possible state your page can be in should be representable in your state. That means that, whenever you'd usually declare a new variable in a `Signal` to handle some state, you can move it into your template's state and handle it there instead, making things cleaner and taking advantage of Perseus' state caching system. If your entire app doesn't use any of this though, you can still trivially use the old state platform if you want to. - -## Example - -This can all be a bit hard to imagine, so here's how it looks in practice with a simple state involving a `username` that the user can type in, and then it'll be displayed back to them. You can see the source [here](https://github.com/arctic-hen7/perseus/blob/main/examples/core/rx_state/src/templates/index.rs). - -```rust -{{#include ../../../../examples/core/rx_state/src/templates/index.rs}} -``` - -The only particularly unergonomic thing here is that we have to `.clone()` the `username` so that we can both `bind:value` to it and display it. Note that this will be made unnecessary with Sycamore's new reactive primitives (which will be released soon). - -## Accessing Another Page's State - -Because every template that uses this pattern will have its state added to a special *page state store*, you can actually access the state of another page quite easily. However, you must be careful doing this, because the other page's state will only be available if it's been loaded by the user. On the server, every page is loaded in its own little silo to prevent corruption, so no other page will ever have been 'loaded'. As for in the browser, you might design an app in which it's only possible to get to a certain page by going through another, but you still can't assume that that page has been loaded, because [state freezing](:reference/state/freezing) can let a user pick up from any page in your app, and such special rendering flows will be shattered. - -All that said, you can access another page's state like so (see [here](https://github.com/arctic-hen7/perseus/blob/main/examples/core/rx_state/src/templates/about.rs)): - -```rust -{{#include ../../../../examples/core/rx_state/src/templates/about.rs}} -``` diff --git a/docs/0.4.x/en-US/reference/static-content.md b/docs/0.4.x/en-US/reference/static-content.md deleted file mode 100644 index 90416d700f..0000000000 --- a/docs/0.4.x/en-US/reference/static-content.md +++ /dev/null @@ -1,21 +0,0 @@ -# Static Content - -It's very routine in a web app to need to access _static content_, like images, and Perseus supports this out-of-the-box. Any and all static content for your website that should be served over the network should be put in a directory called `static/`, which should be at the root of your project (NOT under `src/`!). Any files/folders you put in there will be accessible on your website at `/.perseus/static/[filename-here]` **to anyone**. If you need content to be protected in some way, this is not the mechanism to use (consider a separate API endpoint)! - -## Aliasing Static Content - -One problem with making all static content available under `/.perseus/static/` is that there are sometimes occasions where you need it available at other locations. The most common example of this is `/favicon.ico` (the little logo that appears next to your app's title in a browser tab), which must be at that path. - -_Static aliases_ allow you to handle these conditions with ease, as they let you define static content to be available at any given path, and to map to any given file in your project's directory. - -Here's an example of defining static aliases in `PerseusApp` from [here](https://github.com/arctic-hen7/perseus/blob/main/examples/core/static_content/src/lib.rs): - -```rust -{{#include ../../../examples/core/static_content/src/lib.rs}} -``` - -### Security - -Of course, being able to serve any file on your system in a public-facing app is a major security vulnerability, so Perseus will only allow you to create aliases for paths in the current directory. Any absolute paths or paths that go outside the current directory will be disallowed. Note that these paths are defined relative to the root of your project. - -**WARNING:** if you accidentally violate this requirement, your app **will not load** at all! diff --git a/docs/0.4.x/en-US/reference/stores.md b/docs/0.4.x/en-US/reference/stores.md deleted file mode 100644 index 191ff75ade..0000000000 --- a/docs/0.4.x/en-US/reference/stores.md +++ /dev/null @@ -1,19 +0,0 @@ -# Stores - -Perseus has a very unique system of managing data as far as frameworks go, because it sometimes needs to change files it generated at build-time. This would be fine on an old-school server where you control the filesystem, but many modern hosting providers have read-only filesystems, which makes working with Perseus problematic. - -As a solution to this, Perseus divides its data storage into two types: *mutable* (possibly changing at runtime) and *immutable* (never changing after build-time). These correspond to two types in Perseus: `ImmutableStore` (a `struct`) and `MutableStore` (a `trait`). - -## Immutable Stores - -An immutable store is used for all data that won't be changed after it's initially created, like for data about pages that are pre-rendered at build-time that don't revalidate. Because it's read-only after the build process, it can be used on a hosting provider with a read-only filesystem without problems, and so immutable stores always work on the filesystem. - -## Mutable Stores - -There are two classes of data that need to be modified at runtime in Perseus: data about pages that can revalidate, and pages cached after incremental generation. There are many ways to deploy a Perseus app, and some involve a read-only filesystem, in which case you'll likely want to use an external database or the like for mutable data. Perseus makes this as easy as possible by making `MutableStore` a `trait` with two simple methods: `read` and `write`. You can see more details in the [API docs](https://docs.rs/perseus). - -By default, Perseus will use `FsMutableStore`, an implementation of `MutableStore` that uses the filesystem at the given path, which is set to `.perseus/dist/mutable/` by default. On hosting providers where you can write to the filesystem and have your changes reliably persist, you can leave this as is. But, if you're using a provider like Netlify, which imposes the restriction of a read-only filesystem, you'll need to implement the `MutableStore` `trait` yourself for a database or the like. - -### Performance of External Mutable Stores - -There are significant downsides to using a non-filesystem mutable store in terms of performance, especially if that store is on another server. Remember that every request to an incrementally-generated page or a page that revalidates will use this external store, which means the request has to travel to the server, to the store, from the store, and from the server, twice as many trips as if the store was on the filesystem. For this reason, Perseus splits data storage into mutable and immutable stores, allowing you to incur these performance costs from the smallest amount of data possible. In previous versions, these stores were combined together, which was problematic for large-scale deployments. diff --git a/docs/0.4.x/en-US/reference/strategies/amalgamation.md b/docs/0.4.x/en-US/reference/strategies/amalgamation.md deleted file mode 100644 index fbd93f45ba..0000000000 --- a/docs/0.4.x/en-US/reference/strategies/amalgamation.md +++ /dev/null @@ -1,13 +0,0 @@ -# State Amalgamation - -In the introduction to this section, we mentioned that all these rendering strategies are compatible with one another, though we didn't explain how the two strategies that generate unique properties for a template can possible be compatible. That is, how can you use _build state_ and _request state_ in the same template? To our knowledge, Perseus is the only framework in the world (in any language) that supports using both, and it's made possible by _state amalgamation_, which lets you provide an arbitrary function that can merge conflicting states from these two strategies! - -## Usage - -Here's an example from [here](https://github.com/arctic-hen7/perseus/blob/main/examples/core/state_generation/src/templates/amalgamation.rs): - -```rust -{{#include ../../../../examples/core/state_generation/src/templates/amalgamation.rs}} -``` - -This example illustrates a very simple amalgamation, taking the states of both strategies to produce a new state that combines the two. Note that this also uses `RenderFnWithCause` as a return type (see the section on the [_build state_](:reference/strategies/build-state) strategy for more information). It will be passed an instance of `States`, which you can learn more about in the [API docs](https://docs.rs/perseus). As usual, serialization of your returned state is done with the `#[perseus::autoserde(amalgamate_states)]` macro, though the components of `States` will **not** be deserialized, and you'll have to do that manually. Note that the next major version of Perseus will deserialize the components of `States` automatically. diff --git a/docs/0.4.x/en-US/reference/strategies/build-paths.md b/docs/0.4.x/en-US/reference/strategies/build-paths.md deleted file mode 100644 index cd41fd4c3c..0000000000 --- a/docs/0.4.x/en-US/reference/strategies/build-paths.md +++ /dev/null @@ -1,19 +0,0 @@ -# Build Paths - -As touched on in the documentation on the _build state_ strategy, Perseus can easily turn one template into many pages (e.g. one blog post template into many blog post pages) with the _build paths_ strategy, which is a function that returns a `Vec<String>` of paths to build. - -Note that it's often unwise to use this strategy to render all your blog posts or the like, but only render the top give most commonly accessed or the like, if any at all. This is relevant mostly when you have a large number of pages to be generated. The _incremental generation_ strategy is better suited for this, and it also allows you to never need to rebuild your site for new content (as long as the server can access the new content). - -Note that, like _build state_, this strategy may be invoked at build-time or while the server is running if you use the _revalidation_ strategy (_incremental generation_ doesn't affect _build paths_ though). - -## Usage - -Here's the same example as given in the previous section (taken from [here](https://github.com/arctic-hen7/perseus/blob/main/examples/core/state_generation/src/templates/build_paths.rs)), which uses _build paths_ together with _build state_: - -```rust -{{#include ../../../../examples/core/state_generation/src/templates/build_paths.rs}} -``` - -Note the return type of the `get_build_paths` function, which returns a `RenderFnResult<Vec<String>>`, which is just an alias for `Result<T, Box<dyn std::error::Error>>`, which means that you can return any error you want. If you need to explicitly `return Err(..)`, then you should use `.into()` to perform the conversion from your error type to this type automatically. Perseus will then format your errors nicely for you using [`fmterr`](https://github.com/arctic-hen7/fmterr). - -Also note how this page renders the page `/build_paths` by specifying an empty string as one of the paths exported from `get_build_paths`. It's a very common structure to have something like `/blog` and then `/blog/<post-name>` as a website structure, with `/blog` being a list of all posts or the like. However, Perseus would require this to all be done in the same template (since it's all under the same URL), so you'd need to use an `enum` as your state that would then render an alternative view for the root page. Alternatively, you could use a `/post/<post-name>` and `/posts` structure, which would let you use two different templates. diff --git a/docs/0.4.x/en-US/reference/strategies/build-state.md b/docs/0.4.x/en-US/reference/strategies/build-state.md deleted file mode 100644 index 5521132fc6..0000000000 --- a/docs/0.4.x/en-US/reference/strategies/build-state.md +++ /dev/null @@ -1,35 +0,0 @@ -# Build State - -The most commonly-used rendering strategy for Perseus is static generation, which renders your pages to static HTML files. These can then be served by the server with almost no additional processing, which makes for an extremely fast experience! - -Note that, depending on other strategies used, Perseus may call this strategy at build-time or while the server is running, so you shouldn't depend on anything only present in a build environment (particularly if you're using the _incremental generation_ or _revalidation_ strategies). - -_Note: if you want to export your app to purely static files, see [this section](:reference/exporting), which will help you use Perseus without any server._ - -## Usage - -### Without _Build Paths_ or _Incremental Generation_ - -On its own, this strategy will simply generate properties for your template to turn it into a page, which would be perfect for something like a list of blog posts (just fetch the list from the filesystem, a database, etc.). Here's an example from [here](https://github.com/arctic-hen7/perseus/blob/main/examples/core/state_generation/src/templates/build_state.rs) for a simple greeting: - -```rust -{{#include ../../../../examples/core/state_generation/src/templates/build_state.rs}} -``` - -Note that Perseus passes around properties to pages as `String`s, so the function used for this strategy is expected to return a string, but this serialization is done for you with the `#[perseus::autoserde(build_state)]` macro. Note also the return type `RenderFnResultWithCause`, a Perseus type that represents the possibility of returning almost any kind of error, with an attached cause declaration that blames either the client or the server for the error. Most of the time, the server will be at fault (e.g. if serializing some obvious properties fails), and this is the default if you use `?` or `.into()` on another error type to run an automatic conversion. However, if you want to explicitly state a different cause (or provide a different HTTP status code), you can construct `GenericErrorWithCause` or use the more convenient `blame_err!` macro, as done in the below example (under the next subheading) if the path is `post/tests`. We set the error (a `Box<dyn std::error::Error>`) and then set the cause to be the client (they navigated to an illegal page) and tell the server to return a 404, which means our app will display something like _Page not found_. - -### With _Build Paths_ or _Incremental Generation_ - -You may have noticed in the above example that the build state function takes a `path` parameter. This becomes useful once you bring the _build paths_ or _incremental generation_ strategies into play, which allow you to render many paths for a single template. In the following example (taken from [here](https://github.com/arctic-hen7/perseus/blob/main/examples/core/state_generation/src/templates/incremental_generation.rs)), all three strategies are used together to pre-render some blog posts at build-time, and allow the rest to be requested and rendered if they exist (here, any post will exist except one called `tests`): - -```rust -{{#include ../../../../examples/core/state_generation/src/templates/incremental_generation.rs}} -``` - -When either of these additional strategies are used, _build state_ will be passed the path of the page to be rendered, which allows it to prepare unique properties for that page. In the above example, it just turns the URL into a title and renders that. - -For further details on _build paths_ and _incremental generation_, see the following sections. - -## Common Pitfalls - -When a user goes to your app from another website, Perseus will send all the data they need down straight away (in the [initial loads](:advanced/initial-loads) system), which involves setting any state you provide in a JavaScript global variable so that the browser can access it without needing to talk to the server again (which would slow things down). Unfortunately, JavaScript's concept of 'raw strings' (in which you don't need to escape anything) is quite a bit looser than Rust's, and so Perseus internally escapes any instances of backticks or `${` (JS interpolation syntax). This should all work fine, but, when your state is deserialized, it's not considered acceptable for it to contain _control characters_. In other words, anything like `\n`, `\t` or the like that have special meanings in strings must be escaped before being sent through Perseus! Note that this is a requirement imposed by the lower-level module [`serde_json`](https://github.com/serde-rs/json), not Perseus itself. diff --git a/docs/0.4.x/en-US/reference/strategies/incremental.md b/docs/0.4.x/en-US/reference/strategies/incremental.md deleted file mode 100644 index 83bffa732c..0000000000 --- a/docs/0.4.x/en-US/reference/strategies/incremental.md +++ /dev/null @@ -1,19 +0,0 @@ -# Incremental Generation - -Arguably the most powerful strategy in Perseus is _incremental generation_, which is an extension of _build paths_ such that any path in the template's root path domain (more info on that concept [here](:reference/templates/intro)) will result in calling the _build state_ strategy while the server is running. - -A perfect example of this would be an retail site with thousands of products, all using the `product` template. If we built all these with _build paths_, and they all require fetching information from a database, builds could take a very long time. Instead, it's far more efficient to use _incremental generation_, which will allow any path under `/product` to call the _build state_ strategy, which you can then use to render the product when it's first requested. This is on-demand building. But how is this different from the _request state_ strategy? It caches the pages after they've been built the first time, meaning **you build once on-demand, and then it's static generation from there**. In other words, this strategy provides support for rendering thousands, millions, or even billions of pages from a single template while maintaining static generation times of less than a second! - -Also, this strategy is fully compatible with _build paths_, meaning you could pre-render you most common pages at build-time, and have the rest built on-demand and then cached. - -## Usage - -This is the simplest strategy in Perseus to enable, needing only one line of code. Here's the example from earlier (which you can find [here](https://github.com/arctic-hen7/perseus/blob/main/examples/core/state_generation/src/templates/incremental_generation.rs)) that uses _incremental generation_ together with _build paths_ (and of course _build state_, which is mandatory for _incremental generation_ to work): - -```rust -{{#include ../../../../examples/core/state_generation/src/templates/incremental_generation.rs}} -``` - -All we need to do is run `.incremental_generation()` on the `Template`, and it's ready. - -Note that this example throws a _404 Not Found_ error if we go to `/incremental_generation/tests`, which is considered an illegal URL. This is a demonstration of preventing certain pages from working with this strategy, and such filtering should be done in the _build state_ strategy. diff --git a/docs/0.4.x/en-US/reference/strategies/intro.md b/docs/0.4.x/en-US/reference/strategies/intro.md deleted file mode 100644 index 28507521c7..0000000000 --- a/docs/0.4.x/en-US/reference/strategies/intro.md +++ /dev/null @@ -1,3 +0,0 @@ -# Rendering Strategies - -This section describes Perseus' rendering strategies, which differentiate it from every other framework in the world right now. Note that all the strategies detailed here can be used together, and the [state generation example](https://github.com/arctic-hen7/perseus/tree/main/examples/core/state_generation) is the best example of seeing how each one can be used. diff --git a/docs/0.4.x/en-US/reference/strategies/request-state.md b/docs/0.4.x/en-US/reference/strategies/request-state.md deleted file mode 100644 index 09aa3df98a..0000000000 --- a/docs/0.4.x/en-US/reference/strategies/request-state.md +++ /dev/null @@ -1,24 +0,0 @@ -# Request State - -While build-time strategies fulfill many use-cases, there are also scenarios in which you may need access to information only available at request-time, like an authentication key that the client sends over HTTP as a cookie. For these cases, Perseus supports the _request state_ strategy, which is akin to traditional server-side rendering, whereby you render the page when a client requests it. - -If you can avoid this strategy, do, because it will bring your app's TTFB (time to first byte) down, remember that anything done in this strategy is done on the server while the client is waiting for a page. - -## Usage - -Here's an example taken from [here](https://github.com/arctic-hen7/perseus/blob/main/examples/core/state_generation/src/templates/request_state.rs) of using this strategy to tell the user their own IP address (albeit not hugely reliably as this header can be trivially spoofed, but this is for demonstration purposes): - -```rust -{{#include ../../../../examples/core/state_generation/src/templates/request_state.rs}} -``` - -Note that, just like _build state_, this strategy generates stringified properties that will be passed to the page to render it (serialization is handled by `#[perseus::autoserde(request_state)]`), and it also uses `RenderFnWithCause` (see the section on [build state](:reference/strategies/build-state) for more information). The key difference though is that this strategy receives a second, very powerful parameter: the HTTP request that the user sent (`perseus::Request`). - -<details> -<summary>How do you get the user's request information?</summary> - -The web frameworks Perseus supports automatically pass this information to handlers like Perseus. The slightly difficult thing is then converting this from their custom format to Perseus' (which is just an alias for the [`http`](https://docs.rs/http) module's). This is done in the appropriate server integration crate. - -</details> - -That parameter is actually just an alias for [this](https://docs.rs/http/0.2/http/request/struct.Request.html), which gives you access to all manner of things in the user's HTTP request. The main one we're concerned with in this example though is `X-Forwarded-For`, which contains the user's IP address (unless it's trivially spoofed). Because we can't assume that any HTTP header exists, we fall back to a message saying the IP address is hidden if we can't access the header. diff --git a/docs/0.4.x/en-US/reference/strategies/revalidation.md b/docs/0.4.x/en-US/reference/strategies/revalidation.md deleted file mode 100644 index b6ea2d9a5d..0000000000 --- a/docs/0.4.x/en-US/reference/strategies/revalidation.md +++ /dev/null @@ -1,39 +0,0 @@ -# Revalidation - -While the _build state_ and _build paths_ strategies are excellent for generating pages efficiently, they can't be updated for new content. For example, using these strategies alone, you'd need to rebuild a blog every time you added a new post, even if those posts were stored in a database. With _revalidation_, you can avoid this by instructing Perseus to rebuild a template if certain criteria are met when it's requested. - -There are two types of revalidation: time-based and logic-based. The former lets you re-render a template every 24 hours or the like, while the latter allows you to re-render a template if an arbitrary function returns `true`. - -## Time-Based Revalidation Usage - -Here's an example of time-based revalidation from [here](https://github.com/arctic-hen7/perseus/blob/main/examples/core/state_generation/src/templates/revalidation_and_incremental_generation.rs) (note that this uses _incremental generation_ as well): - -```rust -{{#include ../../../../examples/core/state_generation/src/templates/revalidation_and_incremental_generation.rs}} -``` - -This page displays the time at which it was built (fetched with _build state_), but rebuilds every five seconds. Note that this doesn't translate to the server's actually rebuilding it every five seconds, but rather the server will rebuild it at the next request if more than five seconds have passed since it was last built (meaning templates on the same build schedule will likely go our of sync quickly). - -### Time Syntax - -Perseus uses a very simple syntax inspired by [this JavaScript project](https://github.com/vercel/ms) to specify time intervals in the form `xXyYzZ` (e.g. `1w`, `5s`, `1w5s`), where the lower-case letters are number and the upper-case letters are intervals, the supported of which are listed below: - -- `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) - -## Logic-Based Revalidation Usage - -Here's an example of logic-based revalidation from [here](https://github.com/arctic-hen7/perseus/blob/main/examples/core/state_generation/src/templates/revalidation.rs) (actually, this example uses both types of revalidation): - -```rust -{{#include ../../../../examples/core/state_generation/src/templates/revalidation.rs}} -``` - -If it were just `.should_revalidate_fn()` being called here, this page would always be rebuilt every time it's requested (the closure always returns `true`, note that errors would be `String`s), however, the additional usage of time-based revalidation regulates this, and the page will only be rebuilt every five seconds. In short, your arbitrary revalidation logic will only be executed at the intervals of your time-based revalidation intervals (if none are set, it will run on every request). - -Note that you should avoid lengthy operations in revalidation if at all possible, as, like the _request state_ strategy, this logic will be executed while the client is waiting for their page to load. diff --git a/docs/0.4.x/en-US/reference/styling.md b/docs/0.4.x/en-US/reference/styling.md deleted file mode 100644 index c8b6b5dc41..0000000000 --- a/docs/0.4.x/en-US/reference/styling.md +++ /dev/null @@ -1,27 +0,0 @@ -# Styling - -Perseus aims to make styling as easy as possible, though there are a number of things that you should definitely know about before you start to style a Perseus app! - -It's very easy to import stylesheets with Perseus (be they your own, something like [TailwindCSS](https://tailwindcss.com), etc.). You just add them to the `static/` directory at the root of your project, and then they'll be available at `/.perseus/static/your-filename-here`. That's described in more detail in [this section](:reference/static-content). - -## Full-Page Layouts - -If you've tried to create something like a stick footer, you've probably become extremely frustrated by Perseus, which puts all your content in a container `<div>` (in addition to the `<div id="root"></div>`). Unfortunately, this is necessary until Sycamore supports creating a template for an existing DOM node, and this does lead to some styling problems. - -Notably, there are actually two of these `<div>`s at the moment: one for the content that the server pre-renders in [initial loads](:advanced/initial-loads) and another for when that content is hydrated by Perseus' client-side logic. That means that, if you only style one of these, you'll get a horrible flash of unstyled content, which nobody wants. To make this as easy as possible, Perseus provides a class `__perseus_content` that applies to both of these `<div>`s. Also, note that the `<div>` for the initial content will become `display: none;` as soon as the page is ready, which means you won't get it interfering with your layouts. - -Knowing this, the main changes you'll need to make to any full-page layout code is to apply the styles to `.__perseus_content` as well as `body` or `#root`. As with CSS generally, if you expect `.__perseus_content` to take up the whole page, you'll need to make all its parents (`#root`, `body`, `html`) also take up the whole page (you can do this by setting `height: 100vh;` on `body`). A good starting point (that supports scrolling as well) is this CSS: - -```css -body, -#root, -.__perseus_content { - min-height: 100%; - min-width: 100%; - height: 100vh; -} -``` - -By using `min-height` and `min-width`, we can ensure that the containers expand to fill the page, even if content goes offscreen (with a scrollable overflow). - -Any other issues should be solvable by inspecting the DOM with your browser's DevTools, but you're more than welcome to ask for help on the [Sycamore Discord server](https://discord.gg/PgwPn7dKEk), where Perseus has its own channel! diff --git a/docs/0.4.x/en-US/reference/templates/intro.md b/docs/0.4.x/en-US/reference/templates/intro.md deleted file mode 100644 index 24cb523ce3..0000000000 --- a/docs/0.4.x/en-US/reference/templates/intro.md +++ /dev/null @@ -1,57 +0,0 @@ -# Templates - -At the core of Perseus is its template system, which is how you'll define every page you'll ever build! However, it's important to understand a few of the nuances of this system so that you can build the best apps possible. - -## Templates vs Pages - -In Perseus, the idea of a _template_ is very different to the idea of a _page_. - -A _page_ corresponds to a URL in your app, like the about page, the landing page, or an individual blog post. - -A _template_ can generate _many_ pages or only one by using _rendering strategies_. - -The best way to illustrate this is with the example of a simple blog, with each page stored in something like a CMS (content management system). This app would only need two templates (in addition to a landing page, an about page, etc.): `blog` and `post`. For simplicity, we'll put the list of all blog posts in `blog`, and then each post will have its own URL under `post`. - -The `blog` template will be rendered to `/blog`, and will only use the _build state_ strategy, fetching a list of all our posts from the CMS every time the blog is rebuilt (or you could use revalidation and incremental generation to mean you never have to rebuild at all, but that's beyond the scope of this section). This template only generates one page, providing it the properties of the list of blog posts. So, in this case, the `blog` template has generated the `/blog` page. - -The `post` template is more complex, and it will generate _many_ pages, one for each blog post. This would probably use the _build paths_ strategy, which lets you fetch a list of blog posts from the CMS at build-time and invoke _build state_ for each of them, which would then get their content, metadata, etc. Thus, the `post` template generates many pages. - -Hopefully that explains the difference between a template and a page. This is a somewhat unintuitive part of Perseus, but it should be clear in the documentation what the difference is. Note however that old versions of the examples in the repository used these terms interchangeably, when they used to be the same. If you see any remaining ambiguity in the docs, please [open an issue](https://github.com/arctic-hen7/perseus/issues/new/choose)! - -## Defining a Template - -You can define a template like so (taken from [the basic example](https://github.com/arctic-hen7/perseus/blob/main/examples/core/basic/src/templates/about.rs)'s about page): - -```rust -{{#include ../../../../examples/core/basic/src/templates/about.rs}} -``` - -It's seen as convention in Perseus to define each template in its own file, which should expose a `get_template()` function. Note that this is just convention, and as long as you get an instance of `Template<G>` to the `PerseusApp`, it really doesn't matter how you do it. That said, using community conventions makes your code easier to understand and debug for others. - -There's a list of all the methods available on a template [here](https://docs.rs/perseus/0.3/perseus/template/struct.Template.html#implementations), along with explanations of what they all do. Technically, you could just define a template without calling any of these, but that would just render a blank page, which would probably be useless. - -Also note the use of the `#[perseus::template(...)]` macro on the `about_page` function in the above example. As mentioned elsewhere in the book, this will perform some boilerplate work for you: namely deserializing your template's properties for you (if it takes any). While you don't have to use this, it makes things more convenient and there's no reason not to. - -## Routing - -Perseus' routing system is basically invisible, there's no router that you need to work with, nor any place for you to define explicit routes. Instead, Perseus automatically infers the routes for all your templates and the pages they generate from their names! - -The general rule is this: a template called `X` will be rendered at `/X`. Simple. What's more difficult to understand is what we call _template path domains_, which is the system that makes route inference possible. **A template can only ever generate pages within the scope of its root path.** Its root path is its name. In the example of a template called `X`, it can render `/X/Y`, `/X/Y/Z`, etc., but it can _never_ render `/A`. - -To generate paths within a template's domain, you can use the _build paths_ and _incremental generation_ strategies (more on those later). Both of these support dynamic parameters (which might be denoted in other languages as `/post/<title>/info` or the like). - -### Dynamic Parameters Above the Domain - -One niche case is defining a route like this: `/<locale>/about`. In this case, the `about` template is rendered underneath a dynamic parameter. This is currently impossible in Perseus, but the most common reason to need it, internationalization (making your app work in many language), is support out-of-the-box with Perseus. - -### Different Templates in the Same Domain - -It's perfectly possible in Perseus to define one template for `/post` (and its children) and a different one for `/post/new`. In fact, this is very similar to what [this example](https://github.com/arctic-hen7/perseus/tree/main/examples/core/state_generation/src/templates/build_paths.rs) does, and you can check it out for inspiration. This is based on a simple idea: **more specific templates win** the routing contest. - -There is one use-case though that requires a bit more fiddling: having a different template for the root path. A very common use-case for this would be having one template for `/posts`'s children (one URl for each blog post) and a different template for `/posts` itself that lists all available posts. Currently, the only way to do this is to define a property on the `posts` template that will be `true` if you're rendering for that root, and then to conditionally render the list of posts. Otherwise, you would render the given post content. This does require a lot of `Option<T>`s, but they could be safely unwrapped (data passing in Perseus is logical and safe). - -## Checking Render Environment - -It's often necessary to make sure you're only running some logic on the client-side, particularly anything to do with `web_sys`, which will `panic!` if used on the server. Because Perseus renders your templates in both environments, you'll need to explicitly check if you want to do something only on the client (like get an authentication token from a cookie). This can be done trivially with Sycamore, just use `G::IS_BROWSER` (where `G` is the type parameter on your template). - -This is a very contrived example, but what you should note if you try this is the flash from `server` to `client` (when you go to the page from the URL bar, not when you go in from the link on the index page), because the page is pre-rendered on the server and then hydrated on the client. This is an important principle of Perseus, and you should be aware of this potential flashing (easily solved by a less contrived example) when your users [initially load](:advanced/initial-loads) a page. diff --git a/docs/0.4.x/en-US/reference/templates/metadata-modification.md b/docs/0.4.x/en-US/reference/templates/metadata-modification.md deleted file mode 100644 index 9c969589c8..0000000000 --- a/docs/0.4.x/en-US/reference/templates/metadata-modification.md +++ /dev/null @@ -1,21 +0,0 @@ -# Modifying the `<head>` - -A big issue with only having one `index.html` file for your whole app is that you don't have the ability to define different `<title>`s and HTML metadata for each page. - -However, Perseus overcomes this easily by allowing you to specify `.head()` on a `Template<G>`, which should be a function that returns a `Template<SsrNode>` (but you can use write `perseus::HeadFn` as the return type, it's an alias for that). The `view!` you define here will be rendered to a `String` and directly interpolated into the `<head>` of any pages this template renders. If you need it to be different based on the properties, you're covered, it takes the same properties as the normal template function! (They're deserialized automatically by the `#[perseus::head]` macro.) - -The only particular thing to note here is that, because this is rendered to a `String`, this **can't be reactive**. Variable interpolation is fine, but after it's been rendered once, the `<head>` **will not change**. If you need to update it later, you should do that with [`web_sys`](https://docs.rs/web-sys), which lets you directly access any DOM element with similar syntax to JavaScript (in fact, it's your one-stop shop for all things interfacing with the browser, as well as it's companion [`js-sys`](https://docs.rs/js-sys)). - -Here's an example of modifying a page's metadata (taken from [here](https://github.com/arctic-hen7/perseus/blob/main/examples/core/basic/src/templates/index.rs)): - -```rust -{{#lines_include ../../../../examples/core/basic/src/templates/index.rs:28:33}} -``` - -## Script Loading - -One unfortunate caveat with Perseus' current approach to modifying the `<head>` is that any new `<script>`s you add will fail to load. This is because browsers only run new new scripts if they're appended as individual nodes, and Perseus sets the entire new `<head>` in bulk. for this reason, you should put `<script>`s at the top of the rest of your template instead. That way, they'll still load before your code, but they'll also actually load! - -If you really need to put a `<script>` in the `<head>` for some reason, you could append it directly using [`web_sys`](https://docs.rs/web-sys), though you should make sure that it doesn't work with the rest of your code first. - -Note that any scripts in your `index.html` are constant across all pages, and will load correctly. diff --git a/docs/0.4.x/en-US/reference/templates/router-state.md b/docs/0.4.x/en-US/reference/templates/router-state.md deleted file mode 100644 index 057f6e9e55..0000000000 --- a/docs/0.4.x/en-US/reference/templates/router-state.md +++ /dev/null @@ -1,19 +0,0 @@ -# Listening to the Router - -Given that Perseus loads new pages without reloading the browser tab, users will have no way to know that their clicking on a link actually did anything, which can be extremely annoying for your users, and may even dissaude them from using your site! Usually, this is circumvented by how quickly Perseus can load a new page, but, if a user happens to be on a particularly slow connection, it could take several seconds. - -To avoid this, many modern frameworks support a loading bar at the top of the page to show that something is actully happening. Some sites prefer a more intrusive full page overlay with a loading indicator. No matter what approach you choose, Perseus gets out of your way and lets you build it, by using *router state*. This is a Sycamore `ReadSignal` that you can get access to in your templates and then use to listen for events on the router. - -## Usage - -This example (taken from [here](https://github.com/arctic-hen7/perseus/blob/main/examples/core/router_state/src/templates/index.rs)) shows using router state to create a simple indicator of the router's current state, though this could easily be extended into a progress bar, loading indicator, or the like. - -```rust -{{#include ../../../../examples/core/router_state/src/templates/index.rs}} -``` - -The first step in using router state is accessing it, which can be done through Sycamore's [context API](https://sycamore-rs.netlify.app/docs/advanced/contexts). Specifically, we access the context of type `perseus::templates::RenderCtx` (which also includes other information), which has a field `router_state`, which contains an instance of `perseus::templates::RouterState`. Then, we can use the `.get_load_state()` method on that to get a `ReadSignal<perseus::templates::RouterLoadState>` (Sycamore-speak for a read-only piece of state). Next, we use Sycamore's `create_memo` to create some derived state (so it will update whenever the router's loading state does) that just turns the router's state into a string to render for the user. - -As you can see, there are three mutually exclusive states the router can be in: `Loaded`, `Loading`, and `Server`. The first two of these have an attached `String` that indicates either the name of the template that has been loaded (in the first case) or the name of the template that is about to be loaded (in the second case). In the third state, you shouldn't do anything, because no router actually exists, as the page is being rendered on the server. Note that anything rendered in the `Server` state will be visible for a brief moment in the browser before the page is made interactive, which can cause ugly UI flashes. - -As noted in the comments in this code, if you were to load this page and click the link to the `/about` page (which has the template name `about`), you would momentarily see `Loading about.` before the page loaded. During this time (i.e. when the router is in the `Loading` state), you may want to render some kind of progress bar or overlay to indicate to the user that a new page is being loaded. diff --git a/docs/0.4.x/en-US/reference/templates/setting-headers.md b/docs/0.4.x/en-US/reference/templates/setting-headers.md deleted file mode 100644 index bdd885e0d3..0000000000 --- a/docs/0.4.x/en-US/reference/templates/setting-headers.md +++ /dev/null @@ -1,11 +0,0 @@ -# Modifying HTTP Headers - -Most of the time, you shouldn't need to touch the HTTP headers of your Perseus templates, but sometimes you will need to. A particular example of this is if you want your users' browsers to only cache a page for a certain amount of time (the default for Perseus if five minutes), then you'd need to set the [`Cache-Control`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control) header. - -Perseus supports inserting arbitrary HTTP headers for any response from the server that successfully returns a page generated from the template those headers are defined for. You can do this like so (taken from [here](https://github.com/arctic-hen7/perseus/blob/main/examples/core/set_headers/src/templates/index.rs)): - -```rust -{{#include ../../../../examples/core/set_headers/src/templates/index.rs}} -``` - -Of note here is the `set_headers_fn` function, which returns a `HeaderMap`. This is then used on the template with `.set_headers_fn()`. Note that the function you provide will be given the state as an argument (ignored here, but it will be deserialized for you with `#[perseus::autoserde(set_headers)]`), and you must return some headers (you can't return an error). diff --git a/docs/0.4.x/en-US/reference/testing/checkpoints.md b/docs/0.4.x/en-US/reference/testing/checkpoints.md deleted file mode 100644 index b09dd789a6..0000000000 --- a/docs/0.4.x/en-US/reference/testing/checkpoints.md +++ /dev/null @@ -1,48 +0,0 @@ -# Checkpoints - -If you start using Perseus' testing system now, you'll likely hit a snag very quickly, involving errors to do with _stale DOM elements_. This is an unfortunate side-effect of the way Perseus currently handles initial loads (we move a number of DOM elements around after they've been sent down from the server), which results in the WebDriver thinking half the page has just disappeared out from under it! - -This, and many similar problems, are easily solvable using one of Perseus' most powerful testing tools: _checkpoints_. When you run your app with `perseus test`, a system is enabled in the background that writes a new DOM element to a hidden list of them when any app code calls `checkpoint()`. This can then be detected with Fantoccini! Admittedly, a far nicer solution would be DOM events, but the WebDriver protocol doesn't yet support listening for them (understandable since it's mimicking a user's interaction with the browser). - -Note that checkpoints will never be reached if your app is not run with `perseus test`. If you use `--no-run` and then execute the server binary manually, be sure to provide the `PERSEUS_TESTING=true` environment variable. - -You can wait for a Perseus checkpoint to be reached like so (taken from [here](https://github.com/arctic-hen7/perseus/blob/main/examples/core/basic/tests/main.rs)): - -```rust -{{#include ../../../../examples/core/basic/tests/main.rs}} -``` - -Note in particular the use of the `wait_for_checkpoint!` macro, which accepts three arguments: - -- Name of the checkpoint -- Version of the checkpoint -- Fantoccini client - -For want of a better term, that second argument refers to how Perseus manages checkpoints. Because a single checkpoint might be emitted multiple times, Perseus attaches a number to the end of each. The final element `id` looks like this: `__perseus_checkpoint-<checkpoint_name>-<number>`, where `<number>` starts from 0 and increments. - -_Note: checkpoints are not cleared until the page is fully reloaded, so clicking a link to another page will not clear them!_ - -## Custom Checkpoints - -In addition to Perseus' internal checkpoints (listed below), you can also use your own checkpoints, though they must follow the following criteria: - -- Must not include hyphens (used as a delimiter character), use underscores instead -- Must not conflict with an internal Perseus checkpoint name - -The best way to uphold the latter of those criteria is to prefix your own checkpoints with something like the name of your app, or even just `custom_`. Of course, if your app has a name like `router`, then that will be a problem (many Perseus checkpoints begin with `router_`), but Perseus will never generate checkpoints internally that begin with `custom_`. - -Note that it's not enough to make sure that your checkpoints don't clash with any existing checkpoints, as new checkpoints may be added in any new release of Perseus, so conflicts may arise with the tiniest of updates! - -## Internal Checkpoints - -Perseus has a number of internal checkpoints that are listed below. Note that this list will increase over time, and potentially in patch releases. - -- `begin` -- when the Perseus system has been initialized -- `router_entry` -- when the Perseus router has reached a verdict and is about to either render a new page, detect the user's locale and redirect, or show an error page -- `not_found` -- when the page wasn't found -- `app_shell_entry` -- when the page was found and it's being rendered -- `initial_state_present` -- when the page has been rendered for the first time, and the server has preloaded everything (see [here](:advanced/initial-loads) for details) -- `page_visible` -- when the user is able to see page content (but the page isn't interactive yet) -- `page_interactive` -- when the page has been hydrated, and is now interactive -- `initial_state_not_present` -- when the initial state is not present, and the app shell will need to fetch page data from the server -- `initial_state_error` -- when initial state showed an error diff --git a/docs/0.4.x/en-US/reference/testing/fantoccini-basics.md b/docs/0.4.x/en-US/reference/testing/fantoccini-basics.md deleted file mode 100644 index 684e89ea18..0000000000 --- a/docs/0.4.x/en-US/reference/testing/fantoccini-basics.md +++ /dev/null @@ -1,27 +0,0 @@ -# Fantoccini Basics - -Now that you know a bit more about how Perseus tests work, it's time to go through how to write them! - -Remember, you're controlling an actual browser, so you basically have everything available to you that a user can do (mostly). You can even take screenshots! All this is achieved with [Fantoccini](https://github.com/jonhoo/fantoccini), which you can learn more about [here](https://docs.rs/fantoccini). For now though, here's a quick tutorial on the basics, using [this](https://github.com/arctic-hen7/perseus/blob/main/examples/core/basic/tests/main.rs) example: - -```rust -{{#include ../../../../examples/core/basic/tests/main.rs}} -``` - -## Going to a Page - -You can trivially go to a page of your app by running `c.goto("...")`. The above example ensures that the URL is valid, but you shouldn't have to do this unless you're testing a page that automatically redirects the user. Also, if you're using [i18n](:reference/i18n/intro), don't worry about testing automatic locale redirection, we've already done that for you! - -Once you've arrived at a page, you should wait for the `router_entry` (this example uses `begin` because it tests internal parts of Perseus) checkpoint, which will be reached when Perseus has decided what to do with your app. If you're testing particular page logic, you should wait instead for `page_visible`, which will be reached when the user could see content on your page, and then for `page_interactive`, which will be reached when the page is completely ready. Remember though, you only need to wait for the checkpoints that you actually use (e.g. you don't need to wait for `page_visible` and `page_interactive` if you're not doing anything in between). - -## Finding an Element - -You can find an element easily by using Fantoccini's `Locator` `enum`. This has two options, `Id` or `Css`. The former will find an element by its HTML `id`, and the latter will use a CSS selector ([here](https://www.w3schools.com/cssref/css_selectors.asp)'s a list of them). In the above example, we've used `Locator::Css("p")` to get all paragraph elements, and then we've plugged that into `c.find()` to get the first one. Then, we can get its `innerText` with `.text()` and assert that is what we want it to be. - -### Caveats - -As you may have noticed above, asserting on the contents of a `<title>` is extremely unintuitive, as it requires using `.html(false)` (meaning include the element tag itself) and asserting against that. For some reason, neither `.html(true)` nor `.text()` return anything. There's a tracking issue for this [here](https://github.com/jonhoo/fantoccini/issues/136). - -## Miscellaneous - -For full documentation of how Fantoccini works, see its API documentation [here](https://docs.rs/fantoccini). diff --git a/docs/0.4.x/en-US/reference/testing/intro.md b/docs/0.4.x/en-US/reference/testing/intro.md deleted file mode 100644 index 18a66d9f35..0000000000 --- a/docs/0.4.x/en-US/reference/testing/intro.md +++ /dev/null @@ -1,62 +0,0 @@ -# Testing - -When building a web app, testing is extremely important, and also extremely helpful. If you're familiar with Rust, you're probably used to having two types of tests (unit tests and integration tests), but Perseus follows the JavaScript model of testing slightly more, which is better suited to a user-facing web app, and has three types of tests: - -- Unit tests -- same as in Rust, they test a small amount of logic in isolation -- Integration tests -- same as in Rust, they test the system itself, but sometimes mocking things like a database -- End-to-end tests -- not mocking anything at all, and fully testing the entire system as if a real user were operating it - -It's that last type that Perseus is particularly concerned with, because that's the way that you can create highly resilient web apps that are tested for real user interaction. In fact, most of Perseus itself is tested this way! Also, E2E tests are more effective at automating otherwise manual testing of going through a browser and checking that things work, and they're far less brittle than any other type of test (all that matters is the final user experience). - -In terms of unit tests, these can be done for normal logic (that doesn't render something) with Rust's own testing system. Any integration tests, as well as unit tests that do render things, should be done with [`wasm-bindgen-test`](https://rustwasm.github.io/wasm-bindgen/wasm-bindgen-test/index.html). This module provides a custom _test harness_ macro (alternative to `#[test]`) that spins up a _headless browser_ (browser without a GUI) that can be used to render your code. Note that this should be done for testing Sycamore components, and not for testing integrated Perseus systems. - -When you want to test logic flows in your app, like the possibilities of how a user will interact with a login form, the best way is to use end-to-end testing, which Perseus supports with a custom test harness macro that can be used like so (taken from [here](https://github.com/arctic-hen7/perseus/blob/main/examples/core/basic/tests/main.rs)): - -```rust -{{#include ../../../../examples/core/basic/tests/main.rs}} -``` - -The first thing to note is the module that this test imports. It's called [Fantoccini](https://github.com/jonhoo/fantoccini), and it basically lets you control a web browser with code. We'll get to exactly how this works soon. This test goes to <http://localhost:8080> (where a Perseus app is hosted) and then clicks a link on it and makes sure that it's been taken to the correct new URL. - -The other important thing to note is the signature of this test function. First, it's annotated with `#[perseus::test]`, which means this will expand into a larger function that makes your function work. It takes a Fantoccini client as a parameter (which we've called `c` for convenience, you'll use it a lot), and returns a result. **In Perseus E2E tests, you shouldn't panic, but return an error gracefully instead**, which gives the harness time to disconnect from the headless browser. If you don't do this, you'll leave the browser in limbo, and other connections will fail, and everything will blow up in your face. Note that `assert!`, `assert_eq!`, and `assert_ne!` do `panic!` if they fail, which will cause the browser to be left in limbo. - -## Writing a Test - -You can write your own tests by creating files of whatever names you'd like under `test/` in your project's root directory (as you would with traditional Rust integration tests), and then you can write tests like the above example. Don't worry if you stuff up the arguments or the return type slightly, Perseus will let you know. Also note that **test functions must be asynchronous**. - -You'll also need to add the following to your `Cargo.toml` (`tokio` is needed for running your tests asynchronously): - -```toml -{{#lines_include ../../../../examples/core/basic/Cargo.toml:14:16}} -``` - -## Running Tests - -Perseus tests can be run with `cargo test` as usual, but you'll need to provide the `PERSEUS_RUN_WASM_TESTS` environment variable as true. This makes sure that you don't accidentally run tests that have external dependencies (like a headless browser). Note that, by default, your tests will run in a full browser, so you'll get GUI windows opening on your screen that are controlled by your tests. These can be extremely useful for debugging, but they're hardly helpful on CI, so you can remove them and run _headlessly_ (without a GUI window) by providing the `PERSEUS_RUN_WASM_TESTS_HEADLESS` environment variable. - -Before running E2E tests, you need to have two things running in the background: - -- Something that allows you to interact with a headless browser using the _WebDriver_ protocol (see below) -- Your app, invoked with `perseus test` (different to `perseus serve`) - -<details> -<summary>How would I automate all that?</summary> - -It may be most convenient to create a shell script to do these for you, or to use a tool like [Bonnie](https://github.com/arctic-hen7/bonnie) to automate the process. You can see an example of how this is done for a large number of tests across multiple different example apps in the [Perseus repository](https://github.com/arctic-hen7/perseus). - -</details> - -_Note: Cargo runs your tests in parallel by default, which won't work with some WebDrivers, like Firefox's `geckodriver`. To run your tests sequentially instead (slower), use `cargo test -- --test-threads 1` (this won't keep your tests in the same order though, but that's generally unnecessary)._ - -## WebDrivers? - -So far, we've mostly gone through this without explaining the details of a headless browser, which will be necessary to have some basic understanding of. Your web browser is composed a huge number of complex moving parts, and these are perfect for running end-to-end tests. They have rendering engines, Wasm execution environments, etc. Modern browsers support certain protocols that allow them to be controlled by code, and this can be done through a server like [Selenium](https://selenium.dev). In the case of Perseus though, we don't need something quite so fancy, and a simple system like [`geckodriver`](https://github.com/mozilla/geckodriver) for Firefox or [`chromedriver`](https://chromedriver.chromium.org/) for Chromium/Chrome will do fine. - -If you're completely new to headless browsers, here's a quick how-to guide with Firefox so we're all on the same page (there are similar steps for Google Chrome as well): - -1. Install [Firefox](https://firefox.com). -2. Install [`geckodriver`](https://github.com/mozilla/geckodriver). On Ubuntu, this can be done with `sudo apt install firefox-geckodriver`. -3. Run `geckodriver` in a terminal window on its own and run your Perseus tests elsewhere. -4. Press Ctrl+C in the `geckodriver` terminal when you're done. - -_Note: if your WebDriver instance is running somewhere other than <http://localhost:4444>, you can specify that with `#[perseus::test(webdriver_url = "custom-url-here")]`._ diff --git a/docs/0.4.x/en-US/reference/testing/manual.md b/docs/0.4.x/en-US/reference/testing/manual.md deleted file mode 100644 index cb68f49301..0000000000 --- a/docs/0.4.x/en-US/reference/testing/manual.md +++ /dev/null @@ -1,38 +0,0 @@ -# Manual Testing - -Occasionally, the Perseus testing harness may be a little too brittle for your needs, particularly if you'd like to pass custom arguments to the WebDriver (e.g. to spoof media streams). In these cases, you'll want to break out of it entirely and work with Fantoccini manually. - -You should do this by wrapping your normal test function in another function and annotating that with `[tokio::test]`, which will mark it as a normal asynchronous test. Then, you can mimic the behavior of the Perseus test harness almost exactly with the following code (adapted from the macro [here](https://github.com/arctic-hen7/perseus/blob/main/packages/perseus-macro/src/test.rs)): - -```rust -// Only run the test if the environment variable is specified (avoids having to do exclusions for workspace `cargo test`) -if ::std::env::var("PERSEUS_RUN_WASM_TESTS").is_ok() { - let headless = ::std::env::var("PERSEUS_RUN_WASM_TESTS_HEADLESS").is_ok(); - // Set the capabilities of the client - let mut capabilities = ::serde_json::Map::new(); - let firefox_opts; - let chrome_opts; - if headless { - firefox_opts = ::serde_json::json!({ "args": ["--headless"] }); - chrome_opts = ::serde_json::json!({ "args": ["--headless"] }); - } else { - firefox_opts = ::serde_json::json!({ "args": [] }); - chrome_opts = ::serde_json::json!({ "args": [] }); - } - capabilities.insert("moz:firefoxOptions".to_string(), firefox_opts); - capabilities.insert("goog:chromeOptions".to_string(), chrome_opts); - - let mut client = ::fantoccini::ClientBuilder::native() - .capabilities(capabilities) - .connect(&"http://localhost:4444").await.expect("failed to connect to WebDriver"); - let output = fn_internal(&mut client).await; - // Close the client no matter what - client.close().await.expect("failed to close Fantoccini client"); - // Panic if the test failed - if let Err(err) = output { - panic!("test failed: '{}'", err.to_string()) - } -} -``` - -Then, you can relatively easily modify the properties sent to the WebDriver instance with `firefox_opts` and `chrome_opts`. You can see the documentation for their options [here](https://developer.mozilla.org/en-US/docs/Web/WebDriver/Capabilities/firefoxOptions) (Firefox) and [here](https://sites.google.com/a/chromium.org/chromedriver/capabilities) (Chrome/Chromium). diff --git a/docs/0.4.x/en-US/reference/views.md b/docs/0.4.x/en-US/reference/views.md deleted file mode 100644 index 37afe1d1fc..0000000000 --- a/docs/0.4.x/en-US/reference/views.md +++ /dev/null @@ -1,11 +0,0 @@ -# Writing Views - -Perseus is fundamentally a high-level framework around [Sycamore](https://github.com/sycamore-rs/sycamore), which provides all the underlying reactivity and the ability to write code that turns into visible HTML elements. - -It would be foolish to reproduce here all the fantastic work of Sycamore, and you can read [their docs](https://sycamore-rs.netlify.app/docs/v0.6/getting_started/installation) to understand how reactivity, variable interpolation, and all the rest of their amazing systems work. - -Note that Perseus makes some sections of Sycamore's docs irrelevant (namely the sections on routing and SSR), as they're managed internally. Note that if you want to use Perseus without the CLI (*very* brave), these sections will be extremely relevant. - -## Using Sycamore without Perseus - -If you want to create a pure SPA without all the overhead of Perseus, you may want to use Sycamore without Perseus. Note that this won't provide as good SEO (search engine optimization), and you'll miss out on a number of additional features (like i18n, inferred routing, rendering strategies, and pre-optimized static exporting without a server), but for applications where these are unnecessary, Sycamore is perfect on its own. diff --git a/docs/0.4.x/en-US/tutorials/auth.md b/docs/0.4.x/en-US/tutorials/auth.md deleted file mode 100644 index 5b12601274..0000000000 --- a/docs/0.4.x/en-US/tutorials/auth.md +++ /dev/null @@ -1,85 +0,0 @@ -# Authentication - -If you're building an app with multiple users that might have different preferences or the like, chances are you'll need some way for those users to log in, you'll need an authentication system. This is fairly easy to achieve in Perseus with the [global state](:reference/state/global) system, though there are a few gotchas -- hence this tutorial! - -## Concepts - -Authentication as a process involves the user wanting to go to some page that they need to be logged in to access (e.g. a dashboard of bank statements), logging in, accessing that page, and then maybe logging back out a little later. All that really boils down to in terms of code is a system to manage whether or not the user is logged in, a system of authenticating the user's password (or however else they're logging in), and a system of providing access on certain pages only to authenticated users. - -The first part can be achieved through an entry in an app's global state that describes whether or not the user is logged in, what their username is, etc. Notably, this could be saved through [state freezing](:reference/state/freezing) to IndexedDB (which would preserve all the properties of the user's login) (not covered in this tutorial), though we still need a way of securely confirming that the user is who they say they are. For that, we would need to store a token, often done in the browser's [local storage](TODO) API, which we'll use in this example. As for how this token works, that requires a good deal of thought to security and how your app will work, and so is elided here (we'll just use a very insecure 'token' that tells us what the user's username is). - -The final part of this is controlling access to protected pages, which is the part where Perseus becomes more relevant as a framework. There are two types of protected pages that you might have, user-specific and non-user-specific. If a protected page is user-specific, then it's useless without the user's personal data. For example, a list of bank statements is completely worthless to an attacker without the user's bank statements populating it, rather than a loading skeleton. For these kinds of pages, we can render a skeleton on the server that's then populated with the user's information once the page is loaded in the browser. This means we don't have to go to any extra lengths to prevent access to the skeleton, since we'll assume that the user's data can only be accessed over an APi that needs some unique token that can only be generated with the user's password, or something similar. - -If a protected page is non-user-specific, that means it contains content that's the same for all users, but that should only be accessible to users who have logged in. These are more complex because protecting them requires that you don't prerender them on the server at all, and that the code for the protected content not be in your codebase. That may seem weird -- how can you render it at all if it's not in your codebase? Well, you'd have to check if the user is authenticated, and then use some token to fetch the protected content from a server and then display that. If you were to have the protected content anywhere in your code, then it would be accessible to any user willing to reverse-engineer the generated WebAssembly (which isn't too tricky), and hence not really protected at all. - -## Secure Authentication - -TODO - -## Building the App - -### Setup - -To start with, we'll set up a fairly typical Perseus app by initializing a new Rust project with `cargo new --lib`. Then, put the following in `Cargo.toml` (changing the package name as you want): - -```toml -{{#include ../../../examples/demos/auth/Cargo.toml}} -``` - -The only things of particular note here are the dependency on `web-sys`, from which we use the `Storage` feature (important later), as well as not using Sycamore's [hydration](:reference/hydration) system, as it doesn't handle the kinds of page changes from unauthenticated to authenticated that we'll need in this app. Note that hydration will likely work fine with this in future version of Sycamore (it's currently experimental though). - -Now add the following to `src/lib.rs`: - -```rust -{{#include ../../../examples/demos/auth/src/lib.rs}} -``` - -This is a very typical scaffold, but the use of the global state creator is important, and that's what we'll look at next. You can put whatever you want into `src/error_pages.rs` to serve as your app's error pages, but that isn't the subject of this tutorial. You can read more about error pages [here](:reference/error-pages) though. - -In `src/global_state.rs`, put the following code: - -```rust -{{#include ../../../examples/demos/auth/src/global_state.rs}} -``` - -This is fairly intense, so let's break it down. - -The first thing we do is create the function we call from `src/lib.rs`, `get_global_state_creator`, which initializes a `GlobalStateCreator` with the `get_build_state` function to create the initial global state (generated on the server and passed to the client). What that function does is generates an instance of `AppState`, a `struct` that will store our app's global state (which can include anything you want), which crucially has the `auth` field, an instance of `AuthData`, which will store the data for user authentication. Notably, all these `struct`s are annotated with `.make_rx()` to make them work with Perseus' state platform (note that `AppState` declares nested reactivity for the `auth` field, which you can read more about [here](:reference/state/global)). - -`AuthData` has two fields: `state` and `username`. The first is a `LoginState`, which can be `Yes` (the user is logged in), `No` (the user is not logged in), or `Server` (the page has been rendered on the server and we don't have any information about the user's login status yet). The reason for these three possibilities is so we don't assume the user to be logged out before we've even gotten to their browser, as that might result in an ugly flash between pages, or worse an inappropriate redirection to a login page. By forcing ourselves to handle the `Server` case, we make our code more robust and clearer. - -You might be wondering why we don't store `username`, which is just a `String`, as a property of `LoginState::Yes`, which would seem to be a much smarter data structure. This is absolutely true, but the problem is that the `make_rx` macro isn't smart enough to handle `enum`s, so we'd have to implement the `MakeRx` trait manually, which is a little tedious. To keep things simple, we'll go with storing `username` separately, but if you have multiple fields of information only relevant to authenticated users, you may want to take the more complex approach for cleanliness. - -Next, we implement some functions on `AuthDataRx`, the reactive version of `AuthData`, not bothering to do so on the original because we'll only use these functions in templates, in which we have the reactive version. The first method is `.detect_state()`, which will, if the state is `LoginState::Server`, check if the user is logged in by checking the `username` key in the browser's storage (not IndexedDB, local storage instead, which is more appropriate for this sort of thing). Note that this kind of 'token' management is absolutely atrocious and completely insecure, and serves only as an example of how you might start with authentication. Do NOT use this in a production app! - -The only other two functions are very simple, just `.login()` and `.logout()`, which alter the storage key and the global state to register a new login state. - -## Templates - -Okay, let's get into writing some views based on all this! We'll create an index page and an about page for demonstration, so set up a `src/templates/` directory with a `mod.rs` that declares both files. Then put the following in `src/templates/index.rs`: - -```rust -{{#include ../../../examples/demos/auth/src/templates/index.rs}} -``` - -The only strange stuff in here is in `index_view()`, the rest is pretty much bog-standard Perseus template code. In `index_view()`, we don't take any template sttate, for demonstration purposes (you easily could), but we do take in the global state, which you'll remember contains all the authentication properties. Then we set up some `Signal`s outside the data model for handling a very simple login input (again, demonstrations). The important thing is the call to `auth.detect_state()`, which will refresh the authentication status by checking the user's browser for the login 'token' being stored. Note that, because we coded this to return straight away if we already know the login state, it's perfectly safe to put this at the start of every template you want to be authentication-protected. We also gate this with `#[cfg(target_arch = "wasm32")]` to make sure it only runs on the browser (because we can't check for storage tokens in the server build process, that will throw plenty of errors!). - -Skipping past the scary `let view = ...` block for a moment, the end of this function is dead simple: we just display a Sycamore `View<G>` stored in a `Signal` (that's in the `view` variable), and then a link to the about page. Anything other than that `(*view.get())` call will be displayed *whether the user is authenticated or not*. - -Now for the fun part. To give us maximum editor support and cleanliness, we define the bulk of the view code outside the `view!` macro and in a variable called `view` instead, a derived `Signal` built with `create_memo` running on `auth.state`. So, if `auth.state` changes, this will also update immediately and automatically! All we do here is handle each of the three possible authentication states with a `match` statement: if we're on the server, we'll display nothing at all; if the user isn't logged in, a login page; and if they are, a welcome message and a logout button. In a real-world app, you'd probably have some code that redirects the user to a login page in the `LoginState::No` case. - -You might be wondering why we display nothing before the login state is known, because this would seem to undercut the purpose of preloading the page at all. The answer to this question is that it does, and in an ideal world you'd process the user's login data on the server-side before serving them the appropriate prerendered page, which you *could* do, but that would be unnecessarily complex. Instead, we can display a blank page for a moment before redirecting or loading the appropriate skeleton. - -In theory though, on some odler mobile devices, this blank screen might be visible for more than a moment (on 3G networks, it could be 2 seconds or more), which is not good at all. To remedy this, you could make `LoginState::Server` and `LoginState::Yes` render the same skeleton (with some blanks for unfetched user information), so you're essentially assuming the user to be logged in. That means only anonymous users get a flash, from the skeleton to a login page. If your login page is at a central route (e.g. `/login`), you could inject some JavaScript code to run before any of your page is rendered that would check if the user is logged in, and then redirect them to the login page before any of the page loaded if not. This is the best solution, which involves no flashing whatsoever, and the display time of your app is optimized for all users, without needing any server-side code! - -*Note: in future, there will likely be a plugin to perform this optimization automatically. If someone wants to create this now, please open a PR!* - -Finally, add the following into the about page (just a very basic unprotected page for comparison): - -```rust -{{#include ../../../examples/demos/auth/src/templates/about.rs}} -``` - -## Conclusion - -Authentication in Perseus is fairly easy to implement, easier than in many other frameworks, though there are a few hurdles to get over and patterns to understand that will make your code more idiomatic. In future, nearly all this will likely be handled automatically by a plugin or library, which would enable more rapid and efficient development of complex apps. For now though, authentication must be built manually into Perseus apps. diff --git a/docs/0.4.x/en-US/tutorials/hello-world.md b/docs/0.4.x/en-US/tutorials/hello-world.md deleted file mode 100644 index 27f8d01ddd..0000000000 --- a/docs/0.4.x/en-US/tutorials/hello-world.md +++ /dev/null @@ -1,95 +0,0 @@ -# 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/comprehensive/tiny/Cargo.toml.example}} -``` - -<details> -<summary>What are those dependencies doing?</summary> - -- `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! - -</details> - -Now, create an `index.html` file at the root of your project and put the following inside: - -```html -{{#include ../../../examples/comprehensive/tiny/index.html}} -``` - -<details> -<summary>Don't I need an `index.html` file?</summary> - -With versions of Perseus before v0.3.4, an `index.html` file was required for Perseus to know how to display in your users' browsers, however, this is no longer required, as Perseus now has a default *index view* built in, with the option to provide your own through either `index.html` or Sycamore code! - -For the requirements of any index views you create, see below. - -</details> - -Now, create a new directory called `src` and add a new file inside called `lib.rs`. Put the following inside: - -```rust -{{#include ../../../examples/comprehensive/tiny/src/lib.rs}} -``` - -<details> -<summary>How does that work?</summary> - -First, we import some things that'll be useful: - -- `perseus::{Html, PerseusApp, Template}` -- the `Html` trait, which lets your code be generic so that it can be rendered on either the server or in a browser (you'll see this throughout Sycamore code written for Perseus); the `PerseusApp` `struct`, which is how you represent a Perseus app; the `Template` `struct`, which represents a *template* in Perseus (which can create pages, as you'll soon learn -- this is the fundamental unit of Perseus) -- `sycamore::view` -- Sycamore's `view!` macro, which lets you write HTML-like code in Rust - -Perseus used to use a macro called `define_app!` to define your app, though this has since been deprecated and replaced with a more modern builder `struct`, which has methods that you can use to add extra features to your app (like internationalization). This is `PerseusApp`, and here, we're just adding one template with the `.template()` call (which you'll run each time you want to add a new template to your app). Here, we create a very simple template called `index`, a special template name that will bind this template to the root of your app, this will be the landing page. We then define the view code for this template with the `.template()` method on the `Template` `struct`, to which we provide a simple closure that returns a Sycamore `view!`, which just renders an HTML paragraph element (`<p>Hello World!</p>` in usual HTML markup). Usually, we'd provide a fully-fledged function here that can do many more things (like access global state stores), but for now we'll keep things nice and simple. - -In most apps, the main things you'll define on `PerseusApp` are `Template`s, though, when you move to production, you'll also want to define `ErrorPages`, which tell Perseus what to do if your app reaches a nonexistent page (a 404 not found error) or similar. For rapid development though, Perseus provides a series of prebuilt error pages (but if you try to use these implicitly in production, you'll get an error message). - -Also notice that we define this `PerseusApp` in a function called `main`, but you can call this anything you like, as long as you put `#[perseus::main]` before it, which turns it into something Perseus can expect (specifically, a special function named `__perseus_entrypoint`). - -</details> - -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 <http://localhost:8080> 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. - -<details> -<summary>Why do I need a CLI?</summary> - -Perseus is a _very_ complex system, and, if you had to write all that complexity yourself, that _Hello World!_ example would be more like 1200 lines of code than 12! 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 three crates (Rust packages): one for your app, another for the server that serves your app, and another for the builder that builds your app. These are what actually run your app, and they import the code you've written. They interface with the `PerseusApp` you define to make all this work. - -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](https://webassembly.org), 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 (note that an app this simple can actually use [static exporting](:reference/exporting), but we'll deal with that later) - -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. - -</details> - -Once that's done, hop over to <http://localhost:8080> in any modern browser (not Internet Explorer...), and you should see _Hello World!_ printed on the screen! If you try going to <http://localhost:8080/about> 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/comprehensive/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/0.4.x/en-US/tutorials/second-app.md b/docs/0.4.x/en-US/tutorials/second-app.md index 8e91c3f5d7..99742cdb70 100644 --- a/docs/0.4.x/en-US/tutorials/second-app.md +++ b/docs/0.4.x/en-US/tutorials/second-app.md @@ -1,175 +1 @@ # 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/core/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/core/basic/Cargo.toml.example}} -``` - -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 (you may not explicitly use this, but you'll need it as a dependency for some Perseus macros) - -## `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 -{{#include ../../../examples/core/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, both from the `templates` module. Specifically, we provide the `.template()` function with another function that produces our template, which allows us to keep each template's code in a separate file. - -We're also using `.error_pages()` here to tell Perseus how to handle errors in our app (like a nonexistent page), and we'll put these in the `error_pages` module. Note that you don't have to do this in development, Perseus has a set of defaults that it can use, but you can't use those in production, so you will have to create some error pages at some stage. - -## Error Handling - -Before we get to the cool part of building the actual pages of the app, we should set up error pages, which we'll do in `src/error_pages.rs`: - -```rust -{{#include ../../../examples/core/basic/src/error_pages.rs}} -``` - -The first thing to note here is the import of [`Html`](https://docs.rs/sycamore/0.7/sycamore/generic_node/trait.Html.html), which we define as a type parameter on the `get_error_pages` function. This makes sure that we can compile these views on the client or the server as long as they're targeting HTML (Sycamore can also target other templating formats for completely different systems, like MacOS desktop apps). - -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_. Note that the error pages we define here are extremely similar to Perseus' defaults, and, in a real app, you'd probably create something much more fancy! - -## `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 in your crate with its own code (this is how folders work in rust projects). Add the following to that file: - -```rust -{{#include ../../../examples/core/basic/src/templates/mod.rs}} -``` - -It's common practice to have a file (or even a folder) 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 -{{#include ../../../examples/core/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` - - `RenderFnResultWithCause` -- see below for an explanation of this - - `Template` -- as before - - `Html` -- as before (this is from Sycamore, but is re-exported by Perseus for convenience) - - `http::header::{HeaderMap, HeaderName}` -- some types for adding HTTP headers to our page - - `SsrNode` -- Sycamore's representation of a node that will only be rendered on the server (this comes from the `sycamore` crate, but Perseus re-exports it for convenience) -- `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) -- `sycamore` - - `component` -- a macro that turns a function into a Sycamore component - - `view` -- the `view!` macro, same as before - - `View` -- the output of the `view!` macro - -Then we define a number of different functions and a `struct`, each of which gets a section now. - -### `IndexPageState` - -This `struct` represents the *state* of the index page. As mentioned in the explanation of Perseus' [core principles](:core-principles), Perseus is fundamentally a system for turning a state and a view into a user interface. In this case, our index page will display a greeting generated at build time. That means that our view code will be generic over the greeting that we generate (which you can see in our `view! {...}` code under `index_page()`). Perseus lets us generate state in [several ways](:reference/strategies), and in this case we'll use the simplest: [build state](:reference/strategies/build-state), which just runs a custom function when we build our app that generates an instance of our state, which is represented by this `struct`. - -Note that we also use the `#[perseus::make_rx(IndexPageStateRx)]` macro here to make our state *reactive*. What this macro does is takes in our `struct` and produces a new version of it called `IndexPageStateRx` that has the exact same fields, but with each one wrapped in a Sycamore `Signal`, which makes each field reactive. This means that our page can mutate its state in one place and every other place that uses that state will automatically update through Sycamore's reactivity system! You can read more about the ideas behind this [here](:reference/state/rx). - -### `index_page()` - -This is the actual component that will render a user interface for your page. Perseus lets you provide a *template function* like this as a simple Rust function that takes in your page's state and produces a Sycamore `View<G>` (again, `G` is ambient here because of the proc macro). However, there's *a lot* of work that goes on behind the scenes to make your state reactive, register it with Perseus, manage [global state](:reference/state/global), and set up a Sycamore component that's usable by the rest of the Perseus code. This is all done with one of two attribute macros: `#[perseus::template(...)]` or `#[perseus::template_rx]`. In previous versions of Perseus, you'd use the former, which would give you an unreactive instance of your state (in our case, `IndexPageState`). However, since v0.3.4, it's recommended to use the latter, which gives you a reactive version (in our case, `IndexPageStateRx`) and can manage more advanced features of Perseus' [reactive state platform](:reference/state/rx), so that's what we use here. Also, `template_rx` manages things like Sycamore components internally for you, minimising the amount of code you actually have to write. - -Note that `index_page()` takes `IndexPageStateRx` as an argument, which it can then access in the `view!`. 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 (remember though that, because we're using the `template_rx` macro, we have to use a reactive state `struct`, which we generate with the `make_rx` macro, so all our fields are wrapped in `Signal`s, which is why we use `.get()`). - -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._ - -### `head()` - -This function is very similar to `index_page()`, except that it isn't a fully fledged Sycamore component, it just returns a `view! {}` instead, and it only takes an unreactive version of your state. What this is used for is to define the content of the `<head>`, which is metadata for your website, like its `<title>`. As you can see, this is given the properties that `index_page()` takes, but we aren't using them for anything in this example. The `#[perseus::head]` macro tells Perseus to do some boilerplate work behind the scenes that's very similar to that done with `index_page`, but specialized for the `<head>`. - -What's really important to note about this function is that it only renders to an `SsrNode`, which means you cannot use reactivity in here! Whatever is rendered the first time will be turned into a `String` and then statically interpolated into the document's `<head>`. That also means that this only runs on the server (if you want to change it on the client, you'll need to do that manually). - -The difference between metadata defined here and metadata defined in your `index.html` file is that the latter will apply to every page, while this will only apply to the template. So, this is more useful for things like titles, while you might use `index.html` to import stylesheets or analytics. - -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. - -### `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_state()` function when your app builds to get some state (an instance of `IndexPageState` to be precise). More on that in a moment. - -#### `.template()` - -This function is what Perseus will call when it wants to render your template (which it does more often than you might think). If you've used the `#[perseus::template_rx]` or `#[perseus::template(...)]` macro on `index_page()`, you can provide `index_page` directly here, but it can be useful to understand what that macro is doing. - -Behind the scenes, that macro transforms your `index_page()` function to take properties as an `Option<String>` instead of as `IndexPageState`, because Perseus actually passes your properties around internally as `String`s. At first, this might seem weird, but it avoids a few common problems that would increase your final Wasm binary size and make your website take a very long time to load. Interestingly, it's actually also more performant to use `String`s everywhere, because we need to perform that conversion anyway when we send your properties to a user's browser. In addition, the `#[perseus::template_rx]` macro will manage setting up and interacting with the more complex features of Perseus' [reactive state platform](:reference/state/rx). - -If that all went over your head, don't worry, that's just what Perseus does behind the scenes, and what you used to have to do by hand! The `#[perseus::template(...)]`/`#[perseus::template_rx]` macros do all that for you. - -#### `.head()` - -This is just the equivalent of `.template()` for the `head()` function, and it does basically the exact same thing. The only particular thing of note here is that the properties this expects are again as an `Option<String>`, and those are deserialized automatically by the `#[perseus::head]` macro that we used on `head()` earlier. - -### `get_build_state()` - -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! - -This function returns a rather special type, `RenderFnResultWithCause<IndexPageState>`, which declares that your function will return `IndexPageState` if it succeeds, and a special error if it fails. That error can be anything you want (it's a `Box<dyn std::error::Error + Send + Sync>` internally), but it will also have a blame assigned to it that records whether it was the server or the client that caused the error, which will impact the final HTTP status code (e.g. 404, 500, etc.). You can use the `blame_err!` macro to create these errors easily, but any time you use `?` in functions that return this type will simply use the default of blaming the server and returning an HTTP status code of _500 Internal Server Error_. - -It may seem a little pointless to blame the client in the build process, but the reason this can happen is because, in more advanced uses of Perseus (particularly [incremental generation](:reference/strategies/incremental)), this function could be called as a result of a client's request with parameters that it provides, which could be invalid. Essentially, know that it's a thing that's important in more complex use-cases of Perseus. - -That `#[perseus::autoserde(build_state)]` is also something you'll see quite a lot of (but not in older versions of Perseus). It's a convenience macro that automatically serializes the return of your function to a `String` for Perseus to use internally, which is basically just the opposite of the annotation we used earlier on `index_page()`. You don't technically need this, but it eliminates some boilerplate code that you don't need to bother writing yourself. - -## `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 -{{#include ../../../examples/core/basic/src/templates/about.rs}} -``` - -This is basically exactly the same as `index.rs`, except we don't have any state 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 at build time, ready to be served to your users). - -## Running It - -`perseus export -sw` - -That's all. Every time you build a Perseus app, that's all you need to do. - -*Note: because this app is very simple and doesn't use any server-requiring features, we can use [static exporting](:reference/exporting). For some more complex apps, you'll need to use `perseus serve -w` to spin up a full server.* - -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 `PERSEUS_HOST`/`PERSEUS_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. - -You can also try changing some of the code for your app (like the greeting generated), and you'll see that your app will automatically rebuild. When it's done, your browser will reload the new version of your app (even keeping as much old state as it can, meaning you can keep working without losing your place, see [here](:reference/state/hsr) for the details)! under the hood, this process is similar to the *hot module reloading* that most JavaScript frameworks perform, but it's actually even more advanced and resilient. - -<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. - -*Note: technically, it's possible to 'compile' Wasm into JavaScript, and we're looking into possibly supporting this inside Perseus for sites that need to target very old browsers. At the moment though, this is not supported through Perseus.* - -</details> - -## 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/0.4.x/en-US/what-is-perseus.md b/docs/0.4.x/en-US/what-is-perseus.md index 07e880d760..2f380c0d27 100644 --- a/docs/0.4.x/en-US/what-is-perseus.md +++ b/docs/0.4.x/en-US/what-is-perseus.md @@ -1,8 +1,10 @@ # What is Perseus? -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). +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! -If none of that makes any sense, this is the section for you! If you're not in the mood for a lecture, there's a TL;DR at the bottom of this page! +To most people though, none of that will make any sense, and that's the reason these docs exist! If you're familiar with [NextJS](https://nextjs.org), Perseus is like that for Rust. If you're familiar with [SvelteKit](https://kit.svelte.dev), it's that for [Sycamore](https://github.com/sycamore-rs/sycamore). If you're still scratching your head, read on! + +*Note: If you're not in the mood for a lecture, there's a TL;DR at the bottom of this page!* ### Rust web development @@ -58,6 +60,16 @@ Perseus supports SSR _and_ SSG out of the box, along with the ability to use bot 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. +## What about all that state stuff? + +At the beginning, we mentioned that Perseus is state-focused, which might seem a little cryptic. In Perseus, your app's *state* is the input to a template, which creates a page. This is all Perseus-specific jargon, so we'll simplify for now: imagine you've got a documentation website, like this one, and you want to have many pages of documentation that all have the same basic layout, but that just change their content between each page. With most frameworks, you can just write this code once, and then plug in all your content from a filesystem, database, etc., and then have all your final pages just generated. Perseus can do this too, and we call the code you write a *template*, which creates *pages*. The stuff that differentiates one documentation page from another is a bit of information that contains the documentation page's title, content, author, etc. In other words, *template + state = page*. Don't worry if you're not completely getting this yet, it's a little complex, and we'll explain it in much more detail later with some real code. + +Perseus focuses on that idea of *state* though, and allows you to generate it in all sorts of different ways. For instance, you might want to get all that documentation content from a database when you build your app. But, that database might change pretty often, so every 24 hours or so you might want to check if a page has an update, and then rebuild it while your app's still running. Perseus makes this a breeze. Or, you might have millions of pages of documentation that take a long time to build, so you might only want to build a page the first time it's requested, and then keep it cached for future users. Perseus makes that literally one line of code. And what if you want to support your site in many different languages? You supply the translations, we'll supply the infrastructure to integrate them seamlessly into your website with around four lines of code. + +What's more, Perseus makes all state reactive on the client-side, which means you can do something like this. Let's say you've got a form on your site, and you generate some default values for that form at build-time (in a few microseconds). you can set your site up easily so that any changes to the fields of the form by the user will update that state for them, and Perseus will persist it *automatically* even if they go to another page of your site. With around ten more lines of code, you can set Perseus up to cache your entire app's state, for all pages, and store it as a string (in the user's browser, in a database, anywhere!). When a user comes back, that state can be thawed out and placed right back into their browser. So yes, a user can have their progress in a form saved automatically for months with around ten lines of code from you. + +And if none of that appeals, it's all entirely optional anyway! Perseus is still lightning fast and a brilliant tool for creating fantastic websites and apps without it! + ## 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) or [Warp](https://github.com/seanmonstar/warp) (either is supported), some of the fastest web servers in the world. Essentially, Perseus is built on the fastest tech and is itself made to be fast. @@ -80,9 +92,10 @@ Perseus aims to be more convenient than any other Rust web framework by taking a Basically, here's your workflow: 1. Create a new project. -2. Define your app in around 12 lines of code and some listing. +2. Define your app in around 12 lines of code. 3. Code your amazing app. -4. Run `perseus serve`. +4. Run `perseus export -sw` or `perseus serve -w`. +5. Change some code and watch your app live update in the browser, restoring the previous state (if you're working on a long form, what you've typed can be saved automatically, even as you change the code). ## How stable is it? @@ -97,3 +110,4 @@ If all that was way too long, here's a quick summary of what Perseus does and wh - JS is slow and a bit of a mess, [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) +- Managing complex app state is made easy with Perseus, and it supports saving state to allow users to immediately return to exactly where they were (automatically!)