Skip to content

Commit

Permalink
docs: wrote further fundamental docs
Browse files Browse the repository at this point in the history
  • Loading branch information
arctic-hen7 committed Jan 10, 2023
1 parent 2b39064 commit 5bb9bd3
Show file tree
Hide file tree
Showing 9 changed files with 229 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/next/en-US/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
# Fundamentals

- [`PerseusApp`](/docs/fundamentals/perseus-app)
- [The reactor](/docs/fundamentals/reactor)
- [Routing and navigation](/docs/fundamentals/routing)
- [Preloading](/docs/fundamentals/preloading)
- [Internationalization](/docs/fundamentals/i18n)
Expand Down
23 changes: 23 additions & 0 deletions docs/next/en-US/fundamentals/debugging.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Debugging

For all its features, Perseus isn't a miracle-worker, and, until AI replaces all programmers, you're still going to need to do your fair share of debugging when you're building a Perseus app. Most of time, bugs will be caught neatly by the compiler system, which you can run in a loop with `perseus check -w`. This will re-run every time you change some code, and it will check both the engine-side and the client-side of your app, making sure you don't miss any bugs. If you want to also catch any of what we call *build-time errors* (which are runtime errors in Rust, but they occur at build-time, so they're more similar to compile-time errors from Perseus; perspective), you can run `perseus check -gw` to also test state generation.

In the vast majority of cases, if `perseus check -gw` passes, then any other Perseus command will also pass. Any deviations from this are most likely to be bugs in your request-time logic (e.g. incorrectly parsing a cookie).

## Client-side debugging

Unfortunately, debugging Wasm isn't the best experience yet, as debuggers really aren't too well equipped for this just yet. Usually, the best policy here is to use some good old `println!` logging, but you might quickly discover that `println!()`, `dbg!()`, etc. don't actually work at all in the browser. One day, this will hopefully change, but, for now, you can use [`web_log!()`](=macro.web_log@perseus), which behaves exactly like `println!()` to print to the browser console. Note that Perseus enforces that all the types it exposes implement `Debug`, so you shouldn't have any problems when debugging things coming from Perseus.

Using this macro on the engine-side will lead to it just calling `println!()`, but you could also use `dbg!()` in such cases, as it's often more convenient.

## Engine-side logging

However, if you try to, say, call `dbg!()` in your build-time logic, you might discover that you get absolutely zilch output in the console unless the whole process fails. This is because Perseus takes the conservative route, and only prints the output of its undelrying calls to `cargo` if the build process fails. This can make subtle logic errors very difficult to debug, so Perseus provides the `snoop` commands to help you. There are three:

- `perseus snoop build` will run the build process directly, with no frills, allowing you to see all the output of your own code (Perseus performs no logging)
- `perseus snoop wasm-build` will run the Wasm build process, which is just compiling your code to Wasm (you probably won't use this unless you're having Wasm-specific compiler errors)
- `perseus snoop serve` will run the server directly, allowing you to see any `dbg!()` calls or the like that occur on requests

Importantly, you'll have to run `perseus build` before `perseus snoop serve`, since it expects your app to be built before it executes. If you have errors about files not being found, you've probably forgotten `perseus build`.

Note that the output in `perseus snoop serve` may differ depending on the server integration you're using (e.g. Actix Web will clearly output when a thread fails).
43 changes: 43 additions & 0 deletions docs/next/en-US/fundamentals/i18n.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Internationalization

One of the most useful features of Perseus for larger apps is its inbuilt support for *internatinalization*, or *i18n* for short, which means making your app available in multiple languages. This is typically done by replacing all instances of human language in your code (e.g. the `Hello World!` string) with translation IDs, which are then resolved automatically to the correct text based on what *locale* the user is viewing the page in. Locales are defined in Perseus, as in other systems, as consisting of a language code and a region code: for example, `en-US` represents United States English, whereas `en-GB` represents British English. Note that this locale system is [far from perfect], but it's currently a global standard, and it's used by browsers for declaring the preferred languages of their users.

When you make your app available in multiple languages, Perseus will automatically take each of the locales you've specified and build every page in every single one of those locales (this will increase build times, but this is usually imperceptible, especially since everything is aggressively parallelized). Let's say your app is available in three languages: US English, Spanish, and French. This would mean your three locales might be `en-US`, `fr-FR`, and `es-ES` (`es` for Español). This leads to Perseus taking your landing page (previously available at `/`), and localizing it to `/en-US/`, `/fr-FR/`, and `/es-ES/`. Similarly, your about page (formerly at `/about`) will become `/en-US/about`, `/fr-FR/about`, and `/es-ES/about`. You get the picture.

But how do we know what language a user wants their pages in? Some sites figure this out by detecting what country you're in, to the peril of anyone using a VPN who slowly starts to learn Dutch against their will. The much better way of doing this is to just ask the browser, because users can configure their browsers with an arbitrary number of ordered locale preferences. For example, a Chinese native speaker who lives in Germany but is fluent in English might number her preferences as: `zh-CN`, `de-DE`, `en`, in that order. Notice the lack of a region code on the final preference (this is common). The process of *locale detection* is a complex one that requires comparing the languages an app has available with those a user would like to see. Unlike all other current frameworks, Perseus performs this process totally automatically according to web standards (see [RFC ????]). So, if our Chinese-German English speaker from before goes to `/about`, she will be redirected to `/en-US/about` automatically (since her first two preferences are unavailable). From here, any links will keep her in the `en-US` locale.

You can set up internationalization in your app through `PerseusApp` like so:

```
{{#include ../../../examples/core/i18n/src/main.rs}}
```

## Translations

Translations in Perseus are handled through the [`TranslationsManager`](=i18n/trait.TranslationsManager@perseus) trait, which is described in further detail [here](:fundamentals/perseus-app), but you'll usually store them in a folder called `translations/` at the root of your project. The translator you're using will determine the format of these.

In Perseus, translators are controlled by feature flags, which are mutually exclusive. Currently, there are just two: the [Fluent] translator, and the simple translator. The former uses `.ftl` files, which are a complex system of defining translations that can handle gender, pluralization, and all sorts of other linguistic difficulties, whereas the latter is a drop-dead-simple JSON file of translation IDs with very basic variable interpolation. Generally, it's recommended to only use the Fluent translator if you really need it, because it will add about 100kB of extra Wasm to your `bundle.wasm`, which will slow down initial loads a little (this is pre-compression, however). The Fluent translator is enabled by the `translator-fluent` feature flag, and the simple one corresponds to `translator-lightweight`.

Take a look at [this example] for how a full i18n-ed app looks (or you can take a look at the source code of this website!). Once you've defined some translations IDs, you can use them like so:

```
{{#include ../../../examples/core/i18n/src/templates/index.rs}}
```

The critical point here is the use of [`t!`](=prelude/macro.t@perseus) macro, which takes in the render context and a translation ID, and outputs the localized version of the ID in the current locale (assuming it exists, otherwise it will panic). Variables can be interpolated by providing a third object, as shown in the above example.

## Localized routing

To write an `href` or imperative routing call to another page in an app using i18n, you want to make sure you're going to the right locale, and not causing locale detection all over again. To do this, you can use the [`link!`](=prelude/macro.link@perseus) macro, which automatically prepends the correct locale.

## Switching locales

Switching locales is actually incredibly easy: there's no context to update, or special subroutine to inform, you just navigate appropriately, and Perseus figures it out (because it's in charge of routing). By not using the `link!` macro, and instead navigating directly to a page like `/fr-FR/about`, users will be switched into the `fr-FR` locale, which the `link!` macro will then automatically apply after that.

If you're using a component to perform locale switching (often included in the header or footer), you'll want to check what path a user is currently on so you switch the locale for the current page. This is typically done through a `Reactor` convenience method:

```
Reactor::<G>::from_cx(cx).switch_locale("fr-FR")
```

Here, we're of course switching to `fr-FR`. This will implicitly involve a navigation and the fetching of the new translations.
19 changes: 19 additions & 0 deletions docs/next/en-US/fundamentals/preloading.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Preloading

One superpower of Perseus is its caching system, which takes any pages the user has already been to, figures out the minimium amount of information necessary to restore them without any network requests, and stores that, ensuring that pressing the back button leads to an instant response. Sometimes, however, you want this to work in the other direction too: if you are fairly confident of which page a user will go to next, you can *preload* it to make sure they get the content immediately.

Now, usually you would do preloading through the browser, whcih will fetch resources intelligently to minimize load times, but, again, Perseus knows better than the browser in a lot of cases. To render a new page, all it needs is the page's state and its document metadata, which actually come from a special internal link (behind `/.perseus/page`). Preloading this through the browser is finicky, and it doesn't allow Perseus to do some pre-parsing to keep things speedy, so Perseus provides its own imperative preloading interface.

There are two ways of using this interface: there's the easy way, and the fine-grained way. The easy way is to use the `.preload()` method on the `Reactor`, which spawns a future for you and panics on errors that you caused (like a misspelled route), while silently failing on errors from the server. Alternately, you could use the `.try_preload()` method, which lets you handle the errors, and forces you to manage the asynchronicity yourself. If you want more control over the error handling (which applies especially if you're preloading a route that you haven't hardcoded), then you should use this method instead.

Here's an example of using preloading:

```
#{include ../../../examples/core/preload/src/templates/index.rs}
```

(Don't worry about the weird links at the bottom, they're just for showing how preloading works with internationalization.)

When that `.preload()` call is hit, Perseus will continue going with execution, meaning the main thread isn't blocked, while simultaneously loading the preloading route (the `about` page) in the background. This means that, when the user clicks on the link to the about page, they'll see it immediately (and we mean literally instantaneously).

As the comments in the above example mention, however, you can't preload across locales, that would lead to errors. The reason is because Perseus can only manage one set of translations in memory at once, deliberately so (since translations can be *extremely* heavy).
15 changes: 15 additions & 0 deletions docs/next/en-US/fundamentals/reactor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# The reactor

Whenever you're working with Perseus' internals, whether it's to determine the current locale, hook into the router state, preload a page, or forcibly evict a large page from the state store, you'll need to get familiar with the [`Reactor`](=prelude/struct.Reactor@perseus). This is a multi-platform (i.e. available on the engine-side *and* the browser-side) type responsible for creating a unified environment for Perseus renders. Almost everything Perseus does in the background on the browser-side is event-driven (e.g. navigate to page X when the user presses this button, display error Y when this invalid thing is done), and the reactor is the side of all this.

The other thing the reactor does is manage all reactivity in Perseus. See, reactivity, *reactor*? Nifty, eh?

Accessing the reactor is very simple, as it's provided through Sycamore's context system, and it has a method for extracting itself therefrom:

```
Reactor::<G>::from_cx(cx)
```

Note the presence of the `G` type parameter, which is provided because the reactor behaves differently on the engine-side and the client-side. It also needs to know whether or not it's hydrating, because it's responsible for rendering. Note that putting in a type other than `G` here will lead to the `Reactor` not being found *sometimes*, and being found at other times. This can lead to headache-inducing errors that seem to make almost no sense.

It is also important to be aware of the fact that Perseus aligns the `G` parameter with the rendering environment, such that being on the client-side is guaranteed to lead to a `DomNode`/`HydrateNode` (depending on the `hydrate` feature flag), and being on the engine-side is guaranteed to lead to an `SsrNode`. Trying to manually violate this pattern, say by trying to render a page to a string on the client-side through Perseus, will lead to panics, which Perseus uses to prevent undefined behavior. If you want to do server-side rendering in the user's browser, you should do it directly through Sycamore's functions, and you *must not* use capsules, because those will *definitely* panic if they're rendered in weird circumstances like those.
23 changes: 23 additions & 0 deletions docs/next/en-US/fundamentals/routing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Routing and navigation

One of things Perseus is big on is *page-based programming*, where each separate view in your app is a completely separate page, since this lets you manage their states all independently. However, one of the thing that needs to happen for you to be able to work like this is *routing*: you need to be able to go from one page to another.

Under the hood, Perseus uses a slightly modified version of [Sycamore's router], which means you can use typical Sycamore conventions for both imperative and declarative routing.

## Declarative routing

Declarative routing is when you create an element that will cause routing when it's clicked, and then, when the user clicks it, the routing occurs. In HTML, you would do this by creating a simple anchor tag (`<a>`) with an `href` property equal to where you want to go, and this is...well, exactly what you do in Perseus too! The Sycamore router will automatically detect any links in your app and appropriate them from the browser, so that Perseus can use its special routing behavior to minimize page load times and improve performance (since we know more about the structure of the app than the browser). A link looks like this:

```rust
a(href = "about") { "Click me to go to the about page!" }
```

Remember though, Perseus sets a `<base />` tag that tells the browser to treat all routes as relative to the root of your site. So, if you're at `/my/test/page`, routing to `foo` will go to `/foo`, *not* `/my/test/foo`! This is an important difference between Perseus and a lot of other frameworks. (The reason it's like this is to make it much easier to deploy Perseus under a relative path, like `framesurge.sh/perseus`.)

## Imperative routing

Sometimes, you'll need to write some code that causes a route change, which you can do with the [`navigate`](=prelude/fn.navigate@perseus) function, which is re-exported from the `sycamore-router` package for convenience. You provide this function with a route, and it will take you there! If you want to *replace* the current page in the navigation history, which you can understand by imagining the browser history as a stack of plates that you add things to (`navigate` adds a new plate, replacement navigation replaces the previous plate, meaning the user can't press the back button to go back to it), you can use [`navigate_replace`](=prelude/fn.navigate_replace@perseus). Generally, you won't have a need for this though, as it's really only used in hard redirects and locale redirection (which is handled automatically by Perseus).

## Localized routing

If you're using internationalization, there are a few quirks of routing you should be aware of, which are covered in greater detail on [this page](:fundamentals/i18n). As a summary, put all your links (in `href`s, in `navigate()` calls, etc.) in the `link!` macro, which will prepend the current locale to make sure the user ends up in the right place.
Loading

0 comments on commit 5bb9bd3

Please sign in to comment.