From 5bb9bd3a191a29d2ead1b0508e7f83a5c753f27d Mon Sep 17 00:00:00 2001 From: arctic-hen7 Date: Wed, 11 Jan 2023 09:52:42 +1100 Subject: [PATCH] docs: wrote further fundamental docs --- docs/next/en-US/SUMMARY.md | 1 + docs/next/en-US/fundamentals/debugging.md | 23 +++++++ docs/next/en-US/fundamentals/i18n.md | 43 +++++++++++++ docs/next/en-US/fundamentals/preloading.md | 19 ++++++ docs/next/en-US/fundamentals/reactor.md | 15 +++++ docs/next/en-US/fundamentals/routing.md | 23 +++++++ .../en-US/fundamentals/serving-exporting.md | 61 +++++++++++++++++++ .../next/en-US/fundamentals/static-content.md | 15 +++++ docs/next/en-US/fundamentals/styling.md | 29 +++++++++ 9 files changed, 229 insertions(+) create mode 100644 docs/next/en-US/fundamentals/debugging.md create mode 100644 docs/next/en-US/fundamentals/i18n.md create mode 100644 docs/next/en-US/fundamentals/preloading.md create mode 100644 docs/next/en-US/fundamentals/reactor.md create mode 100644 docs/next/en-US/fundamentals/routing.md create mode 100644 docs/next/en-US/fundamentals/serving-exporting.md create mode 100644 docs/next/en-US/fundamentals/static-content.md create mode 100644 docs/next/en-US/fundamentals/styling.md diff --git a/docs/next/en-US/SUMMARY.md b/docs/next/en-US/SUMMARY.md index 8e638a3602..2715b4d94b 100644 --- a/docs/next/en-US/SUMMARY.md +++ b/docs/next/en-US/SUMMARY.md @@ -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) diff --git a/docs/next/en-US/fundamentals/debugging.md b/docs/next/en-US/fundamentals/debugging.md new file mode 100644 index 0000000000..1835ea09e5 --- /dev/null +++ b/docs/next/en-US/fundamentals/debugging.md @@ -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). diff --git a/docs/next/en-US/fundamentals/i18n.md b/docs/next/en-US/fundamentals/i18n.md new file mode 100644 index 0000000000..95ee7db4e4 --- /dev/null +++ b/docs/next/en-US/fundamentals/i18n.md @@ -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::::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. diff --git a/docs/next/en-US/fundamentals/preloading.md b/docs/next/en-US/fundamentals/preloading.md new file mode 100644 index 0000000000..b660ecbe8d --- /dev/null +++ b/docs/next/en-US/fundamentals/preloading.md @@ -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). diff --git a/docs/next/en-US/fundamentals/reactor.md b/docs/next/en-US/fundamentals/reactor.md new file mode 100644 index 0000000000..8102713014 --- /dev/null +++ b/docs/next/en-US/fundamentals/reactor.md @@ -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::::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. diff --git a/docs/next/en-US/fundamentals/routing.md b/docs/next/en-US/fundamentals/routing.md new file mode 100644 index 0000000000..02b82ee90d --- /dev/null +++ b/docs/next/en-US/fundamentals/routing.md @@ -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 (``) 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 `` 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. diff --git a/docs/next/en-US/fundamentals/serving-exporting.md b/docs/next/en-US/fundamentals/serving-exporting.md new file mode 100644 index 0000000000..962c5199d8 --- /dev/null +++ b/docs/next/en-US/fundamentals/serving-exporting.md @@ -0,0 +1,61 @@ +# Servers and exporting + +There are two ways to run a Perseus app: you can *serve* it, or you can *export* it. To understand the difference, we'll need to dive a little deeper into how Perseus builds your app. + +## 1. Preparation + +In the first stage, Perseus compiles both itself and your project, and then takes a look at your `PerseusApp`. This tells it what to expect in terms of internationalization, templates, and capsules, and allows it to formulate a plan to move ahead with. + +## 2. Building + +Now, Perseus calls the build-time logic on every single template in your app, building as many as it possibly can. Those with capsules that would stall this process are rescheduled if that's permitted. Once this stage is done, Perseus compiles the *render configuration*, which defines all the templates in your app, and then terminates. + +## 3. Wasm building + +*(White lie: this actually happens in parallel with steps 1 and 2.)* + +Next, Perseus will compile itself and your app to Wasm. Once this is done, there's no further processing performed on the Wasm bundle unless we're in release mode (in which case it's optimized to the nth degree). + +## 4a. Serving + +From here, if you've chosen to serve your app, Perseus will take the engine it used to build your app and repurpose that for serving your app (one binary does both, reducing compilation times substantially). This will spin up a server according to your `#[perseus::main(..)]` settings, and that will run until your terminate it, serving your app where you've specified. + +This server responds to each request by passing it through special pathways that are capable of calling request-time logic, state amalgamation, revalidation, and all manner of other things. This part of Perseus involves *just-in-time nested capsule resolution*, which is by far the most complex part of Perseus. All this has to be done upon receiving a user's request, so this binary is deliberately optimized for speed (meaning it can easily blow out to very large sizes) in release mode. + +### Server integrations + +Since Perseus tries to be as open as possible, it allows you to provide a custom function to `#[perseus::main(..)]` that will run your server. Usually, you'll just use the default server provided by one of the integrations, but you can also customize this however you like.. + +Server integrations are special crates, like `perseus-axum`, that provide the boilerplate to host Perseus through a particular server framework. Currently, Perseus has server integrations for [Actix Web], [Warp], and [Axum]. All of these have a `dflt-server` feature flag, which you can enable to gain access to the `perseus-::dflt_server` function, which will spin up a server that just hosts Perseus. + +However, most apps also have several API routes associated with them, especially if you're working with a database. Since you can provide a custom function to host Perseus, you can also add arbitrary API routes. You can take a look at the [custom server example] for further details on this, or take a look at the source code for the server integration you're using. + +*Note: due to [this bug], Warpthe Warp integration must currently be used with the [`warp-fix-171`] crate, rather than the `warp` crate itself.* + +#### Writing a server integration + +If you're unhappy with the defaults a particualr server integration provides, you are *strongly* encouraged to modify it yourself, which isn't a difficult process at all. Deliberately, Perseus abstracts nearly all serving functionality into the core, behind a type called `Turbine`, a static reference to which is provided to server integrations. This means all you're doing in a server integration is spinning up the right routes. In fact, all server integrations follow a pre-defined pattern of exactly what they have to do, making it much easier to write and modify them for your needs. If you really want, you can even fly solo without a server integration, and implement everything yourself (although this is not recommended unless you have very specific requirements). + +Currently, Perseus has no support for arbitrary middleware, and modifying the server integration is the only way to do this. Bear in mind, however, that the Axum integration is only one file with 173 lines of code and comments in it --- these integrations are designed to be tweaked! + +## 4b. Exporting + +Alternately, if you've chosen to export, Perseus will mimic what the server does a little. It will first rearrange all the files the build process generated into folders that mimic the structure of the requests the client will send (e.g. things in `dist/static` get moved to `dist/exported/.perseus/static`), and then it will loop through all the pages and create initial load files for all of them. If you were serving your app, Perseus would insert page fragments into your *index view* at request-time, but exported apps do this ahead-of-time so no processing is required at request-time. + +The output of exporting is a folder (`dist/exported`) that contains everything needed to run your app. Importantly, exported apps don't have access to any request-time features, like incremental generation or revalidation, and using any of these will cause the build process to neatly fail. However, exported apps are often a lot easier to deploy. For example, deploying this website is just a matter of uploading the generated static files to the `gh-pages` branch, and GitHub oes the rest for us (because there's no server involved, it's much easier to orchestrate these kinds of deployments). Exported apps are also usually cheaper to host. + +When you run `perseus deploy -e`, Perseus will build your Wasm in release mode and move everything into a `pkg/` folder that's fully optimized for production. + +*Note: `perseus export -s` exists to spin up a miniature file server to avooid your needing to bring your own in development.* + +### Error pages in exported apps + +One thing exported apps often struggle with is proper error handling. Once the Wasm bundle has been delivered to the client, they're fine and dandy, and can display all the errors they like, but the server-side is trickier. When Perseus controls it, it can carefully format error pages with exactly the right information, but typical file servers aren't quite so subtle. Especially for internationalized apps, this can be a problem. The best solution is to export your error pages to static files, which can be done like so: + +``` +perseus export-error-page --code 404 --output pkg/404.html +``` + +Here, we export the 404 page to `pkg/404.html`, where is will be picked up and served in the event of a 404 error by most file hosts. However, since we don't know the user's locale in advance, we can't localize this page appropriately, or even send the right translations. For apps not using i18n, this won't be a problem, but i18n-ed apps should prefer serving over exporting where possible. + +Note that adding `-s` to a `perseus export` command in development will automatically export your 404 page, but the deployment system will not do this, so you may wish to make this a separate stage in your deployment process, depending on whether or not your file host supports this pattern. diff --git a/docs/next/en-US/fundamentals/static-content.md b/docs/next/en-US/fundamentals/static-content.md new file mode 100644 index 0000000000..4473badc75 --- /dev/null +++ b/docs/next/en-US/fundamentals/static-content.md @@ -0,0 +1,15 @@ +# Static content + +As nice as it is to write everything in Rust, you will, in web development, undoubtedly have to have some static content eventually. This is usually in the form of stylesheets, or images, or even binaries that your users can download, but there will be something. This is where Perseus' static content system comes into play. The simplest way to use this is through a `static/` directory in the root of your project, which Perseus will serve at `/.perseus/static`. For example, if you create `static/index.css`, that will be available in a `` tag at `href=".perseus/static/index.css"`. *Notice the lack of a leading forward slash!* This is because it's unnecessary: Perseus implants a `` tag that makes it so, and this makes your app flexible to being served at relative paths (like `framesurge.sh/perseus`). + +Sometimes, however, you'll want static content from outside `static/`, which is where *static aliases* come into play. These allow you to link in arbitrary files hosted at arbitrary paths, which will be served unquestioningly by Perseus. You can declare static aliases on your `PerseusApp` like so: + +``` +{{#include ../../../examples/core/static_content/src/main.rs}} +``` + +The first argument to `.static_alias()` is the path at which the file should be hosted, and the second is the path to the file, relative to the root of your project. Importantly, Perseus will reject any files outside the root of your project, simply because this can be a major security risk: static aliases are resolved at runtime, so if you set a `../../passwd` static alias, and that happens to map to an innocuous file on your local system, but a sensitive file on your server, that's a big problem! If you need access to such files, use symbolic links (which Perseus will begrudgingly work with). + +## Generated static content + +Usually, when static content is generated by an external tool, like a CSS bundler, or a plugin, it's better to put it in `dist/`, where it will be ignored by the the `-w` part of `perseus serve -w` and similar commands. This will also be exempt from version control, keeping your Git repos a bit more lightweight. diff --git a/docs/next/en-US/fundamentals/styling.md b/docs/next/en-US/fundamentals/styling.md new file mode 100644 index 0000000000..30d89633c5 --- /dev/null +++ b/docs/next/en-US/fundamentals/styling.md @@ -0,0 +1,29 @@ +# Styling + +In any kind of web development, you probably want your site to look good, and that will involve working with a language called *CSS*, short for *Cascading Style Sheets*. It's well beyond the scope of these docs to explain CSS, so we'll leave that to [this fantastic introduction] if you're new to it. + +Right now, Perseus and Sycamore have limited inbuilt styling capabilities, and you're best off using either traditional styling (i.e. set a class `header-button` and style that in `header.css`, etc.), or a styling library like [Tailwind], which provides utility classes like `rounded` and `dark:shadow-lg`. + +*There is currently work ongoing on a styling framework for Sycamore/Perseus called [Jacaranda](https://github.com/framesurge/jacaranda), which will support fully typed styling!* + +## Full-page layouts + +A lot of websites these days are based on *full-page layouts*, which are when the entire page is taken up, usually by a header, some main content, and a footer. Getting this to work well, however, if unreasonably complicated in many cases. So, here's an example of exactly what CSS you need to get it working: + +```css +{{#include ../../../examples/demos/full_page_layout/static/style.css}} +``` + +The comments in this file should make it fairly self-explanatory, but what it does is creates a sticky header that maintains its spot when the user scrolls, while the footer will always be at the bottom of the page (but is not sticky when the content overflows the page). You can combine this with a layout component like this to get an easy way of creating full-page layouts for your sites: + +```rust +{{#include ../../../examples/demos/full_page_layout/src/components/layout.rs}} +``` + +You can then use this like so: + +``` +{{#include ../../../examples/demos/full_page_layout/src/templates/index.rs}} +``` + +For more about full page layouts, see [this example].