diff --git a/.actrc b/.actrc
new file mode 100644
index 0000000000..8fa59f2aaf
--- /dev/null
+++ b/.actrc
@@ -0,0 +1,9 @@
+# Act configuration for Perseus project
+# Use Rust image that has cargo pre-installed
+-P ubuntu-latest=rust:latest
+
+# Reuse containers to speed up repeated runs
+--reuse
+
+# Use bind mount for performance
+--use-gitignore=false
diff --git a/.cargo/config.toml b/.cargo/config.toml
index d335760a16..a55b6f4b7f 100644
--- a/.cargo/config.toml
+++ b/.cargo/config.toml
@@ -1,3 +1,3 @@
[build]
-rustflags = [ "--cfg", "engine" ]
-rustdocflags = [ "--cfg", "engine" ]
+rustflags = ["--cfg", "engine"]
+rustdocflags = ["--cfg", "engine"]
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 540b882cf9..65c4398fff 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -4,6 +4,7 @@ on:
push:
branches:
- main
+ - update-v5.0
pull_request:
jobs:
@@ -69,6 +70,8 @@ jobs:
- run: rustup target add wasm32-unknown-unknown
- name: Run CLI tests
run: bonnie test cli
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# We now have a separate job for each example's E2E testing because they all take a while, we may as well run them in parallel
# The job for each E2E test is exactly the same except for a minor difference, so we'll use a matrix based on listing the subdirectories
e2e-example-test:
@@ -145,26 +148,20 @@ jobs:
target/
key: ${{ runner.os }}-cargo-${{ hashFiles('Cargo.lock') }}
- # # Also cache the apt packages we need for testing
- # - uses: awalsh128/cache-apt-pkgs-action@latest
- # with:
- # packages: firefox
- # version: 1.0
+ # Setup Chrome and ChromeDriver for E2E tests
+ - name: Setup Chrome and ChromeDriver
+ uses: browser-actions/setup-chrome@v1
+ with:
+ chrome-version: stable
+ install-chromedriver: true
- # # And finally cache Geckodriver itself
- # - uses: actions/cache@v3
- # id: geckocache
- # with:
- # path: |
- # ~/.geckodriver
- # # The cache should be OS-specific
- # key: ${{ runner.os }}-geckodriver
- # - name: Install Geckodriver
- # if: steps.geckocache.outputs.cache-hit != 'true'
- # run: wget -O ~/geckodriver-archive https://github.com/mozilla/geckodriver/releases/download/v0.32.0/geckodriver-v0.32.0-linux64.tar.gz && tar -xvzf ~/geckodriver-archive -C ~/ && mv ~/geckodriver ~/.geckodriver && chmod +x ~/.geckodriver
- - run: sudo apt update && sudo apt install firefox
+ - run: rustup target add wasm32-unknown-unknown
- - name: Run Firefox WebDriver
- run: geckodriver &
+ - name: Run Chrome WebDriver
+ run: chromedriver --port=4444 &
+ - name: Wait for Chromedriver to start
+ run: sleep 3
- name: Run E2E tests for example ${{ matrix.name }} in category ${{ matrix.type }}
run: bonnie test example-all-integrations ${{ matrix.type }} ${{ matrix.name }}
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.gitignore b/.gitignore
index 773e6e1d60..3a72dd534d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,10 +2,14 @@
/target_engine
/target_engine_clientdoc
/target_wasm
-Cargo.lock
pkg/
.tribble/
.idea/
target_engine/
target_wasm/
dist/
+.claude/
+AGENTS.md
+doc/
+*.sh
+*.backup
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 32c852b655..f6f3e531e1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,95 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
+## [0.5.0](https://github.com/framesurge/perseus/compare/v0.4.3...v0.5.0) (2025-01-XX)
+
+### ⚠ BREAKING CHANGES
+
+This release upgrades Perseus from Sycamore 0.8.x to Sycamore 0.9.2, bringing significant API improvements but requiring migration of existing code.
+
+#### View Function Signature Changes
+
+```rust
+// Before (0.4.x with Sycamore 0.8)
+fn my_page(cx: Scope) -> View {
+ view! { cx, p { "Hello" } }
+}
+
+// After (0.5.0 with Sycamore 0.9.2)
+fn my_page() -> View {
+ view! { p { "Hello" } }
+}
+```
+
+#### Signal API Changes
+
+```rust
+// Before
+let count = cx.create_signal(0);
+state.items.modify().push(item);
+
+// After
+let count = create_signal(0);
+state.items.update(|items| items.push(item));
+```
+
+#### Reactor Access Changes
+
+```rust
+// Before
+let reactor = Reactor::::from_cx(cx);
+
+// After
+let reactor = Reactor::::from_cx();
+```
+
+### Features
+
+* **components:** Add `Link` component for client-side navigation ([0ee5abd](https://github.com/framesurge/perseus/commit/0ee5abd8))
+ ```rust
+ // New way to create internal links
+ Link(to = "/about") { "About Us" }
+ ```
+* **docs:** Add complete 0.5.x documentation with Sycamore 0.9.2 syntax ([e0938b0](https://github.com/framesurge/perseus/commit/e0938b06))
+* **docs:** Add comprehensive migration guide from 0.4.x to 0.5.x
+
+### Bug Fixes
+
+* **compat:** Fix WASM client crashes with Sycamore 0.9.2 ([fcc2230](https://github.com/framesurge/perseus/commit/fcc2230f))
+* **state:** Preserve reactive state across client-side navigation ([eb70dd5](https://github.com/framesurge/perseus/commit/eb70dd5e))
+* **state:** Create global state signals in root scope to survive navigation ([2f26554](https://github.com/framesurge/perseus/commit/2f26554a))
+* **cli:** Fix relative path bug in serve_exported ([2695a36](https://github.com/framesurge/perseus/commit/2695a368))
+* **cli:** Handle GitHub API rate limiting for wasm-opt version checks ([a5ab490](https://github.com/framesurge/perseus/commit/a5ab4900))
+* **cli:** Catch minify-js panics and fall back to unminified JS ([e1ae2f9](https://github.com/framesurge/perseus/commit/e1ae2f96))
+* **hydration:** Fix hydration issues with new Sycamore API ([f40321f](https://github.com/framesurge/perseus/commit/f40321f3))
+* **context:** Ensure reactor context accessible in child scopes ([2cd25c5](https://github.com/framesurge/perseus/commit/2cd25c54))
+* **compat:** Update PanicInfo to PanicHookInfo for Rust 1.82+ ([661d4ce](https://github.com/framesurge/perseus/commit/661d4ce9))
+
+### Code Refactoring
+
+* Update all examples to use Sycamore 0.9.2 syntax
+* Update website components for Sycamore 0.9.2 compatibility
+* Remove deprecated `Scope` parameter from all view functions
+* Replace `` generics with concrete `View` type
+
+### Dependencies
+
+* `sycamore` → 0.9.2
+* `sycamore-router` → 0.9.2
+
+### Migration Guide
+
+For detailed migration information, see the [migration guide](https://framesurge.sh/perseus/en-US/docs/migrating) or the `docs/0.5.x/en-US/migrating.md` file.
+
+Key changes:
+
+1. **Remove Scope parameters** from all view functions
+2. **Remove `` generics** - use `View` instead of `View`
+3. **Update view! macro** - remove `cx` as first argument
+4. **Use Link component** for internal navigation instead of `a(href=...)`
+5. **Update signal access** - use `create_signal(value)` as free function
+6. **Update Reactor access** - use `Reactor::::from_cx()`
+
### [0.4.3](/home/arctic-hen7/me/.main-mirror.git/compare/v0.4.2...v0.4.3) (2024-07-19)
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000000..04df712603
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,5864 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "actix-codec"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a"
+dependencies = [
+ "bitflags 2.10.0",
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "memchr",
+ "pin-project-lite",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "actix-files"
+version = "0.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c0d87f10d70e2948ad40e8edea79c8e77c6c66e0250a4c1f09b690465199576"
+dependencies = [
+ "actix-http",
+ "actix-service",
+ "actix-utils",
+ "actix-web",
+ "bitflags 2.10.0",
+ "bytes",
+ "derive_more 2.0.1",
+ "futures-core",
+ "http-range",
+ "log",
+ "mime",
+ "mime_guess",
+ "percent-encoding",
+ "pin-project-lite",
+ "v_htmlescape",
+]
+
+[[package]]
+name = "actix-http"
+version = "3.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7926860314cbe2fb5d1f13731e387ab43bd32bca224e82e6e2db85de0a3dba49"
+dependencies = [
+ "actix-codec",
+ "actix-rt",
+ "actix-service",
+ "actix-utils",
+ "base64 0.22.1",
+ "bitflags 2.10.0",
+ "brotli 8.0.2",
+ "bytes",
+ "bytestring",
+ "derive_more 2.0.1",
+ "encoding_rs",
+ "flate2",
+ "foldhash",
+ "futures-core",
+ "h2 0.3.27",
+ "http 0.2.12",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "language-tags",
+ "local-channel",
+ "mime",
+ "percent-encoding",
+ "pin-project-lite",
+ "rand 0.9.2",
+ "sha1",
+ "smallvec",
+ "tokio",
+ "tokio-util",
+ "tracing",
+ "zstd",
+]
+
+[[package]]
+name = "actix-macros"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb"
+dependencies = [
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "actix-router"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8"
+dependencies = [
+ "bytestring",
+ "cfg-if 1.0.4",
+ "http 0.2.12",
+ "regex",
+ "regex-lite",
+ "serde",
+ "tracing",
+]
+
+[[package]]
+name = "actix-rt"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92589714878ca59a7626ea19734f0e07a6a875197eec751bb5d3f99e64998c63"
+dependencies = [
+ "futures-core",
+ "tokio",
+]
+
+[[package]]
+name = "actix-server"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502"
+dependencies = [
+ "actix-rt",
+ "actix-service",
+ "actix-utils",
+ "futures-core",
+ "futures-util",
+ "mio",
+ "socket2 0.5.10",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "actix-service"
+version = "2.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "actix-utils"
+version = "3.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8"
+dependencies = [
+ "local-waker",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "actix-web"
+version = "4.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2233f53f6cb18ae038ce1f0713ca0c72ca0c4b71fe9aaeb59924ce2c89c6dd85"
+dependencies = [
+ "actix-codec",
+ "actix-http",
+ "actix-macros",
+ "actix-router",
+ "actix-rt",
+ "actix-server",
+ "actix-service",
+ "actix-utils",
+ "actix-web-codegen",
+ "bytes",
+ "bytestring",
+ "cfg-if 1.0.4",
+ "cookie 0.16.2",
+ "derive_more 2.0.1",
+ "encoding_rs",
+ "foldhash",
+ "futures-core",
+ "futures-util",
+ "impl-more",
+ "itoa",
+ "language-tags",
+ "log",
+ "mime",
+ "once_cell",
+ "pin-project-lite",
+ "regex",
+ "regex-lite",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "smallvec",
+ "socket2 0.6.1",
+ "time",
+ "tracing",
+ "url",
+]
+
+[[package]]
+name = "actix-web-codegen"
+version = "4.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8"
+dependencies = [
+ "actix-router",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "ahash"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
+dependencies = [
+ "cfg-if 1.0.4",
+ "once_cell",
+ "version_check",
+ "zerocopy",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "0.7.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "alloc-no-stdlib"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
+
+[[package]]
+name = "alloc-stdlib"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
+dependencies = [
+ "alloc-no-stdlib",
+]
+
+[[package]]
+name = "allocator-api2"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anstream"
+version = "0.6.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
+
+[[package]]
+name = "assert_cmd"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bcbb6924530aa9e0432442af08bbcafdad182db80d2e560da42a6d442535bf85"
+dependencies = [
+ "anstyle",
+ "bstr",
+ "libc",
+ "predicates",
+ "predicates-core",
+ "predicates-tree",
+ "wait-timeout",
+]
+
+[[package]]
+name = "assert_fs"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a652f6cb1f516886fcfee5e7a5c078b9ade62cfcb889524efe5a64d682dd27a9"
+dependencies = [
+ "anstyle",
+ "doc-comment",
+ "globwalk",
+ "predicates",
+ "predicates-core",
+ "predicates-tree",
+ "tempfile",
+]
+
+[[package]]
+name = "async-compression"
+version = "0.3.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "942c7cd7ae39e91bde4820d74132e9862e62c2f386c3aa90ccf55949f5bad63a"
+dependencies = [
+ "brotli 3.5.0",
+ "flate2",
+ "futures-core",
+ "memchr",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "async-compression"
+version = "0.4.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93c1f86859c1af3d514fa19e8323147ff10ea98684e6c7b307912509f50e67b2"
+dependencies = [
+ "compression-codecs",
+ "compression-core",
+ "futures-core",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "async-stream"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
+dependencies = [
+ "async-stream-impl",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-stream-impl"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "async-trait"
+version = "0.1.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "atomic"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba"
+
+[[package]]
+name = "atomic"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340"
+dependencies = [
+ "bytemuck",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "autocfg"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+[[package]]
+name = "axum"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b098575ebe77cb6d14fc7f32749631a6e44edbef6b796f89b020e99ba20d425"
+dependencies = [
+ "axum-core",
+ "bytes",
+ "form_urlencoded",
+ "futures-util",
+ "http 1.4.0",
+ "http-body 1.0.1",
+ "http-body-util",
+ "hyper 1.8.1",
+ "hyper-util",
+ "itoa",
+ "matchit",
+ "memchr",
+ "mime",
+ "percent-encoding",
+ "pin-project-lite",
+ "serde_core",
+ "serde_json",
+ "serde_path_to_error",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "axum-core"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http 1.4.0",
+ "http-body 1.0.1",
+ "http-body-util",
+ "mime",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "base64"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
+
+[[package]]
+name = "base64"
+version = "0.21.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "binascii"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "383d29d513d8764dcdc42ea295d979eb99c3c9f00607b3692cf68a431f7dca72"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "block2"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
+dependencies = [
+ "objc2",
+]
+
+[[package]]
+name = "brotli"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d640d25bc63c50fb1f0b545ffd80207d2e10a4c965530809b40ba3386825c391"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+ "brotli-decompressor 2.5.1",
+]
+
+[[package]]
+name = "brotli"
+version = "8.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+ "brotli-decompressor 5.0.0",
+]
+
+[[package]]
+name = "brotli-decompressor"
+version = "2.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+]
+
+[[package]]
+name = "brotli-decompressor"
+version = "5.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+]
+
+[[package]]
+name = "brotlic"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f552f56f302af0006c32b50bfa2bdb4696fd6ba33c3ab9f6225fefdb1efdc680"
+dependencies = [
+ "brotlic-sys",
+]
+
+[[package]]
+name = "brotlic-sys"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "afdec5c62bc97b56349053cf66ba503af5c2448591be61c3ad70a5f11b57e574"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "bstr"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
+dependencies = [
+ "memchr",
+ "regex-automata",
+ "serde",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
+
+[[package]]
+name = "bytemuck"
+version = "1.24.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "bytes"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
+
+[[package]]
+name = "bytestring"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "113b4343b5f6617e7ad401ced8de3cc8b012e73a594347c307b90db3e9271289"
+dependencies = [
+ "bytes",
+]
+
+[[package]]
+name = "camino"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "cargo-lock"
+version = "11.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf53e0ebbbc6e45357b199f3b213f3eb330792c8b370e548499f5685470ecb11"
+dependencies = [
+ "semver",
+ "serde",
+ "toml 0.9.8",
+ "url",
+]
+
+[[package]]
+name = "cargo-platform"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "122ec45a44b270afd1402f351b782c676b173e3c3fb28d86ff7ebfb4d86a4ee4"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "cargo_metadata"
+version = "0.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef987d17b0a113becdd19d3d0022d04d7ef41f9efe4f3fb63ac44ba61df3ade9"
+dependencies = [
+ "camino",
+ "cargo-platform",
+ "semver",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.17",
+]
+
+[[package]]
+name = "cargo_toml"
+version = "0.22.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77"
+dependencies = [
+ "serde",
+ "toml 0.9.8",
+]
+
+[[package]]
+name = "cc"
+version = "1.2.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36"
+dependencies = [
+ "find-msvc-tools",
+ "jobserver",
+ "libc",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
+[[package]]
+name = "chrono"
+version = "0.4.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
+dependencies = [
+ "iana-time-zone",
+ "js-sys",
+ "num-traits",
+ "wasm-bindgen",
+ "windows-link",
+]
+
+[[package]]
+name = "clap"
+version = "4.5.51"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.5.51"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.5.49"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
+
+[[package]]
+name = "command-group"
+version = "5.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a68fa787550392a9d58f44c21a3022cfb3ea3e2458b7f85d3b399d0ceeccf409"
+dependencies = [
+ "nix 0.27.1",
+ "winapi",
+]
+
+[[package]]
+name = "compression-codecs"
+version = "0.4.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "680dc087785c5230f8e8843e2e57ac7c1c90488b6a91b88caa265410568f441b"
+dependencies = [
+ "brotli 8.0.2",
+ "compression-core",
+ "flate2",
+ "memchr",
+]
+
+[[package]]
+name = "compression-core"
+version = "0.4.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a9b614a5787ef0c8802a55766480563cb3a93b435898c422ed2a359cf811582"
+
+[[package]]
+name = "console"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b430743a6eb14e9764d4260d4c0d8123087d504eeb9c48f2b2a5e810dd369df4"
+dependencies = [
+ "encode_unicode",
+ "libc",
+ "once_cell",
+ "unicode-width",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "console_error_panic_hook"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
+dependencies = [
+ "cfg-if 1.0.4",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "convert_case"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
+
+[[package]]
+name = "cookie"
+version = "0.16.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb"
+dependencies = [
+ "percent-encoding",
+ "time",
+ "version_check",
+]
+
+[[package]]
+name = "cookie"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
+dependencies = [
+ "percent-encoding",
+ "time",
+ "version_check",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "core_maths"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30"
+dependencies = [
+ "libm",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+dependencies = [
+ "cfg-if 1.0.4",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "crypto-common"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "css-minify"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "874c6e2d19f8d4a285083b11a3241bfbe01ac3ed85f26e1e6b34888d960552bd"
+dependencies = [
+ "derive_more 0.99.20",
+ "indexmap 1.9.3",
+ "nom",
+]
+
+[[package]]
+name = "ctrlc"
+version = "3.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73736a89c4aff73035ba2ed2e565061954da00d4970fc9ac25dcc85a2a20d790"
+dependencies = [
+ "dispatch2",
+ "nix 0.30.1",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "darling"
+version = "0.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "data-encoding"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
+
+[[package]]
+name = "deranged"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587"
+dependencies = [
+ "powerfmt",
+]
+
+[[package]]
+name = "derive_more"
+version = "0.99.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f"
+dependencies = [
+ "convert_case",
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "syn",
+]
+
+[[package]]
+name = "derive_more"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
+dependencies = [
+ "derive_more-impl",
+]
+
+[[package]]
+name = "derive_more-impl"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "unicode-xid",
+]
+
+[[package]]
+name = "devise"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1d90b0c4c777a2cad215e3c7be59ac7c15adf45cf76317009b7d096d46f651d"
+dependencies = [
+ "devise_codegen",
+ "devise_core",
+]
+
+[[package]]
+name = "devise_codegen"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71b28680d8be17a570a2334922518be6adc3f58ecc880cbb404eaeb8624fd867"
+dependencies = [
+ "devise_core",
+ "quote",
+]
+
+[[package]]
+name = "devise_core"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b035a542cf7abf01f2e3c4d5a7acbaebfefe120ae4efc7bde3df98186e4b8af7"
+dependencies = [
+ "bitflags 2.10.0",
+ "proc-macro2",
+ "proc-macro2-diagnostics",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "difflib"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "directories"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "dispatch2"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec"
+dependencies = [
+ "bitflags 2.10.0",
+ "block2",
+ "libc",
+ "objc2",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "dissimilar"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8975ffdaa0ef3661bfe02dbdcc06c9f829dfafe6a3c474de366a8d5e44276921"
+
+[[package]]
+name = "doc-comment"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "780955b8b195a21ab8e4ac6b60dd1dbdcec1dc6c51c0617964b08c81785e12c9"
+
+[[package]]
+name = "either"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+
+[[package]]
+name = "encode_unicode"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
+dependencies = [
+ "cfg-if 1.0.4",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "fantoccini"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d0086bcd59795408c87a04f94b5a8bd62cba2856cfe656c7e6439061d95b760"
+dependencies = [
+ "base64 0.22.1",
+ "cookie 0.18.1",
+ "futures-util",
+ "http 1.4.0",
+ "http-body-util",
+ "hyper 1.8.1",
+ "hyper-tls",
+ "hyper-util",
+ "mime",
+ "openssl",
+ "serde",
+ "serde_json",
+ "time",
+ "tokio",
+ "url",
+ "webdriver",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+
+[[package]]
+name = "figment"
+version = "0.10.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3"
+dependencies = [
+ "atomic 0.6.1",
+ "pear",
+ "serde",
+ "toml 0.8.23",
+ "uncased",
+ "version_check",
+]
+
+[[package]]
+name = "filetime"
+version = "0.2.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed"
+dependencies = [
+ "cfg-if 1.0.4",
+ "libc",
+ "libredox",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844"
+
+[[package]]
+name = "flate2"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "float-cmp"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "fluent-bundle"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01203cb8918f5711e73891b347816d932046f95f54207710bda99beaeb423bf4"
+dependencies = [
+ "fluent-langneg",
+ "fluent-syntax",
+ "intl-memoizer",
+ "intl_pluralrules",
+ "rustc-hash 2.1.1",
+ "self_cell",
+ "smallvec",
+ "unic-langid",
+]
+
+[[package]]
+name = "fluent-langneg"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7eebbe59450baee8282d71676f3bfed5689aeab00b27545e83e5f14b1195e8b0"
+dependencies = [
+ "unic-langid",
+]
+
+[[package]]
+name = "fluent-syntax"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54f0d287c53ffd184d04d8677f590f4ac5379785529e5e08b1c8083acdd5c198"
+dependencies = [
+ "memchr",
+ "thiserror 2.0.17",
+]
+
+[[package]]
+name = "fmterr"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a224f8df6326425eee955a654c558a6459ae6f2c9a5a715b42856790df24222"
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "fs_extra"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
+
+[[package]]
+name = "fsevent-sys"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "futures"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
+
+[[package]]
+name = "futures-task"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
+
+[[package]]
+name = "futures-util"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "generator"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e"
+dependencies = [
+ "cc",
+ "libc",
+ "log",
+ "rustversion",
+ "windows",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getopts"
+version = "0.2.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df"
+dependencies = [
+ "unicode-width",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
+dependencies = [
+ "cfg-if 1.0.4",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+dependencies = [
+ "cfg-if 1.0.4",
+ "libc",
+ "r-efi",
+ "wasip2",
+]
+
+[[package]]
+name = "glob"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
+
+[[package]]
+name = "globset"
+version = "0.4.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3"
+dependencies = [
+ "aho-corasick 1.1.4",
+ "bstr",
+ "log",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "globwalk"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757"
+dependencies = [
+ "bitflags 2.10.0",
+ "ignore",
+ "walkdir",
+]
+
+[[package]]
+name = "gloo-net"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2899cb1a13be9020b010967adc6b2a8a343b6f1428b90238c9d53ca24decc6db"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-sink",
+ "gloo-utils",
+ "js-sys",
+ "pin-project",
+ "serde",
+ "serde_json",
+ "thiserror 1.0.69",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-timers"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "gloo-utils"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e"
+dependencies = [
+ "js-sys",
+ "serde",
+ "serde_json",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "h2"
+version = "0.3.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d"
+dependencies = [
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "http 0.2.12",
+ "indexmap 2.12.0",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "h2"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "http 1.4.0",
+ "indexmap 2.12.0",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
+[[package]]
+name = "hashbrown"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
+dependencies = [
+ "ahash",
+ "bumpalo",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+dependencies = [
+ "ahash",
+ "allocator-api2",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
+
+[[package]]
+name = "headers"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270"
+dependencies = [
+ "base64 0.21.7",
+ "bytes",
+ "headers-core 0.2.0",
+ "http 0.2.12",
+ "httpdate",
+ "mime",
+ "sha1",
+]
+
+[[package]]
+name = "headers"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "headers-core 0.3.0",
+ "http 1.4.0",
+ "httpdate",
+ "mime",
+ "sha1",
+]
+
+[[package]]
+name = "headers-core"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429"
+dependencies = [
+ "http 0.2.12",
+]
+
+[[package]]
+name = "headers-core"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4"
+dependencies = [
+ "http 1.4.0",
+]
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hermit-abi"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
+
+[[package]]
+name = "html-escape"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476"
+dependencies = [
+ "utf8-width",
+]
+
+[[package]]
+name = "http"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
+dependencies = [
+ "bytes",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
+dependencies = [
+ "bytes",
+ "http 0.2.12",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+dependencies = [
+ "bytes",
+ "http 1.4.0",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http 1.4.0",
+ "http-body 1.0.1",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "http-range"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573"
+
+[[package]]
+name = "http-range-header"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
+name = "hyper"
+version = "0.14.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "h2 0.3.27",
+ "http 0.2.12",
+ "http-body 0.4.6",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "socket2 0.5.10",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "want",
+]
+
+[[package]]
+name = "hyper"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "h2 0.4.12",
+ "http 1.4.0",
+ "http-body 1.0.1",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "pin-utils",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.27.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
+dependencies = [
+ "http 1.4.0",
+ "hyper 1.8.1",
+ "hyper-util",
+ "rustls",
+ "rustls-pki-types",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+]
+
+[[package]]
+name = "hyper-tls"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
+dependencies = [
+ "bytes",
+ "http-body-util",
+ "hyper 1.8.1",
+ "hyper-util",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+ "tower-service",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "http 1.4.0",
+ "http-body 1.0.1",
+ "hyper 1.8.1",
+ "ipnet",
+ "libc",
+ "percent-encoding",
+ "pin-project-lite",
+ "socket2 0.6.1",
+ "system-configuration",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "windows-registry",
+]
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.64"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "log",
+ "wasm-bindgen",
+ "windows-core",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "icu_collections"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526"
+dependencies = [
+ "displaydoc",
+ "yoke 0.7.5",
+ "zerofrom",
+ "zerovec 0.10.4",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "yoke 0.8.1",
+ "zerofrom",
+ "zerovec 0.11.5",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6"
+dependencies = [
+ "displaydoc",
+ "litemap 0.8.1",
+ "tinystr 0.8.2",
+ "writeable 0.6.2",
+ "zerovec 0.11.5",
+]
+
+[[package]]
+name = "icu_locid"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637"
+dependencies = [
+ "displaydoc",
+ "litemap 0.7.5",
+ "tinystr 0.7.6",
+ "writeable 0.5.5",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599"
+dependencies = [
+ "icu_collections 2.1.1",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider 2.1.1",
+ "smallvec",
+ "zerovec 0.11.5",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a"
+
+[[package]]
+name = "icu_properties"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99"
+dependencies = [
+ "icu_collections 2.1.1",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider 2.1.1",
+ "zerotrie",
+ "zerovec 0.11.5",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899"
+
+[[package]]
+name = "icu_provider"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9"
+dependencies = [
+ "displaydoc",
+ "icu_locid",
+ "icu_provider_macros",
+ "stable_deref_trait",
+ "tinystr 0.7.6",
+ "writeable 0.5.5",
+ "yoke 0.7.5",
+ "zerofrom",
+ "zerovec 0.10.4",
+]
+
+[[package]]
+name = "icu_provider"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "writeable 0.6.2",
+ "yoke 0.8.1",
+ "zerofrom",
+ "zerotrie",
+ "zerovec 0.11.5",
+]
+
+[[package]]
+name = "icu_provider_macros"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "icu_segmenter"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a717725612346ffc2d7b42c94b820db6908048f39434504cb130e8b46256b0de"
+dependencies = [
+ "core_maths",
+ "displaydoc",
+ "icu_collections 1.5.0",
+ "icu_locid",
+ "icu_provider 1.5.0",
+ "icu_segmenter_data",
+ "utf8_iter",
+ "zerovec 0.10.4",
+]
+
+[[package]]
+name = "icu_segmenter_data"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1e52775179941363cc594e49ce99284d13d6948928d8e72c755f55e98caa1eb"
+
+[[package]]
+name = "idb"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3afe8830d5802f769dc0be20a87f9f116798c896650cb6266eb5c19a3c109eed"
+dependencies = [
+ "js-sys",
+ "num-traits",
+ "thiserror 1.0.69",
+ "tokio",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
+[[package]]
+name = "idna"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "ignore"
+version = "0.4.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a"
+dependencies = [
+ "crossbeam-deque",
+ "globset",
+ "log",
+ "memchr",
+ "regex-automata",
+ "same-file",
+ "walkdir",
+ "winapi-util",
+]
+
+[[package]]
+name = "impl-more"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2"
+
+[[package]]
+name = "include_dir"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd"
+dependencies = [
+ "include_dir_macros",
+]
+
+[[package]]
+name = "include_dir_macros"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75"
+dependencies = [
+ "proc-macro2",
+ "quote",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
+dependencies = [
+ "autocfg",
+ "hashbrown 0.12.3",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.16.0",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "indicatif"
+version = "0.18.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9375e112e4b463ec1b1c6c011953545c65a30164fbab5b581df32b3abf0dcb88"
+dependencies = [
+ "console",
+ "portable-atomic",
+ "unicode-width",
+ "unit-prefix",
+ "web-time",
+]
+
+[[package]]
+name = "inlinable_string"
+version = "0.1.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb"
+
+[[package]]
+name = "inotify"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3"
+dependencies = [
+ "bitflags 2.10.0",
+ "inotify-sys",
+ "libc",
+]
+
+[[package]]
+name = "inotify-sys"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "intl-memoizer"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "310da2e345f5eb861e7a07ee182262e94975051db9e4223e909ba90f392f163f"
+dependencies = [
+ "type-map",
+ "unic-langid",
+]
+
+[[package]]
+name = "intl_pluralrules"
+version = "7.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972"
+dependencies = [
+ "unic-langid",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
+
+[[package]]
+name = "iri-string"
+version = "0.7.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397"
+dependencies = [
+ "memchr",
+ "serde",
+]
+
+[[package]]
+name = "is-terminal"
+version = "0.4.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+
+[[package]]
+name = "itoa"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
+
+[[package]]
+name = "jobserver"
+version = "0.1.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
+dependencies = [
+ "getrandom 0.3.4",
+ "libc",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8"
+dependencies = [
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "kqueue"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a"
+dependencies = [
+ "kqueue-sys",
+ "libc",
+]
+
+[[package]]
+name = "kqueue-sys"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b"
+dependencies = [
+ "bitflags 1.3.2",
+ "libc",
+]
+
+[[package]]
+name = "language-tags"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "libc"
+version = "0.2.177"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
+
+[[package]]
+name = "libm"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
+
+[[package]]
+name = "libredox"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb"
+dependencies = [
+ "bitflags 2.10.0",
+ "libc",
+ "redox_syscall",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
+
+[[package]]
+name = "litemap"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856"
+
+[[package]]
+name = "litemap"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
+
+[[package]]
+name = "local-channel"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+ "local-waker",
+]
+
+[[package]]
+name = "local-waker"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487"
+
+[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
+
+[[package]]
+name = "loom"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5"
+dependencies = [
+ "cfg-if 1.0.4",
+ "generator",
+ "scoped-tls",
+ "serde",
+ "serde_json",
+ "tracing",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "matchers"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
+dependencies = [
+ "regex-automata",
+]
+
+[[package]]
+name = "matchit"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
+
+[[package]]
+name = "memchr"
+version = "2.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
+
+[[package]]
+name = "memory_units"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3"
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "mime_guess"
+version = "2.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
+dependencies = [
+ "mime",
+ "unicase",
+]
+
+[[package]]
+name = "minify-html-onepass"
+version = "0.10.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c89548d0be6d3c7295335473fbe5c021fde64de738e01312301c90b9f1dd8476"
+dependencies = [
+ "aho-corasick 0.7.20",
+ "css-minify",
+ "lazy_static",
+ "memchr",
+ "minify-js 0.4.3",
+ "rustc-hash 1.1.0",
+]
+
+[[package]]
+name = "minify-js"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c300f90ba1138b5c5daf5d9441dc9bdc67b808aac22cf638362a2647bc213be4"
+dependencies = [
+ "lazy_static",
+ "parse-js 0.10.3",
+]
+
+[[package]]
+name = "minify-js"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1fa5546ee8bd66024113e506cabe4230e76635a094c06ea2051b66021dda92e"
+dependencies = [
+ "aho-corasick 0.7.20",
+ "lazy_static",
+ "parse-js 0.20.1",
+]
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
+[[package]]
+name = "mio"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873"
+dependencies = [
+ "libc",
+ "log",
+ "wasi",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "multer"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b"
+dependencies = [
+ "bytes",
+ "encoding_rs",
+ "futures-util",
+ "http 1.4.0",
+ "httparse",
+ "memchr",
+ "mime",
+ "spin",
+ "tokio",
+ "tokio-util",
+ "version_check",
+]
+
+[[package]]
+name = "multiparty"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed1ec6589a6d4a1e0b33b4c0a3f6ee96dfba88ebdb3da51403fd7cf0a24a4b04"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "httparse",
+ "memchr",
+ "pin-project-lite",
+ "try-lock",
+]
+
+[[package]]
+name = "native-tls"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
+dependencies = [
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
+[[package]]
+name = "nix"
+version = "0.27.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
+dependencies = [
+ "bitflags 2.10.0",
+ "cfg-if 1.0.4",
+ "libc",
+]
+
+[[package]]
+name = "nix"
+version = "0.30.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
+dependencies = [
+ "bitflags 2.10.0",
+ "cfg-if 1.0.4",
+ "cfg_aliases",
+ "libc",
+]
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "normalize-line-endings"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
+
+[[package]]
+name = "notify"
+version = "8.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3"
+dependencies = [
+ "bitflags 2.10.0",
+ "fsevent-sys",
+ "inotify",
+ "kqueue",
+ "libc",
+ "log",
+ "mio",
+ "notify-types",
+ "walkdir",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "notify-types"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d"
+
+[[package]]
+name = "nu-ansi-term"
+version = "0.50.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "num-conv"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "objc2"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05"
+dependencies = [
+ "objc2-encode",
+]
+
+[[package]]
+name = "objc2-encode"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+
+[[package]]
+name = "openssl"
+version = "0.10.75"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328"
+dependencies = [
+ "bitflags 2.10.0",
+ "cfg-if 1.0.4",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
+
+[[package]]
+name = "openssl-src"
+version = "300.5.4+3.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a507b3792995dae9b0df8a1c1e3771e8418b7c2d9f0baeba32e6fe8b06c7cb72"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.111"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321"
+dependencies = [
+ "cc",
+ "libc",
+ "openssl-src",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if 1.0.4",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-link",
+]
+
+[[package]]
+name = "parse-js"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30534759e6ad87aa144c396544747e1c25b1020bd133356fd758c8facec764e5"
+dependencies = [
+ "aho-corasick 0.7.20",
+ "lazy_static",
+ "memchr",
+]
+
+[[package]]
+name = "parse-js"
+version = "0.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2742b5e32dcb5930447ed9f9e401a7dfd883867fc079c4fac44ae8ba3593710e"
+dependencies = [
+ "aho-corasick 0.7.20",
+ "bumpalo",
+ "hashbrown 0.13.2",
+ "lazy_static",
+ "memchr",
+]
+
+[[package]]
+name = "paste"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+
+[[package]]
+name = "pear"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bdeeaa00ce488657faba8ebf44ab9361f9365a97bd39ffb8a60663f57ff4b467"
+dependencies = [
+ "inlinable_string",
+ "pear_codegen",
+ "yansi",
+]
+
+[[package]]
+name = "pear_codegen"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bab5b985dc082b345f812b7df84e1bef27e7207b39e448439ba8bd69c93f147"
+dependencies = [
+ "proc-macro2",
+ "proc-macro2-diagnostics",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "perseus"
+version = "0.5.0"
+dependencies = [
+ "async-trait",
+ "chrono",
+ "console_error_panic_hook",
+ "fluent-bundle",
+ "fmterr",
+ "fs_extra",
+ "futures",
+ "http 1.4.0",
+ "intl-memoizer",
+ "js-sys",
+ "minify-html-onepass",
+ "perseus-macro",
+ "regex",
+ "rexie",
+ "serde",
+ "serde_json",
+ "sycamore",
+ "sycamore-futures",
+ "sycamore-reactive",
+ "sycamore-router",
+ "thiserror 2.0.17",
+ "tokio",
+ "unic-langid",
+ "urlencoding",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "perseus-actix-web"
+version = "0.5.0"
+dependencies = [
+ "actix-files",
+ "actix-web",
+ "futures",
+ "perseus",
+]
+
+[[package]]
+name = "perseus-axum"
+version = "0.5.0"
+dependencies = [
+ "axum",
+ "perseus",
+ "tokio",
+ "tower-http",
+]
+
+[[package]]
+name = "perseus-cli"
+version = "0.5.0"
+dependencies = [
+ "assert_cmd",
+ "assert_fs",
+ "brotlic",
+ "cargo-lock",
+ "cargo_metadata",
+ "cargo_toml",
+ "clap",
+ "command-group",
+ "console",
+ "ctrlc",
+ "directories",
+ "flate2",
+ "fmterr",
+ "fs_extra",
+ "futures",
+ "include_dir",
+ "indicatif",
+ "minify-js 0.6.0",
+ "notify",
+ "openssl",
+ "predicates",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "shell-words",
+ "tar",
+ "thiserror 2.0.17",
+ "tokio",
+ "tokio-stream",
+ "ureq",
+ "walkdir",
+ "warp",
+]
+
+[[package]]
+name = "perseus-example-auth"
+version = "0.5.0"
+dependencies = [
+ "perseus",
+ "perseus-integration",
+ "serde",
+ "serde_json",
+ "sycamore",
+ "tokio",
+ "web-sys",
+]
+
+[[package]]
+name = "perseus-example-base"
+version = "0.5.0"
+dependencies = [
+ "fantoccini",
+ "perseus",
+ "perseus-integration",
+ "serde",
+ "serde_json",
+ "sycamore",
+ "tokio",
+]
+
+[[package]]
+name = "perseus-example-basic"
+version = "0.5.0"
+dependencies = [
+ "fantoccini",
+ "perseus",
+ "perseus-axum",
+ "serde",
+ "serde_json",
+ "sycamore",
+ "tokio",
+]
+
+[[package]]
+name = "perseus-example-capsules"
+version = "0.5.0"
+dependencies = [
+ "fantoccini",
+ "lazy_static",
+ "perseus",
+ "perseus-axum",
+ "serde",
+ "serde_json",
+ "sycamore",
+ "tokio",
+]
+
+[[package]]
+name = "perseus-example-custom-server"
+version = "0.5.0"
+dependencies = [
+ "fantoccini",
+ "perseus",
+ "perseus-warp",
+ "serde",
+ "serde_json",
+ "sycamore",
+ "tokio",
+ "warp-fix-171",
+]
+
+[[package]]
+name = "perseus-example-custom-server-rocket"
+version = "0.5.0"
+dependencies = [
+ "fantoccini",
+ "perseus",
+ "perseus-rocket",
+ "rocket",
+ "serde",
+ "serde_json",
+ "sycamore",
+ "tokio",
+]
+
+[[package]]
+name = "perseus-example-fetching"
+version = "0.5.0"
+dependencies = [
+ "perseus",
+ "perseus-integration",
+ "reqwasm",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "sycamore",
+ "tokio",
+]
+
+[[package]]
+name = "perseus-example-freezing-and-thawing"
+version = "0.3.2"
+dependencies = [
+ "fantoccini",
+ "perseus",
+ "perseus-integration",
+ "serde",
+ "serde_json",
+ "sycamore",
+ "tokio",
+]
+
+[[package]]
+name = "perseus-example-full-page-layout"
+version = "0.5.0"
+dependencies = [
+ "fantoccini",
+ "perseus",
+ "perseus-integration",
+ "serde",
+ "serde_json",
+ "sycamore",
+ "tokio",
+]
+
+[[package]]
+name = "perseus-example-global-state"
+version = "0.3.2"
+dependencies = [
+ "fantoccini",
+ "perseus",
+ "perseus-integration",
+ "serde",
+ "serde_json",
+ "sycamore",
+ "tokio",
+]
+
+[[package]]
+name = "perseus-example-helper-build-state"
+version = "0.5.0"
+dependencies = [
+ "fantoccini",
+ "perseus",
+ "perseus-integration",
+ "serde",
+ "serde_json",
+ "sycamore",
+ "tokio",
+]
+
+[[package]]
+name = "perseus-example-i18n"
+version = "0.1.0"
+dependencies = [
+ "fantoccini",
+ "fluent-bundle",
+ "perseus",
+ "perseus-integration",
+ "serde",
+ "serde_json",
+ "sycamore",
+ "tokio",
+ "urlencoding",
+]
+
+[[package]]
+name = "perseus-example-idb-freezing"
+version = "0.3.2"
+dependencies = [
+ "fantoccini",
+ "perseus",
+ "perseus-integration",
+ "serde",
+ "serde_json",
+ "sycamore",
+ "tokio",
+ "wasm-bindgen-futures",
+]
+
+[[package]]
+name = "perseus-example-index-view"
+version = "0.5.0"
+dependencies = [
+ "fantoccini",
+ "perseus",
+ "perseus-integration",
+ "serde",
+ "serde_json",
+ "sycamore",
+ "tokio",
+]
+
+[[package]]
+name = "perseus-example-js-interop"
+version = "0.5.0"
+dependencies = [
+ "fantoccini",
+ "perseus",
+ "perseus-integration",
+ "serde",
+ "serde_json",
+ "sycamore",
+ "tokio",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "perseus-example-plugins"
+version = "0.5.0"
+dependencies = [
+ "fantoccini",
+ "perseus",
+ "perseus-integration",
+ "serde",
+ "serde_json",
+ "sycamore",
+ "tokio",
+ "toml 0.9.8",
+]
+
+[[package]]
+name = "perseus-example-preload"
+version = "0.5.0"
+dependencies = [
+ "fantoccini",
+ "perseus",
+ "perseus-integration",
+ "serde",
+ "serde_json",
+ "sycamore",
+ "tokio",
+]
+
+[[package]]
+name = "perseus-example-router-state"
+version = "0.3.2"
+dependencies = [
+ "perseus",
+ "perseus-integration",
+ "serde",
+ "serde_json",
+ "sycamore",
+ "tokio",
+]
+
+[[package]]
+name = "perseus-example-rx-state"
+version = "0.3.2"
+dependencies = [
+ "fantoccini",
+ "perseus",
+ "perseus-integration",
+ "serde",
+ "serde_json",
+ "sycamore",
+ "tokio",
+]
+
+[[package]]
+name = "perseus-example-set-headers"
+version = "0.3.2"
+dependencies = [
+ "fantoccini",
+ "perseus",
+ "perseus-integration",
+ "serde",
+ "serde_json",
+ "sycamore",
+ "tokio",
+ "ureq",
+]
+
+[[package]]
+name = "perseus-example-state-generation"
+version = "0.3.2"
+dependencies = [
+ "anyhow",
+ "fantoccini",
+ "perseus",
+ "perseus-integration",
+ "serde",
+ "serde_json",
+ "sycamore",
+ "tokio",
+]
+
+[[package]]
+name = "perseus-example-static-content"
+version = "0.3.2"
+dependencies = [
+ "fantoccini",
+ "perseus",
+ "perseus-integration",
+ "serde",
+ "serde_json",
+ "sycamore",
+ "tokio",
+]
+
+[[package]]
+name = "perseus-example-suspense"
+version = "0.5.0"
+dependencies = [
+ "fantoccini",
+ "gloo-timers",
+ "perseus",
+ "perseus-integration",
+ "serde",
+ "serde_json",
+ "sycamore",
+ "tokio",
+]
+
+[[package]]
+name = "perseus-example-tiny"
+version = "0.5.0"
+dependencies = [
+ "perseus",
+ "perseus-integration",
+ "sycamore",
+ "tokio",
+]
+
+[[package]]
+name = "perseus-example-unreactive"
+version = "0.3.2"
+dependencies = [
+ "fantoccini",
+ "perseus",
+ "perseus-integration",
+ "serde",
+ "serde_json",
+ "sycamore",
+ "tokio",
+]
+
+[[package]]
+name = "perseus-integration"
+version = "0.5.0"
+dependencies = [
+ "perseus-actix-web",
+ "perseus-axum",
+ "perseus-rocket",
+ "perseus-warp",
+]
+
+[[package]]
+name = "perseus-macro"
+version = "0.5.0"
+dependencies = [
+ "darling",
+ "proc-macro2",
+ "quote",
+ "serde",
+ "sycamore",
+ "syn",
+ "trybuild",
+]
+
+[[package]]
+name = "perseus-rocket"
+version = "0.5.0"
+dependencies = [
+ "perseus",
+ "rocket",
+ "rocket_async_compression",
+]
+
+[[package]]
+name = "perseus-warp"
+version = "0.5.0"
+dependencies = [
+ "perseus",
+ "warp-fix-171",
+]
+
+[[package]]
+name = "perseus-website"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "gloo-timers",
+ "js-sys",
+ "lazy_static",
+ "perseus",
+ "pulldown-cmark",
+ "regex",
+ "serde",
+ "serde_json",
+ "sycamore",
+ "thiserror 2.0.17",
+ "tokio",
+ "walkdir",
+ "wasm-bindgen",
+ "web-sys",
+ "wee_alloc",
+]
+
+[[package]]
+name = "perseus-website-example-app-in-a-file"
+version = "0.4.3-beta.8"
+dependencies = [
+ "perseus",
+ "perseus-axum",
+ "serde",
+ "serde_json",
+ "sycamore",
+ "tokio",
+]
+
+[[package]]
+name = "perseus-website-example-i18n"
+version = "0.4.3-beta.8"
+dependencies = [
+ "perseus",
+ "perseus-axum",
+ "serde",
+ "serde_json",
+ "sycamore",
+ "tokio",
+]
+
+[[package]]
+name = "perseus-website-example-state-generation"
+version = "0.4.3-beta.8"
+dependencies = [
+ "perseus",
+ "perseus-axum",
+ "serde",
+ "serde_json",
+ "sycamore",
+ "thiserror 2.0.17",
+ "tokio",
+]
+
+[[package]]
+name = "pin-project"
+version = "1.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
+
+[[package]]
+name = "portable-atomic"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
+
+[[package]]
+name = "potential_utf"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77"
+dependencies = [
+ "zerovec 0.11.5",
+]
+
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "predicates"
+version = "3.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573"
+dependencies = [
+ "anstyle",
+ "difflib",
+ "float-cmp",
+ "normalize-line-endings",
+ "predicates-core",
+ "regex",
+]
+
+[[package]]
+name = "predicates-core"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa"
+
+[[package]]
+name = "predicates-tree"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c"
+dependencies = [
+ "predicates-core",
+ "termtree",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "proc-macro2-diagnostics"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "version_check",
+ "yansi",
+]
+
+[[package]]
+name = "pulldown-cmark"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0"
+dependencies = [
+ "bitflags 2.10.0",
+ "getopts",
+ "memchr",
+ "pulldown-cmark-escape",
+ "unicase",
+]
+
+[[package]]
+name = "pulldown-cmark-escape"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae"
+
+[[package]]
+name = "quote"
+version = "1.0.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
+dependencies = [
+ "rand_chacha 0.9.0",
+ "rand_core 0.9.3",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.9.3",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom 0.2.16",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
+dependencies = [
+ "getrandom 0.3.4",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags 2.10.0",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
+dependencies = [
+ "getrandom 0.2.16",
+ "libredox",
+ "thiserror 2.0.17",
+]
+
+[[package]]
+name = "ref-cast"
+version = "1.0.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d"
+dependencies = [
+ "ref-cast-impl",
+]
+
+[[package]]
+name = "ref-cast-impl"
+version = "1.0.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "regex"
+version = "1.12.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
+dependencies = [
+ "aho-corasick 1.1.4",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
+dependencies = [
+ "aho-corasick 1.1.4",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-lite"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d942b98df5e658f56f20d592c7f868833fe38115e65c33003d8cd224b0155da"
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
+
+[[package]]
+name = "reqwasm"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05b89870d729c501fa7a68c43bf4d938bbb3a8c156d333d90faa0e8b3e3212fb"
+dependencies = [
+ "gloo-net",
+]
+
+[[package]]
+name = "reqwest"
+version = "0.12.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "encoding_rs",
+ "futures-core",
+ "futures-util",
+ "h2 0.4.12",
+ "http 1.4.0",
+ "http-body 1.0.1",
+ "http-body-util",
+ "hyper 1.8.1",
+ "hyper-rustls",
+ "hyper-tls",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "mime",
+ "native-tls",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustls-pki-types",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tokio-native-tls",
+ "tokio-util",
+ "tower",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm-streams",
+ "web-sys",
+]
+
+[[package]]
+name = "rexie"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "887466cfa8a12c08ee4b174998135cea8ff0fd84858627cd793e56535a045bc9"
+dependencies = [
+ "idb",
+ "thiserror 1.0.69",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if 1.0.4",
+ "getrandom 0.2.16",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rocket"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a516907296a31df7dc04310e7043b61d71954d703b603cc6867a026d7e72d73f"
+dependencies = [
+ "async-stream",
+ "async-trait",
+ "atomic 0.5.3",
+ "binascii",
+ "bytes",
+ "either",
+ "figment",
+ "futures",
+ "indexmap 2.12.0",
+ "log",
+ "memchr",
+ "multer",
+ "num_cpus",
+ "parking_lot",
+ "pin-project-lite",
+ "rand 0.8.5",
+ "ref-cast",
+ "rocket_codegen",
+ "rocket_http",
+ "serde",
+ "state",
+ "tempfile",
+ "time",
+ "tokio",
+ "tokio-stream",
+ "tokio-util",
+ "ubyte",
+ "version_check",
+ "yansi",
+]
+
+[[package]]
+name = "rocket_async_compression"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "caf1f3cb4a0cc79d44f6e5eaf7841134c6acdb756fd84286e595de0d5dcbcc13"
+dependencies = [
+ "async-compression 0.4.33",
+ "futures",
+ "lazy_static",
+ "log",
+ "rocket",
+]
+
+[[package]]
+name = "rocket_codegen"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "575d32d7ec1a9770108c879fc7c47815a80073f96ca07ff9525a94fcede1dd46"
+dependencies = [
+ "devise",
+ "glob",
+ "indexmap 2.12.0",
+ "proc-macro2",
+ "quote",
+ "rocket_http",
+ "syn",
+ "unicode-xid",
+ "version_check",
+]
+
+[[package]]
+name = "rocket_http"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e274915a20ee3065f611c044bd63c40757396b6dbc057d6046aec27f14f882b9"
+dependencies = [
+ "cookie 0.18.1",
+ "either",
+ "futures",
+ "http 0.2.12",
+ "hyper 0.14.32",
+ "indexmap 2.12.0",
+ "log",
+ "memchr",
+ "pear",
+ "percent-encoding",
+ "pin-project-lite",
+ "ref-cast",
+ "serde",
+ "smallvec",
+ "stable-pattern",
+ "state",
+ "time",
+ "tokio",
+ "uncased",
+]
+
+[[package]]
+name = "rustc-hash"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
+
+[[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustix"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
+dependencies = [
+ "bitflags 2.10.0",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f"
+dependencies = [
+ "log",
+ "once_cell",
+ "ring",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-pemfile"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
+dependencies = [
+ "base64 0.21.7",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a"
+dependencies = [
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "ryu"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "schannel"
+version = "0.1.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "scoped-tls"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "security-framework"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
+dependencies = [
+ "bitflags 2.10.0",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "self_cell"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16c2f82143577edb4921b71ede051dac62ca3c16084e918bf7b40c96ae10eb33"
+
+[[package]]
+name = "semver"
+version = "1.0.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
+dependencies = [
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.145"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
+dependencies = [
+ "itoa",
+ "memchr",
+ "ryu",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "serde_path_to_error"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
+dependencies = [
+ "itoa",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sha1"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
+dependencies = [
+ "cfg-if 1.0.4",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "shell-words"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
+
+[[package]]
+name = "slab"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
+
+[[package]]
+name = "slotmap"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a"
+dependencies = [
+ "version_check",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+
+[[package]]
+name = "socket2"
+version = "0.5.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "socket2"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
+dependencies = [
+ "libc",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "spin"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+
+[[package]]
+name = "stable-pattern"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4564168c00635f88eaed410d5efa8131afa8d8699a612c80c455a0ba05c21045"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
+name = "state"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b8c4a4445d81357df8b1a650d0d0d6fbbbfe99d064aa5e02f3e4022061476d8"
+dependencies = [
+ "loom",
+]
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "sycamore"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92f6ab6fc21f9a534ede6713b0a18c1407ccf12ea1adc9e7af6c509f053e76e3"
+dependencies = [
+ "futures",
+ "hashbrown 0.14.5",
+ "indexmap 2.12.0",
+ "paste",
+ "sycamore-core",
+ "sycamore-futures",
+ "sycamore-macro",
+ "sycamore-reactive",
+ "sycamore-web",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "sycamore-core"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "770c4701a63be2c929fc5654982cb2f495239b1779cf2ce31dff4ce7f82ec9d5"
+dependencies = [
+ "hashbrown 0.14.5",
+ "paste",
+ "sycamore-futures",
+ "sycamore-reactive",
+]
+
+[[package]]
+name = "sycamore-futures"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f965c511d00b55094f012c2c16e550ddf606d99ca1ee277787cb87a34ef57244"
+dependencies = [
+ "futures",
+ "pin-project",
+ "sycamore-macro",
+ "sycamore-reactive",
+ "tokio",
+ "wasm-bindgen-futures",
+]
+
+[[package]]
+name = "sycamore-macro"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37b71839612a5ca843e501d774e6bd750f80a063ff622542500eb8455fc144cc"
+dependencies = [
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "rand 0.8.5",
+ "sycamore-view-parser",
+ "syn",
+]
+
+[[package]]
+name = "sycamore-reactive"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd7b0c93f5d4587db7a636fe08128ff97812f58cd219793252804c4ff1cca760"
+dependencies = [
+ "paste",
+ "slotmap",
+ "smallvec",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "sycamore-router"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa743f7e8bdfa307df6123d98b1720f7ad2e8875f8efaae3ad9a1e853eb889d1"
+dependencies = [
+ "sycamore",
+ "sycamore-router-macro",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "sycamore-router-macro"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34b672517fd97a1afdd38ceea25c10ad461182ffa293301b6e9fcdb3625c11c6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "sycamore-view-parser"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a0ddef2ecbaa20b71dd6194dbafbfa0dc5925de2d99772381316519368e8653"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "sycamore-web"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf0d42c89e84e7458adef638ece27d19d707dfa6c41bd5f8982f42b70da3dacd"
+dependencies = [
+ "async-stream",
+ "futures",
+ "html-escape",
+ "js-sys",
+ "once_cell",
+ "paste",
+ "smallvec",
+ "sycamore-core",
+ "sycamore-futures",
+ "sycamore-macro",
+ "sycamore-reactive",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.110"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sync_wrapper"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "system-configuration"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
+dependencies = [
+ "bitflags 1.3.2",
+ "core-foundation",
+ "system-configuration-sys",
+]
+
+[[package]]
+name = "system-configuration-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "tar"
+version = "0.4.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a"
+dependencies = [
+ "filetime",
+ "libc",
+ "xattr",
+]
+
+[[package]]
+name = "target-triple"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "591ef38edfb78ca4771ee32cf494cb8771944bee237a9b91fc9c1424ac4b777b"
+
+[[package]]
+name = "tempfile"
+version = "3.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16"
+dependencies = [
+ "fastrand",
+ "getrandom 0.3.4",
+ "once_cell",
+ "rustix",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "termtree"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683"
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl 1.0.69",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
+dependencies = [
+ "thiserror-impl 2.0.17",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
+dependencies = [
+ "cfg-if 1.0.4",
+]
+
+[[package]]
+name = "time"
+version = "0.3.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d"
+dependencies = [
+ "deranged",
+ "itoa",
+ "num-conv",
+ "powerfmt",
+ "serde",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b"
+
+[[package]]
+name = "time-macros"
+version = "0.2.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
+dependencies = [
+ "displaydoc",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869"
+dependencies = [
+ "displaydoc",
+ "serde_core",
+ "zerovec 0.11.5",
+]
+
+[[package]]
+name = "tokio"
+version = "1.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
+dependencies = [
+ "bytes",
+ "libc",
+ "mio",
+ "parking_lot",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "socket2 0.6.1",
+ "tokio-macros",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tokio-native-tls"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.26.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
+dependencies = [
+ "rustls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-stream"
+version = "0.1.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-tungstenite"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54319c93411147bced34cb5609a80e0a8e44c5999c93903a81cd866630ec0bfd"
+dependencies = [
+ "futures-util",
+ "log",
+ "tokio",
+ "tungstenite 0.18.0",
+]
+
+[[package]]
+name = "tokio-tungstenite"
+version = "0.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "489a59b6730eda1b0171fcfda8b121f4bee2b35cba8645ca35c5f7ba3eb736c1"
+dependencies = [
+ "futures-util",
+ "log",
+ "tokio",
+ "tungstenite 0.27.0",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "toml"
+version = "0.8.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
+dependencies = [
+ "serde",
+ "serde_spanned 0.6.9",
+ "toml_datetime 0.6.11",
+ "toml_edit",
+]
+
+[[package]]
+name = "toml"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8"
+dependencies = [
+ "indexmap 2.12.0",
+ "serde_core",
+ "serde_spanned 1.0.3",
+ "toml_datetime 0.7.3",
+ "toml_parser",
+ "toml_writer",
+ "winnow",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.22.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
+dependencies = [
+ "indexmap 2.12.0",
+ "serde",
+ "serde_spanned 0.6.9",
+ "toml_datetime 0.6.11",
+ "toml_write",
+ "winnow",
+]
+
+[[package]]
+name = "toml_parser"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e"
+dependencies = [
+ "winnow",
+]
+
+[[package]]
+name = "toml_write"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
+
+[[package]]
+name = "toml_writer"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2"
+
+[[package]]
+name = "tower"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-http"
+version = "0.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9cf146f99d442e8e68e585f5d798ccd3cad9a7835b917e09728880a862706456"
+dependencies = [
+ "async-compression 0.4.33",
+ "bitflags 2.10.0",
+ "bytes",
+ "futures-core",
+ "futures-util",
+ "http 1.4.0",
+ "http-body 1.0.1",
+ "http-body-util",
+ "http-range-header",
+ "httpdate",
+ "iri-string",
+ "mime",
+ "mime_guess",
+ "percent-encoding",
+ "pin-project-lite",
+ "tokio",
+ "tokio-util",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
+[[package]]
+name = "tracing"
+version = "0.1.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
+dependencies = [
+ "log",
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
+dependencies = [
+ "once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
+dependencies = [
+ "matchers",
+ "nu-ansi-term",
+ "once_cell",
+ "regex-automata",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing",
+ "tracing-core",
+ "tracing-log",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[package]]
+name = "trybuild"
+version = "1.0.114"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e17e807bff86d2a06b52bca4276746584a78375055b6e45843925ce2802b335"
+dependencies = [
+ "dissimilar",
+ "glob",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "target-triple",
+ "termcolor",
+ "toml 0.9.8",
+]
+
+[[package]]
+name = "tungstenite"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30ee6ab729cd4cf0fd55218530c4522ed30b7b6081752839b68fcec8d0960788"
+dependencies = [
+ "base64 0.13.1",
+ "byteorder",
+ "bytes",
+ "http 0.2.12",
+ "httparse",
+ "log",
+ "rand 0.8.5",
+ "sha1",
+ "thiserror 1.0.69",
+ "url",
+ "utf-8",
+]
+
+[[package]]
+name = "tungstenite"
+version = "0.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eadc29d668c91fcc564941132e17b28a7ceb2f3ebf0b9dae3e03fd7a6748eb0d"
+dependencies = [
+ "bytes",
+ "data-encoding",
+ "http 1.4.0",
+ "httparse",
+ "log",
+ "rand 0.9.2",
+ "sha1",
+ "thiserror 2.0.17",
+ "utf-8",
+]
+
+[[package]]
+name = "type-map"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90"
+dependencies = [
+ "rustc-hash 2.1.1",
+]
+
+[[package]]
+name = "typenum"
+version = "1.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
+
+[[package]]
+name = "ubyte"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f720def6ce1ee2fc44d40ac9ed6d3a59c361c80a75a7aa8e75bb9baed31cf2ea"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "uncased"
+version = "0.9.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697"
+dependencies = [
+ "serde",
+ "version_check",
+]
+
+[[package]]
+name = "unic-langid"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05"
+dependencies = [
+ "unic-langid-impl",
+]
+
+[[package]]
+name = "unic-langid-impl"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658"
+dependencies = [
+ "tinystr 0.8.2",
+]
+
+[[package]]
+name = "unicase"
+version = "2.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
+
+[[package]]
+name = "unicode-width"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
+[[package]]
+name = "unit-prefix"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3"
+
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "ureq"
+version = "3.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d39cb1dbab692d82a977c0392ffac19e188bd9186a9f32806f0aaa859d75585a"
+dependencies = [
+ "base64 0.22.1",
+ "flate2",
+ "log",
+ "percent-encoding",
+ "rustls",
+ "rustls-pki-types",
+ "ureq-proto",
+ "utf-8",
+ "webpki-roots",
+]
+
+[[package]]
+name = "ureq-proto"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d81f9efa9df032be5934a46a068815a10a042b494b6a58cb0a1a97bb5467ed6f"
+dependencies = [
+ "base64 0.22.1",
+ "http 1.4.0",
+ "httparse",
+ "log",
+]
+
+[[package]]
+name = "url"
+version = "2.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+]
+
+[[package]]
+name = "urlencoding"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
+
+[[package]]
+name = "utf-8"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
+
+[[package]]
+name = "utf8-width"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3"
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "v_htmlescape"
+version = "0.15.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c"
+
+[[package]]
+name = "valuable"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "wait-timeout"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
+name = "warp"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51d06d9202adc1f15d709c4f4a2069be5428aa912cc025d6f268ac441ab066b0"
+dependencies = [
+ "bytes",
+ "futures-util",
+ "headers 0.4.1",
+ "http 1.4.0",
+ "http-body 1.0.1",
+ "http-body-util",
+ "hyper 1.8.1",
+ "hyper-util",
+ "log",
+ "mime",
+ "mime_guess",
+ "percent-encoding",
+ "pin-project",
+ "scoped-tls",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "tokio",
+ "tokio-tungstenite 0.27.0",
+ "tokio-util",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "warp-fix-171"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "afa7a5e17e84c5cd48c21fa2e5ca4d0f1ce323d01b73c52663337e0638d794c9"
+dependencies = [
+ "async-compression 0.3.15",
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "headers 0.3.9",
+ "http 0.2.12",
+ "hyper 0.14.32",
+ "log",
+ "mime",
+ "mime_guess",
+ "multiparty",
+ "percent-encoding",
+ "pin-project",
+ "rustls-pemfile",
+ "scoped-tls",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "tokio",
+ "tokio-stream",
+ "tokio-tungstenite 0.18.0",
+ "tokio-util",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasip2"
+version = "1.0.1+wasi-0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd"
+dependencies = [
+ "cfg-if 1.0.4",
+ "once_cell",
+ "rustversion",
+ "serde",
+ "serde_json",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.56"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c"
+dependencies = [
+ "cfg-if 1.0.4",
+ "js-sys",
+ "once_cell",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "wasm-streams"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
+dependencies = [
+ "futures-util",
+ "js-sys",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "web-time"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "webdriver"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91d53921e1bef27512fa358179c9a22428d55778d2c2ae3c5c37a52b82ce6e92"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "cookie 0.16.2",
+ "http 0.2.12",
+ "icu_segmenter",
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "thiserror 1.0.69",
+ "time",
+ "url",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "wee_alloc"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e"
+dependencies = [
+ "cfg-if 0.1.10",
+ "libc",
+ "memory_units",
+ "winapi",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
+dependencies = [
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.62.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-link",
+ "windows-result",
+ "windows-strings",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.59.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-registry"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
+dependencies = [
+ "windows-link",
+ "windows-result",
+ "windows-strings",
+]
+
+[[package]]
+name = "windows-result"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
+dependencies = [
+ "windows-targets 0.53.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm 0.52.6",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.53.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
+dependencies = [
+ "windows-link",
+ "windows_aarch64_gnullvm 0.53.1",
+ "windows_aarch64_msvc 0.53.1",
+ "windows_i686_gnu 0.53.1",
+ "windows_i686_gnullvm 0.53.1",
+ "windows_i686_msvc 0.53.1",
+ "windows_x86_64_gnu 0.53.1",
+ "windows_x86_64_gnullvm 0.53.1",
+ "windows_x86_64_msvc 0.53.1",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
+
+[[package]]
+name = "winnow"
+version = "0.7.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.46.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
+
+[[package]]
+name = "writeable"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
+
+[[package]]
+name = "writeable"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
+
+[[package]]
+name = "xattr"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
+dependencies = [
+ "libc",
+ "rustix",
+]
+
+[[package]]
+name = "yansi"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
+dependencies = [
+ "is-terminal",
+]
+
+[[package]]
+name = "yoke"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40"
+dependencies = [
+ "serde",
+ "stable_deref_trait",
+ "yoke-derive 0.7.5",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954"
+dependencies = [
+ "stable_deref_trait",
+ "yoke-derive 0.8.1",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
+
+[[package]]
+name = "zerotrie"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851"
+dependencies = [
+ "displaydoc",
+ "yoke 0.8.1",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079"
+dependencies = [
+ "yoke 0.7.5",
+ "zerofrom",
+ "zerovec-derive 0.10.3",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002"
+dependencies = [
+ "serde",
+ "yoke 0.8.1",
+ "zerofrom",
+ "zerovec-derive 0.11.2",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "zstd"
+version = "0.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
+dependencies = [
+ "zstd-safe",
+]
+
+[[package]]
+name = "zstd-safe"
+version = "7.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
+dependencies = [
+ "zstd-sys",
+]
+
+[[package]]
+name = "zstd-sys"
+version = "2.0.16+zstd.1.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
diff --git a/Cargo.toml b/Cargo.toml
index 2282934816..ec93830034 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -2,7 +2,6 @@
members = [
"packages/*",
"examples/core/*",
- "examples/demos/*",
"examples/comprehensive/*",
"examples/website/*",
"website",
diff --git a/README.md b/README.md
index 171f84093f..d85b1648fb 100644
--- a/README.md
+++ b/README.md
@@ -8,15 +8,15 @@
Perseus is a blazingly fast frontend web development framework built in Rust with support for generating page state at build-time, request-time, incrementally, or whatever you'd like! It supports reactivity using [Sycamore](https://github.com/sycamore-rs/sycamore), and builds on it to provide a fully-fledged framework for developing modern apps.
-- 📕 Supports static generation (serving only static resources)
-- 🗼 Supports server-side rendering (serving dynamic resources)
-- 🔧 Supports revalidation after time and/or with custom logic (updating rendered pages)
-- 🛠️ Supports incremental regeneration (build on demand)
-- 🏭 Open build matrix (use any rendering strategy with anything else)
-- 🖥️ CLI harness that lets you build apps with ease and confidence
-- 🌐 Full i18n support out-of-the-box with [Fluent](https://projectfluent.org)
-- 🏎 Lighthouse scores of 100 on desktop and over 95 on mobile
-- ⚡ Support for *hot state reloading* (reload your entire app's state after you make any code changes in development, Perseus is the only framework in the world that can do this, to our knowledge)
+- 📕 Supports static generation (serving only static resources)
+- 🗼 Supports server-side rendering (serving dynamic resources)
+- 🔧 Supports revalidation after time and/or with custom logic (updating rendered pages)
+- 🛠️ Supports incremental regeneration (build on demand)
+- 🏭 Open build matrix (use any rendering strategy with anything else)
+- 🖥️ CLI harness that lets you build apps with ease and confidence
+- 🌐 Full i18n support out-of-the-box with [Fluent](https://projectfluent.org)
+- 🏎 Lighthouse scores of 100 on desktop and over 95 on mobile
+- ⚡ Support for _hot state reloading_ (reload your entire app's state after you make any code changes in development, Perseus is the only framework in the world that can do this, to our knowledge)
## What's it like?
@@ -27,17 +27,20 @@ use perseus::prelude::*;
use sycamore::prelude::*;
#[perseus::main(perseus_axum::dflt_server)]
-pub fn main() -> PerseusApp {
+pub fn main() -> PerseusApp {
PerseusApp::new()
.template(
Template::build("index")
- .view(|cx| {
- view! { cx,
- p { "Hello World!" }
- }
- })
+ .view(index_page)
.build()
)
+ .error_views(ErrorViews::unlocalized_development_default())
+}
+
+fn index_page() -> View {
+ view! {
+ p { "Hello World!" }
+ }
}
```
@@ -47,7 +50,7 @@ Check out [the book](https://framesurge.sh/perseus/en-US/docs) to learn how to t
If you want to start working with Perseus right away, run the following commands and you'll have a basic app ready in no time! (Or, more accurately, after Cargo compiles everything...)
-``` shell
+```shell
cargo install perseus-cli
perseus new my-app
cd my-app/
@@ -56,6 +59,17 @@ perseus serve -w
Then, hop over to and see a placeholder app, in all its glory! If you change some code, that'll automatically update, reloading the browser all by itself. (This rebuilding might take a while though, see [here](https://framesurge.sh/perseus/en-US/docs/next/fundamentals/compilation-times/) for how to speed things up.)
+## Sycamore 0.9.2
+
+Perseus v0.5.0 uses Sycamore 0.9.2, which brings significant API improvements:
+
+- **Simpler view functions** - No more `cx: Scope` parameter or `` generics
+- **Cleaner syntax** - `view! { div { "Hello" } }` instead of `view! { cx, div { "Hello" } }`
+- **Built-in Link component** - `Link(to = "/about") { "About" }` for client-side navigation
+- **Improved signals** - `create_signal(value)` as a free function
+
+See the [migration guide](https://framesurge.sh/perseus/en-US/docs/migrating) for upgrading from v0.4.x.
+
## Aim
Support every major rendering strategy and provide developers the ability to efficiently create super-fast apps with Rust and a fantastic developer experience!
diff --git a/TESTING.md b/TESTING.md
new file mode 100644
index 0000000000..3870e3b364
--- /dev/null
+++ b/TESTING.md
@@ -0,0 +1,144 @@
+# Perseus Testing Guide
+
+## Prerequisites
+
+The test infrastructure automatically handles geckodriver setup! You just need:
+
+1. **rust-script** (for running test automation scripts):
+ ```bash
+ cargo install rust-script
+ ```
+
+The tests will automatically:
+- Check if geckodriver is installed
+- Install it using your system's package manager if not found
+- Start geckodriver on port 4444 if not already running
+
+### Manual Installation (Optional)
+
+If you prefer to install geckodriver manually or the automatic installation fails:
+
+```bash
+# On Debian/Ubuntu
+sudo apt install firefox-geckodriver
+
+# On Fedora/RHEL
+sudo dnf install geckodriver
+
+# On Arch
+sudo pacman -S geckodriver
+
+# On macOS
+brew install geckodriver
+
+# On Windows
+choco install geckodriver
+
+# Or download from: https://github.com/mozilla/geckodriver/releases
+```
+
+## Running Tests Locally
+
+### Run All Tests
+```bash
+bonnie test
+```
+This will automatically start geckodriver if needed, then run all core tests, CLI tests, and example E2E tests.
+
+### Run Specific Example E2E Test
+```bash
+# Using the bonnie command (recommended - automatically starts geckodriver):
+bonnie test example-all-integrations core basic
+
+# Direct pattern (you may need to start geckodriver manually):
+EXAMPLE_INTEGRATION=axum bonnie dev example test
+
+# Examples:
+bonnie test example-all-integrations core freezing_and_thawing
+bonnie test example-all-integrations core idb_freezing
+bonnie test example-all-integrations core i18n
+bonnie test example-all-integrations core index_view
+bonnie test example-all-integrations core preload
+bonnie test example-all-integrations core rx_state
+```
+
+### Clean Build Before Testing
+```bash
+# Clean example dist directory
+rm -rf examples/core//dist
+
+# Then run test
+EXAMPLE_INTEGRATION=axum bonnie dev example core test
+```
+
+### Debugging Tests
+
+1. **View full test output**:
+ ```bash
+ EXAMPLE_INTEGRATION=axum bonnie dev example core freezing_and_thawing test 2>&1
+ ```
+
+2. **Search for specific output**:
+ ```bash
+ EXAMPLE_INTEGRATION=axum bonnie dev example core freezing_and_thawing test 2>&1 | grep "DEBUG"
+ ```
+
+3. **Save output to file**:
+ ```bash
+ EXAMPLE_INTEGRATION=axum bonnie dev example core freezing_and_thawing test 2>&1 | tee test_output.log
+ ```
+
+### Troubleshooting
+
+#### "Session is already started" Error
+geckodriver has a stale session. Restart it:
+```bash
+pkill -9 geckodriver
+pkill -9 firefox
+sleep 1
+geckodriver --port 4444
+```
+
+#### Tests Timing Out
+Increase timeout or check if the server started properly on http://localhost:8080
+
+#### WASM Compilation Issues
+The WASM build uses `--cfg=client` flag automatically via Perseus CLI.
+Check `packages/perseus-cli/src/build.rs` for build configuration.
+
+## Test File Locations
+
+- **Example E2E tests**: `examples/core//tests/main.rs`
+- **Test fixtures**: Look for `wait_for_checkpoint!` macros in test files
+- **Template code**: `examples/core//src/templates/`
+
+## Understanding Test Commands
+
+bonnie.toml defines test commands:
+- `bonnie test` - Runs all tests
+- `bonnie test example-all-integrations` - Runs all example E2E tests
+- The `EXAMPLE_INTEGRATION=axum` sets which server integration to use (axum, actix-web, or warp)
+
+## Cargo Check for cfg-specific Code
+
+Check client-side code (WASM):
+```bash
+RUSTFLAGS="--cfg=client" CARGO_TARGET_DIR="target_wasm" cargo check --target wasm32-unknown-unknown
+```
+
+Check engine-side code:
+```bash
+RUSTFLAGS="--cfg=engine" CARGO_TARGET_DIR="target_engine" cargo check
+```
+
+## Current Known Issues (as of migration to Sycamore 0.9.2)
+
+1. **freezing_and_thawing**: `freeze()` returns empty string
+ - File: `examples/core/freezing_and_thawing/src/templates/about.rs`
+ - Issue: `render_ctx.freeze()` returns empty, likely in `packages/perseus/src/reactor/state.rs`
+
+2. **Navigation after thaw**: URL doesn't change after thaw operation
+ - Related to freeze returning empty
+
+3. Other failing tests: idb_freezing, i18n, index_view, preload, rx_state
+ - Likely related to Sycamore 0.9.2 reactive system changes
diff --git a/bonnie.toml b/bonnie.toml
index 0672724ce3..2c4fee56d8 100644
--- a/bonnie.toml
+++ b/bonnie.toml
@@ -180,7 +180,7 @@ test.cmd = [
"bonnie test example-all-integrations core suspense",
"bonnie test example-all-integrations core unreactive",
]
-test.desc = "runs all tests headlessly (assumes geckodriver running in background)"
+test.desc = "runs all tests headlessly (automatically starts geckodriver if needed)"
# This sometimes works, and sometimes fails, depending on the mood of Cargo's caching (just re-run it a few times, restart, the usual)
test.subcommands.core.cmd = [
# This will ignore end-to-end tests, but it will run long-running ones
@@ -195,13 +195,14 @@ test.subcommands.cli.cmd = [
]
test.subcommands.cli.desc = "runs the cli tests (all are long-running, this will take a while)"
test.subcommands.example-all-integrations.cmd = [
- "EXAMPLE_INTEGRATION=actix-web bonnie dev example %category %example test",
- "EXAMPLE_INTEGRATION=warp bonnie dev example %category %example test",
- "EXAMPLE_INTEGRATION=axum bonnie dev example %category %example test",
- "EXAMPLE_INTEGRATION=rocket bonnie dev example %category %example test"
+ # Ensure geckodriver is installed and running before running tests
+ "rust-script scripts/ensure_webdriver.rs",
+ # Since Perseus 0.5.0, examples use specific integration crates directly,
+ # so we only test each example once with its configured integration
+ "EXAMPLE_INTEGRATION=axum bonnie dev example %category %example test"
]
test.subcommands.example-all-integrations.args = [ "category", "example" ]
-test.subcommands.example-all-integrations.desc = "tests a single example with all integrations (assumes geckodriver running in background)"
+test.subcommands.example-all-integrations.desc = "tests a single example with its configured integration (automatically starts geckodriver if needed)"
# Releases the project (maintainers only)
# We commit all staged files so we can manually bump the Cargo version
diff --git a/docs/0.5.x/en-US/SUMMARY.md b/docs/0.5.x/en-US/SUMMARY.md
new file mode 100644
index 0000000000..1e4a9e94da
--- /dev/null
+++ b/docs/0.5.x/en-US/SUMMARY.md
@@ -0,0 +1,60 @@
+# Introduction
+
+- [Introduction](/docs/intro)
+- [Quickstart](/docs/quickstart)
+- [What is Perseus?](/docs/what-is-perseus)
+- [Core Principles](/docs/core-principles)
+
+# Your First App
+
+- [Installing Perseus](/docs/first-app/installation)
+- [Defining your app](/docs/first-app/defining)
+- [Generating pages](/docs/first-app/generating-pages)
+- [Development cycle](/docs/first-app/dev-cycle)
+- [Error handling](/docs/first-app/error-handling)
+- [Deploying your app](/docs/first-app/deploying)
+
+# Fundamentals
+
+- [`PerseusApp`](/docs/fundamentals/perseus-app)
+- [The reactor](/docs/fundamentals/reactor)
+- [Routing and navigation](/docs/fundamentals/routing)
+ - [Preloading](/docs/fundamentals/preloading)
+- [Internationalization](/docs/fundamentals/i18n)
+- [Error views](/docs/fundamentals/error-views)
+- [Hydration](/docs/fundamentals/hydration)
+- [Static content](/docs/fundamentals/static-content)
+- [Heads and headers](/docs/fundamentals/head-headers)
+- [Styling](/docs/fundamentals/styling)
+- [Working with JS](/docs/fundamentals/js-interop)
+- [Servers and exporting](/docs/fundamentals/serving-exporting)
+- [Debugging](/docs/fundamentals/debugging)
+- [Writing tests](/docs/fundamentals/testing)
+- [Plugins](/docs/fundamentals/plugins)
+- [Improving Compilation Times](/docs/fundamentals/compilation-times)
+
+# The State Platform
+
+- [Understanding state](/docs/state/intro)
+- [Build-time state](/docs/state/build)
+- [Request-time state](/docs/state/request)
+- [Revalidation](/docs/state/revalidation)
+- [Incremental generation](/docs/state/incremental)
+- [State amalgamation](/docs/state/amalgamation)
+- [Using state](/docs/state/browser)
+- [Global state](/docs/state/global)
+- [Helper state](/docs/state/helper)
+- [Suspended state](/docs/state/suspense)
+- [Freezing and thawing](/docs/state/freezing-thawing)
+- [Manually implementing `ReactiveState`](/docs/state/manual)
+
+# Capsules
+
+- [Introduction](/docs/capsules/intro)
+- [Using capsules](/docs/capsules/using)
+- [Capsules vs. components](/docs/capsules/capsules-vs-components)
+
+# Miscellaneous
+
+- [Migrating from v0.4.x](/docs/migrating)
+- [Common pitfalls and FAQs](/docs/faq)
diff --git a/docs/0.5.x/en-US/capsules/capsules-vs-components.md b/docs/0.5.x/en-US/capsules/capsules-vs-components.md
new file mode 100644
index 0000000000..2d0d3c7ec6
--- /dev/null
+++ b/docs/0.5.x/en-US/capsules/capsules-vs-components.md
@@ -0,0 +1,36 @@
+# Capsules vs. components
+
+With all this capsules stuff, you might be wondering whether you should be using capsules for everything: here's the short answer, **don't**. This page will go through the differences between normal [Sycamore components](https://sycamore-rs.netlify.app/docs/basics/components), which don't integrate with the Perseus state platform, and full-blown capsules.
+
+## View generation
+
+The first major similarity between capsules and components is that they both generate Sycamore views, but they do this at different times. If you use a component for something, then Perseus will render it immediately, no matter what, whereas capsules need to know what their state is, which has to be retrieved from the engine, meaning their renders will be delayed until a request has been completed. Note however that this isn't the case with *initial loads* (when a user comes to your app from the outside internet), and, then, all capsules will be served together as one HTML bundle.
+
+This difference might seem minor, but it can cause major problems if you start to use capsules for everything. For example, let's say you create a styling library with capsules for buttons, checkboxes, etc., thinking it's a great idea because you can generate state for them in advance, perhaps using some advanced usage of incremental generation as a property-parsing system. However, if you then have a capsule for a sidebar, with a capsule inside that for a section, and *another* capsule inside that for a button, you'll end up with three layers of nested capsules. Importantly, Perseus doesn't know what capsules something (either a page or a widget) depends on *until it renders it*. That means Perseus has to render your page, putting the sidebar in a loading state until we have its state, and then it can render the sidebar, but then it finds that that's dependent on a section widget, so it goes and gets that one's state, and *then* it finds the button capsule, and has to go and get its state. Now, if a capsule has no state, then no server trip is necessary, but **capsules without state are always better as Sycamore components**, since all a capsule is is a component with access to the Perseus state platform.
+
+The main thing to take away from all this rendering complexity is that **the more levels of capsule nesting you have, the slower your page will be**. Specifically, if it takes *n* seconds to get a single widget's state from the server, and you have *l* levels of nesting, your entire page will need to be re-rendered *l + 1* times on the engine-side to create an initial load, and it will take *(n + 1) * l* seconds to render your entire page for a subsequent load. If a single server trip takes even one second, and you have three levels of capsule nesting, that's four seconds to render your whole page, during which the user will be put through a slew of loading states. To our knowledge, this is the most efficient way we can do this while maintaining the ergonomics of Perseus (such as making sure you don't have to define which widgets a page uses in advance, which would severely limit the utility of the capsules system), but it has substantial tradeoffs when you start to use capsules in an overly nested way. Again, if it can be implemented as a component, it probably should be.
+
+## State and properties
+
+The second big difference between capsules and components is how flexible they are. A capsule is a full-on template that can be filled in with the Perseus state platform, and then modified by some properties the calling page/widget provides, while a component just has those properties. This doesn't mean widgets are always better, it means they're a great tool *when you need state*. For example, there is absolutely no point whatsoever in making a button an independent capsule, because it doesn't need state. Any customization of it can (and should) be performed using the properties system, and therefore it shoudl be a component, not a capsule.
+
+This is the reason why Perseus deliberately does not support adding Sycamore's `Children<'_>` type to the properties of a capsule, or passing through HTML properties like `class` or `style` through, since capsules are intended to be *sections of pages*, not atomic units. Despite the next version of Sycamore supporting this kind of property passthrough, Perseus *will not* support this with capsules to remove ambiguity about their purpose.
+
+## The intuition
+
+By now, you're probably waiting for some kind of general rule to decide whether you should use a capsule or a component, and here it is: **do not use a capsule where a component would do**, because, chances are, it will just slow down both your development cycle and your app. If you need the state platform though, and you avoid high levels of capsule nesting, capsules can be an incredibly powerful tool to greatly *improve* the speed of your app, while simplifying complex workflows and enabling previously impossible coding patterns.
+
+But, from here, there's still more to be said. If you apply what you've learned so far in this section to capsules, you'll probably use them only very rarely, but this isn't the intention. Capsules fit very nicely with any of the versions of Brad Frost's [*Atomic Design*](https://atomicdesign.bradfrost.com/chapter-2/) system, whereby you split your design up into *atoms*, *molecules*, *organisms*, *templates*, and *pages*. The atoms would be things like buttons, etc., for which you should use components, since these will be customized largely by properties, and won't need state of their own. Molecules are small units created with atoms, like a search bar. Generally, these too are better as components, but sometimes they'll need state, and they should be implemented as capsules (this is certainly the fuzziest category). Organisms are sections of your page, comprised of a number of molecules to form a functioning interface, like a header or sidebar. These should nearly always be implemented as capsules if they have any parts that need state, since this will allow persisting things like the entry typed in a search bar in a header across pages, due to Perseus' unique state caching system. When you have reactive molecules within organisms, make the reactive parts the state of the organism, and make it a capsule. Finally, you have templates and pages, which, as you might notice, already have a fairly prominent place in Perseus! (And no, we didn't design Perseus on the back of atomic design, it's just a methodology that happens to make a heck of a lot of sense when applied to Perseus specifically, but the naming similarity in the last two is purely coincidental, and they do mean slightly different things in the atomic method.)
+
+## The rules
+
+So, since we're programmers, and we like to have a nice methodology to follow, here's one for you. By all means, use this, don't use it, rewrite it yourself, do whatever you like. There will be many cases that will not be covered by these rules, and there will be others that are poorly covered. They are made to be broken, and you should break them if it makes sense to do so! Nonetheless, they will be helpful to some, especially those starting out with Perseus, so here they are:
+
+1. If it doesn't have state, it should be a component.
+2. If it can be implemented as a component, it should be a component.
+3. If it's a small, composable unit of a larger interface, it should be a component (even if it might have reactivity of its own, like a search bar; think instead about how that reactivity should be cached, e.g. you probably don't want all the search bars for completely difference things on your site synchronizing their states).
+4. If it's a somewhat self-contained interface on your pages, like a sidebar or header, that has state of its own, it should be a capsule.
+5. If it's something like in number 4, but it doesn't have any state, it should be a component.
+6. If it has a constant form, but many versions (e.g. a product display), it should be a capsule so it can use incremental generation.
+7. Avoid nesting capsules wherever possible! A nesting of two or three is fine, but any more than that and you'll likely start to see performance problems!
+8. If you want to delay loading a heavy part of your page, make it a capsule, and use `.delayed_widget()`.
diff --git a/docs/0.5.x/en-US/capsules/intro.md b/docs/0.5.x/en-US/capsules/intro.md
new file mode 100644
index 0000000000..228a948b0c
--- /dev/null
+++ b/docs/0.5.x/en-US/capsules/intro.md
@@ -0,0 +1,90 @@
+# Capsules
+
+Capsules are one of Perseus' most powerful features for building efficient, cacheable apps.
+
+## What are Capsules?
+
+Remember how **template + state = page**? Similarly, **capsule + state = widget**.
+
+Widgets are like mini-pages that can be embedded within other pages. They:
+- Don't have their own ``
+- Are embedded inside pages
+- Have full access to Perseus' state platform
+- Are aggressively cached for performance
+
+## Why Use Capsules?
+
+When Perseus loads a new page, it won't re-request capsules it already has. This means apps using capsules can dramatically reduce network traffic!
+
+### Example: E-commerce Product Carousel
+
+Imagine a product page with a "Similar Products" carousel:
+
+1. Create a `product` capsule that renders product cards
+2. The main product is one widget, carousel items are additional widgets
+3. If a user clicks a carousel item, the page loads **instantly** (it's already cached!)
+
+## Capsules vs Templates
+
+| Feature | Templates | Capsules |
+|---------|-----------|----------|
+| Produces | Pages | Widgets |
+| Has `` | Yes | No |
+| Takes full page | Yes | No |
+| Can be cached | Yes | Aggressively |
+| Accepts properties | No | Yes |
+
+## Initial Load vs Subsequent Load
+
+**Initial Load** (user comes from external site):
+- Page + all capsules sent as one HTML file
+- States included for everything
+
+**Subsequent Load** (navigating within your app):
+- Only new page state sent
+- Widgets loaded separately (in parallel)
+- Fallback view shown while loading
+
+## Real-World Use Cases
+
+### Blog Series Sidebar
+
+```
+Page: /post/chapter-1
+├── Main content (page state)
+└── Series widget showing all chapters (capsule)
+
+When user visits /post/chapter-2:
+├── Main content (new page state - fetched)
+└── Series widget (same capsule - already cached!)
+```
+
+### Search Suggestions
+
+Capsules in search suggestions implicitly preload content. If the user searches for "rust tips" and hovers over suggestions, those widgets cache in the background.
+
+### Dynamic Dashboards
+
+Dashboard panels as widgets:
+- Rearrangeable by users
+- Each panel independently cached
+- Failed panel shows fallback, others still work
+
+## Key Concepts
+
+1. **Properties**: Static data passed to widgets by the caller
+2. **Fallback Views**: Shown while widget is loading
+3. **Referential Definition**: Use `lazy_static` for capsule references
+4. **Rescheduling**: When build-time pages use request-time widgets
+
+## Performance Benefits
+
+- Cached widgets load instantly on subsequent navigations
+- Parallel widget loading for faster perceived performance
+- Reduced data transfer (no duplicate widget data)
+- Incremental rendering keeps pages responsive
+
+## Next Steps
+
+- [Using Capsules](/docs/capsules/using) - Create and embed widgets
+- [Capsules vs Components](/docs/capsules/capsules-vs-components) - When to use each
diff --git a/docs/0.5.x/en-US/capsules/using.md b/docs/0.5.x/en-US/capsules/using.md
new file mode 100644
index 0000000000..3c5ef4ae06
--- /dev/null
+++ b/docs/0.5.x/en-US/capsules/using.md
@@ -0,0 +1,311 @@
+# Using Capsules
+
+This guide shows how to create and use capsules in your Perseus app.
+
+## Project Structure
+
+Create a `capsules/` directory alongside your `templates/`:
+
+```
+src/
+├── main.rs
+├── templates/
+│ └── index.rs
+└── capsules/
+ ├── mod.rs
+ └── greeting.rs
+```
+
+## Defining a Capsule
+
+Use the referential definition pattern with `lazy_static`:
+
+```rust
+// src/capsules/greeting.rs
+use perseus::prelude::*;
+use serde::{Deserialize, Serialize};
+use sycamore::prelude::*;
+
+// Define state
+#[derive(Serialize, Deserialize, ReactiveState, Clone)]
+#[rx(alias = "GreetingStateRx")]
+struct GreetingState {
+ message: String,
+}
+
+// Define properties (passed by caller)
+#[derive(Clone)]
+pub struct GreetingProps {
+ pub size: String,
+}
+
+// Capsule view function - note the extra props parameter
+#[auto_scope]
+fn greeting_widget(
+ state: GreetingStateRx,
+ props: GreetingProps,
+) -> View {
+ view! {
+ div(class = format!("greeting {}", props.size)) {
+ (state.message.get_clone())
+ }
+ }
+}
+
+// Fallback while loading
+fn greeting_fallback(_props: GreetingProps) -> View {
+ view! {
+ div(class = "greeting-skeleton") {
+ "Loading..."
+ }
+ }
+}
+
+// Build state
+#[engine_only_fn]
+async fn get_build_state(_info: StateGeneratorInfo<()>) -> GreetingState {
+ GreetingState {
+ message: "Hello from capsule!".to_string(),
+ }
+}
+
+// Create the capsule (use lazy_static for referential access)
+lazy_static::lazy_static! {
+ pub static ref GREETING: Capsule = {
+ Capsule::build(
+ Template::build("greeting")
+ .build_state_fn(get_build_state)
+ )
+ .fallback(greeting_fallback)
+ .view_with_state(greeting_widget)
+ .build()
+ };
+}
+```
+
+## Key Differences from Templates
+
+| Aspect | Template | Capsule |
+|--------|----------|---------|
+| View function | Takes state only | Takes state + props |
+| Has fallback | No | Yes (required) |
+| Created with | `Template::build()` | `Capsule::build(Template::build(...))` |
+| Registered with | `.template()` | `.capsule_ref()` |
+
+## Registering Capsules
+
+Add capsules to your `PerseusApp`:
+
+```rust
+// src/main.rs
+use perseus::prelude::*;
+
+mod capsules;
+mod templates;
+
+#[perseus::main(perseus_axum::dflt_server)]
+pub fn main() -> PerseusApp {
+ PerseusApp::new()
+ .template(crate::templates::index::get_template())
+ .capsule_ref(&*crate::capsules::greeting::GREETING)
+ .error_views(ErrorViews::unlocalized_development_default())
+}
+```
+
+## Using Widgets in Templates
+
+Embed widgets using `.widget()`:
+
+```rust
+// src/templates/index.rs
+use perseus::prelude::*;
+use sycamore::prelude::*;
+use crate::capsules::greeting::{GREETING, GreetingProps};
+
+fn index_page() -> View {
+ view! {
+ h1 { "My Page" }
+
+ // Embed the greeting widget
+ (GREETING.widget(
+ "", // Widget path (empty = root)
+ GreetingProps { size: "large".to_string() }
+ ))
+
+ p { "More content below the widget" }
+ }
+}
+
+pub fn get_template() -> Template {
+ Template::build("index")
+ .view(index_page)
+ .build()
+}
+```
+
+## Widget Paths
+
+The path argument specifies which widget to render:
+
+```rust
+// Capsule "products" with build_paths: ["apple", "banana", "orange"]
+
+// Render the apple widget
+PRODUCTS.widget("apple", ProductProps::default())
+
+// Render the banana widget
+PRODUCTS.widget("banana", ProductProps::default())
+```
+
+For capsules without build paths (single widget), use empty string `""`.
+
+## Widgets Without State
+
+For capsules without state:
+
+```rust
+#[derive(Clone)]
+pub struct ButtonProps {
+ pub label: String,
+}
+
+fn button_widget(props: ButtonProps) -> View {
+ view! {
+ button { (props.label) }
+ }
+}
+
+fn button_fallback(_props: ButtonProps) -> View {
+ view! {
+ button(disabled = true) { "..." }
+ }
+}
+
+lazy_static::lazy_static! {
+ pub static ref BUTTON: Capsule = {
+ Capsule::build(Template::build("button"))
+ .fallback(button_fallback)
+ .view(button_widget)
+ .build()
+ };
+}
+```
+
+## Widgets Without Properties
+
+Use unit type `()` for properties:
+
+```rust
+fn simple_widget() -> View {
+ view! { div { "Simple content" } }
+}
+
+lazy_static::lazy_static! {
+ pub static ref SIMPLE: Capsule = {
+ Capsule::build(Template::build("simple"))
+ .empty_fallback()
+ .view(simple_widget)
+ .build()
+ };
+}
+
+// Usage
+SIMPLE.widget("", ())
+```
+
+## Delayed Widgets
+
+For widgets that should load after the main page:
+
+```rust
+// Normal widget (included in initial HTML)
+(MY_CAPSULE.widget("path", props))
+
+// Delayed widget (loaded after page renders)
+(MY_CAPSULE.delayed_widget("path", props))
+```
+
+Use delayed widgets for heavy content that would slow down initial page load.
+
+## Rescheduling
+
+If a build-time page uses a request-time widget, Perseus needs permission to reschedule:
+
+```rust
+// Template that uses a request-time capsule
+Template::build("my-page")
+ .build_state_fn(get_build_state)
+ .allow_rescheduling() // Required!
+ .view(page_view)
+ .build()
+```
+
+Only add `.allow_rescheduling()` when you get the error - don't add it preemptively.
+
+## Nested Widgets
+
+Widgets can contain other widgets:
+
+```rust
+fn wrapper_widget(state: WrapperStateRx, _props: ()) -> View {
+ view! {
+ div(class = "wrapper") {
+ h2 { (state.title.get_clone()) }
+ // Embed another widget
+ (INNER_CAPSULE.widget("", InnerProps::default()))
+ }
+ }
+}
+```
+
+**Warning**: Deep nesting requires multiple render passes. Limit to 2-3 levels.
+
+## Capsule with Build Paths
+
+```rust
+#[engine_only_fn]
+async fn get_build_paths() -> BuildPaths {
+ BuildPaths {
+ paths: vec!["a".to_string(), "b".to_string(), "c".to_string()],
+ extra: ().into(),
+ }
+}
+
+#[engine_only_fn]
+async fn get_build_state(info: StateGeneratorInfo<()>) -> LetterState {
+ LetterState {
+ letter: info.path.clone(),
+ }
+}
+
+lazy_static::lazy_static! {
+ pub static ref LETTER: Capsule = {
+ Capsule::build(
+ Template::build("letter")
+ .build_paths_fn(get_build_paths)
+ .build_state_fn(get_build_state)
+ )
+ .empty_fallback()
+ .view_with_state(letter_widget)
+ .build()
+ };
+}
+
+// Usage
+LETTER.widget("a", ()) // Renders letter A
+LETTER.widget("b", ()) // Renders letter B
+```
+
+## Tips
+
+1. **Use capsules for reusable UI with state** - Headers, footers, sidebars
+2. **Consider caching benefits** - Frequently reused content across pages
+3. **Keep fallbacks simple** - Skeletons or loading indicators
+4. **Watch nesting depth** - Each level adds a render pass
+5. **Test with slow networks** - See fallbacks in action
+
+## Getting Help
+
+Capsules are a novel architecture. If you have issues:
+- [GitHub Discussions](https://github.com/framesurge/perseus/discussions)
+- [Discord](https://discord.com/invite/GNqWYWNTdp)
diff --git a/docs/0.5.x/en-US/core-principles.md b/docs/0.5.x/en-US/core-principles.md
new file mode 100644
index 0000000000..bf16bd808b
--- /dev/null
+++ b/docs/0.5.x/en-US/core-principles.md
@@ -0,0 +1,70 @@
+# Core Principles
+
+Before you dive into Perseus, you might want to get a better idea of the fundamentals on which the framework is built. If you'd prefer to dive straight in though, check out [the tutorial](:first-app/installation), and then maybe come back here later.
+
+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.
+
+## Templates
+
+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...).
+
+There are two things you need to know about templates:
+
+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**
+
+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`.
+
+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 to think 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*](:state/build) strategy. Are there too many to build all at once? Use [*incremental generation*](:state/incremental) 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 its 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 lot 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 built 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 it 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.)
+
+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.)
+
+One more thing to briefly note is about the `dist/target_wasm/` and `dist/target_engine/` directories. As you might have inferred, the purpose of this is to provide a separate compilation space for Wasm code, which is used under the hood by the CLI whenever it builds your app to Wasm. The reason for this is so that we can build the engine and the browser sides in parallel. With only one `target/` directory, Cargo would make us wait until one had completed before starting the other, which slows down compilation. In testing, there tends to be a significant reduction in compilation times as a result of this separation of targets.
+
+Finally, note that the Perseus CLI will automatically install the `wasm-bindgen` and `wasm-opt` CLIs in a system-wide cache (see [here](https://docs.rs/directories/latest/directories/struct.ProjectDirs.html#method.cache_dir) for how that's calculated), or in `dist/tools/` if that fails (there's an option to ensure the local cache is used as well, which you might want to set for more reproducible builds).
diff --git a/docs/0.5.x/en-US/faq.md b/docs/0.5.x/en-US/faq.md
new file mode 100644
index 0000000000..d0e7c53d7c
--- /dev/null
+++ b/docs/0.5.x/en-US/faq.md
@@ -0,0 +1,78 @@
+# Common pitfalls and FAQs
+
+This page is a list of common pitfalls and FAQs in Perseus, and will be updated regularly. If you're having an issue with Perseus, check through this list to see if your problem already has a solution.
+
+## Is Perseus ready for production?
+
+This is a really hard question to answer completely. At this very moment, Perseus v0.4.x is stable and working, and, if you're already using it without problems, it should be *reasonably safe* for production (given it's been out in beta for a year and it seems to work excellently).
+
+All that said, both Perseus and Sycamore are still in v0.x.x, meaning neither project has yet reached a 'stable' 1.0 release. Both projects strive to make sure that no breaking changes are introduced except in bumps of the v0.**X**.x number, and both projects are actively maintained with fantastic communities behind them, so any problems you're having will probably be rapidly resolved. However, if you're looking for something with set-in-stone functionality that is totally reliable, Perseus isn't quite there yet. For personal projects and internal tools, we *absolutely* recommend Perseus, it's a great choice! But, for enterprise production applications, there is a *very small* chance of something going horribly wrong. That said, to date the Perseus project has received no reports of any production failures caused by our code, so things seem to be going pretty well!
+
+If you'd like to use Perseus in full mission-critical production though, we would recommend waiting until v1.0.0 comes out, which will denote production-safety and stability. This will be pending the release of that version for Sycamore, as well as broader stability in Perseus (there is no timeline for this at present, though we would be looking at v1.0.0 hopefully in early-to-mid 2024).
+
+## I'm getting errors about `mio` and `tokio` feature flags on Wasm...
+
+Chances are, you're trying to use Perseus in a Cargo workspace, which you can certainly do, but you'll need to add this line in the `[workspace]` table of your *root* `Cargo.toml`:
+
+```toml
+resolver = "2"
+```
+
+This is because Perseus uses `tokio` on the engine-side, which has all sorts of asynchronous magic that can't be compiled into the browser. The problem is that we also use a few things that depend on very small parts of `tokio` in the browser, but Cargo will go "oh, you're using these features on the engine, so I'll put them everywhere to save space", which doesn't work nicely with the fact that Perseus compiles for the browser as well! The above configuration will tell Cargo to use its more advanced feature resolve, which will one day be the default in Rust.
+
+If you're not trying to use Perseus in a workspace, and the output of `perseus --version` is *identical* to the version of `perseus` in your `Cargo.toml` (they don't have to be *identical*, but it's a good idea if you're getting errors), and you're not doing anything really weird (like trying to build a Wasm-native compiler...), then you should try the steps below for when you're getting strange errors with Cargo, which involves deleting your Cargo registry (equivalent to telling Cargo to completely start over on your system), which sometimes fixes things. If you're still having problems, please let us know, and we'll see what we can do to help you out!
+
+## I'm getting JSON error messages...
+
+If an error occurs during `perseus serve`, it's very possible that you'll get error messages in JSON, which are utterly unreadable. This is because of the way the server is run, the Perseus CLI needs a JSON output so that it can figure out where the server binary is. You can access the human-readable logs by 'snooping' on the output though, which you can do by running `perseus snoop serve` (but make sure you've run `perseus build` first).
+
+## Cargo is putting out strange errors...
+
+If you're getting errors along the lines of not being able to find the latest Perseus version, or you have Perseus version mismatches even though you only installed it once, you've probably got some kind of Cargo corruption. Usually, this can be fixed by running `perseus clean && cargo clean`, which will delete `dist/` and `target/` and start again from scratch.
+
+However, sometimes you'll need to purge your system's Cargo cache, which can be done safely by running the following commands:
+
+```shell
+cd ~/.cargo
+mkdir old
+mv git old
+mv registry old
+```
+
+That will archive the `git/` and `registry/` folders in `~/.cargo/`, which should resolve any corruptions. Then, just run `cargo build` in your project (after `perseus clean && cargo clean`) and everything should work! If not and you have no idea what's going on, feel free to ask on our [Discord server](https://discord.com/invite/GNqWYWNTdp)!
+
+## Hydration doesn't work with X
+
+Perseus v0.4.x uses Sycamore v0.8.x, which may still have a few very minor hydration bugs (though literally dozens have been fixed since v0.7.x), so there are a few things that won't work with it yet. In fact, as a general rule, if you're getting weird layout bugs that make absolutely no logical sense, try disabling hydration, it will often fix things at the moment. This shouldn't have any major impact on user experience or performance that's appreciable, though it *may* lower your app's Lighthouse scores. Please be sure to report your problem to [Sycamore](https://github.com/sycamore-rs/sycamore) (or Perseus if you're not sure whose fault it is, and we'll probably figure it out eventually!).
+
+## I'm getting really weird errors with a page's ``...
+
+Alright, this can mean about a million things. There is one that could be known to be Perseus' fault though: if you go to a page in your app, then reload it, then go to another page, and then navigate *back* to the original page (using a link inside your app, *not* your browser's back button), and there are problems with the `` that weren't there before, then you should disable the `cache-initial-load` feature on Perseus, since Perseus is having problems figuring out how your `` works. Typically, a delimiter `` is added to the end of the ``, but if you're using a plugin that's adding anything essential after this, that will be lost on transition to the new page. Any advanced manipulation of the `` at runtime could also cause this. Note that disabling this feature (which is on by default) will prevent caching of the first page the user loads, and it will have to be re-requested if they go back to it, which incurs the penalty of a network request.
+
+## I'm getting a 'mismatched render backends' error
+
+This is a very rare kind of error that Perseus will emit if it knows that running your app in its current state will cause undefined behavior: it's a safeguard against far worse things happening. If you're using the reference pattern of managing your templates and/or capsules, where you define them in `lazy_static!`s, and then bring those into `.template_ref()`/`.capsule_ref()`, this problem is almost certainly caused by your using the incorrect *render backend generic*. In those statics, you have to specify a concrete value for that `G: Html` you see floating around the place. You might have chosen `DomNode`, or `SsrNode`, or maybe even `HydrateNode`, but each of these is only valid sometimes! Perseus internally knows when it uses each one, and it provides a clever little type alias that can handle all this for you: `PerseusNodeType`. If you use that, this error shoudl go away, adn your app should work perfectly!
+
+Alternately, this error can occur if you try to do something very inadvisable, like putting a widget in a `view!` that you try to `render_to_string` on the browser-side. In fact, any attempt to render to a string in the browser that uses widgets is almost certain to trigger this exact error. This is because `PerseusNodeType` automatically resolves to `DomNode`/`HydrateNode` (depending on whether or not you've enabled the `hydrate` feature) on the browser-side, because Perseus doesn't need to do any server-side rendering there (unsurprisingly). That means, when you bring in a widget that's defined as a `lazy_static!` using `PerseusNodeType`, your `View` might be a `View`, but the `MY_WIDGET.widget()` function will take that `SsrNode`, hold it for a moment, and check the type of itself, which it will find to be `PerseusNodeType`. Since `DomNode != SsrNode` and `HydrateNode != SsrNode`, it will find that you're trying to use a browser-side widget in a server-side rendered view, which is a type mismatch. Normally, this sort of thing could be caught by Rust at compilation-time, but Perseus uses some transmutes internally to make it safe to use `PerseusNodeType`, as long as it lines up with the actual type of the `View` being rendered. if you try to server-side render in the browser though, the types don't line up, and Perseus has the choice of either panicking or causing undefined behavior. To maintain safety, it panics.
+
+Note that this doesn't mean it's actually impossible to server-side render a widget on the browser-side, you can use the functional pattern to do this easily. Rather than using `MY_CAPSULE.widget()`, just use `crate::path::to::my::widget::get_capsule().widget()`, because `get_capsule()` is generic over `G: Html` meaning it will just work with Rust's normal typing system.
+
+If you're still getting this error, and none of these solutions make sense with what you're doing, then you've possibly encountered a rather serious Perseus bug, which we'd like to know about so we can fix it! Please report it [on GitHub](https://github.com/framesurge/perseus/issues/new/choose).
+
+## Problem binding to `http://localhost:3100`
+
+This means another instance of Perseus is already running. The reason this talks about rather than port 8080 is because 3100 is where the live reload server runs by default.
+
+## I'm getting an error about not being able to modify the panic handler?
+
+If Perseus panics, it will output an error, but sometimes, especially if you're using `-w`, Perseus will also try to reload for new code, while in a panicking state, which will lead to *another* panic where Rust complains about Perseus trying to fix things naively itself. Basically, this is just some overzealous reloading most of the time, and it can be easily fixed by reloading the page. If you want to see what the *original* panic message was, check your browser console.
+
+## `BorrowMut` errors
+
+These are a very sneaky kind of error in Rust that can occur at runtime, and Perseus unfortunately uses the `RefCell`s that can cause these *a lot* internally. We believe our usage of them is perfectly sound, but bugs like this have occurred in the past. Be aware that HSR can sometimes cause these in development spontaneously, just as a result of what we think are weird browser timing race conditions, and those can be fixed by reloading the page (and they shoudl never occur in production), but any persistent `BorrowMut` errors should be reported to use right away, because, chances are, they denote a bug in Perseus.
+
+## Minification is failing
+
+Perseus has several feature flags related to minification, which is used to compress your HTML, CSS, and JS to make it smaller. Normally, this works perfectly fine and you don't have to worry about it, but sometimes it will fail, either as a result of a bug in the minifier (pretty rare now), or some invalid HTML that you've provided. Importantly though, the minifier is much stricter than a browser, which will let you pretty much do whatever you like, so it's important to check if what you're doing is actually valid. For example, `
` is actually semantically invalid HTML! That doesn't mean a browser won't reluctantly parse it, but it does mean the minifier will throw a tantrum at you. The best way of figuring out what code is the problem is to start by commenting out all your view code for a page, and progressively uncomment it until you trigger the bug, then uncommenting smaller and smaller sections until you figure out what's causing the issue. Then do a quick search to make sure you're doing something valid. If you think you are, try to get it down to a minimal reproduction, and, if you're certain that should work, let us know with a bug report!
+
+In the meantime, or if you're passing through HTML from somewhere else that might be invalid (please sanitize it!), then you may want to disable the minifier entirely, which can be done by setting `default-features = false` for the `perseus` package in your `Cargo.toml`, and then by re-enabling each default feature you want, which should be all of them, except for the ones starting with `minify`.
diff --git a/docs/0.5.x/en-US/first-app/defining.md b/docs/0.5.x/en-US/first-app/defining.md
new file mode 100644
index 0000000000..d2a19bb16d
--- /dev/null
+++ b/docs/0.5.x/en-US/first-app/defining.md
@@ -0,0 +1,161 @@
+# Defining a Perseus App
+
+Once you've got Perseus installed, it's time to create your app's entry point. This guide explains how Perseus apps are structured.
+
+## The Two Sides of Perseus
+
+Perseus has two parts:
+- **Engine-side (server)**: Runs on your server, handles rendering and state generation
+- **Client-side (browser)**: Runs as WebAssembly in the user's browser
+
+While you *could* write separate code for each side, Perseus provides a convenient `#[perseus::main]` macro that handles this automatically.
+
+## Basic App Structure
+
+Here's the simplest Perseus app:
+
+```rust
+use perseus::prelude::*;
+
+mod templates;
+
+#[perseus::main(perseus_axum::dflt_server)]
+pub fn main() -> PerseusApp {
+ PerseusApp::new()
+ .template(crate::templates::index::get_template())
+ .error_views(ErrorViews::unlocalized_development_default())
+}
+```
+
+Let's break this down:
+
+### The `#[perseus::main]` Macro
+
+```rust
+#[perseus::main(perseus_axum::dflt_server)]
+pub fn main() -> PerseusApp {
+ // ...
+}
+```
+
+This macro:
+1. Creates the engine-side `main()` function that handles server operations
+2. Creates the client-side entry point for WebAssembly
+3. Uses the specified server (here `perseus_axum`) to serve your app
+
+**Available servers:**
+- `perseus_axum` - Uses [Axum](https://github.com/tokio-rs/axum) (recommended)
+- `perseus_warp` - Uses [Warp](https://github.com/seanmonstar/warp)
+- `perseus_actix_web` - Uses [Actix Web](https://github.com/actix/actix-web)
+
+### The `PerseusApp` Builder
+
+```rust
+PerseusApp::new()
+ .template(crate::templates::index::get_template())
+ .error_views(ErrorViews::unlocalized_development_default())
+```
+
+`PerseusApp` is your app's configuration. Common methods include:
+
+| Method | Description |
+|--------|-------------|
+| `.template(t)` | Adds a template that generates pages |
+| `.error_views(e)` | Sets error handling views |
+| `.index_view(f)` | Customizes the HTML shell |
+| `.global_state_creator(g)` | Sets up global state |
+| `.locales_and_translations_manager(...)` | Enables internationalization |
+
+## Project Structure
+
+A typical Perseus project looks like this:
+
+```
+my-app/
+├── Cargo.toml
+├── src/
+│ ├── main.rs # App entry point
+│ ├── error_views.rs # Error handling (optional)
+│ └── templates/
+│ ├── mod.rs # Template exports
+│ ├── index.rs # Landing page template
+│ └── about.rs # About page template
+```
+
+### The Templates Module
+
+Create `src/templates/mod.rs`:
+
+```rust
+pub mod about;
+pub mod index;
+```
+
+Each template file exports a `get_template()` function that you register in `main.rs`.
+
+## A Complete Example
+
+Here's a full `src/main.rs` with multiple templates:
+
+```rust
+use perseus::prelude::*;
+
+mod error_views;
+mod templates;
+
+#[perseus::main(perseus_axum::dflt_server)]
+pub fn main() -> PerseusApp {
+ PerseusApp::new()
+ // Register templates
+ .template(crate::templates::index::get_template())
+ .template(crate::templates::about::get_template())
+ // Set up error handling
+ .error_views(crate::error_views::get_error_views())
+}
+```
+
+## Custom Index View
+
+By default, Perseus uses a minimal HTML shell. You can customize it:
+
+```rust
+use perseus::prelude::*;
+use sycamore::prelude::*;
+
+#[perseus::main(perseus_axum::dflt_server)]
+pub fn main() -> PerseusApp {
+ PerseusApp::new()
+ .template(crate::templates::index::get_template())
+ .error_views(ErrorViews::unlocalized_development_default())
+ .index_view(|| {
+ view! {
+ html {
+ head {
+ meta(charset = "UTF-8")
+ meta(name = "viewport", content = "width=device-width, initial-scale=1.0")
+ link(rel = "stylesheet", href = "/.perseus/static/styles.css")
+ }
+ body {
+ PerseusRoot() // Your app renders here
+ }
+ }
+ }
+ })
+}
+```
+
+The `PerseusRoot()` component is where your pages will be rendered.
+
+## Development vs Production
+
+During development, you can use:
+
+```rust
+.error_views(ErrorViews::unlocalized_development_default())
+```
+
+This provides sensible error pages. However, for production, you should create custom error views (see [Error Handling](/docs/first-app/error-handling)).
+
+## Next Steps
+
+Now that your app is set up, let's [generate some pages](/docs/first-app/generating-pages)!
diff --git a/docs/0.5.x/en-US/first-app/deploying.md b/docs/0.5.x/en-US/first-app/deploying.md
new file mode 100644
index 0000000000..8471f51e20
--- /dev/null
+++ b/docs/0.5.x/en-US/first-app/deploying.md
@@ -0,0 +1,208 @@
+# Deploying Your App
+
+You've built your Perseus app - now let's deploy it!
+
+## Test Before Deploying
+
+First, make sure everything compiles:
+
+```sh
+perseus check
+```
+
+Then test locally:
+
+```sh
+perseus serve
+```
+
+Visit and verify:
+- Pages load correctly
+- Navigation works (links should be instant)
+- Error pages work (try )
+
+## Production Build
+
+When you're ready to deploy:
+
+```sh
+perseus deploy
+```
+
+This command:
+1. Optimizes your code for production
+2. Minimizes the Wasm bundle size
+3. Creates a `pkg/` directory with everything needed
+
+The build takes longer than development builds because of these optimizations.
+
+## Running in Production
+
+The `pkg/` folder contains:
+- `server` - The server binary
+- Static assets and prerendered pages
+
+To run:
+
+```sh
+./pkg/server
+```
+
+Your app will be available at .
+
+### Configure Host and Port
+
+Use environment variables:
+
+```sh
+PERSEUS_HOST=0.0.0.0 PERSEUS_PORT=80 ./pkg/server
+```
+
+| Variable | Default | Description |
+|----------|---------|-------------|
+| `PERSEUS_HOST` | `127.0.0.1` | Bind address |
+| `PERSEUS_PORT` | `8080` | Port number |
+
+### HTTPS
+
+Perseus doesn't handle HTTPS directly. Use a reverse proxy:
+- Nginx
+- Caddy
+- Cloud provider load balancer
+
+## Static Export
+
+If your app doesn't need server-side features (no request-time state), you can export as static files:
+
+```sh
+perseus deploy -e
+```
+
+This creates static HTML files you can host anywhere:
+- GitHub Pages
+- Netlify
+- Vercel
+- Any static file host
+
+### Test Static Export Locally
+
+```sh
+perseus export -s
+```
+
+The `-s` flag starts a file server for testing.
+
+### Serve with Python (for testing)
+
+```sh
+python -m http.server -d pkg/
+```
+
+## When to Use Each Deployment Type
+
+| Use Case | Deployment Type |
+|----------|----------------|
+| Static content (docs, blog) | Export (`-e`) |
+| User authentication | Server |
+| Request-time data | Server |
+| Real-time features | Server |
+| Cheapest hosting | Export |
+
+## Performance Tips
+
+### Wasm Bundle Size
+
+The Wasm bundle is the main factor in initial load time. To minimize it:
+
+1. **Use release builds** (automatic with `perseus deploy`)
+2. **Minimize dependencies** - Only include what you need in browser code
+3. **Use `#[cfg(client)]`** - Keep server-only code out of the bundle
+
+### Alternative Allocators
+
+For smaller bundles, consider alternative allocators (use only for client):
+
+```rust
+#[cfg(client)]
+#[global_allocator]
+static ALLOC: lol_alloc::LeakingAllocator = lol_alloc::LeakingAllocator;
+```
+
+**Warning**: Only use these for the client build, not the server!
+
+## Hosting Options
+
+### Self-Hosted
+
+Run the server binary on any Linux server:
+- AWS EC2
+- DigitalOcean Droplets
+- Your own server
+
+### Container Deployment
+
+Create a Dockerfile:
+
+```dockerfile
+FROM rust:1.75 as builder
+WORKDIR /app
+COPY . .
+RUN cargo install perseus-cli
+RUN perseus deploy
+
+FROM debian:bookworm-slim
+WORKDIR /app
+COPY --from=builder /app/pkg ./pkg
+EXPOSE 8080
+CMD ["./pkg/server"]
+```
+
+### Static Hosting (Export Only)
+
+For exported apps:
+- **GitHub Pages**: Free, easy setup
+- **Netlify**: Free tier, automatic deploys
+- **Vercel**: Free tier, edge functions
+- **Cloudflare Pages**: Free, fast CDN
+
+## Deployment Checklist
+
+- [ ] Custom error views (not development defaults)
+- [ ] `perseus check` passes
+- [ ] Tested locally with `perseus serve`
+- [ ] Environment variables configured
+- [ ] HTTPS set up (if using server deployment)
+
+## Troubleshooting
+
+### "Development error views not allowed in production"
+
+Create custom error views. See [Error Handling](/docs/first-app/error-handling).
+
+### Server exits immediately
+
+Check logs for errors. Common issues:
+- Port already in use
+- Missing permissions
+- Missing files in `pkg/`
+
+### Export fails
+
+Your app might use features that require a server:
+- Request-time state
+- Revalidation
+- Server-side APIs
+
+Use server deployment instead, or refactor to use build-time state only.
+
+## What's Next?
+
+Congratulations on deploying your first Perseus app!
+
+Explore more:
+- [State Management](/docs/state/intro) - Dynamic data handling
+- [Internationalization](/docs/fundamentals/i18n) - Multi-language support
+- [Capsules](/docs/capsules/intro) - Reusable widgets with state
+- [Examples](https://github.com/framesurge/perseus/tree/main/examples) - Real-world code samples
+
+Need help? [Open a discussion](https://github.com/framesurge/perseus/discussions) or join our [Discord](https://discord.com/invite/GNqWYWNTdp)!
diff --git a/docs/0.5.x/en-US/first-app/dev-cycle.md b/docs/0.5.x/en-US/first-app/dev-cycle.md
new file mode 100644
index 0000000000..b499d1bd63
--- /dev/null
+++ b/docs/0.5.x/en-US/first-app/dev-cycle.md
@@ -0,0 +1,131 @@
+# Development Cycle
+
+This guide covers the commands and workflow for developing Perseus apps.
+
+## Two Modes of Development
+
+When developing a Perseus app, you'll alternate between:
+
+1. **Coding mode** - Writing business logic, checking for errors
+2. **Preview mode** - Seeing your app in the browser, styling, testing features
+
+## Quick Type Checking
+
+For fast feedback while coding, use:
+
+```sh
+perseus check -w
+```
+
+This runs `cargo check` on both engine-side and browser-side code. It's much faster than a full build because it only checks for errors without compiling.
+
+Add `-g` to also check your build logic:
+
+```sh
+perseus check -gw
+```
+
+## IDE Setup
+
+To get proper syntax highlighting and error detection in your IDE, create `.cargo/config.toml`:
+
+```toml
+[build]
+rustflags = [ "--cfg", "engine" ]
+```
+
+This tells your IDE to check the engine-side code by default.
+
+**Tip**: When working on browser-only logic, temporarily change `engine` to `client` to get proper IDE support for that code.
+
+## Running Your App
+
+When you need to see your app in a browser:
+
+```sh
+perseus serve -w
+```
+
+This:
+1. Builds your app
+2. Starts a development server
+3. Opens your app at
+
+The `-w` flag enables watch mode - changes to your code trigger automatic rebuilds.
+
+## Rebuild Speed
+
+| Change Type | Rebuild Speed |
+|-------------|--------------|
+| Static files (CSS, images) | Near instant |
+| Rust code | Slower (Rust compilation) |
+
+This is a tradeoff of Rust web development: slower builds but faster, more reliable apps.
+
+## Debugging with `perseus snoop`
+
+The standard commands hide most output. To see all logs and debug output:
+
+```sh
+perseus snoop build # View build output
+perseus snoop wasm-build # View Wasm compilation output
+perseus snoop serve # View server output
+```
+
+Use these when you need to see `dbg!()` output or detailed error messages.
+
+## Custom Watch Paths
+
+Watch additional directories beyond your source code:
+
+```sh
+perseus serve -w --custom-watch ../docs
+```
+
+Exclude paths with `!`:
+
+```sh
+perseus serve -w --custom-watch !./generated
+```
+
+## Common Commands Reference
+
+| Command | Purpose |
+|---------|---------|
+| `perseus check -w` | Fast type checking with watch |
+| `perseus serve -w` | Development server with watch |
+| `perseus build` | Build without serving |
+| `perseus export` | Export as static files |
+| `perseus deploy` | Production build |
+| `perseus clean` | Clear build artifacts |
+| `perseus snoop [cmd]` | Run command with full output |
+
+## Workflow Tips
+
+1. **Use `check` while coding** - It's much faster than `serve`
+2. **Only `serve` when you need to see the UI** - Visual testing, styling
+3. **Use `snoop` for debugging** - See all output including `dbg!()` calls
+4. **Keep terminal visible** - Watch mode shows compile errors immediately
+
+## Example Workflow
+
+```sh
+# Start development session
+cd my-perseus-app
+
+# Fast iteration while coding
+perseus check -w
+
+# When ready to test in browser
+perseus serve -w
+
+# If something isn't working, debug
+perseus snoop serve
+
+# Build for production
+perseus deploy
+```
+
+## Next Steps
+
+Ready to ship? Learn about [deploying your app](/docs/first-app/deploying)!
diff --git a/docs/0.5.x/en-US/first-app/error-handling.md b/docs/0.5.x/en-US/first-app/error-handling.md
new file mode 100644
index 0000000000..5001723776
--- /dev/null
+++ b/docs/0.5.x/en-US/first-app/error-handling.md
@@ -0,0 +1,178 @@
+# Error Handling
+
+Perseus gives you full control over how errors are displayed. Instead of generic error pages, you define custom views for different types of errors.
+
+## Why Custom Error Views?
+
+Perseus doesn't want to show bright red error messages if your website uses a completely different style. You provide `View`s that match your app's design.
+
+## Basic Error Views
+
+Here's a simple error handling setup:
+
+```rust
+// src/error_views.rs
+use perseus::prelude::*;
+use sycamore::prelude::*;
+
+pub fn get_error_views() -> ErrorViews {
+ ErrorViews::new(|error, _error_context, _error_position| {
+ // Return (head, body)
+ (
+ view! {
+ title { "Error" }
+ },
+ match &error {
+ ClientError::ServerError { status, .. } => match status.as_u16() {
+ 404 => view! {
+ h1 { "Page Not Found" }
+ p { "The page you're looking for doesn't exist." }
+ Link(to = "/") { "Go Home" }
+ },
+ _ => view! {
+ h1 { "Server Error" }
+ p { (format!("Error {}", status)) }
+ },
+ },
+ ClientError::Panic(_) => view! {
+ h1 { "Critical Error" }
+ p { "The app has crashed. Please refresh the page." }
+ },
+ ClientError::FetchError(_) => view! {
+ h1 { "Connection Error" }
+ p { "Please check your internet connection and try again." }
+ },
+ _ => view! {
+ h1 { "Something Went Wrong" }
+ p { "An unexpected error occurred." }
+ },
+ }
+ )
+ })
+}
+```
+
+## Understanding ClientError
+
+The `ClientError` enum has several variants:
+
+| Variant | When It Occurs |
+|---------|---------------|
+| `ServerError` | Server-side errors (404, 500, etc.) |
+| `Panic` | App crashed due to a panic |
+| `FetchError` | Network failure or server unreachable |
+| `InvariantError` | Internal Perseus errors |
+
+## Registering Error Views
+
+Add your error views to `PerseusApp`:
+
+```rust
+// src/main.rs
+use perseus::prelude::*;
+
+mod error_views;
+mod templates;
+
+#[perseus::main(perseus_axum::dflt_server)]
+pub fn main() -> PerseusApp {
+ PerseusApp::new()
+ .template(crate::templates::index::get_template())
+ .error_views(crate::error_views::get_error_views())
+}
+```
+
+## Error Position
+
+Sometimes errors appear as popups instead of full pages. This happens when:
+
+- The page content rendered fine but the client couldn't initialize
+- The user can still see content, it's just not interactive
+
+For popup errors, the head is ignored and your body renders in a popup styled with `#__perseus_popup_error`.
+
+```rust
+ErrorViews::new(|error, _ctx, position| {
+ match position {
+ ErrorPosition::Page => {
+ // Full page error
+ (
+ view! { title { "Error" } },
+ view! { h1 { "Error" } p { "Something went wrong." } }
+ )
+ },
+ ErrorPosition::Popup => {
+ // Popup error (head is ignored)
+ (
+ view! {},
+ view! { p { "An error occurred. The page may not be interactive." } }
+ )
+ },
+ }
+})
+```
+
+## Development vs Production
+
+During development, you can use the built-in default:
+
+```rust
+.error_views(ErrorViews::unlocalized_development_default())
+```
+
+For production, **always create custom error views**. Perseus won't let you deploy with the development defaults.
+
+## HTTP Status Codes
+
+When handling `ServerError`, check the status code:
+
+| Code | Meaning |
+|------|---------|
+| 404 | Page not found |
+| 403 | Forbidden |
+| 500 | Internal server error |
+| 502/503 | Server unavailable |
+
+```rust
+ClientError::ServerError { status, .. } => {
+ let code = status.as_u16();
+ if code == 404 {
+ // Not found
+ } else if code >= 500 {
+ // Server error
+ } else {
+ // Other client error
+ }
+}
+```
+
+## Styling Error Pages
+
+Error pages are just normal views. Style them with CSS:
+
+```css
+/* In your stylesheet */
+.error-page {
+ text-align: center;
+ padding: 2rem;
+}
+
+.error-page h1 {
+ color: #e53e3e;
+}
+
+/* Popup error styling */
+#__perseus_popup_error {
+ position: fixed;
+ bottom: 1rem;
+ right: 1rem;
+ background: #fef2f2;
+ border: 1px solid #fca5a5;
+ padding: 1rem;
+ border-radius: 0.5rem;
+}
+```
+
+## Next Steps
+
+Now that you've set up error handling, let's [run your app](/docs/first-app/dev-cycle)!
diff --git a/docs/0.5.x/en-US/first-app/generating-pages.md b/docs/0.5.x/en-US/first-app/generating-pages.md
new file mode 100644
index 0000000000..03e0065eab
--- /dev/null
+++ b/docs/0.5.x/en-US/first-app/generating-pages.md
@@ -0,0 +1,277 @@
+# Generating Pages
+
+Perseus uses **templates** to generate **pages**. This is a core concept that makes Perseus powerful and flexible.
+
+## Templates and Pages
+
+Think of it this way:
+- A **template** is like a stencil with holes
+- **State** is the data that fills those holes
+- A **page** is what you get when you combine them
+
+**Template + State = Page**
+
+For example, a blog post template might have:
+- A hole for the title
+- A hole for the content
+- A hole for the author
+
+Each blog post fills these holes with different data, creating different pages from the same template.
+
+## Your First Template
+
+Let's create a simple template without state:
+
+```rust
+// src/templates/about.rs
+use perseus::prelude::*;
+use sycamore::prelude::*;
+
+fn about_page() -> View {
+ view! {
+ h1 { "About Us" }
+ p { "Welcome to our website!" }
+ Link(to = "/") { "Go Home" }
+ }
+}
+
+pub fn get_template() -> Template {
+ Template::build("about")
+ .view(about_page)
+ .build()
+}
+```
+
+**Key points:**
+- `fn about_page() -> View` - View functions return `View`
+- `view! { ... }` - Creates HTML-like elements
+- `Link(to = "/")` - Client-side navigation component
+- `Template::build("about")` - Creates a template at `/about`
+
+## Adding a Head
+
+Every page needs metadata like a title. Use the `head` function:
+
+```rust
+use perseus::prelude::*;
+use sycamore::prelude::*;
+
+fn about_page() -> View {
+ view! {
+ h1 { "About Us" }
+ p { "Welcome to our website!" }
+ }
+}
+
+#[engine_only_fn]
+fn head() -> View {
+ view! {
+ title { "About | My Website" }
+ meta(name = "description", content = "Learn about our company")
+ }
+}
+
+pub fn get_template() -> Template {
+ Template::build("about")
+ .view(about_page)
+ .head(head)
+ .build()
+}
+```
+
+The `#[engine_only_fn]` macro marks this function as server-side only - it won't be included in your browser bundle.
+
+## Adding State
+
+Most pages need dynamic data. Here's how to add state:
+
+### Step 1: Define Your State
+
+```rust
+use perseus::prelude::*;
+use serde::{Deserialize, Serialize};
+use sycamore::prelude::*;
+
+#[derive(Serialize, Deserialize, ReactiveState, Clone)]
+#[rx(alias = "GreetingStateRx")]
+struct GreetingState {
+ name: String,
+ message: String,
+}
+```
+
+**Understanding the derives:**
+- `Serialize, Deserialize` - State is sent over the network as JSON
+- `ReactiveState` - Makes fields reactive (they update the UI automatically)
+- `Clone` - Required by Perseus internals
+- `#[rx(alias = "...")]` - Creates a type alias for the reactive version
+
+### Step 2: Create Your View
+
+```rust
+#[auto_scope]
+fn greeting_page(state: GreetingStateRx) -> View {
+ view! {
+ h1 { "Hello, " (state.name.get_clone()) "!" }
+ p { (state.message.get_clone()) }
+ }
+}
+```
+
+**Key points:**
+- `#[auto_scope]` - Handles complex lifetime requirements
+- `state: GreetingStateRx` - Receives the *reactive* version of state
+- `.get_clone()` - Gets a clone of the value (use for `String`)
+- `.get()` - Gets a reference to the value (use for `Copy` types like `i32`)
+
+### Step 3: Generate State at Build Time
+
+```rust
+#[engine_only_fn]
+async fn get_build_state(_info: StateGeneratorInfo<()>) -> GreetingState {
+ GreetingState {
+ name: "World".to_string(),
+ message: "Welcome to Perseus!".to_string(),
+ }
+}
+```
+
+This function runs at build time and can:
+- Read files
+- Query databases
+- Call APIs
+- Do anything async
+
+### Step 4: Wire It Together
+
+```rust
+pub fn get_template() -> Template {
+ Template::build("greeting")
+ .build_state_fn(get_build_state)
+ .view_with_state(greeting_page)
+ .build()
+}
+```
+
+Note: Use `.view_with_state()` when your view receives state.
+
+## Complete Example
+
+Here's a complete template with state:
+
+```rust
+// src/templates/index.rs
+use perseus::prelude::*;
+use serde::{Deserialize, Serialize};
+use sycamore::prelude::*;
+
+#[derive(Serialize, Deserialize, ReactiveState, Clone)]
+#[rx(alias = "IndexStateRx")]
+struct IndexState {
+ greeting: String,
+ count: i32,
+}
+
+#[auto_scope]
+fn index_page(state: IndexStateRx) -> View {
+ view! {
+ h1 { (state.greeting.get_clone()) }
+
+ // Interactive counter
+ p { "Count: " (state.count.get()) }
+ button(on:click = move |_| {
+ state.count.set(*state.count.get() + 1);
+ }) {
+ "Increment"
+ }
+
+ // Navigation
+ nav {
+ Link(to = "/about") { "About" }
+ }
+ }
+}
+
+#[engine_only_fn]
+fn head(state: IndexState) -> View {
+ view! {
+ title { (format!("{} | My App", state.greeting)) }
+ }
+}
+
+#[engine_only_fn]
+async fn get_build_state(_info: StateGeneratorInfo<()>) -> IndexState {
+ IndexState {
+ greeting: "Hello, World!".to_string(),
+ count: 0,
+ }
+}
+
+pub fn get_template() -> Template {
+ Template::build("index")
+ .build_state_fn(get_build_state)
+ .view_with_state(index_page)
+ .head_with_state(head)
+ .build()
+}
+```
+
+## Reactive State in Action
+
+The `ReactiveState` derive creates reactive signals for each field:
+
+```rust
+// Original struct
+struct MyState {
+ count: i32,
+ name: String,
+}
+
+// What ReactiveState creates (simplified)
+struct MyStateRx {
+ count: Signal,
+ name: Signal,
+}
+```
+
+This means:
+- When you call `.set()`, the UI updates automatically
+- Changes persist across page navigations (stored in Page State Store)
+- No need to manually manage state updates
+
+## Error Handling
+
+State generation functions can return `Result`:
+
+```rust
+use perseus::prelude::*;
+
+#[engine_only_fn]
+async fn get_build_state(
+ _info: StateGeneratorInfo<()>
+) -> Result> {
+ let data = std::fs::read_to_string("data.json")
+ .map_err(|e| BlamedError::server(None, e))?;
+
+ Ok(serde_json::from_str(&data).unwrap())
+}
+```
+
+See [Build-Time State](/docs/state/build) for more on error handling.
+
+## Template Routing
+
+The string in `Template::build()` determines the URL:
+
+| Template Name | URL Path |
+|--------------|----------|
+| `"index"` | `/` (special case) |
+| `"about"` | `/about` |
+| `"blog/post"` | `/blog/post` |
+
+## Next Steps
+
+- [Error Handling](/docs/first-app/error-handling) - Handle errors gracefully
+- [Build-Time State](/docs/state/build) - Generate state at build time
+- [Request-Time State](/docs/state/request) - Generate state per request
+- [Incremental Generation](/docs/state/incremental) - Generate pages on demand
diff --git a/docs/0.5.x/en-US/first-app/installation.md b/docs/0.5.x/en-US/first-app/installation.md
new file mode 100644
index 0000000000..584b7705b6
--- /dev/null
+++ b/docs/0.5.x/en-US/first-app/installation.md
@@ -0,0 +1,165 @@
+# Installing Perseus
+
+This guide walks you through setting up Perseus from scratch.
+
+## Prerequisites
+
+1. **Install Rust** using [rustup](https://rustup.rs):
+ ```sh
+ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
+ ```
+
+2. **Add the WebAssembly target**:
+ ```sh
+ rustup target add wasm32-unknown-unknown
+ ```
+
+## Install the Perseus CLI
+
+The CLI manages building, serving, and deploying your app:
+
+```sh
+cargo install perseus-cli
+```
+
+## Create a New Project
+
+### Quick Start (Recommended)
+
+```sh
+perseus new my-app
+cd my-app
+```
+
+This creates a ready-to-run project.
+
+### Manual Setup
+
+If you prefer to understand each piece, create manually:
+
+```sh
+cargo new my-app
+cd my-app
+```
+
+#### 1. Configure IDE Support
+
+Create `.cargo/config.toml`:
+
+```toml
+[build]
+rustflags = [ "--cfg", "engine" ]
+rustdocflags = [ "--cfg", "engine" ]
+```
+
+This enables proper IDE support. Change `engine` to `client` when working on browser-only code.
+
+#### 2. Set Up Dependencies
+
+Replace your `Cargo.toml` with:
+
+```toml
+[package]
+name = "my-app"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+perseus = { version = "0.5", features = ["hydrate"] }
+sycamore = "0.9"
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+
+[target.'cfg(engine)'.dependencies]
+tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
+perseus-axum = "0.5"
+```
+
+## Understanding the Dependencies
+
+| Dependency | Purpose |
+|------------|---------|
+| `perseus` | The framework core |
+| `sycamore` | UI library for views |
+| `serde`, `serde_json` | Serialization for state transfer |
+| `tokio` | Async runtime (engine-only) |
+| `perseus-axum` | Server integration (engine-only) |
+
+## Engine vs Client
+
+Perseus has two build targets:
+
+- **Engine**: Runs on your server (prerendering, serving)
+- **Client**: Runs in the browser (WebAssembly, interactivity)
+
+Use `#[cfg(engine)]` and `#[cfg(client)]` to target specific code:
+
+```rust
+#[cfg(engine)]
+fn server_only_function() {
+ // Only compiled for the server
+}
+
+#[cfg(client)]
+fn browser_only_function() {
+ // Only compiled for WebAssembly
+}
+```
+
+**Why separate them?**
+- Smaller browser bundles (no server code in Wasm)
+- Faster compilation (only compile what's needed)
+- Access platform-specific APIs
+
+## Server Integrations
+
+Perseus supports multiple server frameworks:
+
+| Integration | Crate |
+|-------------|-------|
+| Axum (recommended) | `perseus-axum` |
+| Warp | `perseus-warp` |
+| Actix Web | `perseus-actix-web` |
+
+## Cargo Workspaces
+
+If using Perseus in a Cargo workspace, add this to your root `Cargo.toml`:
+
+```toml
+[workspace]
+resolver = "2"
+```
+
+This is **required** - Perseus won't compile without it.
+
+## Verify Installation
+
+Create a minimal app and run it:
+
+```sh
+# If you used `perseus new`
+perseus serve
+
+# Should open at http://localhost:8080
+```
+
+## Troubleshooting
+
+### "target not found" errors
+
+Make sure you added the Wasm target:
+```sh
+rustup target add wasm32-unknown-unknown
+```
+
+### IDE shows errors everywhere
+
+Check that `.cargo/config.toml` exists with the `rustflags` set.
+
+### Workspace compilation fails
+
+Ensure `resolver = "2"` is set in your workspace root.
+
+## Next Steps
+
+Now let's [define your app](/docs/first-app/defining)!
diff --git a/docs/0.5.x/en-US/fundamentals/compilation-times.md b/docs/0.5.x/en-US/fundamentals/compilation-times.md
new file mode 100644
index 0000000000..e23829bf45
--- /dev/null
+++ b/docs/0.5.x/en-US/fundamentals/compilation-times.md
@@ -0,0 +1,147 @@
+# Improving Compilation Times
+
+Perseus apps can take a while to compile due to the framework's complexity and Rust's compilation model. Here's how to speed things up.
+
+## Quick Wins
+
+### 1. Use Nightly Rust
+
+Switch to nightly for faster compilation:
+
+```bash
+rustup override set nightly
+```
+
+This alone can nearly halve compile times. Switch back to stable for production builds if desired.
+
+### 2. Export Instead of Serve
+
+If your app doesn't need request-time features, use `#[perseus::main_export]`:
+
+```rust
+#[perseus::main_export]
+pub fn main() -> PerseusApp {
+ PerseusApp::new()
+ // ...
+}
+```
+
+This avoids compiling a server integration.
+
+## Advanced Optimizations
+
+### Cranelift Backend
+
+[Cranelift](https://github.com/bytecodealliance/wasmtime/tree/main/cranelift) is an alternative compiler backend that prioritizes compile speed over runtime performance.
+
+1. **Install**: Follow the [precompiled builds guide](https://github.com/bjorn3/rustc_codegen_cranelift/#precompiled-builds)
+
+2. **Verify installation**:
+```bash
+cargo-clif -h
+```
+
+3. **Use with Perseus**:
+```bash
+perseus serve -w --cargo-engine-path cargo-clif
+```
+
+**Warning**: Only use Cranelift for development. Production builds should use the standard compiler.
+
+### What Cranelift Affects
+
+Cranelift only applies to the engine (server) binary:
+- ✅ State generation
+- ✅ Server-side rendering
+- ❌ Wasm compilation (no Cranelift support yet)
+
+Fortunately, Wasm builds are already reasonably fast due to Perseus' target-gated compilation.
+
+## Benchmark Results
+
+Testing on the `basic` example with a cold cache:
+
+| Configuration | Time |
+|---------------|------|
+| Stable, no optimizations | 28s |
+| Nightly + Cranelift | 7s |
+
+That's a **75% reduction** in compile time!
+
+## Development vs Production
+
+| Stage | Toolchain | Backend | Command |
+|-------|-----------|---------|---------|
+| Development | Nightly | Cranelift | `perseus serve -w --cargo-engine-path cargo-clif` |
+| Testing | Nightly | Standard | `perseus test` |
+| Production | Stable | Standard | `perseus deploy` |
+
+## Additional Tips
+
+### Use Watch Mode
+
+```bash
+perseus serve -w # Incremental rebuilds
+```
+
+Watch mode only recompiles changed code.
+
+### Minimize Dependencies
+
+Large dependency trees slow compilation. Audit your `Cargo.toml`:
+
+```bash
+cargo tree | wc -l # Count dependencies
+```
+
+### Feature Flags
+
+Only enable features you need:
+
+```toml
+[dependencies]
+perseus = { version = "0.5", default-features = false, features = ["..."] }
+```
+
+### Parallel Compilation
+
+Ensure Cargo uses all cores:
+
+```bash
+# In .cargo/config.toml
+[build]
+jobs = 16 # Adjust to your CPU
+```
+
+### Incremental Compilation
+
+Enable (usually on by default):
+
+```bash
+# In .cargo/config.toml
+[build]
+incremental = true
+```
+
+### SSD Storage
+
+Compile on SSD, not HDD. Rust's compilation is I/O intensive.
+
+## Profile-Guided Optimization
+
+For the fastest production builds (at the cost of longer compile times):
+
+```bash
+# Build with PGO
+RUSTFLAGS="-Cprofile-generate=/tmp/pgo" perseus deploy
+# Run the app to generate profile data
+# Then rebuild with profiles
+RUSTFLAGS="-Cprofile-use=/tmp/pgo" perseus deploy
+```
+
+This is rarely necessary but can squeeze out extra runtime performance.
+
+## Related
+
+- [Debugging](/docs/fundamentals/debugging)
+- [Serving and Exporting](/docs/fundamentals/serving-exporting)
diff --git a/docs/0.5.x/en-US/fundamentals/debugging.md b/docs/0.5.x/en-US/fundamentals/debugging.md
new file mode 100644
index 0000000000..57bef2c437
--- /dev/null
+++ b/docs/0.5.x/en-US/fundamentals/debugging.md
@@ -0,0 +1,208 @@
+# Debugging
+
+Perseus apps run on two platforms: the engine (server) and the client (browser). Each requires different debugging approaches.
+
+## Compile-Time Checks
+
+Before debugging runtime issues, catch compile errors:
+
+```bash
+perseus check -w # Watch mode, checks both platforms
+perseus check -gw # Also runs state generation
+```
+
+If `perseus check -gw` passes, most Perseus commands will work. Remaining issues are usually runtime bugs in request-time logic.
+
+## Client-Side Debugging
+
+### Console Logging
+
+Standard `println!` and `dbg!` don't work in the browser. Use `web_log!`:
+
+```rust
+use perseus::prelude::*;
+
+fn my_component() -> View {
+ // Logs to browser console
+ web_log!("Component rendered");
+ web_log!("Value: {:?}", some_value);
+
+ view! {
+ p { "Hello" }
+ }
+}
+```
+
+On the engine-side, `web_log!` falls back to `println!`.
+
+### Browser DevTools
+
+Use your browser's developer tools:
+
+1. **Console** - View `web_log!` output
+2. **Network** - Monitor state fetches
+3. **Elements** - Inspect rendered HTML
+4. **Application** - Check localStorage, cookies
+
+### Wasm-Specific Issues
+
+For Wasm compilation errors:
+
+```bash
+perseus snoop wasm-build
+```
+
+This shows the raw Wasm build output without Perseus' formatting.
+
+## Engine-Side Debugging
+
+### Build-Time Logging
+
+By default, Perseus hides build output unless errors occur. To see everything:
+
+```bash
+perseus snoop build
+```
+
+This runs the build process directly, showing all `dbg!` and `println!` output.
+
+### Server Logging
+
+For request-time debugging:
+
+```bash
+perseus build # Build first
+perseus snoop serve # Then run server directly
+```
+
+Now you'll see all server-side logging. Note: You must run `perseus build` first.
+
+### State Generation Debugging
+
+```rust
+#[engine_only_fn]
+async fn get_build_state(info: StateGeneratorInfo<()>) -> MyState {
+ dbg!(&info.path); // Shows in `perseus snoop build`
+ println!("Generating state for: {}", info.path);
+
+ MyState { /* ... */ }
+}
+```
+
+## Common Issues
+
+### Hydration Mismatches
+
+**Symptom**: Page renders, then content changes or errors appear.
+
+**Causes**:
+- Random values during SSR
+- Time-dependent content
+- Browser-only APIs called during SSR
+
+**Solution**:
+
+```rust
+fn my_view() -> View {
+ // Don't do this - different values on server vs client
+ // let random = rand::random::();
+
+ // Do this instead - consistent or client-only
+ #[cfg(target_arch = "wasm32")]
+ let random = rand::random::();
+ #[cfg(not(target_arch = "wasm32"))]
+ let random = 0;
+
+ view! { p { (random) } }
+}
+```
+
+### State Not Updating
+
+**Symptom**: State changes don't reflect in the UI.
+
+**Causes**:
+- Not using reactive state correctly
+- Wrong signal access method
+
+**Solution**:
+
+```rust
+fn counter() -> View {
+ let count = create_signal(0);
+
+ view! {
+ p { (count.get()) } // Reactive - updates automatically
+ button(on:click = move |_| {
+ count.set(count.get() + 1);
+ }) {
+ "Increment"
+ }
+ }
+}
+```
+
+### Template Not Found
+
+**Symptom**: 404 errors for pages that should exist.
+
+**Causes**:
+- Template not registered in `PerseusApp`
+- Wrong template name
+- Missing build paths
+
+**Check**:
+
+```rust
+PerseusApp::new()
+ .template(crate::templates::index::get_template()) // Is this registered?
+ .template(crate::templates::post::get_template())
+```
+
+### Capsule Panics
+
+**Symptom**: Panic when rendering a capsule.
+
+**Causes**:
+- Capsule rendered outside Perseus context
+- Missing capsule registration
+
+**Solution**: Ensure capsules are registered and only rendered within Perseus views:
+
+```rust
+PerseusApp::new()
+ .capsule_ref(&*crate::capsules::my_capsule::MY_CAPSULE)
+```
+
+## Debug vs Release
+
+Some issues only appear in release mode:
+
+```bash
+perseus serve -r # Test release mode locally
+```
+
+Release builds:
+- Optimize Wasm aggressively
+- Remove debug assertions
+- May expose timing-sensitive bugs
+
+## Logging Levels
+
+For verbose Perseus output:
+
+```bash
+RUST_LOG=debug perseus serve
+```
+
+For specific modules:
+
+```bash
+RUST_LOG=perseus=debug,my_app=trace perseus serve
+```
+
+## Related
+
+- [Testing](/docs/fundamentals/testing)
+- [Error Views](/docs/fundamentals/error-views)
+- [Hydration](/docs/fundamentals/hydration)
diff --git a/docs/0.5.x/en-US/fundamentals/error-views.md b/docs/0.5.x/en-US/fundamentals/error-views.md
new file mode 100644
index 0000000000..590ce0d04a
--- /dev/null
+++ b/docs/0.5.x/en-US/fundamentals/error-views.md
@@ -0,0 +1,277 @@
+# Error Views
+
+When errors occur in Perseus (not in your business logic), error views tell Perseus how to display them. These are framework-level error handlers for things like 404s, network failures, and panics.
+
+## When to Use Error Views
+
+Error views handle **framework errors**, not your app's errors:
+
+| Error Views Handle | You Handle Manually |
+|-------------------|---------------------|
+| 404 Not Found | Invalid form input |
+| Network failure | Authentication failure |
+| Hydration errors | Business logic errors |
+| Panics | API response errors |
+
+## Basic Error Views
+
+```rust
+use perseus::prelude::*;
+use sycamore::prelude::*;
+
+pub fn get_error_views() -> ErrorViews {
+ ErrorViews::new(|error, _error_context, error_position| {
+ match error_position {
+ ErrorPosition::Page => {
+ // Full page error
+ (
+ view! { title { "Error" } },
+ match &error {
+ ClientError::ServerError { status, .. } => {
+ if status.as_u16() == 404 {
+ view! {
+ h1 { "Page Not Found" }
+ p { "The page you requested doesn't exist." }
+ Link(to = "/") { "Go Home" }
+ }
+ } else {
+ view! {
+ h1 { "Server Error" }
+ p { (format!("Error: {}", status)) }
+ }
+ }
+ },
+ ClientError::Panic(_) => view! {
+ h1 { "Application Crashed" }
+ p { "Please reload the page." }
+ },
+ _ => view! {
+ h1 { "Something Went Wrong" }
+ },
+ }
+ )
+ },
+ ErrorPosition::Popup => {
+ // Popup error (head is ignored)
+ (
+ view! {},
+ view! {
+ p { "An error occurred. The page may not be fully interactive." }
+ }
+ )
+ },
+ ErrorPosition::Widget => {
+ // Widget error (head is ignored)
+ (
+ view! {},
+ view! {
+ p { "Widget failed to load." }
+ }
+ )
+ },
+ }
+ })
+}
+```
+
+## ClientError Variants
+
+### ServerError
+
+Errors propagated from the server (404, 500, etc.):
+
+```rust
+ClientError::ServerError { status, message } => {
+ match status.as_u16() {
+ 404 => view! { h1 { "Not Found" } },
+ 403 => view! { h1 { "Forbidden" } },
+ 500 => view! { h1 { "Server Error" } },
+ code if code >= 400 && code < 500 => view! {
+ h1 { "Client Error" }
+ },
+ code if code >= 500 => view! {
+ h1 { "Server Error" }
+ },
+ _ => view! { h1 { "Error" } },
+ }
+}
+```
+
+### FetchError
+
+Network communication failures:
+
+```rust
+ClientError::FetchError(_) => view! {
+ h1 { "Connection Error" }
+ p { "Please check your internet connection." }
+}
+```
+
+### Panic
+
+Application crash (recovery not possible):
+
+```rust
+ClientError::Panic(panic_info) => view! {
+ h1 { "Application Crashed" }
+ p { "Please reload the page to continue." }
+ // Optionally display panic info in development
+}
+```
+
+### Other Variants
+
+| Variant | Description |
+|---------|-------------|
+| `PluginError` | Plugin-related errors |
+| `ThawError` | State deserialization failures |
+| `PlatformError` | Critical platform failures |
+| `PreloadError` | Preloading failures (usually mistyped paths) |
+| `InvariantError` | Internal Perseus failures |
+
+## Error Position
+
+Perseus chooses where to display errors based on context:
+
+| Position | When Used |
+|----------|-----------|
+| `Page` | Server errors on initial load |
+| `Popup` | Client-side errors (preserves content) |
+| `Widget` | Errors in capsule widgets |
+
+### Why Popup Errors?
+
+If hydration fails but the server rendered content fine, the user can still **see** the content. Replacing it with an error message would be worse UX. Popup errors preserve readable content while indicating limited interactivity.
+
+Style popups with:
+```css
+#__perseus_popup_error {
+ position: fixed;
+ bottom: 1rem;
+ right: 1rem;
+ background: #fef2f2;
+ border: 1px solid #fca5a5;
+ padding: 1rem;
+ border-radius: 0.5rem;
+ z-index: 9999;
+}
+```
+
+## Error Context
+
+The `ErrorContext` indicates what Perseus features are available:
+
+```rust
+ErrorViews::new(|error, error_context, position| {
+ match error_context {
+ ErrorContext::Full => {
+ // Full app available (translator, router, etc.)
+ },
+ ErrorContext::PluginsOnly => {
+ // Only plugins available
+ },
+ ErrorContext::Static => {
+ // Nothing available (static 404 page)
+ },
+ ErrorContext::None => {
+ // Critical failure, minimal rendering
+ },
+ }
+ // ...
+})
+```
+
+## Development vs Production
+
+Development default (not for production):
+```rust
+.error_views(ErrorViews::unlocalized_development_default())
+```
+
+For production, you **must** create custom error views. Perseus won't let you deploy with development defaults.
+
+## Registering Error Views
+
+```rust
+#[perseus::main(perseus_axum::dflt_server)]
+pub fn main() -> PerseusApp {
+ PerseusApp::new()
+ .template(crate::templates::index::get_template())
+ .error_views(crate::error_views::get_error_views())
+}
+```
+
+## Custom Subsequent Load Handling
+
+Override how subsequent load errors are positioned:
+
+```rust
+ErrorViews::new(/* ... */)
+ .subsequent_load_determinant_fn(|error| {
+ // Return ErrorPosition::Page or ErrorPosition::Popup
+ match error {
+ ClientError::ServerError { status, .. } if status.as_u16() == 404 => {
+ ErrorPosition::Page // Show 404 as full page
+ },
+ _ => ErrorPosition::Popup,
+ }
+ })
+```
+
+## Complete Example
+
+```rust
+use perseus::prelude::*;
+use sycamore::prelude::*;
+
+pub fn get_error_views() -> ErrorViews {
+ ErrorViews::new(|error, ctx, pos| {
+ let head = view! {
+ title { "Error | My App" }
+ };
+
+ let body = match pos {
+ ErrorPosition::Page => match &error {
+ ClientError::ServerError { status, .. } => match status.as_u16() {
+ 404 => view! {
+ div(class = "error-page") {
+ h1 { "404" }
+ p { "This page doesn't exist." }
+ Link(to = "/") { "Return Home" }
+ }
+ },
+ _ => view! {
+ div(class = "error-page") {
+ h1 { "Server Error" }
+ p { "Something went wrong on our end." }
+ }
+ },
+ },
+ ClientError::Panic(_) => view! {
+ div(class = "error-page") {
+ h1 { "Crash" }
+ p { "The app has crashed. Please reload." }
+ }
+ },
+ _ => view! {
+ div(class = "error-page") {
+ h1 { "Error" }
+ p { "An unexpected error occurred." }
+ }
+ },
+ },
+ ErrorPosition::Popup | ErrorPosition::Widget => view! {
+ p { "Error loading content." }
+ },
+ };
+
+ (head, body)
+ })
+}
+```
+
+## Related
+
+- [Error Handling Tutorial](/docs/first-app/error-handling)
+- [ClientError API](https://docs.rs/perseus/latest/perseus/errors/enum.ClientError.html)
diff --git a/docs/0.5.x/en-US/fundamentals/head-headers.md b/docs/0.5.x/en-US/fundamentals/head-headers.md
new file mode 100644
index 0000000000..40d4bc3e68
--- /dev/null
+++ b/docs/0.5.x/en-US/fundamentals/head-headers.md
@@ -0,0 +1,247 @@
+# Heads and Headers
+
+Every web page needs metadata in its `` element (title, meta tags, stylesheets) and sometimes custom HTTP headers (caching, cookies). Perseus provides template-level control over both.
+
+## Setting the Head
+
+Define a head function for each template to set page-specific metadata:
+
+### Basic Head (No State)
+
+```rust
+use perseus::prelude::*;
+use sycamore::prelude::*;
+
+#[engine_only_fn]
+fn head() -> View {
+ view! {
+ title { "About Us" }
+ meta(name = "description", content = "Learn about our company")
+ link(rel = "canonical", href = "https://example.com/about")
+ }
+}
+
+fn about_page() -> View {
+ view! {
+ h1 { "About Us" }
+ p { "Our story..." }
+ }
+}
+
+pub fn get_template() -> Template {
+ Template::build("about")
+ .view(about_page)
+ .head(head)
+ .build()
+}
+```
+
+### Head with State
+
+Access your page state for dynamic metadata:
+
+```rust
+use perseus::prelude::*;
+use sycamore::prelude::*;
+use serde::{Deserialize, Serialize};
+
+#[derive(Serialize, Deserialize, Clone, ReactiveState)]
+#[rx(alias = "PostStateRx")]
+struct PostState {
+ title: String,
+ description: String,
+}
+
+#[engine_only_fn]
+fn head(state: PostState) -> View {
+ view! {
+ title { (format!("{} | My Blog", state.title)) }
+ meta(name = "description", content = (state.description))
+ meta(property = "og:title", content = (state.title))
+ }
+}
+
+pub fn get_template() -> Template {
+ Template::build("post")
+ .build_state_fn(get_build_state)
+ .view_with_state(post_page)
+ .head_with_state(head)
+ .build()
+}
+```
+
+### Important Notes
+
+1. **Engine-only** - Head functions are marked `#[engine_only_fn]` because they're prerendered server-side
+2. **Synchronous** - Head functions cannot be async (don't read files here, do it in state generation)
+3. **Return type** - Just returns `View`, Perseus handles the SSR context internally
+4. **Errors** - Head functions can return `Result` if needed, but errors cause the page to fail
+
+## Setting HTTP Headers
+
+Set custom HTTP headers for caching, security, or other purposes:
+
+### Basic Headers (No State)
+
+```rust
+use perseus::prelude::*;
+
+#[engine_only_fn]
+fn set_headers() -> HeaderMap {
+ let mut headers = HeaderMap::new();
+ headers.insert(
+ header::CACHE_CONTROL,
+ "max-age=3600".parse().unwrap()
+ );
+ headers
+}
+
+pub fn get_template() -> Template {
+ Template::build("cached-page")
+ .view(page_view)
+ .set_headers(set_headers)
+ .build()
+}
+```
+
+### Headers with State
+
+```rust
+use perseus::prelude::*;
+
+#[engine_only_fn]
+fn set_headers(state: MyState) -> HeaderMap {
+ let mut headers = HeaderMap::new();
+
+ // Custom header based on state
+ headers.insert(
+ header::HeaderName::from_static("x-content-version"),
+ state.version.parse().unwrap()
+ );
+
+ // Cache control
+ headers.insert(
+ header::CACHE_CONTROL,
+ "public, max-age=86400".parse().unwrap()
+ );
+
+ headers
+}
+
+pub fn get_template() -> Template {
+ Template::build("my-page")
+ .build_state_fn(get_build_state)
+ .view_with_state(page_view)
+ .set_headers_with_state(set_headers)
+ .build()
+}
+```
+
+## Common Head Patterns
+
+### SEO Metadata
+
+```rust
+#[engine_only_fn]
+fn head(state: PageState) -> View {
+ view! {
+ title { (state.title) }
+ meta(name = "description", content = (state.description))
+ meta(name = "robots", content = "index, follow")
+
+ // Open Graph
+ meta(property = "og:title", content = (state.title))
+ meta(property = "og:description", content = (state.description))
+ meta(property = "og:type", content = "website")
+ meta(property = "og:image", content = (state.image_url))
+
+ // Twitter Card
+ meta(name = "twitter:card", content = "summary_large_image")
+ meta(name = "twitter:title", content = (state.title))
+ }
+}
+```
+
+### Stylesheets and Scripts
+
+```rust
+#[engine_only_fn]
+fn head() -> View {
+ view! {
+ title { "My App" }
+ link(rel = "stylesheet", href = ".perseus/static/styles.css")
+ link(rel = "preconnect", href = "https://fonts.gstatic.com")
+ // Note: scripts in head block rendering - use sparingly
+ }
+}
+```
+
+### Favicon and Icons
+
+```rust
+#[engine_only_fn]
+fn head() -> View {
+ view! {
+ title { "My App" }
+ link(rel = "icon", href = ".perseus/static/favicon.ico")
+ link(rel = "apple-touch-icon", href = ".perseus/static/apple-touch-icon.png")
+ link(rel = "manifest", href = ".perseus/static/manifest.json")
+ }
+}
+```
+
+## Common Header Patterns
+
+### Cache Control
+
+```rust
+#[engine_only_fn]
+fn set_headers() -> HeaderMap {
+ let mut headers = HeaderMap::new();
+
+ // Cache for 1 hour, allow CDN caching
+ headers.insert(
+ header::CACHE_CONTROL,
+ "public, max-age=3600, s-maxage=86400".parse().unwrap()
+ );
+
+ headers
+}
+```
+
+### Security Headers
+
+```rust
+#[engine_only_fn]
+fn set_headers() -> HeaderMap {
+ let mut headers = HeaderMap::new();
+
+ headers.insert(
+ header::X_CONTENT_TYPE_OPTIONS,
+ "nosniff".parse().unwrap()
+ );
+ headers.insert(
+ header::X_FRAME_OPTIONS,
+ "DENY".parse().unwrap()
+ );
+
+ headers
+}
+```
+
+## Index View vs Head Function
+
+| Aspect | Index View | Head Function |
+|--------|------------|---------------|
+| Scope | Entire app | Per template |
+| Content | HTML shell, global styles | Page-specific metadata |
+| Set with | `.index_view()` on PerseusApp | `.head()` on Template |
+| Dynamic | No | Yes (can use state) |
+
+Use the index view for global concerns, head functions for page-specific metadata.
+
+## Related
+
+- [PerseusApp Configuration](/docs/fundamentals/perseus-app)
+- [Static Content](/docs/fundamentals/static-content)
+- [Build State](/docs/state/build)
diff --git a/docs/0.5.x/en-US/fundamentals/hydration.md b/docs/0.5.x/en-US/fundamentals/hydration.md
new file mode 100644
index 0000000000..50239dab3c
--- /dev/null
+++ b/docs/0.5.x/en-US/fundamentals/hydration.md
@@ -0,0 +1,144 @@
+# Hydration
+
+Perseus prerenders your pages to HTML on the server, ensuring users see content immediately. But static HTML can't handle button clicks or form submissions. *Hydration* bridges this gap by attaching event handlers to prerendered content.
+
+## Initial vs Subsequent Loads
+
+Perseus handles page loads differently depending on context:
+
+### Initial Load
+
+When a user first visits your site (e.g., from a search engine):
+
+1. Server sends the **app shell**: `bundle.js`, `bundle.wasm`
+2. Server also sends **prerendered HTML** of the requested page
+3. User sees content *immediately* (no blank page)
+4. Wasm bundle loads in the background
+5. Sycamore *hydrates* the page, attaching event handlers
+
+### Subsequent Load
+
+After the initial load, navigating to another page:
+
+1. Perseus only fetches the **page state** (small JSON)
+2. Rust renders the HTML client-side (faster than fetching HTML)
+3. Page transitions feel instant
+4. Previously visited pages are cached and restore *immediately*
+
+## How Hydration Works
+
+During server-side rendering, Sycamore inserts *hydration IDs* throughout the HTML. These markers allow the client to match DOM nodes with your Rust code:
+
+```html
+
+
+```
+
+When the Wasm bundle loads, Sycamore:
+
+1. Walks the existing DOM
+2. Matches elements using hydration IDs
+3. Attaches event handlers from your code
+4. Makes the page fully interactive
+
+## Why Hydration is Fast
+
+Unlike JavaScript frameworks where hydration can take seconds, Rust/Wasm hydration is nearly instant:
+
+| Factor | Benefit |
+|--------|---------|
+| Compiled code | No parsing or JIT compilation needed |
+| Streaming Wasm | Browser executes as it downloads |
+| Efficient diffing | Sycamore's hydration is optimized |
+| Small runtime | No heavy framework overhead |
+
+The limiting factor is download time, not execution. That's why `perseus deploy` aggressively optimizes Wasm for size.
+
+## Hydration Errors
+
+Sometimes hydration fails if the server-rendered HTML doesn't match what the client expects. Common causes:
+
+| Cause | Solution |
+|-------|----------|
+| Random values | Use seeded random or fetch client-side |
+| Timestamps | Use consistent time or fetch client-side |
+| Browser-only APIs | Guard with `#[cfg(target_arch = "wasm32")]` |
+| Different data | Ensure state is serialized correctly |
+
+When hydration fails, Perseus shows an error popup (preserving the readable content) rather than replacing the page with an error message.
+
+## Optimizing for Hydration
+
+### Keep Initial State Minimal
+
+Large state means larger HTML and longer hydration:
+
+```rust
+// Good: Minimal initial state
+#[derive(Serialize, Deserialize, ReactiveState)]
+#[rx(alias = "PostStateRx")]
+struct PostState {
+ title: String,
+ excerpt: String,
+ // Full content loaded on demand
+}
+
+// Avoid: Everything upfront
+struct PostState {
+ title: String,
+ content: String, // Could be huge
+ comments: Vec, // Fetch these client-side
+}
+```
+
+### Use Suspended State for Heavy Data
+
+Fetch non-critical data after hydration:
+
+```rust
+#[derive(Serialize, Deserialize, ReactiveState)]
+#[rx(alias = "PageStateRx")]
+struct PageState {
+ title: String,
+ #[rx(suspense = "comments_handler")]
+ comments: Result, SerdeInfallible>,
+}
+```
+
+### Delay Non-Essential Widgets
+
+Use `delayed_widget` for heavy capsules:
+
+```rust
+// Loads after page is interactive
+(HEAVY_CAPSULE.delayed_widget("", ()))
+```
+
+## The Popup Error System
+
+If hydration fails but the server rendered content successfully, Perseus shows errors as popups rather than replacing the content. This is better UX because:
+
+1. Users can still *read* the content
+2. Static functionality (links, etc.) may still work
+3. The error indicates limited interactivity, not total failure
+
+Style the popup with:
+
+```css
+#__perseus_popup_error {
+ position: fixed;
+ bottom: 1rem;
+ right: 1rem;
+ background: #fef2f2;
+ border: 1px solid #fca5a5;
+ padding: 1rem;
+ border-radius: 0.5rem;
+ z-index: 9999;
+}
+```
+
+## Related
+
+- [Error Views](/docs/fundamentals/error-views)
+- [Suspended State](/docs/state/browser)
+- [Debugging](/docs/fundamentals/debugging)
diff --git a/docs/0.5.x/en-US/fundamentals/i18n.md b/docs/0.5.x/en-US/fundamentals/i18n.md
new file mode 100644
index 0000000000..1e192d4d79
--- /dev/null
+++ b/docs/0.5.x/en-US/fundamentals/i18n.md
@@ -0,0 +1,208 @@
+# Internationalization
+
+Perseus provides built-in support for *internationalization* (i18n), allowing your app to be available in multiple languages. This works by replacing human-readable strings in your code with translation IDs that resolve to the correct text based on the user's *locale*.
+
+## Understanding Locales
+
+Locales consist of a language code and an optional region code:
+
+| Locale | Description |
+|--------|-------------|
+| `en-US` | United States English |
+| `en-GB` | British English |
+| `es-ES` | Spanish (Spain) |
+| `fr-FR` | French (France) |
+| `zh-CN` | Chinese (Simplified) |
+
+When you enable i18n, Perseus builds every page in every locale. Your landing page becomes:
+- `/en-US/`
+- `/fr-FR/`
+- `/es-ES/`
+
+## Setting Up i18n
+
+### 1. Enable the Feature Flag
+
+Add either `translator-fluent` or `translator-lightweight` to your `Cargo.toml`:
+
+```toml
+[dependencies]
+perseus = { version = "0.5", features = ["translator-lightweight"] }
+```
+
+| Feature | When to Use |
+|---------|-------------|
+| `translator-lightweight` | Simple apps, smaller bundle size |
+| `translator-fluent` | Complex translations with pluralization, gender, etc. |
+
+### 2. Configure PerseusApp
+
+```rust
+use perseus::prelude::*;
+
+#[perseus::main(perseus_axum::dflt_server)]
+pub fn main() -> PerseusApp {
+ PerseusApp::new()
+ .template(crate::templates::index::get_template())
+ .locales_and_translations_manager("en-US", &["fr-FR", "es-ES"])
+ .error_views(ErrorViews::unlocalized_development_default())
+}
+```
+
+### 3. Create Translation Files
+
+Create a `translations/` directory with files for each locale:
+
+```
+my-app/
+├── translations/
+│ ├── en-US.json # For lightweight translator
+│ ├── fr-FR.json
+│ └── es-ES.json
+```
+
+For the lightweight translator, use JSON:
+
+```json
+{
+ "greeting": "Hello, {name}!",
+ "welcome": "Welcome to our app"
+}
+```
+
+For Fluent, use `.ftl` files with the [Fluent syntax](https://projectfluent.org).
+
+## Using Translations
+
+Use the `t!` macro in your views:
+
+```rust
+use perseus::prelude::*;
+use sycamore::prelude::*;
+
+fn my_page() -> View {
+ view! {
+ h1 { (t!("welcome")) }
+ p { (t!("greeting", { "name" = "Perseus" })) }
+ }
+}
+```
+
+The `t!` macro:
+- Takes a translation ID as the first argument
+- Optionally takes variables for interpolation
+- Returns the localized string for the current locale
+
+## Localized Navigation
+
+Use the `link!` macro to ensure links respect the current locale:
+
+```rust
+use perseus::prelude::*;
+use sycamore::prelude::*;
+
+fn nav() -> View {
+ view! {
+ // This ensures correct locale prefix
+ Link(to = link!("/about")) { "About" }
+ }
+}
+```
+
+Without `link!`, navigating from `/en-US/home` to `/about` would go to `/about` instead of `/en-US/about`.
+
+## Switching Locales
+
+To switch locales, navigate to the desired locale path:
+
+```rust
+use perseus::prelude::*;
+use sycamore::prelude::*;
+
+fn locale_switcher() -> View {
+ let reactor = Reactor::::from_cx();
+
+ view! {
+ button(on:click = move |_| {
+ reactor.switch_locale("fr-FR");
+ }) {
+ "Switch to French"
+ }
+ }
+}
+```
+
+The `switch_locale` method navigates to the current page in the new locale, fetching the appropriate translations.
+
+## Locale Detection
+
+Perseus automatically detects user locale preferences:
+
+1. Checks browser language preferences (RFC 4647 compliant)
+2. Matches against available locales
+3. Redirects to the best match
+
+For example, a user with preferences `zh-CN, de-DE, en` visiting your app with `en-US`, `fr-FR`, and `es-ES` available would be redirected to `en-US`.
+
+## Getting Current Locale
+
+Access the current locale through the Reactor:
+
+```rust
+fn locale_display() -> View {
+ let reactor = Reactor::::from_cx();
+ let locale = reactor.get_locale();
+
+ view! {
+ p { "Current locale: " (locale) }
+ }
+}
+```
+
+## Performance Considerations
+
+- **Fluent translator** adds ~100kB to your Wasm bundle
+- **Lightweight translator** is much smaller but less feature-rich
+- Translations are loaded per-locale, not all at once
+- You cannot preload pages across locales (translations are heavy)
+
+## Translation File Formats
+
+### Lightweight (JSON)
+
+```json
+{
+ "nav.home": "Home",
+ "nav.about": "About",
+ "greeting": "Hello, {name}!"
+}
+```
+
+### Fluent (.ftl)
+
+```ftl
+nav-home = Home
+nav-about = About
+greeting = Hello, { $name }!
+
+# With pluralization
+items =
+ { $count ->
+ [one] { $count } item
+ *[other] { $count } items
+ }
+```
+
+## Best Practices
+
+1. **Use descriptive IDs** - `nav.home` is better than `home`
+2. **Keep translations organized** - Group by feature or component
+3. **Test all locales** - Build and check each language
+4. **Handle missing translations** - They cause panics
+5. **Consider RTL languages** - Use CSS `direction` property
+
+## Related
+
+- [Routing](/docs/fundamentals/routing)
+- [The Reactor](/docs/fundamentals/reactor)
+- [i18n Example](https://github.com/framesurge/perseus/tree/main/examples/core/i18n)
diff --git a/docs/0.5.x/en-US/fundamentals/js-interop.md b/docs/0.5.x/en-US/fundamentals/js-interop.md
new file mode 100644
index 0000000000..b813247926
--- /dev/null
+++ b/docs/0.5.x/en-US/fundamentals/js-interop.md
@@ -0,0 +1,207 @@
+# Working with JavaScript
+
+While Perseus apps are written entirely in Rust, you may occasionally need to interact with JavaScript libraries, browser APIs, or existing JS code. This is done through `wasm-bindgen`.
+
+## Basic JS Interop
+
+Use `wasm-bindgen` to call JavaScript functions from Rust:
+
+```rust
+use perseus::prelude::*;
+use sycamore::prelude::*;
+use wasm_bindgen::prelude::*;
+
+// Import a JS function from a file
+#[wasm_bindgen(module = "/src/js/helpers.js")]
+extern "C" {
+ fn showNotification(message: &str);
+}
+
+fn notification_button() -> View {
+ view! {
+ button(on:click = move |_| {
+ showNotification("Hello from Rust!");
+ }) {
+ "Show Notification"
+ }
+ }
+}
+```
+
+The JavaScript file (`src/js/helpers.js`):
+
+```javascript
+export function showNotification(message) {
+ if (Notification.permission === "granted") {
+ new Notification(message);
+ } else {
+ alert(message);
+ }
+}
+```
+
+## How It Works
+
+When you use `#[wasm_bindgen(module = "..")]`:
+
+1. `wasm-bindgen` copies the JS file to `dist/`
+2. Perseus serves it at `/.perseus/snippets/`
+3. The import is automatically resolved at runtime
+
+You don't need to manually configure anything—it just works.
+
+## Browser APIs via web-sys
+
+For standard browser APIs, use the `web-sys` crate:
+
+```rust
+use perseus::prelude::*;
+use sycamore::prelude::*;
+use wasm_bindgen::JsCast;
+
+fn current_url() -> View {
+ let url = web_sys::window()
+ .unwrap()
+ .location()
+ .href()
+ .unwrap_or_default();
+
+ view! {
+ p { "Current URL: " (url) }
+ }
+}
+```
+
+Add `web-sys` to your `Cargo.toml` with the features you need:
+
+```toml
+[dependencies]
+web-sys = { version = "0.3", features = ["Window", "Location", "Document"] }
+```
+
+## Common Patterns
+
+### Accessing LocalStorage
+
+```rust
+use wasm_bindgen::JsCast;
+
+fn save_to_storage(key: &str, value: &str) {
+ if let Some(storage) = web_sys::window()
+ .and_then(|w| w.local_storage().ok())
+ .flatten()
+ {
+ let _ = storage.set_item(key, value);
+ }
+}
+
+fn load_from_storage(key: &str) -> Option {
+ web_sys::window()
+ .and_then(|w| w.local_storage().ok())
+ .flatten()
+ .and_then(|s| s.get_item(key).ok())
+ .flatten()
+}
+```
+
+### Calling External Libraries
+
+```rust
+#[wasm_bindgen]
+extern "C" {
+ // Global function
+ #[wasm_bindgen(js_name = "console.log")]
+ fn log(s: &str);
+
+ // From a CDN-loaded library
+ #[wasm_bindgen(js_namespace = ["hljs"])]
+ fn highlightAll();
+}
+
+fn code_block() -> View {
+ // Call after render
+ #[cfg(target_arch = "wasm32")]
+ highlightAll();
+
+ view! {
+ pre {
+ code(class = "language-rust") {
+ "fn main() { }"
+ }
+ }
+ }
+}
+```
+
+### Dynamic Imports
+
+For libraries loaded via CDN:
+
+```rust
+#[wasm_bindgen]
+extern "C" {
+ type Chart;
+
+ #[wasm_bindgen(constructor, js_namespace = ["Chart"])]
+ fn new(ctx: &JsValue, config: &JsValue) -> Chart;
+}
+```
+
+## Platform-Specific Code
+
+Guard browser-only code with cfg attributes:
+
+```rust
+fn platform_aware() -> View {
+ #[cfg(target_arch = "wasm32")]
+ {
+ // Browser-only code
+ web_sys::console::log_1(&"Running in browser".into());
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ {
+ // Server-only code
+ println!("Running on server");
+ }
+
+ view! {
+ p { "Hello from either platform!" }
+ }
+}
+```
+
+## Calling Rust from JavaScript
+
+Export Rust functions to JS:
+
+```rust
+use wasm_bindgen::prelude::*;
+
+#[wasm_bindgen]
+pub fn greet(name: &str) -> String {
+ format!("Hello, {}!", name)
+}
+
+// Then in JS: import { greet } from './my_app';
+// greet("World") // Returns "Hello, World!"
+```
+
+## Tips
+
+1. **Minimize JS** - Use Rust/Wasm for logic, JS only for browser APIs
+2. **Use web-sys** - Standard APIs are well-typed
+3. **Guard platform code** - Use `#[cfg(target_arch = "wasm32")]`
+4. **Error handling** - JS calls can panic; use try/catch patterns
+5. **Keep snippets small** - Large JS files increase bundle size
+
+## Further Reading
+
+- [wasm-bindgen Documentation](https://rustwasm.github.io/docs/wasm-bindgen/)
+- [web-sys API Reference](https://docs.rs/web-sys)
+- [JS FFI Guide](https://rustwasm.github.io/book/reference/js-ffi.html)
+
+## Related
+
+- [Debugging](/docs/fundamentals/debugging)
+- [Static Content](/docs/fundamentals/static-content)
diff --git a/docs/0.5.x/en-US/fundamentals/perseus-app.md b/docs/0.5.x/en-US/fundamentals/perseus-app.md
new file mode 100644
index 0000000000..c1ee0df292
--- /dev/null
+++ b/docs/0.5.x/en-US/fundamentals/perseus-app.md
@@ -0,0 +1,144 @@
+# `PerseusApp`
+
+`PerseusApp` is the central interface between Perseus internals and your code. You use it to define templates, capsules, error views, and configure your app.
+
+## Basic Usage
+
+```rust
+use perseus::prelude::*;
+
+mod templates;
+
+#[perseus::main(perseus_axum::dflt_server)]
+pub fn main() -> PerseusApp {
+ PerseusApp::new()
+ .template(crate::templates::index::get_template())
+ .template(crate::templates::about::get_template())
+ .error_views(ErrorViews::unlocalized_development_default())
+}
+```
+
+## Template/Capsule Definition Patterns
+
+### Functional Definition Pattern
+
+Used primarily with templates:
+
+```rust
+// In templates/index.rs
+pub fn get_template() -> Template {
+ Template::build("index")
+ .view(index_page)
+ .build()
+}
+
+// In main.rs
+.template(crate::templates::index::get_template())
+```
+
+### Referential Definition Pattern
+
+Used primarily with capsules (which need to be accessed from multiple places):
+
+```rust
+// In capsules/greeting.rs
+lazy_static::lazy_static! {
+ pub static ref GREETING: Capsule = {
+ Capsule::build(Template::build("greeting"))
+ .empty_fallback()
+ .view(greeting_widget)
+ .build()
+ };
+}
+
+// In main.rs
+.capsule_ref(&*crate::capsules::greeting::GREETING)
+```
+
+## Index Views
+
+Customize the HTML shell that wraps your app:
+
+```rust
+use perseus::prelude::*;
+use sycamore::prelude::*;
+
+#[perseus::main(perseus_axum::dflt_server)]
+pub fn main() -> PerseusApp {
+ PerseusApp::new()
+ .template(crate::templates::index::get_template())
+ .error_views(ErrorViews::unlocalized_development_default())
+ .index_view(|| {
+ view! {
+ html {
+ head {
+ meta(charset = "UTF-8")
+ meta(name = "viewport", content = "width=device-width, initial-scale=1.0")
+ link(rel = "stylesheet", href = "/.perseus/static/styles.css")
+ }
+ body {
+ PerseusRoot() // Your app renders here
+ }
+ }
+ }
+ })
+}
+```
+
+**Note**: Use `PerseusRoot()` component to mark where your app renders. Nothing in the index view can be reactive.
+
+For raw HTML strings, use `.index_view_str()` instead.
+
+## Mutable Stores
+
+Perseus writes data to two directories in `dist/`:
+
+| Directory | Purpose | Mutability |
+|-----------|---------|------------|
+| `static/` | Prerendered HTML, static pages | Immutable |
+| `mutable/` | Pages that can change (revalidation) | Mutable |
+
+By default, `FsMutableStore` writes to the filesystem. For serverless environments (which have immutable filesystems), you'd need a custom `MutableStore` implementation using a database.
+
+## Translations Management
+
+For internationalized apps, `FsTranslationsManager` is the default, expecting translations in `translations/`:
+
+```
+my-app/
+├── translations/
+│ ├── en-US.ftl
+│ └── es-ES.ftl
+```
+
+## Page State Store (PSS)
+
+Configure how many pages Perseus caches in memory:
+
+```rust
+PerseusApp::new()
+ .pss_max_size(50) // Cache up to 50 pages (default: 25)
+```
+
+**Guidelines:**
+- Higher values = more instant back-navigation, more RAM usage
+- Lower values = less RAM, but slower navigation to old pages
+- Large state apps (like documentation) should use lower values
+- Capsules are cached separately until their parent pages are evicted
+
+## Common Methods
+
+| Method | Purpose |
+|--------|---------|
+| `.template(t)` | Add a template |
+| `.capsule_ref(&c)` | Add a capsule by reference |
+| `.error_views(e)` | Set error handling views |
+| `.index_view(f)` | Customize HTML shell |
+| `.global_state_creator(g)` | Set up global state |
+| `.pss_max_size(n)` | Set page cache size |
+| `.locales_and_translations_manager(...)` | Enable i18n |
+| `.static_alias(path, file)` | Serve static files |
+
+## Full API
+
+See the [PerseusAppBase API docs](https://docs.rs/perseus/latest/perseus/struct.PerseusAppBase.html) for all available methods.
diff --git a/docs/0.5.x/en-US/fundamentals/plugins.md b/docs/0.5.x/en-US/fundamentals/plugins.md
new file mode 100644
index 0000000000..abb8a613a9
--- /dev/null
+++ b/docs/0.5.x/en-US/fundamentals/plugins.md
@@ -0,0 +1,139 @@
+# Plugins
+
+Perseus is extensible through plugins—library crates that hook into the build process and runtime. For most customizations, consider a [custom server](/docs/fundamentals/serving-exporting) first, as it's simpler.
+
+## Plugin Types
+
+### Functional Plugins
+
+Receive data, process it, and return results. Multiple can act on the same opportunity.
+
+**Example use cases**:
+- Adding static aliases
+- Injecting HTML into pages
+- Modifying build configuration
+
+### Control Plugins
+
+Take exclusive control of a feature. Only one can act per opportunity.
+
+**Example use cases**:
+- Replacing the index view
+- Custom routing logic
+- Alternative state stores
+
+## Using Plugins
+
+Add a plugin to your `PerseusApp`:
+
+```rust
+use perseus::prelude::*;
+use some_plugin::SomePlugin;
+
+#[perseus::main(perseus_axum::dflt_server)]
+pub fn main() -> PerseusApp {
+ PerseusApp::new()
+ .template(crate::templates::index::get_template())
+ .plugins(Plugins::new().plugin(
+ SomePlugin::new(),
+ SomePluginData { /* config */ }
+ ))
+ .error_views(ErrorViews::unlocalized_development_default())
+}
+```
+
+## Tinker Plugins
+
+Special plugins that run during `perseus tinker`:
+
+```bash
+perseus tinker
+```
+
+Use cases:
+- Custom build processes
+- Code generation
+- Asset processing
+- Modifying user code
+
+Example registration:
+
+```rust
+Plugins::new()
+ .plugin(
+ MyTinkerPlugin::new(),
+ MyTinkerData::default()
+ )
+```
+
+Tinker plugins have largely been superseded by standard Cargo build scripts, but remain available for Perseus-specific transformations.
+
+## Writing Plugins
+
+Plugins implement specific traits for their opportunities. The basics:
+
+```rust
+use perseus::plugins::{Plugin, PluginAction, PluginEnv};
+
+pub struct MyPlugin;
+
+impl MyPlugin {
+ pub fn new() -> Self {
+ Self
+ }
+}
+
+// Implement specific plugin traits based on what opportunities you need
+```
+
+For detailed plugin development, see:
+- [Plugin API Documentation](https://docs.rs/perseus/latest/perseus/plugins/)
+- [Plugin Example](https://github.com/framesurge/perseus/tree/main/examples/core/plugins)
+
+## Plugin Opportunities
+
+Plugins can hook into various points:
+
+| Opportunity | Type | Purpose |
+|-------------|------|---------|
+| Static aliases | Functional | Add static file mappings |
+| HTML injection | Functional | Inject HTML into pages |
+| Build process | Functional | Modify build behavior |
+| Index view | Control | Replace the HTML shell |
+| Tinker | Functional | Run custom commands |
+
+The number of opportunities will grow in future releases.
+
+## Plugin Registry
+
+Community plugins are listed at [framesurge.sh/perseus/plugins](https://framesurge.sh/perseus/plugins).
+
+- ✓ Endorsed plugins have undergone code review
+- Endorsement doesn't guarantee security
+- Always audit dependencies you install
+
+## Security Considerations
+
+Plugins execute arbitrary code during build and at runtime:
+
+1. **Only install trusted plugins** - Audit code or trust the author
+2. **Check for updates** - Security issues may be patched
+3. **Limit permissions** - Don't give plugins unnecessary access
+4. **Report issues** - Contact the [Perseus maintainer](mailto:arctic.hen@pm.me) for rogue plugins
+
+Perseus cannot be held responsible for third-party plugin behavior.
+
+## When to Use Plugins
+
+| Need | Solution |
+|------|----------|
+| API routes | Custom server |
+| Middleware | Custom server |
+| Build-time code generation | Plugin or build.rs |
+| Modified Perseus behavior | Plugin |
+| Shared functionality across apps | Plugin |
+
+## Related
+
+- [Serving and Exporting](/docs/fundamentals/serving-exporting)
+- [PerseusApp Configuration](/docs/fundamentals/perseus-app)
diff --git a/docs/0.5.x/en-US/fundamentals/preloading.md b/docs/0.5.x/en-US/fundamentals/preloading.md
new file mode 100644
index 0000000000..8d2c1b4a56
--- /dev/null
+++ b/docs/0.5.x/en-US/fundamentals/preloading.md
@@ -0,0 +1,190 @@
+# Preloading
+
+Perseus caches visited pages for instant back-navigation. Preloading extends this: if you know where a user will likely go next, load it in advance for an instant transition.
+
+## Why Preload?
+
+When navigating to a new page, Perseus needs:
+- The page's state (JSON)
+- The page's head metadata
+
+Without preloading, there's a brief loading state. With preloading, the page appears *instantly*.
+
+## Basic Preloading
+
+Use the Reactor's `.preload()` method:
+
+```rust
+use perseus::prelude::*;
+use sycamore::prelude::*;
+
+fn nav() -> View {
+ let reactor = Reactor::::from_cx();
+
+ view! {
+ // Preload on hover
+ div(on:mouseenter = {
+ let reactor = reactor.clone();
+ move |_| {
+ reactor.preload("/about");
+ }
+ }) {
+ Link(to = "/about") { "About Us" }
+ }
+ }
+}
+```
+
+The `.preload()` method:
+- Runs asynchronously (doesn't block the main thread)
+- Silently fails on server errors
+- Panics on programmer errors (misspelled routes)
+
+## Fine-Grained Control
+
+For custom error handling, use `.try_preload()`:
+
+```rust
+use perseus::prelude::*;
+use sycamore::prelude::*;
+
+fn dynamic_nav(path: String) -> View {
+ let reactor = Reactor::::from_cx();
+
+ let preload_result = create_signal(None::);
+
+ view! {
+ div(on:mouseenter = {
+ let reactor = reactor.clone();
+ let path = path.clone();
+ move |_| {
+ let reactor = reactor.clone();
+ let path = path.clone();
+ spawn_local(async move {
+ match reactor.try_preload(&path).await {
+ Ok(()) => preload_result.set(Some("Ready!".to_string())),
+ Err(e) => preload_result.set(Some(format!("Failed: {:?}", e))),
+ }
+ });
+ }
+ }) {
+ Link(to = path.clone()) { (path) }
+ }
+ }
+}
+```
+
+## Common Preloading Patterns
+
+### Preload on Hover
+
+The most common pattern—preload when the user hovers over a link:
+
+```rust
+fn preloaded_link(path: &'static str, label: &'static str) -> View {
+ let reactor = Reactor::::from_cx();
+
+ view! {
+ div(on:mouseenter = {
+ let reactor = reactor.clone();
+ move |_| {
+ reactor.preload(path);
+ }
+ }) {
+ Link(to = path) { (label) }
+ }
+ }
+}
+```
+
+### Preload on Page Load
+
+For predicted navigation flows:
+
+```rust
+fn checkout_page() -> View {
+ let reactor = Reactor::::from_cx();
+
+ // User will likely go to confirmation next
+ reactor.preload("/checkout/confirmation");
+
+ view! {
+ h1 { "Checkout" }
+ // ... checkout form
+ }
+}
+```
+
+### Preload Multiple Pages
+
+```rust
+fn dashboard() -> View {
+ let reactor = Reactor::::from_cx();
+
+ // Preload common next pages
+ reactor.preload("/dashboard/analytics");
+ reactor.preload("/dashboard/settings");
+ reactor.preload("/dashboard/reports");
+
+ view! {
+ h1 { "Dashboard" }
+ // ... dashboard content
+ }
+}
+```
+
+## Preloading with i18n
+
+When using internationalization, preloading works within the current locale only:
+
+```rust
+fn localized_nav() -> View {
+ let reactor = Reactor::::from_cx();
+
+ view! {
+ // This preloads /en-US/about if you're on /en-US/
+ div(on:mouseenter = {
+ let reactor = reactor.clone();
+ move |_| {
+ reactor.preload(link!("/about"));
+ }
+ }) {
+ Link(to = link!("/about")) { "About" }
+ }
+ }
+}
+```
+
+**Important**: You cannot preload across locales. Each locale has its own translations, and Perseus only keeps one set in memory at a time.
+
+## When to Preload
+
+| Scenario | Recommendation |
+|----------|----------------|
+| Main navigation links | Preload on hover |
+| Predictable user flows | Preload on page load |
+| Search results | Don't preload (too many) |
+| Paginated lists | Preload next page |
+| Forms | Preload success page |
+
+## Performance Considerations
+
+- **Don't over-preload** - Each preload is a network request
+- **Prioritize likely paths** - Focus on common user flows
+- **Cache hits are free** - Already-visited pages don't re-fetch
+- **Network conditions** - Preloading on slow connections may hurt UX
+
+## Preloading vs Delayed Widgets
+
+| Feature | Purpose |
+|---------|---------|
+| Preloading | Load full pages before navigation |
+| Delayed widgets | Load widget content after page renders |
+
+Use both together for optimal perceived performance.
+
+## Related
+
+- [Routing and Navigation](/docs/fundamentals/routing)
+- [The Reactor](/docs/fundamentals/reactor)
+- [Capsules](/docs/capsules/intro)
diff --git a/docs/0.5.x/en-US/fundamentals/reactor.md b/docs/0.5.x/en-US/fundamentals/reactor.md
new file mode 100644
index 0000000000..d4176aa9fd
--- /dev/null
+++ b/docs/0.5.x/en-US/fundamentals/reactor.md
@@ -0,0 +1,148 @@
+# The Reactor
+
+The `Reactor` is Perseus' central control system. Use it to:
+- Get the current locale
+- Access router state
+- Preload pages
+- Access global state
+- Manage the page state store
+
+## Accessing the Reactor
+
+```rust
+use perseus::prelude::*;
+use sycamore::prelude::*;
+
+fn my_component() -> View {
+ // Get the reactor
+ let reactor = Reactor::::from_cx();
+
+ // Use it...
+ let locale = reactor.get_locale();
+
+ view! {
+ p { "Current locale: " (locale) }
+ }
+}
+```
+
+## Common Operations
+
+### Get Current Locale
+
+```rust
+let reactor = Reactor::::from_cx();
+let locale = reactor.get_locale();
+```
+
+### Access Global State
+
+```rust
+use crate::global_state::AppStateRx;
+
+let reactor = Reactor::::from_cx();
+let global_state = reactor.get_global_state::();
+
+// Use the reactive global state
+let theme = global_state.theme.get_clone();
+```
+
+### Preload a Page
+
+```rust
+let reactor = Reactor::::from_cx();
+
+// Preload when user hovers over a link
+button(on:mouseenter = move |_| {
+ reactor.preload("/about");
+}) {
+ "Go to About"
+}
+```
+
+### Access Router State
+
+```rust
+let reactor = Reactor::::from_cx();
+let route_info = reactor.router_state.get_load_state();
+```
+
+## Node Types
+
+The reactor is generic over the rendering backend:
+
+| Type | When Used |
+|------|-----------|
+| `BrowserNodeType` | Client-side (browser) |
+| `SsrNode` | Engine-side (server) |
+
+In views, use `BrowserNodeType` since views run in the browser after hydration.
+
+## Engine vs Client
+
+The reactor behaves differently on each platform:
+
+**Engine-side:**
+- Used during server-side rendering
+- Limited functionality (no browser APIs)
+
+**Client-side:**
+- Full functionality
+- Access to browser APIs
+- Manages reactive state
+
+## Example: Theme Toggle with Global State
+
+```rust
+use perseus::prelude::*;
+use sycamore::prelude::*;
+use crate::global_state::ThemeStateRx;
+
+fn theme_toggle() -> View {
+ let reactor = Reactor::::from_cx();
+ let theme = reactor.get_global_state::();
+
+ view! {
+ button(on:click = move |_| {
+ let current = theme.mode.get_clone();
+ theme.mode.set(if current == "light" {
+ "dark".to_string()
+ } else {
+ "light".to_string()
+ });
+ }) {
+ "Toggle Theme: " (theme.mode.get_clone())
+ }
+ }
+}
+```
+
+## Example: Preloading on Hover
+
+```rust
+fn nav_link(path: &'static str, label: &'static str) -> View {
+ let reactor = Reactor::::from_cx();
+
+ view! {
+ Link(
+ to = path,
+ // Preload when user hovers
+ // (Note: actual preload API may vary)
+ ) {
+ (label)
+ }
+ }
+}
+```
+
+## Important Notes
+
+1. **Use correct node type** - Mismatched types cause confusing errors
+2. **Don't mix platforms** - Don't try SSR in the browser through Perseus
+3. **Capsules require proper context** - Rendering capsules outside Perseus causes panics
+
+## Related
+
+- [Global State](/docs/state/global)
+- [Preloading](/docs/fundamentals/preloading)
+- [Routing](/docs/fundamentals/routing)
diff --git a/docs/0.5.x/en-US/fundamentals/routing.md b/docs/0.5.x/en-US/fundamentals/routing.md
new file mode 100644
index 0000000000..223cdf6fdb
--- /dev/null
+++ b/docs/0.5.x/en-US/fundamentals/routing.md
@@ -0,0 +1,216 @@
+# Routing and Navigation
+
+Perseus uses page-based programming where each view is a separate page with its own state. This guide covers how to navigate between pages.
+
+## The Link Component
+
+For internal navigation, use the `Link` component:
+
+```rust
+use perseus::prelude::*;
+use sycamore::prelude::*;
+
+fn my_page() -> View {
+ view! {
+ h1 { "Home" }
+ Link(to = "/about") { "Go to About" }
+ Link(to = "/blog/hello-world") { "Read Blog Post" }
+ }
+}
+```
+
+The `Link` component:
+- Handles client-side navigation (no full page reload)
+- Manages loading states
+- Integrates with Perseus' caching system
+- Works with localized routes automatically
+
+## Imperative Navigation
+
+For programmatic navigation (e.g., after form submission):
+
+```rust
+use perseus::prelude::*;
+use sycamore::prelude::*;
+
+fn login_form() -> View {
+ let handle_login = move |_| {
+ // After successful login...
+ navigate("/dashboard");
+ };
+
+ view! {
+ button(on:click = handle_login) { "Login" }
+ }
+}
+```
+
+### Navigation Functions
+
+| Function | Behavior |
+|----------|----------|
+| `navigate("/path")` | Navigate, add to history |
+| `navigate_replace("/path")` | Navigate, replace current history entry |
+
+Use `navigate_replace` when you don't want the user to go back (e.g., after a redirect).
+
+## Route Behavior
+
+Perseus sets a `` tag that makes all routes relative to the site root:
+
+```rust
+// From any page, these go to:
+Link(to = "/about") // → /about
+Link(to = "/blog/post-1") // → /blog/post-1
+Link(to = "about") // → /about (same as above)
+```
+
+**Note**: Unlike some frameworks, `/my/page` linking to `foo` goes to `/foo`, not `/my/foo`.
+
+## External Links
+
+For external URLs, use regular anchor tags:
+
+```rust
+view! {
+ // Internal - use Link
+ Link(to = "/about") { "About Us" }
+
+ // External - use anchor tag
+ a(href = "https://github.com", target = "_blank") {
+ "GitHub"
+ }
+}
+```
+
+## Localized Routing
+
+For internationalized apps, use the `link!` macro to prepend the current locale:
+
+```rust
+use perseus::prelude::*;
+use sycamore::prelude::*;
+
+fn nav() -> View {
+ view! {
+ // For i18n apps, this ensures correct locale prefix
+ Link(to = link!("/about")) { "About" }
+ }
+}
+```
+
+Without `link!`, navigating from `/en-US/home` to `/about` would go to `/about` instead of `/en-US/about`.
+
+See [Internationalization](/docs/fundamentals/i18n) for more details.
+
+## Query Parameters
+
+Access query parameters in your state generation:
+
+```rust
+#[engine_only_fn]
+async fn get_request_state(
+ info: StateGeneratorInfo<()>,
+ req: Request,
+) -> MyState {
+ // Parse query string from the request URI
+ let uri = req.uri();
+ let query = uri.query().unwrap_or("");
+
+ // Parse as needed...
+ MyState { /* ... */ }
+}
+```
+
+## Preloading
+
+Preload pages before navigation for instant transitions:
+
+```rust
+fn nav_item() -> View {
+ let reactor = Reactor::::from_cx();
+
+ view! {
+ // Preload on hover for faster navigation
+ div(on:mouseenter = move |_| {
+ // Preloading API (see preloading docs)
+ }) {
+ Link(to = "/heavy-page") { "Heavy Page" }
+ }
+ }
+}
+```
+
+See [Preloading](/docs/fundamentals/preloading) for details.
+
+## Dynamic Routes
+
+Create dynamic routes with build paths:
+
+```rust
+// Template: "post"
+// Build paths: ["hello", "world", "rust-tips"]
+//
+// Results in:
+// /post/hello
+// /post/world
+// /post/rust-tips
+
+#[engine_only_fn]
+async fn get_build_paths() -> BuildPaths {
+ BuildPaths {
+ paths: vec![
+ "hello".to_string(),
+ "world".to_string(),
+ "rust-tips".to_string(),
+ ],
+ extra: ().into(),
+ }
+}
+```
+
+Access the current path in your state generator:
+
+```rust
+#[engine_only_fn]
+async fn get_build_state(info: StateGeneratorInfo<()>) -> PostState {
+ let slug = info.path; // "hello", "world", etc.
+ // Fetch post by slug...
+ PostState { /* ... */ }
+}
+```
+
+## 404 Handling
+
+Unknown routes trigger error views with a 404 status:
+
+```rust
+// In your error views
+ClientError::ServerError { status, .. } => {
+ if status.as_u16() == 404 {
+ view! {
+ h1 { "Page Not Found" }
+ Link(to = "/") { "Go Home" }
+ }
+ } else {
+ // Other errors...
+ }
+}
+```
+
+## Summary
+
+| Task | Solution |
+|------|----------|
+| Internal link | `Link(to = "/path")` |
+| External link | `a(href = "https://...")` |
+| Programmatic nav | `navigate("/path")` |
+| Replace history | `navigate_replace("/path")` |
+| Localized link | `link!("/path")` |
+| Preload | See preloading docs |
+
+## Related
+
+- [Preloading](/docs/fundamentals/preloading)
+- [Internationalization](/docs/fundamentals/i18n)
+- [Error Views](/docs/fundamentals/error-views)
diff --git a/docs/0.5.x/en-US/fundamentals/serving-exporting.md b/docs/0.5.x/en-US/fundamentals/serving-exporting.md
new file mode 100644
index 0000000000..eb2fd5e928
--- /dev/null
+++ b/docs/0.5.x/en-US/fundamentals/serving-exporting.md
@@ -0,0 +1,184 @@
+# Serving and Exporting
+
+Perseus apps can be deployed two ways: **served** (with a server) or **exported** (static files only). Each has trade-offs.
+
+## The Build Process
+
+### 1. Preparation
+
+Perseus reads your `PerseusApp` configuration to understand:
+- Templates and capsules
+- Internationalization settings
+- State generation requirements
+
+### 2. Building
+
+Perseus executes build-time logic:
+- Calls `get_build_state` and `get_build_paths` for each template
+- Prerenders pages to HTML
+- Creates the render configuration
+
+### 3. Wasm Compilation
+
+Simultaneously, Perseus compiles your app to WebAssembly:
+- Creates `bundle.js` and `bundle.wasm`
+- Optimizes for size in release mode
+
+### 4a. Serving
+
+If you run `perseus serve`:
+
+```bash
+perseus serve # Development
+perseus serve -r # Release mode
+```
+
+Perseus starts a server that:
+- Handles initial page loads with SSR
+- Serves the Wasm bundle
+- Processes request-time state generation
+- Handles revalidation and incremental generation
+- Resolves nested capsules just-in-time
+
+### 4b. Exporting
+
+If you run `perseus export`:
+
+```bash
+perseus export # Development
+perseus export -s # With local server
+```
+
+Perseus generates static files:
+- Pre-renders all pages to HTML files
+- Organizes files to match URL structure
+- Creates `dist/exported/` ready for deployment
+
+## Serving vs Exporting
+
+| Feature | Served | Exported |
+|---------|--------|----------|
+| Request-time state | ✅ | ❌ |
+| Incremental generation | ✅ | ❌ |
+| Revalidation | ✅ | ❌ |
+| API routes | ✅ | ❌ |
+| Cookies/Auth | ✅ | ❌ |
+| CDN deployment | Harder | Easy |
+| Hosting cost | Higher | Lower |
+| Setup complexity | More | Less |
+
+**Rule of thumb**: If you can export, export. Static files are faster, cheaper, and simpler.
+
+## Server Integrations
+
+Perseus supports multiple server frameworks:
+
+| Integration | Crate |
+|-------------|-------|
+| Axum | `perseus-axum` |
+| Actix Web | `perseus-actix-web` |
+| Warp | `perseus-warp` |
+
+### Default Server
+
+Most apps use the default server:
+
+```rust
+#[perseus::main(perseus_axum::dflt_server)]
+pub fn main() -> PerseusApp {
+ PerseusApp::new()
+ // ...
+}
+```
+
+### Custom Server
+
+Add API routes or middleware:
+
+```rust
+use axum::{Router, routing::get};
+use perseus_axum::ServerOptions;
+
+async fn api_handler() -> &'static str {
+ "Hello from API"
+}
+
+#[perseus::main(custom_server)]
+pub fn main() -> PerseusApp {
+ PerseusApp::new()
+ // ...
+}
+
+async fn custom_server(
+ turbine: &'static Turbine,
+ opts: ServerOptions,
+) {
+ let app = Router::new()
+ .route("/api/hello", get(api_handler))
+ .merge(perseus_axum::get_router(turbine, opts).await);
+
+ let listener = tokio::net::TcpListener::bind("0.0.0.0:8080")
+ .await
+ .unwrap();
+ axum::serve(listener, app).await.unwrap();
+}
+```
+
+## Deployment Commands
+
+### Development
+
+```bash
+perseus serve -w # Watch mode, auto-reload
+perseus export -sw # Export with watch and server
+```
+
+### Production
+
+```bash
+perseus deploy # Served app → pkg/
+perseus deploy -e # Exported app → pkg/
+```
+
+The `deploy` command:
+- Builds in release mode
+- Optimizes Wasm aggressively
+- Creates a ready-to-deploy `pkg/` directory
+
+## Error Pages for Exported Apps
+
+Static file hosts need pre-exported error pages:
+
+```bash
+# Export 404 page
+perseus export-error-page --code 404 --output pkg/404.html
+
+# Export 500 page
+perseus export-error-page --code 500 --output pkg/500.html
+```
+
+Most hosts (GitHub Pages, Netlify, Vercel) automatically serve `404.html` for missing routes.
+
+**Note**: For i18n apps, exported error pages can't be localized since the user's locale isn't known. Prefer serving for i18n apps when possible.
+
+## Performance Tips
+
+### For Served Apps
+
+1. Use a reverse proxy (nginx, Caddy) for TLS
+2. Enable HTTP/2
+3. Set appropriate cache headers
+4. Consider a CDN for static assets
+
+### For Exported Apps
+
+1. Deploy to edge networks (Cloudflare, Vercel)
+2. Enable Brotli/gzip compression
+3. Set long cache times for hashed assets
+4. Pre-compress files if your host supports it
+
+## Related
+
+- [PerseusApp Configuration](/docs/fundamentals/perseus-app)
+- [Debugging](/docs/fundamentals/debugging)
+- [Deploying Tutorial](/docs/first-app/deploying)
diff --git a/docs/0.5.x/en-US/fundamentals/static-content.md b/docs/0.5.x/en-US/fundamentals/static-content.md
new file mode 100644
index 0000000000..58c0ffd090
--- /dev/null
+++ b/docs/0.5.x/en-US/fundamentals/static-content.md
@@ -0,0 +1,173 @@
+# Static Content
+
+Web applications need static content: stylesheets, images, fonts, downloadable files, etc. Perseus provides two ways to serve static content.
+
+## The `static/` Directory
+
+The simplest approach is to place files in a `static/` directory at your project root. Perseus automatically serves these at `/.perseus/static/`.
+
+```
+my-app/
+├── src/
+├── static/
+│ ├── styles.css
+│ ├── logo.png
+│ └── fonts/
+│ └── inter.woff2
+└── Cargo.toml
+```
+
+### Linking Static Files
+
+Reference static files without a leading slash (Perseus uses a `` tag):
+
+```rust
+use perseus::prelude::*;
+use sycamore::prelude::*;
+
+#[engine_only_fn]
+fn head() -> View {
+ view! {
+ title { "My App" }
+ link(rel = "stylesheet", href = ".perseus/static/styles.css")
+ link(rel = "icon", href = ".perseus/static/favicon.ico")
+ }
+}
+
+fn page_content() -> View {
+ view! {
+ img(src = ".perseus/static/logo.png", alt = "Logo")
+ }
+}
+```
+
+## Static Aliases
+
+For files outside `static/` or custom paths, use static aliases in your `PerseusApp`:
+
+```rust
+use perseus::prelude::*;
+
+#[perseus::main(perseus_axum::dflt_server)]
+pub fn main() -> PerseusApp {
+ PerseusApp::new()
+ .template(crate::templates::index::get_template())
+ // Serve robots.txt at the root
+ .static_alias("/robots.txt", "robots.txt")
+ // Serve sitemap
+ .static_alias("/sitemap.xml", "sitemap.xml")
+ // Serve favicon at root
+ .static_alias("/favicon.ico", "static/favicon.ico")
+ .error_views(ErrorViews::unlocalized_development_default())
+}
+```
+
+The first argument is the URL path, the second is the file path relative to your project root.
+
+### Security Note
+
+Perseus rejects files outside your project root for security reasons. If you need external files, use symbolic links within your project directory.
+
+## Generated Content
+
+For content generated by external tools (CSS bundlers, asset pipelines, etc.), place output in `dist/`:
+
+- Ignored by `perseus serve -w` file watching
+- Excluded from version control (add to `.gitignore`)
+- Keeps your repository lightweight
+
+```
+my-app/
+├── dist/
+│ └── generated.css # Generated by build tool
+├── src/
+└── static/ # Static assets
+```
+
+## Common Patterns
+
+### CSS Stylesheets
+
+```rust
+#[engine_only_fn]
+fn head() -> View {
+ view! {
+ link(rel = "stylesheet", href = ".perseus/static/main.css")
+ // Or from a CDN
+ link(
+ rel = "stylesheet",
+ href = "https://fonts.googleapis.com/css2?family=Inter"
+ )
+ }
+}
+```
+
+### Images
+
+```rust
+fn gallery() -> View {
+ view! {
+ div(class = "gallery") {
+ img(src = ".perseus/static/images/photo1.jpg", alt = "Photo 1")
+ img(src = ".perseus/static/images/photo2.jpg", alt = "Photo 2")
+ }
+ }
+}
+```
+
+### Fonts
+
+```css
+/* In static/styles.css */
+@font-face {
+ font-family: 'Inter';
+ src: url('/.perseus/static/fonts/inter.woff2') format('woff2');
+}
+```
+
+### Downloadable Files
+
+```rust
+fn downloads() -> View {
+ view! {
+ a(href = ".perseus/static/files/document.pdf", download = true) {
+ "Download PDF"
+ }
+ }
+}
+```
+
+## Static Content in Index View
+
+You can also reference static content in your index view:
+
+```rust
+PerseusApp::new()
+ .index_view(|| {
+ view! {
+ html {
+ head {
+ meta(charset = "UTF-8")
+ link(rel = "stylesheet", href = ".perseus/static/global.css")
+ link(rel = "icon", href = ".perseus/static/favicon.svg")
+ }
+ body {
+ PerseusRoot()
+ }
+ }
+ }
+ })
+```
+
+## Tips
+
+1. **Use descriptive paths** - Organize files logically in subdirectories
+2. **Optimize assets** - Compress images, minify CSS before deployment
+3. **Cache-friendly names** - Consider content hashing for cache busting
+4. **Relative paths** - Omit leading `/` when using `.perseus/static/`
+5. **External CDNs** - Use CDNs for large libraries and fonts when appropriate
+
+## Related
+
+- [Heads and Headers](/docs/fundamentals/head-headers)
+- [Serving and Exporting](/docs/fundamentals/serving-exporting)
diff --git a/docs/0.5.x/en-US/fundamentals/styling.md b/docs/0.5.x/en-US/fundamentals/styling.md
new file mode 100644
index 0000000000..7548385dd5
--- /dev/null
+++ b/docs/0.5.x/en-US/fundamentals/styling.md
@@ -0,0 +1,285 @@
+# Styling
+
+Perseus works with any CSS approach. This guide covers common styling patterns and recommendations.
+
+## CSS Integration
+
+### Static Stylesheets
+
+Place CSS files in `static/` and link them in your head:
+
+```rust
+use perseus::prelude::*;
+use sycamore::prelude::*;
+
+#[engine_only_fn]
+fn head() -> View {
+ view! {
+ title { "My App" }
+ link(rel = "stylesheet", href = ".perseus/static/styles.css")
+ }
+}
+```
+
+Or in your index view for global styles:
+
+```rust
+PerseusApp::new()
+ .index_view(|| {
+ view! {
+ html {
+ head {
+ link(rel = "stylesheet", href = ".perseus/static/global.css")
+ }
+ body {
+ PerseusRoot()
+ }
+ }
+ }
+ })
+```
+
+### Inline Styles
+
+Apply styles directly to elements:
+
+```rust
+fn styled_component() -> View {
+ view! {
+ div(style = "padding: 1rem; background: #f0f0f0;") {
+ p(style = "color: #333; font-size: 1.2rem;") {
+ "Styled text"
+ }
+ }
+ }
+}
+```
+
+### Dynamic Styles
+
+Use signals for reactive styling:
+
+```rust
+fn dynamic_theme() -> View {
+ let is_dark = create_signal(false);
+
+ view! {
+ div(
+ style = move || if is_dark.get() {
+ "background: #1a1a1a; color: white;"
+ } else {
+ "background: white; color: black;"
+ }
+ ) {
+ button(on:click = move |_| is_dark.set(!is_dark.get())) {
+ "Toggle Theme"
+ }
+ }
+ }
+}
+```
+
+## Tailwind CSS
+
+[Tailwind](https://tailwindcss.com) works excellently with Perseus:
+
+```rust
+fn card() -> View {
+ view! {
+ div(class = "bg-white rounded-lg shadow-md p-6 dark:bg-gray-800") {
+ h2(class = "text-xl font-bold text-gray-900 dark:text-white") {
+ "Card Title"
+ }
+ p(class = "mt-2 text-gray-600 dark:text-gray-300") {
+ "Card content goes here."
+ }
+ }
+ }
+}
+```
+
+### Setting Up Tailwind
+
+1. Install Tailwind:
+```bash
+npm init -y
+npm install -D tailwindcss
+npx tailwindcss init
+```
+
+2. Configure `tailwind.config.js`:
+```javascript
+module.exports = {
+ content: ["./src/**/*.rs"],
+ theme: { extend: {} },
+ plugins: [],
+}
+```
+
+3. Create `static/input.css`:
+```css
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+```
+
+4. Build CSS:
+```bash
+npx tailwindcss -i ./static/input.css -o ./static/styles.css --watch
+```
+
+5. Link in your app:
+```rust
+#[engine_only_fn]
+fn head() -> View {
+ view! {
+ link(rel = "stylesheet", href = ".perseus/static/styles.css")
+ }
+}
+```
+
+## Full-Page Layouts
+
+A common pattern for headers, content, and footers:
+
+### CSS
+
+```css
+/* static/layout.css */
+html, body {
+ height: 100%;
+ margin: 0;
+}
+
+.layout {
+ min-height: 100%;
+ display: flex;
+ flex-direction: column;
+}
+
+.header {
+ position: sticky;
+ top: 0;
+ background: white;
+ border-bottom: 1px solid #e5e5e5;
+ z-index: 100;
+}
+
+.content {
+ flex: 1;
+}
+
+.footer {
+ background: #f5f5f5;
+ border-top: 1px solid #e5e5e5;
+}
+```
+
+### Layout Component
+
+```rust
+use perseus::prelude::*;
+use sycamore::prelude::*;
+
+fn layout(children: View) -> View {
+ view! {
+ div(class = "layout") {
+ header(class = "header") {
+ nav(class = "container mx-auto p-4") {
+ Link(to = "/") { "Home" }
+ Link(to = "/about") { "About" }
+ }
+ }
+
+ main(class = "content") {
+ (children)
+ }
+
+ footer(class = "footer") {
+ div(class = "container mx-auto p-4") {
+ "© 2024 My App"
+ }
+ }
+ }
+ }
+}
+```
+
+### Using the Layout
+
+```rust
+fn home_page() -> View {
+ layout(view! {
+ div(class = "container mx-auto p-4") {
+ h1 { "Welcome Home" }
+ }
+ })
+}
+```
+
+## Dark Mode
+
+### CSS Variables Approach
+
+```css
+:root {
+ --bg-primary: #ffffff;
+ --text-primary: #1a1a1a;
+}
+
+[data-theme="dark"] {
+ --bg-primary: #1a1a1a;
+ --text-primary: #ffffff;
+}
+
+body {
+ background: var(--bg-primary);
+ color: var(--text-primary);
+}
+```
+
+### Toggle Implementation
+
+```rust
+use perseus::prelude::*;
+use sycamore::prelude::*;
+use wasm_bindgen::JsCast;
+
+fn theme_toggle() -> View {
+ view! {
+ button(on:click = move |_| {
+ let document = web_sys::window()
+ .unwrap()
+ .document()
+ .unwrap();
+ let html = document.document_element().unwrap();
+ let current = html.get_attribute("data-theme")
+ .unwrap_or_default();
+ html.set_attribute(
+ "data-theme",
+ if current == "dark" { "light" } else { "dark" }
+ ).unwrap();
+ }) {
+ "Toggle Theme"
+ }
+ }
+}
+```
+
+## CSS-in-Rust (Future)
+
+The [Jacaranda](https://github.com/framesurge/jacaranda) project aims to bring fully typed CSS-in-Rust styling to Sycamore/Perseus. Watch this space!
+
+## Best Practices
+
+1. **Organize by component** - Keep styles near their components
+2. **Use utility classes** - Tailwind or similar speeds up development
+3. **Minimize inline styles** - Use classes for reusable styles
+4. **Consider bundle size** - Large CSS files slow initial load
+5. **Test dark mode** - If supporting, test thoroughly
+6. **Mobile first** - Start with mobile styles, add breakpoints
+
+## Related
+
+- [Static Content](/docs/fundamentals/static-content)
+- [Heads and Headers](/docs/fundamentals/head-headers)
diff --git a/docs/0.5.x/en-US/fundamentals/testing.md b/docs/0.5.x/en-US/fundamentals/testing.md
new file mode 100644
index 0000000000..c535249f2a
--- /dev/null
+++ b/docs/0.5.x/en-US/fundamentals/testing.md
@@ -0,0 +1,224 @@
+# Testing
+
+Perseus apps benefit from multiple testing strategies: unit tests for logic, integration tests for state generation, and end-to-end (E2E) tests for full user flows.
+
+## End-to-End Testing
+
+E2E tests run a real browser against your app, simulating user interactions.
+
+### Writing E2E Tests
+
+Place tests in a `tests/` directory:
+
+```rust
+// tests/main.rs
+use fantoccini::Client;
+use perseus::wait_for_checkpoint;
+
+#[perseus::test]
+async fn test_homepage(client: &mut Client) -> Result<(), fantoccini::error::CmdError> {
+ // Navigate to the app
+ client.goto("http://localhost:8080").await?;
+
+ // Wait for Perseus to initialize
+ wait_for_checkpoint!("page_interactive", 0, client);
+
+ // Check the page content
+ let title = client.find(fantoccini::Locator::Css("h1")).await?;
+ assert_eq!(title.text().await?, "Welcome");
+
+ Ok(())
+}
+
+#[perseus::test]
+async fn test_navigation(client: &mut Client) -> Result<(), fantoccini::error::CmdError> {
+ client.goto("http://localhost:8080").await?;
+ wait_for_checkpoint!("page_interactive", 0, client);
+
+ // Click a link
+ let link = client.find(fantoccini::Locator::Css("a[href='/about']")).await?;
+ link.click().await?;
+
+ // Wait for navigation to complete
+ wait_for_checkpoint!("page_interactive", 1, client);
+
+ // Verify we're on the about page
+ let heading = client.find(fantoccini::Locator::Css("h1")).await?;
+ assert_eq!(heading.text().await?, "About Us");
+
+ Ok(())
+}
+```
+
+### Running Tests
+
+```bash
+# Start a WebDriver (in a separate terminal)
+geckodriver # For Firefox
+chromedriver # For Chrome
+
+# Run tests
+perseus test
+```
+
+The `perseus test` command:
+1. Builds your app in test mode
+2. Starts a test server
+3. Runs all tests (unit, integration, and E2E)
+4. Reports results
+
+### Debugging Tests
+
+Show the browser during tests:
+
+```bash
+perseus test --show-browser
+```
+
+This disables headless mode so you can see what's happening.
+
+## Checkpoints
+
+Perseus emits checkpoints at key moments during testing. Use `wait_for_checkpoint!` to synchronize tests.
+
+### Available Checkpoints
+
+| Checkpoint | Meaning |
+|------------|---------|
+| `begin` | Perseus has initialized |
+| `page_interactive` | Page is fully hydrated and interactive |
+| `error` | An error occurred |
+| `not_found` | Page not found (also emits `error`) |
+
+### Checkpoint Indices
+
+The index (second argument) counts occurrences:
+
+```rust
+// Wait for the first page to be interactive
+wait_for_checkpoint!("page_interactive", 0, client);
+
+// Navigate...
+
+// Wait for the second page to be interactive
+wait_for_checkpoint!("page_interactive", 1, client);
+```
+
+Checkpoints persist across navigations but reset on page refresh.
+
+### Custom Checkpoints
+
+Define your own checkpoints for complex flows:
+
+```rust
+use perseus::checkpoint;
+
+fn my_component() -> View {
+ // Emit a custom checkpoint
+ checkpoint("custom_data_loaded");
+
+ view! { p { "Data loaded!" } }
+}
+
+// In your test
+wait_for_checkpoint!("custom_data_loaded", 0, client);
+```
+
+Custom checkpoint names must:
+- Start with "custom"
+- Contain no hyphens (use underscores)
+
+## Unit Testing
+
+Test pure functions normally:
+
+```rust
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_format_date() {
+ assert_eq!(format_date(2024, 1, 15), "January 15, 2024");
+ }
+
+ #[test]
+ fn test_validate_email() {
+ assert!(validate_email("user@example.com"));
+ assert!(!validate_email("invalid"));
+ }
+}
+```
+
+Run with:
+
+```bash
+cargo test
+```
+
+## Testing State Generation
+
+Test your state generation functions:
+
+```rust
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[tokio::test]
+ async fn test_build_state() {
+ let info = StateGeneratorInfo {
+ path: "my-page".to_string(),
+ locale: "en-US".to_string(),
+ extra: ().into(),
+ };
+
+ let state = get_build_state(info).await;
+ assert_eq!(state.title, "My Page");
+ }
+}
+```
+
+## WebDriver Setup
+
+### Firefox (geckodriver)
+
+1. Install: Download from [Mozilla's releases](https://github.com/mozilla/geckodriver/releases)
+2. Run: `geckodriver` (defaults to port 4444)
+
+### Chrome (chromedriver)
+
+1. Install: Download from [Chrome for Testing](https://googlechromelabs.github.io/chrome-for-testing/)
+2. Run: `chromedriver --port=4444`
+
+### CI Setup
+
+```yaml
+# GitHub Actions example
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: browser-actions/setup-geckodriver@latest
+ - run: geckodriver &
+ - run: perseus test
+```
+
+## Tips
+
+1. **Wait for checkpoints** - Don't assume timing; use checkpoints
+2. **Test critical paths** - Focus on user journeys
+3. **Clean state** - Each test should be independent
+4. **Debug visually** - Use `--show-browser` when tests fail
+5. **Add delays for debugging** - `std::thread::sleep()` to observe state
+
+## Single-Threaded Limitation
+
+E2E tests currently run single-threaded due to WebDriver limitations. This makes them slower but ensures reliability.
+
+## Related
+
+- [Debugging](/docs/fundamentals/debugging)
+- [Checkpoints API](https://docs.rs/perseus/latest/perseus/fn.checkpoint.html)
+- [Fantoccini Documentation](https://docs.rs/fantoccini)
diff --git a/docs/0.5.x/en-US/intro.md b/docs/0.5.x/en-US/intro.md
new file mode 100644
index 0000000000..dcd5296f39
--- /dev/null
+++ b/docs/0.5.x/en-US/intro.md
@@ -0,0 +1,51 @@
+# Welcome to Perseus 0.5!
+
+[Home][repo] • [Crate Page][crate] • [API Documentation][docs] • [Contributing][contrib]
+
+Welcome to the Perseus 0.5.x documentation! This version brings significant improvements including **Sycamore 0.9.2** support with its simplified reactive model.
+
+## What's New in 0.5.x
+
+- **Sycamore 0.9.2**: Simplified view syntax without `Scope` parameters
+- **Cleaner Component API**: Views now return `View` directly without generics
+- **Link Component**: Built-in `Link` component for client-side navigation
+- **Improved Hydration**: Better SSR hydration with data-hk attributes
+- **Enhanced Error Handling**: More robust error views and panic handling
+
+## Quick Example
+
+Here's what a simple Perseus page looks like in 0.5.x:
+
+```rust
+use perseus::prelude::*;
+use sycamore::prelude::*;
+
+fn my_page() -> View {
+ view! {
+ h1 { "Hello, Perseus!" }
+ p { "This is a simple page." }
+ Link(to = "/about") { "Go to About" }
+ }
+}
+
+pub fn get_template() -> Template {
+ Template::build("index")
+ .view(my_page)
+ .build()
+}
+```
+
+Notice how clean the syntax is - no `cx` parameter, no `G: Html` generics, just straightforward Rust code!
+
+## Getting Started
+
+If you're new to Perseus, start with the [Quickstart](/docs/quickstart) guide to get your first app running in minutes.
+
+If you're upgrading from 0.4.x, check out the [Migration Guide](/docs/migrating) for step-by-step instructions.
+
+If you like Perseus, please consider giving us a star [on GitHub](https://github.com/framesurge/perseus)!
+
+[repo]: https://github.com/framesurge/perseus
+[crate]: https://crates.io/crates/perseus
+[docs]: https://docs.rs/perseus
+[contrib]: ./CONTRIBUTING.md
diff --git a/docs/0.5.x/en-US/migrating.md b/docs/0.5.x/en-US/migrating.md
new file mode 100644
index 0000000000..894883c372
--- /dev/null
+++ b/docs/0.5.x/en-US/migrating.md
@@ -0,0 +1,411 @@
+# Migrating from 0.4.x to 0.5.x
+
+This guide helps you upgrade your Perseus app from 0.4.x to 0.5.x. The main changes involve Sycamore 0.9.2's simplified reactive model.
+
+## Quick Summary
+
+| 0.4.x | 0.5.x |
+|-------|-------|
+| `fn view(cx: Scope) -> View` | `fn view() -> View` |
+| `view! { cx, ... }` | `view! { ... }` |
+| `a(href = "/about")` | `Link(to = "/about")` |
+| `cx.create_signal(...)` | `create_signal(...)` |
+| Sycamore 0.8.x | Sycamore 0.9.2 |
+
+## Step-by-Step Migration
+
+### 1. Update Dependencies
+
+```toml
+# Cargo.toml
+[dependencies]
+perseus = { version = "0.5", features = ["hydrate"] }
+sycamore = "0.9" # Was 0.8.x
+```
+
+### 2. Remove Scope Parameters
+
+**Before (0.4.x):**
+```rust
+fn my_page(cx: Scope) -> View {
+ view! { cx,
+ h1 { "Hello World" }
+ }
+}
+```
+
+**After (0.5.x):**
+```rust
+fn my_page() -> View {
+ view! {
+ h1 { "Hello World" }
+ }
+}
+```
+
+### 3. Update View Functions with State
+
+**Before (0.4.x):**
+```rust
+#[auto_scope]
+fn my_page(cx: Scope, state: &MyStateRx) -> View {
+ view! { cx,
+ h1 { (state.title.get_clone()) }
+ }
+}
+```
+
+**After (0.5.x):**
+```rust
+#[auto_scope]
+fn my_page(state: MyStateRx) -> View {
+ view! {
+ h1 { (state.title.get_clone()) }
+ }
+}
+```
+
+### 4. Replace Anchor Tags with Link
+
+**Before (0.4.x):**
+```rust
+view! { cx,
+ a(href = "/about") { "Go to About" }
+}
+```
+
+**After (0.5.x):**
+```rust
+view! {
+ Link(to = "/about") { "Go to About" }
+}
+```
+
+The `Link` component is now built into Perseus and handles client-side navigation automatically.
+
+### 5. Update Signal Creation
+
+**Before (0.4.x):**
+```rust
+fn my_component(cx: Scope) -> View {
+ let count = cx.create_signal(0);
+ view! { cx,
+ p { (count.get()) }
+ button(on:click = |_| count.set(*count.get() + 1)) {
+ "Increment"
+ }
+ }
+}
+```
+
+**After (0.5.x):**
+```rust
+fn my_component() -> View {
+ let count = create_signal(0);
+ view! {
+ p { (count.get()) }
+ button(on:click = move |_| count.set(*count.get() + 1)) {
+ "Increment"
+ }
+ }
+}
+```
+
+Note: `create_signal` is now a free function, not a method on `Scope`.
+
+### 6. Update Head Functions
+
+**Before (0.4.x):**
+```rust
+#[engine_only_fn]
+fn head(cx: Scope) -> View {
+ view! { cx,
+ title { "My Page" }
+ }
+}
+```
+
+**After (0.5.x):**
+```rust
+#[engine_only_fn]
+fn head() -> View {
+ view! {
+ title { "My Page" }
+ }
+}
+```
+
+### 7. Update Head with State
+
+**Before (0.4.x):**
+```rust
+#[engine_only_fn]
+fn head(cx: Scope, state: MyState) -> View {
+ view! { cx,
+ title { (state.title) }
+ }
+}
+```
+
+**After (0.5.x):**
+```rust
+#[engine_only_fn]
+fn head(state: MyState) -> View {
+ view! {
+ title { (state.title) }
+ }
+}
+```
+
+### 8. Update Template Registration
+
+**Before (0.4.x):**
+```rust
+pub fn get_template() -> Template {
+ Template::build("my-page")
+ .view(my_page)
+ .build()
+}
+```
+
+**After (0.5.x):**
+```rust
+pub fn get_template() -> Template {
+ Template::build("my-page")
+ .view(my_page)
+ .build()
+}
+```
+
+### 9. Update Error Views
+
+**Before (0.4.x):**
+```rust
+ErrorViews::new(|cx, error, _ctx, _pos| {
+ (
+ view! { cx, title { "Error" } },
+ view! { cx, h1 { "Error occurred" } }
+ )
+})
+```
+
+**After (0.5.x):**
+```rust
+ErrorViews::new(|error, _ctx, _pos| {
+ (
+ view! { title { "Error" } },
+ view! { h1 { "Error occurred" } }
+ )
+})
+```
+
+### 10. Update Index View
+
+**Before (0.4.x):**
+```rust
+.index_view(|cx| {
+ view! { cx,
+ html {
+ head {}
+ body {
+ PerseusRoot()
+ }
+ }
+ }
+})
+```
+
+**After (0.5.x):**
+```rust
+.index_view(|| {
+ view! {
+ html {
+ head {}
+ body {
+ PerseusRoot()
+ }
+ }
+ }
+})
+```
+
+## Common Patterns
+
+### Conditional Rendering
+
+**Before:**
+```rust
+view! { cx,
+ (if *show.get() {
+ view! { cx, p { "Visible" } }
+ } else {
+ view! { cx, }
+ })
+}
+```
+
+**After:**
+```rust
+view! {
+ (if *show.get() {
+ view! { p { "Visible" } }
+ } else {
+ view! {}
+ })
+}
+```
+
+### Iterating Over Collections
+
+**Before:**
+```rust
+view! { cx,
+ ul {
+ Indexed(
+ iterable = items,
+ view = |cx, item| view! { cx,
+ li { (item) }
+ }
+ )
+ }
+}
+```
+
+**After:**
+```rust
+view! {
+ ul {
+ Indexed(
+ list = items,
+ view = |item| view! {
+ li { (item) }
+ }
+ )
+ }
+}
+```
+
+### Event Handlers
+
+**Before:**
+```rust
+button(on:click = |_| {
+ // handle click
+})
+```
+
+**After:**
+```rust
+button(on:click = move |_| {
+ // handle click - note the 'move' keyword is often needed
+})
+```
+
+## Breaking Changes Summary
+
+1. **No more `Scope` (`cx`) parameter** - Functions no longer receive a scope
+2. **No more `G: Html` generic** - Views return `View` directly
+3. **`view!` macro simplified** - No `cx` as first argument
+4. **`Link` component for navigation** - Replaces `a` tags for internal links
+5. **Free functions for signals** - `create_signal()` instead of `cx.create_signal()`
+6. **Closure captures** - Often need `move` keyword for event handlers
+
+## Troubleshooting
+
+### "expected `View`, found `View`"
+
+Remove the `` generic and change return type to `View`.
+
+### "cannot find value `cx` in this scope"
+
+Remove `cx` from your function signature and `view!` macro calls.
+
+### "use of moved value"
+
+Add `move` keyword to closures in event handlers.
+
+### Navigation not working
+
+Replace `a(href = "...")` with `Link(to = "...")` for internal navigation.
+
+## Full Example
+
+**Before (0.4.x):**
+```rust
+use perseus::prelude::*;
+use sycamore::prelude::*;
+use serde::{Deserialize, Serialize};
+
+#[derive(Serialize, Deserialize, ReactiveState, Clone)]
+#[rx(alias = "IndexStateRx")]
+struct IndexState {
+ greeting: String,
+}
+
+#[auto_scope]
+fn index_page(cx: Scope, state: &IndexStateRx) -> View {
+ view! { cx,
+ h1 { (state.greeting.get_clone()) }
+ a(href = "/about") { "About" }
+ }
+}
+
+#[engine_only_fn]
+fn head(cx: Scope) -> View {
+ view! { cx,
+ title { "Home" }
+ }
+}
+
+pub fn get_template() -> Template {
+ Template::build("index")
+ .build_state_fn(get_build_state)
+ .view_with_state(index_page)
+ .head(head)
+ .build()
+}
+```
+
+**After (0.5.x):**
+```rust
+use perseus::prelude::*;
+use sycamore::prelude::*;
+use serde::{Deserialize, Serialize};
+
+#[derive(Serialize, Deserialize, ReactiveState, Clone)]
+#[rx(alias = "IndexStateRx")]
+struct IndexState {
+ greeting: String,
+}
+
+#[auto_scope]
+fn index_page(state: IndexStateRx) -> View {
+ view! {
+ h1 { (state.greeting.get_clone()) }
+ Link(to = "/about") { "About" }
+ }
+}
+
+#[engine_only_fn]
+fn head() -> View {
+ view! {
+ title { "Home" }
+ }
+}
+
+pub fn get_template() -> Template {
+ Template::build("index")
+ .build_state_fn(get_build_state)
+ .view_with_state(index_page)
+ .head(head)
+ .build()
+}
+```
+
+## Getting Help
+
+If you run into issues:
+
+- Check the [examples](https://github.com/framesurge/perseus/tree/main/examples)
+- Open a [GitHub discussion](https://github.com/framesurge/perseus/discussions)
+- Join the [Discord](https://discord.com/invite/GNqWYWNTdp)
diff --git a/docs/0.5.x/en-US/quickstart.md b/docs/0.5.x/en-US/quickstart.md
new file mode 100644
index 0000000000..8ac3c04f71
--- /dev/null
+++ b/docs/0.5.x/en-US/quickstart.md
@@ -0,0 +1,207 @@
+# Quickstart
+
+Get your first Perseus app running in just a few minutes!
+
+## Prerequisites
+
+1. **Install Rust**: Make sure you have Rust installed. We recommend using [`rustup`](https://rustup.rs):
+ ```sh
+ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
+ ```
+
+2. **Install the WebAssembly target**:
+ ```sh
+ rustup target add wasm32-unknown-unknown
+ ```
+
+## Create Your App
+
+1. **Install the Perseus CLI**:
+ ```sh
+ cargo install perseus-cli
+ ```
+
+2. **Create a new project**:
+ ```sh
+ perseus new my-app
+ cd my-app
+ ```
+
+3. **Start the development server**:
+ ```sh
+ perseus serve -w
+ ```
+
+ Visit and you should see a welcome page!
+
+## Understanding the Project Structure
+
+Your new Perseus app has the following structure:
+
+```
+my-app/
+├── Cargo.toml # Rust dependencies
+├── src/
+│ ├── main.rs # App entry point
+│ └── templates/ # Your page templates
+│ ├── mod.rs
+│ └── index.rs # Landing page
+```
+
+## Your First Page
+
+Let's look at what's in `src/templates/index.rs`:
+
+```rust
+use perseus::prelude::*;
+use sycamore::prelude::*;
+
+fn index_page() -> View {
+ view! {
+ h1 { "Welcome to Perseus!" }
+ p { "This is your landing page." }
+ Link(to = "/about") { "Go to About" }
+ }
+}
+
+pub fn get_template() -> Template {
+ Template::build("index")
+ .view(index_page)
+ .build()
+}
+```
+
+**Key things to notice:**
+- `View` is the return type for all view functions
+- The `view!` macro creates HTML-like syntax in Rust
+- `Link` is used for client-side navigation (no full page reload)
+- `Template::build("index")` creates a template at the root path `/`
+
+## Adding a New Page
+
+Let's create an About page:
+
+1. **Create** `src/templates/about.rs`:
+
+```rust
+use perseus::prelude::*;
+use sycamore::prelude::*;
+
+fn about_page() -> View {
+ view! {
+ h1 { "About Us" }
+ p { "This is the about page." }
+ Link(to = "/") { "Back to Home" }
+ }
+}
+
+#[engine_only_fn]
+fn head() -> View {
+ view! {
+ title { "About | My App" }
+ }
+}
+
+pub fn get_template() -> Template {
+ Template::build("about")
+ .view(about_page)
+ .head(head)
+ .build()
+}
+```
+
+2. **Register the module** in `src/templates/mod.rs`:
+```rust
+pub mod about;
+pub mod index;
+```
+
+3. **Add the template** in `src/main.rs`:
+```rust
+use perseus::prelude::*;
+
+mod templates;
+
+#[perseus::main(perseus_axum::dflt_server)]
+pub fn main() -> PerseusApp {
+ PerseusApp::new()
+ .template(crate::templates::index::get_template())
+ .template(crate::templates::about::get_template())
+ .error_views(ErrorViews::unlocalized_development_default())
+}
+```
+
+Visit to see your new page!
+
+## Adding Dynamic State
+
+Let's add some state to our index page:
+
+```rust
+use perseus::prelude::*;
+use serde::{Deserialize, Serialize};
+use sycamore::prelude::*;
+
+// Define your state structure
+#[derive(Serialize, Deserialize, ReactiveState, Clone)]
+#[rx(alias = "IndexStateRx")]
+struct IndexState {
+ greeting: String,
+ count: i32,
+}
+
+// Your view receives the reactive state
+#[auto_scope]
+fn index_page(state: IndexStateRx) -> View {
+ view! {
+ h1 { (state.greeting.get_clone()) }
+ p { "Count: " (state.count.get()) }
+ button(on:click = move |_| state.count.set(*state.count.get() + 1)) {
+ "Increment"
+ }
+ Link(to = "/about") { "Go to About" }
+ }
+}
+
+// Generate state at build time
+#[engine_only_fn]
+async fn get_build_state(_info: StateGeneratorInfo<()>) -> IndexState {
+ IndexState {
+ greeting: "Hello, Perseus!".to_string(),
+ count: 0,
+ }
+}
+
+pub fn get_template() -> Template {
+ Template::build("index")
+ .build_state_fn(get_build_state)
+ .view_with_state(index_page)
+ .build()
+}
+```
+
+**Key concepts:**
+- `#[derive(ReactiveState)]` makes your state reactive
+- `#[rx(alias = "...")]` creates a type alias for the reactive version
+- `#[auto_scope]` handles lifetime management automatically
+- `.get()` reads a reactive value, `.set()` updates it
+- `.get_clone()` clones the value (useful for `String`)
+
+## What's Next?
+
+- [Understanding Templates and Pages](/docs/first-app/generating-pages)
+- [Error Handling](/docs/first-app/error-handling)
+- [Deploying Your App](/docs/first-app/deploying)
+- [State Management](/docs/state/intro)
+
+## Build Stages
+
+When you run `perseus serve`, several things happen:
+
+1. **Generate your app**: Compiles the server-side code and builds all pages
+2. **Build to Wasm**: Compiles the browser code to WebAssembly
+3. **Start server**: Launches the development server
+
+The `-w` flag enables watch mode - your app automatically rebuilds when you change code.
+
+**Tip**: For faster builds during development, see [Improving Compilation Times](/docs/fundamentals/compilation-times).
diff --git a/docs/0.5.x/en-US/state/amalgamation.md b/docs/0.5.x/en-US/state/amalgamation.md
new file mode 100644
index 0000000000..301df8e401
--- /dev/null
+++ b/docs/0.5.x/en-US/state/amalgamation.md
@@ -0,0 +1,13 @@
+# State amalgamation
+
+There are quite a few cases when you're using the state generation platform where you might like to generate state at both build-time *and* request-time, and Perseus has several ways of handling this. Generally, the request-time state will just completely override the build-time state, which is a little pointless, since it doesn't have access to the build-time state, and therefore there would really be no point in even using build-time state. However, you can also specify a custom strategy for resolving the two states, which is called *state amalgamation*. To our knowledge, Perseus is currently the only framework in the world that supports this (for some reason, since it's really not that hard to implement).
+
+Like [other state generation functions](:state/build), your state amalgamation function can be either fallible (with a [`BlamedError`](=prelude/struct.BlamedError@perseus)) or infallible, and it has access to a [`StateGeneratorInfo`](=prelude/struct.StateGeneratorInfo@perseus) instance. It's also asynchronous, and returns your state. The difference between it and other functions is that it also takes, as arguments, your build-time and request-time states (it does *not* take the HTTP request, so you'll have to extract any data from this that you want and put it into your request-time state). Here's an example of using it (albeit a rather contrived one):
+
+```rust
+{{#include ../../../examples/core/state_generation/src/templates/amalgamation.rs}}
+```
+
+Real-world examples of using state amalgamation are difficult to find, because no other framework supports this feature, although there have been requests for it to be supported in some very niche cases in the past. Since it involves very little code from Perseus, it is provided for those niche cases, and for cases where it would be generally useful as an alternative solution to a problem.
+
+One particular case that can be useful is having an `enum` state with variants for build-time, request-time, and post-amalgamation. The build-time state can be used for anything that can be done that early, and then the request-time state performs authentication, while the amalgamation draws it all together, ensuring that only the necessary stuff is actually sent to the client. Unfortunately, doing this would require a manual implementation of the traits that `ReactiveState` would normally implement, since it doesn't yet support `enum`s (but it will in a future version).
diff --git a/docs/0.5.x/en-US/state/browser.md b/docs/0.5.x/en-US/state/browser.md
new file mode 100644
index 0000000000..751bcc53b9
--- /dev/null
+++ b/docs/0.5.x/en-US/state/browser.md
@@ -0,0 +1,281 @@
+# Using State in Views
+
+This guide explains how to use state in your view functions with Sycamore 0.9.2.
+
+## Reactive State Basics
+
+When you derive `ReactiveState`, Perseus creates a reactive version of your struct:
+
+```rust
+// Your definition
+#[derive(Serialize, Deserialize, ReactiveState, Clone)]
+#[rx(alias = "CounterStateRx")]
+struct CounterState {
+ count: i32,
+ name: String,
+}
+
+// What Perseus creates (conceptually)
+struct CounterStateRx {
+ count: Signal,
+ name: Signal,
+}
+```
+
+## Using State in Views
+
+Use `#[auto_scope]` to simplify view function signatures:
+
+```rust
+#[auto_scope]
+fn my_view(state: CounterStateRx) -> View {
+ view! {
+ h1 { "Hello, " (state.name.get_clone()) "!" }
+ p { "Count: " (state.count.get()) }
+
+ button(on:click = move |_| {
+ state.count.set(*state.count.get() + 1);
+ }) {
+ "Increment"
+ }
+ }
+}
+```
+
+### Reading Values
+
+| Method | Use For | Example |
+|--------|---------|---------|
+| `.get()` | `Copy` types (`i32`, `bool`, etc.) | `state.count.get()` |
+| `.get_clone()` | `Clone` types (`String`, etc.) | `state.name.get_clone()` |
+
+### Setting Values
+
+```rust
+// Set to a new value
+state.count.set(42);
+
+// Update based on current value
+state.count.set(*state.count.get() + 1);
+
+// For strings
+state.name.set("New Name".to_string());
+```
+
+## Template Registration
+
+Use `.view_with_state()` for views that receive state:
+
+```rust
+pub fn get_template() -> Template {
+ Template::build("counter")
+ .build_state_fn(get_build_state)
+ .view_with_state(my_view) // Not .view()
+ .build()
+}
+```
+
+## Unreactive State
+
+For static content that doesn't need reactivity:
+
+```rust
+#[derive(Serialize, Deserialize, UnreactiveState, Clone)]
+struct PageInfo {
+ title: String,
+ description: String,
+}
+
+fn page_view(state: PageInfo) -> View {
+ view! {
+ h1 { (state.title) } // Direct access, no .get()
+ p { (state.description) }
+ }
+}
+
+pub fn get_template() -> Template {
+ Template::build("info")
+ .build_state_fn(get_build_state)
+ .view_with_unreactive_state(page_view)
+ .build()
+}
+```
+
+### When to Use Unreactive State
+
+- Static content that won't change client-side
+- Simpler API (no `.get()` / `.set()`)
+- Excluded from Hot State Reload (HSR) by default
+
+## Nested State
+
+For complex state structures, use `#[rx(nested)]`:
+
+```rust
+#[derive(Serialize, Deserialize, ReactiveState, Clone)]
+#[rx(alias = "UserStateRx")]
+struct UserState {
+ name: String,
+ age: i32,
+}
+
+#[derive(Serialize, Deserialize, ReactiveState, Clone)]
+#[rx(alias = "PageStateRx")]
+struct PageState {
+ #[rx(nested)]
+ user: UserState,
+ page_title: String,
+}
+
+#[auto_scope]
+fn page_view(state: PageStateRx) -> View {
+ view! {
+ h1 { (state.page_title.get_clone()) }
+ p { "Name: " (state.user.name.get_clone()) }
+ p { "Age: " (state.user.age.get()) }
+ }
+}
+```
+
+## Page State Store (PSS)
+
+Perseus caches page states for seamless navigation:
+
+```
+User visits /page1 → State stored in PSS
+User visits /page2 → State stored in PSS
+User returns to /page1 → State restored from PSS (with user's changes!)
+```
+
+### Implications
+
+- Form inputs are preserved when returning to a page
+- User interactions persist
+- No network request needed for cached pages
+
+### Configuring PSS Size
+
+```rust
+PerseusApp::new()
+ .pss_max_size(100) // Cache up to 100 pages (default: 25)
+```
+
+## Hot State Reload (HSR)
+
+During development, Perseus can preserve state across rebuilds.
+
+To exclude a state type from HSR (so you see fresh content):
+
+```rust
+#[derive(Serialize, Deserialize, ReactiveState, Clone)]
+#[rx(alias = "BlogPostRx")]
+#[rx(hsr_ignore)] // Add this
+struct BlogPost {
+ content: String,
+}
+```
+
+This is useful when editing content you want to preview immediately.
+
+## Complete Example
+
+```rust
+use perseus::prelude::*;
+use serde::{Deserialize, Serialize};
+use sycamore::prelude::*;
+
+#[derive(Serialize, Deserialize, ReactiveState, Clone)]
+#[rx(alias = "FormStateRx")]
+struct FormState {
+ name: String,
+ email: String,
+ message: String,
+ submitted: bool,
+}
+
+#[auto_scope]
+fn contact_form(state: FormStateRx) -> View {
+ let handle_submit = move |_| {
+ // In a real app, you'd send this to a server
+ web_sys::console::log_1(
+ &format!(
+ "Submitted: {} - {} - {}",
+ state.name.get_clone(),
+ state.email.get_clone(),
+ state.message.get_clone()
+ ).into()
+ );
+ state.submitted.set(true);
+ };
+
+ view! {
+ (if *state.submitted.get() {
+ view! {
+ div(class = "success") {
+ h2 { "Thank you!" }
+ p { "We'll be in touch soon." }
+ }
+ }
+ } else {
+ view! {
+ form(on:submit = handle_submit) {
+ label {
+ "Name: "
+ input(
+ type = "text",
+ bind:value = state.name
+ )
+ }
+ label {
+ "Email: "
+ input(
+ type = "email",
+ bind:value = state.email
+ )
+ }
+ label {
+ "Message: "
+ textarea(bind:value = state.message)
+ }
+ button(type = "submit") { "Send" }
+ }
+ }
+ })
+ }
+}
+
+#[engine_only_fn]
+async fn get_build_state(_info: StateGeneratorInfo<()>) -> FormState {
+ FormState {
+ name: String::new(),
+ email: String::new(),
+ message: String::new(),
+ submitted: false,
+ }
+}
+
+pub fn get_template() -> Template {
+ Template::build("contact")
+ .build_state_fn(get_build_state)
+ .view_with_state(contact_form)
+ .build()
+}
+```
+
+## Summary
+
+| Concept | Usage |
+|---------|-------|
+| Reactive state | `#[derive(ReactiveState)]` with `#[rx(alias = "...")]` |
+| View function | `#[auto_scope] fn view(state: StateRx) -> View` |
+| Read copy types | `state.field.get()` |
+| Read clone types | `state.field.get_clone()` |
+| Update state | `state.field.set(value)` |
+| Nested state | `#[rx(nested)]` on fields |
+| Unreactive state | `#[derive(UnreactiveState)]` |
+
+## Next Steps
+
+- [Global State](/docs/state/global) - Shared state across all pages
+- [Suspended State](/docs/state/suspense) - Client-side state loading
+- [Freezing and Thawing](/docs/state/freezing-thawing) - State persistence
diff --git a/docs/0.5.x/en-US/state/build.md b/docs/0.5.x/en-US/state/build.md
new file mode 100644
index 0000000000..3deb692a5b
--- /dev/null
+++ b/docs/0.5.x/en-US/state/build.md
@@ -0,0 +1,279 @@
+# Build-time State
+
+Build-time state is generated when you run `perseus build`. It's the most common state generation method.
+
+## Basic Example
+
+```rust
+use perseus::prelude::*;
+use serde::{Deserialize, Serialize};
+use sycamore::prelude::*;
+
+#[derive(Serialize, Deserialize, ReactiveState, Clone)]
+#[rx(alias = "PageStateRx")]
+struct PageState {
+ greeting: String,
+}
+
+#[auto_scope]
+fn page_view(state: PageStateRx) -> View {
+ view! {
+ h1 { (state.greeting.get_clone()) }
+ }
+}
+
+#[engine_only_fn]
+async fn get_build_state(_info: StateGeneratorInfo<()>) -> PageState {
+ PageState {
+ greeting: "Hello from build time!".to_string(),
+ }
+}
+
+pub fn get_template() -> Template {
+ Template::build("greeting")
+ .build_state_fn(get_build_state)
+ .view_with_state(page_view)
+ .build()
+}
+```
+
+## How It Works
+
+1. During `perseus build`, your `get_build_state` function runs
+2. The returned state is serialized and saved
+3. The page is pre-rendered to HTML with this state
+4. On request, the pre-rendered page is served instantly
+
+## StateGeneratorInfo
+
+Your build state function receives `StateGeneratorInfo`:
+
+```rust
+#[engine_only_fn]
+async fn get_build_state(info: StateGeneratorInfo<()>) -> PageState {
+ // Get the page path (e.g., "hello" for /greeting/hello)
+ let path = info.path;
+
+ // Get the locale (e.g., "en-US")
+ let locale = info.locale;
+
+ // Access helper state (covered later)
+ // let helper = info.extra;
+
+ PageState {
+ greeting: format!("Hello from {}", path),
+ }
+}
+```
+
+The generic `` is for helper state. Use `()` if you don't need it.
+
+## Error Handling
+
+You can return errors instead of panicking:
+
+```rust
+use std::fs;
+
+#[engine_only_fn]
+async fn get_build_state(
+ info: StateGeneratorInfo<()>
+) -> Result> {
+ let content = fs::read_to_string("content.txt")
+ .map_err(|e| BlamedError::server(None, e))?;
+
+ Ok(PageState { greeting: content })
+}
+```
+
+### BlamedError
+
+`BlamedError` annotates errors with who's responsible:
+
+```rust
+// Server's fault (most common)
+BlamedError::server(None, my_error)
+
+// With HTTP status code
+BlamedError::server(Some(StatusCode::INTERNAL_SERVER_ERROR), my_error)
+
+// Client's fault (for request-time)
+BlamedError::client(Some(StatusCode::BAD_REQUEST), my_error)
+```
+
+**Tip**: Use `?` with `.into()` for automatic conversion:
+```rust
+let data = fs::read_to_string("file.txt")?; // Auto-blames server
+```
+
+## Build Paths
+
+Create multiple pages from one template using build paths:
+
+```rust
+use perseus::prelude::*;
+use serde::{Deserialize, Serialize};
+use sycamore::prelude::*;
+
+#[derive(Serialize, Deserialize, ReactiveState, Clone)]
+#[rx(alias = "PostStateRx")]
+struct PostState {
+ title: String,
+ content: String,
+}
+
+#[auto_scope]
+fn post_view(state: PostStateRx) -> View {
+ view! {
+ h1 { (state.title.get_clone()) }
+ p { (state.content.get_clone()) }
+ }
+}
+
+#[engine_only_fn]
+async fn get_build_paths() -> BuildPaths {
+ BuildPaths {
+ paths: vec![
+ "hello-world".to_string(),
+ "rust-tips".to_string(),
+ "getting-started".to_string(),
+ ],
+ extra: ().into(),
+ }
+}
+
+#[engine_only_fn]
+async fn get_build_state(info: StateGeneratorInfo<()>) -> PostState {
+ // info.path is "hello-world", "rust-tips", etc.
+ match info.path.as_str() {
+ "hello-world" => PostState {
+ title: "Hello World".to_string(),
+ content: "Welcome to my blog!".to_string(),
+ },
+ "rust-tips" => PostState {
+ title: "Rust Tips".to_string(),
+ content: "Some useful Rust tips...".to_string(),
+ },
+ _ => PostState {
+ title: info.path.clone(),
+ content: "Content for this post".to_string(),
+ },
+ }
+}
+
+pub fn get_template() -> Template {
+ Template::build("post")
+ .build_paths_fn(get_build_paths)
+ .build_state_fn(get_build_state)
+ .view_with_state(post_view)
+ .build()
+}
+```
+
+This creates:
+- `/post/hello-world`
+- `/post/rust-tips`
+- `/post/getting-started`
+
+### BuildPaths Structure
+
+```rust
+BuildPaths {
+ // List of page paths under this template
+ paths: vec!["path1".to_string(), "path2".to_string()],
+
+ // Helper state (use () if not needed)
+ extra: ().into(),
+}
+```
+
+### Special Paths
+
+| Path | Result |
+|------|--------|
+| `""` (empty) | Template root (`/post` for `post` template) |
+| `"nested/path"` | Nested URL (`/post/nested/path`) |
+| `"with spaces"` | Auto URL-encoded |
+
+## Practical Example: Markdown Blog
+
+```rust
+use perseus::prelude::*;
+use serde::{Deserialize, Serialize};
+use sycamore::prelude::*;
+use std::fs;
+
+#[derive(Serialize, Deserialize, ReactiveState, Clone)]
+#[rx(alias = "PostStateRx")]
+struct PostState {
+ title: String,
+ html_content: String,
+}
+
+#[auto_scope]
+fn post_view(state: PostStateRx) -> View {
+ view! {
+ article {
+ h1 { (state.title.get_clone()) }
+ div(dangerously_set_inner_html = &state.html_content.get_clone())
+ }
+ }
+}
+
+#[engine_only_fn]
+async fn get_build_paths() -> BuildPaths {
+ // Read all .md files from the posts directory
+ let paths: Vec = fs::read_dir("posts")
+ .unwrap()
+ .filter_map(|entry| {
+ let path = entry.ok()?.path();
+ if path.extension()? == "md" {
+ path.file_stem()?.to_str().map(String::from)
+ } else {
+ None
+ }
+ })
+ .collect();
+
+ BuildPaths {
+ paths,
+ extra: ().into(),
+ }
+}
+
+#[engine_only_fn]
+async fn get_build_state(
+ info: StateGeneratorInfo<()>
+) -> Result> {
+ let markdown = fs::read_to_string(format!("posts/{}.md", info.path))?;
+
+ // Parse markdown (you'd use a proper parser here)
+ let title = markdown.lines().next().unwrap_or("Untitled").to_string();
+ let html_content = markdown; // In reality, convert to HTML
+
+ Ok(PostState { title, html_content })
+}
+
+pub fn get_template() -> Template {
+ Template::build("post")
+ .build_paths_fn(get_build_paths)
+ .build_state_fn(get_build_state)
+ .view_with_state(post_view)
+ .build()
+}
+```
+
+## When to Use Build-time State
+
+**Good for:**
+- Blog posts and articles
+- Documentation pages
+- Product catalogs
+- Any content known at build time
+
+**Not suitable for:**
+- User-specific data
+- Real-time information
+- Content that changes per request
+
+For dynamic content, see [Request-time State](/docs/state/request).
diff --git a/docs/0.5.x/en-US/state/freezing-thawing.md b/docs/0.5.x/en-US/state/freezing-thawing.md
new file mode 100644
index 0000000000..c15edcf5ab
--- /dev/null
+++ b/docs/0.5.x/en-US/state/freezing-thawing.md
@@ -0,0 +1,35 @@
+# Freezing and thawing
+
+One of the most unique, and most powerful features of the Perseus state platform is its system of *state freezing*. Imagine this: all your reactive (and unreactive) state types implement `Serialize` and `Deserialize`, right? We also have an internal cache of them that monitors all the updates that occur to the states of the last *N* pages a user has visited (by default, *N* is 25). So what if we iterated through all of those, serialized them to a string, and stored that? It would be a fullly stringified representation of the state of the app. And, if you build your app with all reactive components built into your state type (i.e. not using rogue `Signal`s that aren't a part of your page state), then you could restore your entire app perfectly from this string.
+
+Since v0.3.5, that has been built into Perseus.
+
+In fact, it's this feature that powers one of Perseus' most powerful development features: *hot state reloading* (HSR). In JS-land, there's *hot module reloading*, where the bundlers intelligently only swaps out the tiny little chunks of JS needed to update your app, allowing you, the developer, to stay in the same place while you're developing. If you're four states deep into debugging a login form, not having to be thrown back to the beginning every time you reposition a button is something you will *really* appreciate! However, this seems impossible in Wasm, because we don't have chunking yet. Perseus changes this by implementing state freezing/thawing at the framework level, allowing Perseus to automatically freeze your entire app's state, save it into the browser, reload the page to get the new code, and then instantly thaw your app, meaning the only times you will get thrown back to the beginning of that login form are when you change your app's data model.
+
+## Understanding state freezing
+
+State freezing can be slightly difficult to understand at an implementation level, because of the complexity of the internals of Perseus. Generally though, you can think of it like this: all your pages are literally having their states serialized to `String`s, and then those are all being combined with your global state (if you have one), and some other details, like the current route. This can then all be used by Perseus to *thaw* that string by deserializing everything and reconstituting it.
+
+## The process of thawing
+
+Critically, Perseus **does not** restore your state all at once, and this can be difficult to wrap your head around. The problem is that Perseus doesn't record any of your state types internally: it gets them from your view functions, and that means it can't thaw all your state at once, because it doesn't know what to deserialize your states into. For all it knows, your page states might by `u8`s! So, Perseus stores all the frozen state internally, and, each time the user goes to a new page, it checks if there's some frozen state known for that page, deserializing it if it can. If this fails, a popup error will be emitted, which can usually be solved by reloading the page to dispose of the corrupted frozen state. (Note that most accidental corruptions would break the very JSON structure of the thing, and would be caught immediately.) This also goes for the global state (frozen state is checked on the first `.get_global_state()` call to [`Reactor`](=prelude/struct.Reactor@perseus)).
+
+Note that Perseus will also automatically navigate back to the route the user was on when their state was thawed.
+
+You can control many aspects of thawing, including whether frozen state or new state is preferred, on a page-by-page basis using the [`ThawPrefs`](=state/struct.ThawPrefs@perseus), which you can read about at that link.
+
+## Example
+
+Here's a more complex example of using state freezing. There are two inputs, one for the global state, and one for the page state, which will be used to reactively set them, and then a button that freezes the whole app (using the `reactor.freeze()` method, which really is all you need to do!). For demonstration purposes, that's then synchronized to an input that takes in state that can be used to thaw the app, which is a slightly more complex (and fallible) process. Note the use of `#[cfg(client)]`, since state freezing/thawing can only take place on the client-side.
+
+```rust
+{{#include ../../../examples/core/freezing_and_thawing/src/templates/index.rs}}
+```
+
+## Storing frozen state
+
+Freezing your app's state can be extremely powerful, and it's often very useful to simply store this frozen state in a database, allowing your users to return to exactly where they left off after they log back in, or something similar. However, there is also the option of storing the state in the browser itself through [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API), a database that can be used to store complex objects. Interfacing with IndexedDB is extremely complex in JS, let alone in Wasm (where we have to use `web-sys` bindings), so Perseus uses [`rexie`](https://docs.rs/rexie/latest/rexie) to provide a convenient wrapper when the `idb-freezing` feature flag is enabled. This is managed through the [`IdbFrozenStateStore`](=state/struct.IdbFrozenStateStore@perseus) type, which uses a named database. If you like, you can do this manually: this type is provided as a common convenience, and because it's used internally for HSR.
+
+## Offline state replication
+
+*Coming soon!*
diff --git a/docs/0.5.x/en-US/state/global.md b/docs/0.5.x/en-US/state/global.md
new file mode 100644
index 0000000000..b933889315
--- /dev/null
+++ b/docs/0.5.x/en-US/state/global.md
@@ -0,0 +1,294 @@
+# Global State
+
+Global state is shared across all pages in your app. Use it for user preferences, authentication status, or any data that should persist across navigation.
+
+## Basic Example
+
+```rust
+// src/global_state.rs
+use perseus::prelude::*;
+use serde::{Deserialize, Serialize};
+
+#[derive(Serialize, Deserialize, ReactiveState)]
+#[rx(alias = "AppStateRx")]
+pub struct AppState {
+ pub theme: String,
+ pub logged_in: bool,
+ pub username: Option,
+}
+
+pub fn get_global_state_creator() -> GlobalStateCreator {
+ GlobalStateCreator::new()
+ .build_state_fn(get_build_state)
+}
+
+#[engine_only_fn]
+async fn get_build_state(_locale: String) -> AppState {
+ AppState {
+ theme: "light".to_string(),
+ logged_in: false,
+ username: None,
+ }
+}
+```
+
+## Registering Global State
+
+Add it to your `PerseusApp`:
+
+```rust
+// src/main.rs
+use perseus::prelude::*;
+
+mod global_state;
+mod templates;
+
+#[perseus::main(perseus_axum::dflt_server)]
+pub fn main() -> PerseusApp {
+ PerseusApp::new()
+ .template(crate::templates::index::get_template())
+ .global_state_creator(crate::global_state::get_global_state_creator())
+ .error_views(ErrorViews::unlocalized_development_default())
+}
+```
+
+## Accessing Global State
+
+Use the reactor to access global state from any view:
+
+```rust
+use perseus::prelude::*;
+use sycamore::prelude::*;
+use crate::global_state::AppStateRx;
+
+fn my_view() -> View {
+ // Get the reactor
+ let reactor = Reactor::::from_cx();
+
+ // Access global state
+ let global_state = reactor.get_global_state::();
+
+ view! {
+ div(class = format!("theme-{}", global_state.theme.get_clone())) {
+ h1 { "My App" }
+
+ (if *global_state.logged_in.get() {
+ view! {
+ p { "Welcome, " (global_state.username.get_clone().unwrap_or_default()) }
+ button(on:click = move |_| {
+ global_state.logged_in.set(false);
+ global_state.username.set(None);
+ }) {
+ "Logout"
+ }
+ }
+ } else {
+ view! {
+ button(on:click = move |_| {
+ global_state.logged_in.set(true);
+ global_state.username.set(Some("User".to_string()));
+ }) {
+ "Login"
+ }
+ }
+ })
+ }
+ }
+}
+```
+
+## Global State Methods
+
+| Method | Description |
+|--------|-------------|
+| `.get_global_state::()` | Get global state (panics if wrong type) |
+| `.try_get_global_state::()` | Get global state (returns `Option`) |
+
+## Request-time Global State
+
+You can also generate global state per request:
+
+```rust
+pub fn get_global_state_creator() -> GlobalStateCreator {
+ GlobalStateCreator::new()
+ .build_state_fn(get_build_state)
+ .request_state_fn(get_request_state)
+}
+
+#[engine_only_fn]
+async fn get_build_state(_locale: String) -> AppState {
+ AppState {
+ theme: "light".to_string(),
+ logged_in: false,
+ username: None,
+ }
+}
+
+#[engine_only_fn]
+async fn get_request_state(
+ _locale: String,
+ req: Request,
+) -> Result> {
+ // Check for auth cookie
+ let logged_in = req.headers()
+ .get("Cookie")
+ .map(|c| c.to_str().unwrap_or("").contains("session="))
+ .unwrap_or(false);
+
+ Ok(AppState {
+ theme: "light".to_string(),
+ logged_in,
+ username: if logged_in {
+ Some("User".to_string())
+ } else {
+ None
+ },
+ })
+}
+```
+
+### Combining Build and Request State
+
+Use amalgamation to merge both:
+
+```rust
+pub fn get_global_state_creator() -> GlobalStateCreator {
+ GlobalStateCreator::new()
+ .build_state_fn(get_build_state)
+ .request_state_fn(get_request_state)
+ .amalgamate_states_fn(amalgamate_states)
+}
+
+#[engine_only_fn]
+fn amalgamate_states(
+ build_state: AppState,
+ request_state: AppState,
+) -> AppState {
+ AppState {
+ // Keep theme from build (or could use request)
+ theme: build_state.theme,
+ // Use auth info from request
+ logged_in: request_state.logged_in,
+ username: request_state.username,
+ }
+}
+```
+
+## Common Use Cases
+
+### Theme Preference
+
+```rust
+#[derive(Serialize, Deserialize, ReactiveState)]
+#[rx(alias = "ThemeStateRx")]
+pub struct ThemeState {
+ pub mode: String, // "light" or "dark"
+}
+
+// In a component:
+fn theme_toggle() -> View {
+ let reactor = Reactor::::from_cx();
+ let theme = reactor.get_global_state::();
+
+ view! {
+ button(on:click = move |_| {
+ let current = theme.mode.get_clone();
+ theme.mode.set(if current == "light" {
+ "dark".to_string()
+ } else {
+ "light".to_string()
+ });
+ }) {
+ "Toggle Theme"
+ }
+ }
+}
+```
+
+### Shopping Cart
+
+```rust
+#[derive(Serialize, Deserialize, ReactiveState)]
+#[rx(alias = "CartStateRx")]
+pub struct CartState {
+ #[rx(nested)]
+ pub items: Vec,
+ pub total: f64,
+}
+
+#[derive(Serialize, Deserialize, ReactiveState, Clone)]
+#[rx(alias = "CartItemRx")]
+pub struct CartItem {
+ pub id: String,
+ pub name: String,
+ pub price: f64,
+ pub quantity: i32,
+}
+```
+
+## Important Warnings
+
+### Request-time Global State Pitfalls
+
+If you use **only** request-time global state:
+
+1. **Build-time access will panic** - Any page using `.get_global_state()` during build will crash
+2. **Hydration errors** - Pages with build state won't see request data until client-side
+
+**Recommendation**: Always provide build-time defaults, even if you override at request-time.
+
+### Safe Pattern
+
+```rust
+pub fn get_global_state_creator() -> GlobalStateCreator {
+ GlobalStateCreator::new()
+ .build_state_fn(get_build_state) // Always provide defaults
+ .request_state_fn(get_request_state) // Override per-request
+}
+
+#[engine_only_fn]
+async fn get_build_state(_locale: String) -> AppState {
+ // Safe defaults
+ AppState {
+ logged_in: false,
+ username: None,
+ }
+}
+```
+
+## Differences from Page State
+
+| Feature | Page State | Global State |
+|---------|------------|--------------|
+| Scope | Single page | Entire app |
+| Cached | In PSS (25 pages) | Always available |
+| Updates | Per-page | Shared across pages |
+| Clone required | Yes | No |
+
+## Summary
+
+```rust
+// 1. Define global state
+#[derive(Serialize, Deserialize, ReactiveState)]
+#[rx(alias = "AppStateRx")]
+pub struct AppState { /* ... */ }
+
+// 2. Create the creator
+pub fn get_global_state_creator() -> GlobalStateCreator {
+ GlobalStateCreator::new()
+ .build_state_fn(get_build_state)
+}
+
+// 3. Register in PerseusApp
+PerseusApp::new()
+ .global_state_creator(get_global_state_creator())
+
+// 4. Use in views
+let reactor = Reactor::::from_cx();
+let state = reactor.get_global_state::();
+```
+
+## Next Steps
+
+- [Freezing and Thawing](/docs/state/freezing-thawing) - Persist state to localStorage
+- [The Reactor](/docs/fundamentals/reactor) - More reactor capabilities
diff --git a/docs/0.5.x/en-US/state/helper.md b/docs/0.5.x/en-US/state/helper.md
new file mode 100644
index 0000000000..334340fcd7
--- /dev/null
+++ b/docs/0.5.x/en-US/state/helper.md
@@ -0,0 +1,21 @@
+# Helper state
+
+For a long time, the Perseus state platform consisted only of what you've read about so far, but there was a problem with this, one that's quite subtle. Let's say you have a blog where posts can be organized into series, and then there's a `series` template that lists each series in order. How would you write the state generation code for the series template? (Assuming it can all be done at build-time, for simplicity.)
+
+Well, you might think, we can iterate over all the blog posts in the build paths logic, and read their series metadata properties to collate a list of all the series, so that's the first part done. (Right on!) And then for the actual build state generation, you'd just need to find all the blog posts that are a part of the given series. But how can we do that?
+
+The best way is to iterate through all the blog posts again, which means, since the builds for all the series pages are done in parallel, if you have ten series, you're iterating through all those posts and reading every single one of them *eleven* times (+1 for the build paths logic). This is totally unreasonable, especially if your blog posts are on a server, rather than a local directory, and this could massively slow down build times. What would be good is if we could somehow only iterate through everything once, and just store a map of which posts are in what series that we can share through all the actual build state generations.
+
+Because the only solutions to this problem are ugly workarounds, we decided to implement this as a first-class feature in Perseus: helper state! This is what that generic on [`StateGeneratorInfo`](=prelude/struct.StateGeneratorInfo@perseus) is all about: it denotes the type of your helper state.
+
+Importantly, helper state isn't really like any of the other state systems in Perseus, because it's not available to the views you create, and it never gets to the client: it's just a helper for the rest of your state generation. Internally, Perseus calls this *extra state*, but helper state has come to be its name outside the codebase.
+
+Here's an example of using helper/extra state:
+
+```rust
+{{#include ../../../examples/core/helper_build_state/src/templates/index.rs}}
+```
+
+Here, we've defined a special extra type called `HelperState` (but it can be called anything you like), and then we've used that for the `extra` parameter of [`BuildPaths`](=prelude/struct.BuildPaths@perseus). This allows the build paths function, which is executed once, to pass on useful information to the build state systems, potentially reducing the volume of computations that need to be performed. Note the use of `.into()` on the `HelperState` to convert it into a `Box`ed form that Perseus is more comfortable with internally. In fact, it's only when we call `.get_extra()` on the [`StateGeneratorInfo`](=prelude/struct.StateGeneratorinfo@perseus) provided to the `get_build_state` function that Perseus performs the conversions necessary to retrieve our helper state type (which means specifying the generic incorrectly can lead to panics at build-time, but these would be caught before your app went live, don't worry). Finally, the `.0` is just used to access the `String` inside `HelperState`.
+
+That's pretty much all there is to helper state, and it's available at all stages of the state generation process, right up to [request-time state](:state/request). If there are any parts of request-time state that you can do at build-time, this is the best way to do them if you're not using [state amalgamation](:state/amalgamation).
diff --git a/docs/0.5.x/en-US/state/incremental.md b/docs/0.5.x/en-US/state/incremental.md
new file mode 100644
index 0000000000..45559453b9
--- /dev/null
+++ b/docs/0.5.x/en-US/state/incremental.md
@@ -0,0 +1,31 @@
+# Incremental generation
+
+One of the most powerful features of Perseus' state generation platform is the *incremental generation* system, which can be thought of as the request-time counterpart to the *build paths* strategy. Let's say you run an e-commerce website, and you have ten million products. Do you want to build ten million pages at build-time? Probably not!
+
+A much better way of handling this would be to instead pre-render only your top 100 products or so at build-time (remember that Perseus builds are lightning fast after Rust compilation, so even that many is still light; this website generates several hundred documentation pages in less than half a second), and somehow render the others later, only when they're requested. This kind of 'on-demand' approach would be best if, when a user requested a page that wasn't prerendered at build-time, it's not just built for them, but also cached for future use, *as if* it had been built at build time. This kind of extension of the build process to just keep happening also allows you to add new products to your site in the future, and they'll be prerendered properly the first time somebody requests them (using [revalidation](:state/revalidation) on some kind of inventory page makes the most sense here).
+
+All this is supported with literally one single line of code: `.incremental_generation()`. No arguments, no special functions, that's all you need, and Perseus will change its routing algorithm slightly to still match all the pages you render at build-time, but to also say "when a page under this template is requested that we don't know about yet, bear with it and try it out on the server anyway". The server will see if it's been prerendered in the past, and it'll provide it if it was, and otherwise it will run your `get_build_state` function, providing whatever path the user gave.
+
+Of course, this could mean that somebody might go to the page `/product/faster-than-light-engine`, which might unfortunately still be in development, so that page shouldn't exist. And *this* is why we have `BlamedError` in build state! So that you can say "if this page actually shouldn't exist, return an error that's blamed on the *client*, with HTTP status 404". This will be rendered by Perseus into a *404 Not Found* page automatically (but error views won't be cached, meaning that, if this product becomes available in the future, everything will work out).
+
+Note that incremental generation is fully compatible with all other state generation methods, including request-time state generation and both forms of revalidation.
+
+Here's an example of incremental generation:
+
+```rust
+{{#include ../../../examples/core/state_generation/src/templates/incremental_generation.rs}}
+```
+
+Note the use of build paths (you still have to generate *some* pages, otherwise incremental generation will be completely ignored and you'll just get an index page), and the conditional in `get_build_state` that checks for the illegal path `tests`, returning a `BlamedError` with blame `ErrorBlame::Client(Some(404))`, where `404` is the HTTP status code for a page not being found! Here, we're accompanying that with a `std::io::Error`, but you could use any error type you like.
+
+Note that incrementally generated pages will be placed in the *mutable store*, which you should keep in mind when deploying to read-only environments, such as serverless functions (work to support serverless functions with Perseus for more advanced apps is ongoing: they will *work*, but caching will not be ideal at all).
+
+
+
+How does Vercel handle that?
+
+If you're from the JS world, you might be familiar with NextJS, which also supports incremental generation, but they offer a serverless function service that works with it seamlessly. Details about how this works are not public, but they seem to be using a colocated database setup to achieve this, or they may be using function-specific incremental caches (which would lead to lower performance, so this is unlikely).
+
+You might wonder if Perseus could run in the same system. So have we, and this is an avenue we intend to explore in 2023.
+
+
diff --git a/docs/0.5.x/en-US/state/intro.md b/docs/0.5.x/en-US/state/intro.md
new file mode 100644
index 0000000000..bb8a42a61a
--- /dev/null
+++ b/docs/0.5.x/en-US/state/intro.md
@@ -0,0 +1,150 @@
+# Understanding State
+
+State is at the core of Perseus. This section explains how state works and when to use different generation strategies.
+
+## What is State?
+
+Think of a **template** as a stencil with holes. **State** is the data that fills those holes.
+
+For a blog post template, the state might include:
+- Title
+- Author
+- Content
+- Tags
+
+When you combine template + state, you get a **page**.
+
+## State Lifecycle
+
+State starts on the engine-side (server) where it's generated, then travels to the client-side (browser) where it becomes reactive.
+
+```
+Engine-side: Client-side:
+┌─────────────┐ ┌─────────────┐
+│ Generate │──────>│ Deserialize │
+│ State │ │ State │
+└─────────────┘ └──────┬──────┘
+ │
+ ┌──────▼──────┐
+ │ Make │
+ │ Reactive │
+ └──────┬──────┘
+ │
+ ┌──────▼──────┐
+ │ Store in │
+ │ Page State │
+ │ Store (PSS) │
+ └─────────────┘
+```
+
+## When is State Generated?
+
+State can be generated in three places:
+
+| Method | When | Use Case |
+|--------|------|----------|
+| **Build-time** | During `perseus build` | Static content, blog posts, docs |
+| **Request-time** | On each HTTP request | User-specific data, auth |
+| **Client-side** | In the browser | Suspended/lazy loading |
+
+### Build-time State
+
+Generated without knowledge of who's viewing the page. Perfect for:
+- Blog posts from Markdown files
+- Product catalogs
+- Documentation
+
+### Request-time State
+
+Generated for each request, with access to cookies, headers, etc. Use for:
+- Personalized dashboards
+- Authenticated content
+- Real-time data
+
+### Suspended State
+
+Generated client-side. Useful when:
+- Part of the page loads slower
+- You want to show the rest of the page first
+- Data depends on client-side conditions
+
+## Templates and Pages
+
+A single template can produce many pages through **build paths**:
+
+```rust
+// Template: "post"
+// Build paths: ["hello-world", "rust-tips", "web-dev"]
+//
+// Results in pages:
+// - /post/hello-world
+// - /post/rust-tips
+// - /post/web-dev
+```
+
+Without explicit build paths, a template produces one page at its own path (e.g., `about` template → `/about` page).
+
+## Reactive State
+
+When state reaches the browser, Perseus makes it **reactive**:
+
+```rust
+// Your state definition
+#[derive(Serialize, Deserialize, ReactiveState, Clone)]
+#[rx(alias = "CounterStateRx")]
+struct CounterState {
+ count: i32,
+}
+
+// What ReactiveState creates (conceptually)
+struct CounterStateRx {
+ count: Signal,
+}
+```
+
+This means:
+- Call `.get()` to read values
+- Call `.set()` to update values
+- UI automatically updates when state changes
+
+## State Storage
+
+Perseus stores all page states in the **Page State Store (PSS)**:
+
+- When you visit a page, its state is cached
+- Return to a page? State is restored (including form inputs!)
+- Default capacity: 25 pages
+
+Configure with:
+```rust
+PerseusApp::new()
+ .pss_max_size(50) // Store up to 50 pages
+```
+
+## What if I Don't Need State?
+
+You can use as much or as little of the state platform as you need:
+
+| Scenario | Recommendation |
+|----------|---------------|
+| Static site | Build-time state only |
+| No dynamic data | Unreactive state |
+| Purely static pages | Template without state |
+| Not using Perseus features | Consider plain Sycamore |
+
+## Quick Reference
+
+| State Type | When to Use |
+|------------|-------------|
+| Build-time | Content known at build time |
+| Request-time | Content depends on the request |
+| Revalidation | Build-time content that needs refreshing |
+| Incremental | Many possible pages, generate on demand |
+| Suspended | Heavy components that can load later |
+| Global | Shared across all pages |
+
+## Next Steps
+
+- [Build-time State](/docs/state/build) - The most common approach
+- [Request-time State](/docs/state/request) - Per-request generation
+- [Using State](/docs/state/browser) - Working with reactive state in views
diff --git a/docs/0.5.x/en-US/state/manual.md b/docs/0.5.x/en-US/state/manual.md
new file mode 100644
index 0000000000..399e6648be
--- /dev/null
+++ b/docs/0.5.x/en-US/state/manual.md
@@ -0,0 +1,82 @@
+# Manually implementing `ReactiveState`
+
+For all its benefits, the `ReactiveState` derive macro does have limitations, and you'll occasionally come across a state type that you just can't derive it on. Currently, this will apply to any `enum` state type (though this will be fixed in a future version), any `struct` with generics, and any other type where you need fine-grained control over exactly how its reactivity works. Most of the time, however, this will be totally unnecessary (though reading this page is still recommended for a conceptual understanding of how the macro works).
+
+Note that, if you want custom reactive primitives, such as a reactive `Vec`, `HashMap`, etc., these already exist [here](=state/rx_collections@perseus), once you enable the `rx-collections` feature flag! If you'd like to extend these, see the [module documentation](=state/rx_collections@perseus), since it's highly structured to enable easy user extension (and please consider contributing your new types back to the community through a crate, and let us know if you do!).
+
+## What the macro does
+
+The `ReactiveState` macro is responsible for the following (assuming your state is called `MyState`, with reactive alias `MyStateRx`):
+
+- Creating a reactive version of your state as a separate type (`MyStateRx`)
+- Implementing `MakeRx` for `MyState`
+- Implementing `MakeUnrx` for `MyStateRx` (including [suspense](:state/suspense) implementation)
+- Implementing [`Freeze`](=state/trait.Freeze@perseus) for `MyStateRx`
+
+One thing worth noting is that the reactive type isn't actually called `MyStateRx`, it's named internally, and then given a type alias (but this behavior may change in future).
+
+## How to do that yourself
+
+Your best resource for understanding how the macro works is the code itself, which is fairly self-explanatory if you look mostly at the `quote!` sections (which output the actual code the macro creates). Even if you have no experience with macro development, this code should at least be somewhat helpful to you: you can find it [here](https://github.com/framesurge/perseus/blob/main/packages/perseus-macro/src/rx_state.rs).
+
+### 1. Creating a reactive type
+
+This is probably the easiest stage, because it just involves copying and pasting your existing type, just with all the fields being either wrapped in `RcSignal`s or being their respective reactive version (e.g. if you're nesting the field `foo` of type `FooState`, which has `ReactiveState` derived, then you would use `FooStateRx` or similar here).
+
+Be sure to derive `Clone` on this type.
+
+### 2. Implementing `MakeRx`
+
+The [`MakeRx`](=state/trait.MakeRx@perseus) trait is the backbone of the Perseus reactive state platform, but it's actually surprisingly simply to implement! All you need to do is something like this:
+
+```rust
+impl MakeRx for MyState {
+ type Rx = MyStateRx;
+ fn make_rx(self) -> Self::Rx {
+ // Convert `MyState` -> `MyStateRx`
+ }
+}
+```
+
+Usually, the body of that `make_rx()` function will be simply wrapping all the existing fields in `create_rc_signal`, or calling `.make_rx()` on them, if they're nested.
+
+### 3. Implementing `MakeUnrx`
+
+The [`MakeUnrx`](=state/trait.MakeUnrx@perseus) trait is slightly more complicated, because it involves converting out of `RcSignal`s, and also the suspense system. Like `MakeRx`, there is an associated type `Unrx`, which should just reference your unreactive state type (which must implement `Serialize + Deserialize + MakeRx`). For nested reactive fields, you can simply call `.make_unrx()` to make them unreactive, whereas non-nested fields will need something like this:
+
+```rust
+(*self.my_field.get_untracked()).clone()
+```
+
+The trickiest part of this is the `compute_suspense()` function (which must be target-gated as `#[cfg(client)]`). If you're not using [suspended state](:state/suspense), you can safely leave the body of this completely empty, but if you are, you'll need to get acquainted with the [`compute_suspense`](=state/fn.compute_suspense@perseus) and [`compute_suspense_nested`](=state/fn.compute_suspense_nested@perseus) functions. These simply take the provided Sycamore reactive scope, a clone of the reactive field, and then the future returned by your suspense handler.
+
+The most complex part of this is the suspense handler, because you want to call the function, but not `.await` on it, meaning the future can be handled by Perseus appropriately. To do this, you'll want to call your handler like this:
+
+```rust
+my_handler(
+ cx,
+ create_ref(cx, self.my_field.clone())
+)
+```
+
+Notice how `create_ref()` is used on the field, which produces a reference scoped to the given context (incidentally, this is how all those scoped lifetimes are handled in Perseus).
+
+### 4. Implementing `Freeze`
+
+Once youv've done `MakeUnrx`, you're over the hump, and now you can pretty much just copy this code, substituting in the names of your state types of course:
+
+```rust
+impl Freeze for MyStateRx {
+ fn freeze(&self) -> String {
+ use perseus::state::MakeUnrx;
+ let unrx = self.clone().make_unrx();
+ serde_json::to_string(&unrx).unwrap()
+ }
+}
+```
+
+That `.unwrap()` is nearly always absolutely safe, provided any maps in your state have simple stringifable keys, as opposed to, say, tuples, which can't be keys in the JSON specification. If you are using a pattern like that, this would always panic, and that would unfortunately not be compatible with the Perseus state platform.
+
+## Unreactive state
+
+If you find the `UnreactiveState` macro doesn't work for some particular one of your types (usually one with generics), you can always implement it manually by implementing the [`UnreactiveState`](=state/trait.UnreactiveState@perseus) trait, which has no methods, no associated types, and nothing else: it's simply a marker trait! Perseus then uses that to figure out how it should handle reactivity for those particular types.
diff --git a/docs/0.5.x/en-US/state/request.md b/docs/0.5.x/en-US/state/request.md
new file mode 100644
index 0000000000..dfda23beaf
--- /dev/null
+++ b/docs/0.5.x/en-US/state/request.md
@@ -0,0 +1,229 @@
+# Request-time State
+
+Request-time state is generated on each HTTP request, giving you access to cookies, headers, and other request data.
+
+## When to Use
+
+- User authentication and personalized content
+- Dynamic data that changes per request
+- Access to cookies or custom headers
+- IP-based customization
+
+## Basic Example
+
+```rust
+use perseus::prelude::*;
+use serde::{Deserialize, Serialize};
+use sycamore::prelude::*;
+
+#[derive(Serialize, Deserialize, ReactiveState, Clone)]
+#[rx(alias = "DashboardStateRx")]
+struct DashboardState {
+ username: String,
+ ip_address: String,
+}
+
+#[auto_scope]
+fn dashboard_view(state: DashboardStateRx) -> View {
+ view! {
+ h1 { "Welcome, " (state.username.get_clone()) "!" }
+ p { "Your IP: " (state.ip_address.get_clone()) }
+ }
+}
+
+#[engine_only_fn]
+async fn get_request_state(
+ _info: StateGeneratorInfo<()>,
+ req: Request,
+) -> Result> {
+ // Access request headers
+ let ip = req
+ .headers()
+ .get("X-Forwarded-For")
+ .and_then(|v| v.to_str().ok())
+ .unwrap_or("Unknown")
+ .to_string();
+
+ // In a real app, you'd validate a session cookie here
+ let username = "User".to_string();
+
+ Ok(DashboardState {
+ username,
+ ip_address: ip,
+ })
+}
+
+pub fn get_template() -> Template {
+ Template::build("dashboard")
+ .request_state_fn(get_request_state)
+ .view_with_state(dashboard_view)
+ .build()
+}
+```
+
+## The Request Object
+
+Your request state function receives a `Request` with:
+
+```rust
+#[engine_only_fn]
+async fn get_request_state(
+ info: StateGeneratorInfo<()>,
+ req: Request,
+) -> MyState {
+ // Access headers
+ let headers = req.headers();
+
+ // Get a specific header
+ if let Some(auth) = headers.get("Authorization") {
+ // Validate token...
+ }
+
+ // Get cookies (from Cookie header)
+ if let Some(cookies) = headers.get("Cookie") {
+ // Parse and use cookies...
+ }
+
+ // info still contains path and locale
+ let path = info.path;
+ let locale = info.locale;
+
+ // ...
+}
+```
+
+**Note**: The request body is not available. Use [custom API endpoints](/docs/fundamentals/head-headers) for POST data.
+
+## Error Handling
+
+Request-time functions use `BlamedError` to indicate who caused the error:
+
+```rust
+#[engine_only_fn]
+async fn get_request_state(
+ _info: StateGeneratorInfo<()>,
+ req: Request,
+) -> Result> {
+ // Check authentication
+ let auth_header = req.headers()
+ .get("Authorization")
+ .ok_or_else(|| BlamedError::client(
+ Some(http::StatusCode::UNAUTHORIZED),
+ AuthError::MissingToken
+ ))?;
+
+ // Validate token
+ let user = validate_token(auth_header)
+ .map_err(|e| BlamedError::client(
+ Some(http::StatusCode::FORBIDDEN),
+ e
+ ))?;
+
+ Ok(MyState { user })
+}
+```
+
+| Blame | When to Use | HTTP Status |
+|-------|-------------|-------------|
+| `BlamedError::client(...)` | Bad request, auth failure | 400, 401, 403 |
+| `BlamedError::server(...)` | Database error, internal issue | 500 |
+
+## Combining with Build State
+
+You can have both build-time and request-time state:
+
+```rust
+pub fn get_template() -> Template {
+ Template::build("page")
+ .build_state_fn(get_build_state) // Runs at build time
+ .request_state_fn(get_request_state) // Runs per request
+ .view_with_state(page_view)
+ .build()
+}
+```
+
+When both are used:
+1. Build state runs first (at build time)
+2. Request state runs per request
+3. Use [state amalgamation](/docs/state/amalgamation) to combine them
+
+## Common Use Cases
+
+### Authentication Check
+
+```rust
+#[engine_only_fn]
+async fn get_request_state(
+ _info: StateGeneratorInfo<()>,
+ req: Request,
+) -> Result> {
+ let session_cookie = req.headers()
+ .get("Cookie")
+ .and_then(|c| parse_session_cookie(c.to_str().ok()?))
+ .ok_or_else(|| BlamedError::client(
+ Some(http::StatusCode::UNAUTHORIZED),
+ AuthError::NotLoggedIn
+ ))?;
+
+ let user = validate_session(&session_cookie)
+ .await
+ .map_err(|e| BlamedError::server(None, e))?;
+
+ Ok(AuthState {
+ user_id: user.id,
+ username: user.name,
+ is_admin: user.role == "admin",
+ })
+}
+```
+
+### Locale-based Content
+
+```rust
+#[engine_only_fn]
+async fn get_request_state(
+ info: StateGeneratorInfo<()>,
+ req: Request,
+) -> ContentState {
+ // Use Accept-Language header or info.locale
+ let preferred_locale = req.headers()
+ .get("Accept-Language")
+ .and_then(|v| v.to_str().ok())
+ .unwrap_or(&info.locale);
+
+ ContentState {
+ greeting: get_localized_greeting(preferred_locale),
+ }
+}
+```
+
+## Performance Considerations
+
+Request-time state runs on every request, so:
+
+- Keep database queries minimal
+- Cache where possible
+- Consider if build-time + revalidation would work instead
+
+For content that changes infrequently, see [Revalidation](/docs/state/revalidation).
+
+## Important Notes
+
+1. **The request is read-only** - Modifying it has no effect
+2. **No request body** - Only headers and method are available
+3. **Can't be exported** - Apps with request state need a server
+
+## Template Configuration
+
+```rust
+Template::build("my-page")
+ .request_state_fn(get_request_state) // Enable request state
+ .view_with_state(my_view)
+ .build()
+```
+
+## Next Steps
+
+- [State Amalgamation](/docs/state/amalgamation) - Combining build and request state
+- [Revalidation](/docs/state/revalidation) - Refreshing build-time state
+- [Heads and Headers](/docs/fundamentals/head-headers) - Setting response headers
diff --git a/docs/0.5.x/en-US/state/revalidation.md b/docs/0.5.x/en-US/state/revalidation.md
new file mode 100644
index 0000000000..3836000485
--- /dev/null
+++ b/docs/0.5.x/en-US/state/revalidation.md
@@ -0,0 +1,29 @@
+# Revalidation
+
+Sometimes, you'll want to use build-time state generation, but you'll want to update the state you've generated at a later date. For example, let's say you have a website that lists the latest news, and build state is used to do that. If you want to update this news every hour, you could do that with revalidation! (This avoids much of the overhead of request-time state, which must be generated before every single page load, and has no opportunity for caching.)
+
+Generally, if you can use it, revalidation will yield better performance than request-time state.
+
+## Time-based revalidation
+
+The first type of revalidation is the simplest: you set a schedule with `.revalidate_after()` on `Template`, which takes either a `Duration` (from `chrono` or the standard library) or a string of the form ``, like `1h` for one hour. You can read more about that [here](=template/struct.TemplateInner@perseus).
+
+This will cause the Perseus build process to, for each page that this template generates, note down the current time, and write that to a file. Then, on each request, it will check if the current time is later than that recorded time, plus the revalidation interval. If so, then it will re-execute the build state function, and update the state accordingly. Templates using revalidation have their pages stored in the mutable store, since they may update later.
+
+Crucially, this is lazy revalidation: Perseus will not immediately revalidate a page once the revalidation interval is reached. For example, if our news site isn't very popular for its first month, and only gets two visits per day, it won't revalidate 24 times, it will probably revalidate twice: because only two people visited. This also means that revalidation can behave in unexpected ways. Let's say you have a page that revalidates every five seconds, and it's built at second 0. If, no-one requests it until second 6, and then there's a request every second, it will revalidate at second 6, then second 11, then second 16, etc. You may need to re-read that to understand this, and it's usually not a problem, unles syou have very strict requirements.
+
+Note that this is all page-specific, so it's entirely possible for two different pages in the same template to have teh same revalidation interval and revalidate at different times.
+
+## Logic-based revalidation
+
+When you have more stringent needs, you might wish to use logic-based revalidation, which is based on the `.should_revalidate_fn()` method on `Template`. To this, you provide an `async` function of the usual sort with the usual `BlamedError` error handling (see [here](:state/build) for an explanation of that) that takes a [`StateGeneratorInfo`](=prelude/struct.StateGeneratorInfo@perseus) instance and the user's request, and you return a `bool`: if it's true, the page will revalidate, but, if `false`, the old state will stand. This can be used to do more advanced things like having a database of new news, but also having a micro-site set to tell you whether or not there is new news. Thus, you can perform the quicker check to the micro-site (which acts as a [canary](https://en.wikipedia.org/wiki/Sentinel_species)) to avoid unnecessary revalidations, which will improve performance.
+
+Using both logic-based revalidation *and* time-based revalidation is perfectly permissible, as the logic-based revalidation will only be executed on the interval of the time-based. For our news site, therefore, we might want to use the logic-based revalidation to check a canary as to whether or not there is any new news, and then only run that check hourly. This would lead to hourly checks of whether or not we *should* revalidate, rather than just blindly doing so, which can improve performance greatly.
+
+## Example
+
+An example of using both logic-based and time-based revalidation together is below.
+
+```rust
+{{#include ../../../examples/core/state_generation/src/templates/revalidation.rs}}
+```
diff --git a/docs/0.5.x/en-US/state/suspense.md b/docs/0.5.x/en-US/state/suspense.md
new file mode 100644
index 0000000000..ec78bb745e
--- /dev/null
+++ b/docs/0.5.x/en-US/state/suspense.md
@@ -0,0 +1,45 @@
+# Suspended state
+
+The vast majority of state generation is handled on the engine-side in Perseus apps, but there's a way to do this kind of thing on the client-side as well, called *suspended state*. This is basically where you tell Perseus to generate a default for one or more of the fields of your state type, but to modify this reactively with an asynchronous function once the page is ready on the client-side. This could be used to, say, render content that is client-specific, but that would be too onerous to render on the engine-side. Generally, unless you're accessing browser-specific parameters, there should be no difference between the capabilities of suspended state and [request-time state](:state/request), except that the former can be faster if it takes a while to fetch the state in question (because the page is still rendered, just not all of it).
+
+If you want to render entire sections of content in a delayed fashion, check out [delayed widgets](:capsules/using), which are a superior solution to that particular problem.
+
+## How is this different from Sycamore's `Suspense`?
+
+Sycamore has a component called `Suspense` that allows you to perform asynchronous rendering, for example to fetch some data before you render. This is very conceptually similar to Perseus' suspended state system, except it's less tightly integrated with the state platform, and it actually proves totally incompatible with the Perseus build process at present. In short, anything you might do with `Suspense` can be done with suspended state instead in a way that is more Perseus-ey.
+
+## Understanding suspended state
+
+Suspended state has no effect on the engine-side, that's the first thing to clear up, and it also works on a field-by-field basis. You'll set it up using the `#[rx(suspense = "my_function")]` derive macro helper, which you can use to annotate a field of any state type that derives `ReactiveState` (but not `UnreactiveState`: you'll soon see why). The `my_function` in that is the name of a function that will be called, once your page is ready on the client-side, to replace whatever value was generated as a default on the engine with something more fitting. This means you still have to render *something* for these suspended fields on the engine-side, and that will be used as a fallback while the 'real' state is being fetched on the client-side.
+
+What `my_function` will then do is be given a copy of the reactive version of *just that field*, and it will be expected to `.set()` it to whatever value it likes. This means you can't use `UnreactiveState` with suspense.
+
+## Suspended state types
+
+You might think you can just whack `#[rx(suspense = "my_function")]` on a field and you're done, but unfortunately it's not that simple: you need to make sure that field is compatible first. Because any kind of asynchronous suspense logic only has access to the one field it's working on, it has no way to directly modify the view. This means that, if an error occurs, it has no way to report it. Hence, Perseus mandates that any suspended fields must be wrapped in a `Result`, where `E` is some error type. If you're certain your suspense can't fail, you can use [`SerdeInfallible`](=prelude/struct.SerdeInfallible@perseus) as the error type (which is a version of `std::convert::Infallible` that can be serialized and deserialized, not that it ever will be). This means you also have to handle any errors directly in your view logic, which enforces correct, and infinitely flexible, error handling of suspended state issues.
+
+If you're using nested suspended state, you should use [`RxResult`](prelude/struct.RxResult@perseus) instead, which is a version of `Result` that's integrated with Perseus' reactive state system. In essence, its reactive version is an `RcSignal, E>>`, which means you can reactively set it to be an error, and you can also reactively set its `Ok` variant. Its reactive version is `RxResultRx`.
+
+Note that you can use suspended state on nested fields without a problem, but you can't do something like have the `nested` field be suspended, as well as having the `nested.foo` field be suspended, because then you could have conflicting settings of `nested.foo`. Attempting to do this will simply not work.
+
+## Suspended state handlers
+
+The handler functions provided to the derive helper macro should have a signature like this:
+
+```rust
+fn my_function<'a>(cx: Scope<'a>, suspended_field: &'a MySuspendedFieldTy) -> Result<(), E>
+```
+
+Notice how this function returns a `Result<(), E>`. This is essentially a convenience: any errors returned from this will be `.set()` on the field provided, since it's guaranteed to be a result. This might seem a bit magical, and you don't have to use it if you don't want to, but it can lead to better ergonomics on occasion, especially with the `?` operator.
+
+The `MySuspendedFieldTy` type is, given some type `T` that you set on the original field (ignoring the result wrapping it), either `RcSignal>` is your field is non-nested, or `RxResult` if it is.
+
+## Example
+
+With all that over, here's an example. It may seem very intimidating at first, but that's just because there are three suspended state handlers to show you how this works with nested state. It's heavily commented, and it's recommended to read through this carefully to understand how suspended state works. This is probably the most complicated part of Perseus to use, because understanding how the state flows through it is a bit tricky (we like to think of it as being borrowed from the main system by your handler and returned with a different value, through `.set()`), so feel free to [open a GitHub discussion](https://github.com/framesurge/perseus/discussions/new/choose) or [ask on Discord](https://discord.com/invite/GNqWYWNTdp) if you're having trouble understanding or using this (or any other) feature.
+
+```rust
+{{#include ../../../examples/core/suspense/src/templates/index.rs}}
+```
+
+Note `#[browser_only_fn]` here, which is the browser equivalent of `#[engine_only_fn]`.
diff --git a/docs/0.5.x/en-US/what-is-perseus.md b/docs/0.5.x/en-US/what-is-perseus.md
new file mode 100644
index 0000000000..e4a0a3201d
--- /dev/null
+++ b/docs/0.5.x/en-US/what-is-perseus.md
@@ -0,0 +1,108 @@
+# What is Perseus?
+
+Perseus is a **web development framework** for the **Rust** programming language that focuses on the **state** of your app. Since there are three main ways you might approach Perseus, we'll break down each one individually here.
+
+## You're familiar with Rust
+
+We can obviously agree that Rust is much better than JavaScript: it's way faster, strongly-typed, has a great compiler, and a fantastic package management system. In the browser, it runs *amazingly*. This is because of [WebAssembly](https://webassembly.org) (abbreviated *Wasm*), which is basically an assembly language for programs like Chrome, Firefox, etc. With it, you can compile your Rust code to run in the browser, and even access browser APIs, allowing you to display content to the user. In the past, Rust has been used with Wasm to perform things like heavy cryptography, but Perseus lets you exile JS completely, and run your whole site with Rust only.
+
+Now, you might have come across other web development libraries and frameworks for Rust before, but there's a big difference between those two terms, so let's sort that out first. A *library* is a piece of code that you use to help you build your site. A *framework* is a mammoth of code that uses your code to build your site. Think of it like the difference between `futures::executor::block_on` and `#[tokio::main]`: one is being used by you to handle a bit of `async`, and the other is using your code to handle *all* the `async`. In the same way, a library is a great choice for when you want to build a small site, or when you want to replace just part of a site with Rust. For these kinds of things, we absolutely recommend [Sycamore](https://github.com/sycamore-rs/sycamore), on which Perseus is based.
+
+However, sometimes you'll need to break out the big guns. Sometimes, you'll need to render content in advance so that your users see it straight away, rather than a blank page while your Wasm boots up. Sometimes, you'll want to have a *stateful* app. This doesn't just mean you've got buttons and forms, etc., but that you're building your app in a special kind of pattern, which Perseus is built around. Let's say you have a simple static blog: you might have a `/post` URL, under which all your posts can be found. Fundamentally, all these posts have the same structure, just with different titles, dates, tags, and contents, so you might choose to create some kind of *template* for them, and then maybe build a Markdown parser or the like to push all that into your app to create *pages*. Essentially, **template + state = page**. In Perseus, this is all handled for you, and you just create templates, like `/post`, along with ways to render their state.
+
+For example, for a blog, you might create a new post template with `Template::build("post")`, and then create a function that takes in some state and plugs it into a Sycamore `view! { .. }` to render some content. You might take in a `struct` containing contents, titles, tags, etc. If you then specify a function that can list the pages that this template should create (e.g. by getting all the Markdown files in a certain directory), and then another one that takes each path and generates state for it, Perseus will string it all together and give a lightning-fast app.
+
+Beyond this, Perseus has all sorts of extra features, like inbuilt error handling systems that allow you to gracefully display error messages if state generation fails, or if your app panics, or something else like that. All you do is match an `enum ClientError`, and Perseus shows your errors to the client. Beyond that, if you want to build an app in multiple languages, Perseus will let you do it straight away: just replace the text in your code with identifiers inside the `t!()` macro, and define a map of translation IDs to text for each language you want to support. Variable interpolation is supported out of the box, and you can unleash the full power of [Fluent](https://projectfluent.org) for handling pluralization rules, genders, etc.
+
+Going even further, Perseus' state generation platform is built for even the most advanced use-cases: let's say you have not a blog, but an ecommerce site selling a thousand products. Well, a thousand would actually build very quickly, so perhaps a million. Still probably looking at less than a second, but we'll go with it. Maybe you don't want to build all that at build time. Simple! Just add `.incremental_generation()` to your template definition and then...you're done. If a user goes to a product page that doesn't exist yet, it will be passed to your state generation functions, and, if it's a page that exists, those functions can produce the page, and Perseus will serve it. For any future users, that page will be cached and returned immediately. It's like building your whole app over time, on-demand. And, if you have an index of all your products, you could automatically *revalidate* that every, say, 24 hours, to make sure users have a fairly up to date listing. Or you could logic-based revalidation that checks each time whether or not there are actually any new products, before rebuilding. You could even combine the two: only check every few hours whether or not there are new products, and, if there are, rebuild that page.
+
+To be clear, and this is important if you aren't familiar with web development, Perseus is not a library, it's a framework. It's a giant engine into which you plug your code that will connect everything together and optimize it, producing a super-fast site that outperforms every JS framework under the sun. It might well seem like you don't need a lot of these features, and, if you don't, you can just run `perseus export` to get a series of static HTML files that you can serve to users however you like, with a simple Wasm bundle making sure whatever interactivity you have works as smoothly as possible (and it will still be unreasonably fast). If you're used to systems programming, the whole idea of a framework might seem a bit absurd, but it's very often required in web development, simply because the best experiences come from complex features, like rendering your site to HTML in advance, or caching transactions, or delayable capsules that can be infinitely nested to create lazy-loaded pages, etc. Some of these are easy to implement, others are not. The point of Perseus is to handle this all for you so you can get on with what you want to write: your app. Even better, Perseus is built on [Sycamore](https://github.com/sycamore-rs/sycamore), which handles reactivity primitives, meaning there is a separation of concerns, unlike with other current Rust frameworks: one team is in charge of the reactivity, and another in charge of the framework, meaning more features are developed more quickly, and bugs are fixed more rapidly, while both systems remain fantastically .
+
+If Perseus doesn't sound like your cup of tea, there are several other Rust frameworks you might like to check out: [Sycamore](https://github.com/sycamore-rs/sycamore) is the library on which Perseus is based, if you want to keep the same sort of style; [Yew](https://yew.rs) is a very popular library; and [Seed](https://seed-rs.org) is another. There's also [Sauron](https://github.com/ivanceras/sauron), [MoonZoon](https://github.com/MoonZoon/MoonZoon), and [Leptos](https://github.com/leptos-rs/leptos), just to name a few. If you'd like to see some more in-depth comparisons between these projects, check out [the comparisons page](comparisons).
+
+## You're familiar with JavaScript, and you've know what NextJS, ReactJS, etc. mean
+
+Alright, you're pretty familiar with what web development is, and why we tend to need frameworks to make things simple and to remove the need to write hundreds of lines of boilerplate code for features we use in every app. But you've probably got plenty of questions about Perseus.
+
+### Why Rust?
+
+Put simply, JS is [a bit of a mess](https://medium.com/netscape/javascript-is-kinda-shit-im-sorry-2e973e36fec4). It's dynamically-typed, and executed at runtime, meaning you can't really catch bugs while you're coding. Sure, an IDE helps with this by showing you squiggly red lines, but it still won't stop you from forgetting about passing a certain argument to a function. TypeScript helps with this by introducing stricter typing rules, but it's really an addition on top of already existing JavaScript, and, let's be honest, how many times have you had to search up solutions for getting your `tsconfig` to work?
+
+[Rust](https://rust-lang.org), on the other hand, is generally thought of as a systems programming language, meaning it's much lower-level and closer to the hardware, letting you do things like memory management more manually. It's certainly got a much steeper learning curve, but, let's walk through a quick example. Imagine you have a variable `data` that contains a very large amount of information. Obviously, copying this is going to slow your program down, so we want to avoid that if possible. In JS, you could do something like this:
+
+```javascript
+const data = "...";
+let valid = isDataValid(data);
+let useful = isDataUseful(data);
+```
+
+You might not realize it, but this code could copy the whole of `data` under certain conditions, because, when you think about it, both `isDataValid()` and `isDataUseful()` need it. In fact, depending on your code's structure, JS might even implicitly copy this whole variable *twice*! This is an oversimplification, and there's a lot more going on here, but, in Rust, you have total control over this:
+
+```rust
+let data = get_data();
+let valid = is_data_valid(&data);
+let useful = is_data_useful(&data);
+```
+
+Here, we're passing *references* to `data` to those functions, which are like telling them where `data` can be found in memory, rather than giving them it's actual value. Again, we're oversimplifying, but the point is that Rust allows you much lower-level control over your data, and it's a compiled language, meaning you have to build your code into an executable, rather than just running it. In this stage, the compiler goes over your code with a fine-toothed comb, finding whole classes of bugs and making them impossible at runtime. And, to make things even better, *undefined behavior*, a special type of bug in C/C++/etc. (which often leads to `Segmentation fault` messages, which you might have seen before), is literally impossible in Rust, because the whole language is built on a clear boundary between *safe* code, and *unsafe* code. The latter might cause UB, and should explicitly clarify what has to be upheld for it to all work properly. Then, if code can be certain that it's upholding the necessary invariants, it can call itself safe. Basically, where the compiler can't prove that your code won't crash and burn, you explicitly have to, and there's no getting around it.
+
+To illustrate just how powerful this model of programming is, let's take a bit of a meta-example. When we were building Perseus v0.4.0, we had to rewrite the entire Perseus core, over 12,000 lines of code. After innumerable cycles of changing some code and seeing errors pop up in the terminal, when we got all the errors fixed and the code actually compiled, the first time we ran `perseus build`, *it worked*. No logic bugs, no syntax errors, it just worked. *That* is the kind of power you get from working with Rust. (and absolutely ludicrous speeds.)
+
+Usefully, the Rust compiler supports compiling for different *targets*, which are basically formats of machine code. Your Rust code can be turned into code that will run on Linux, macOS, Windows, etc. Or, it could run in the browser, through a revolutionary new technology called [WebAssembly](https://webassembly.org), abbreviated as *Wasm*. Technically, any language, like C or C++, could compile into this format, but Rust has the added guarantees of *safety*.
+
+Oh, and did we mention that Rust is [insanely fast](https://medium.com/@xpf6677/40x-faster-we-rewrote-our-project-with-rust-120b006c6abe)?
+
+When you combine that with Wasm, a Rust site is usually >30% faster than the equivalent site built in JavaScript, in terms of runtime performance. And, when we say >30%, we mean >90% on anything modern that's not running Safari (Apple being a bastion of implementing web standards, as usual).
+
+With all this, Rust is the perfect language to implement a next-generation web framework in, and that's exactly what Perseus is.
+
+### Okay, but what *is* it?
+
+As NextJS is to ReactJS, Perseus is to [Sycamore](https://github.com/sycamore-rs/sycamore). Sycamore is a low-level reactive library for building websites in Rust that uses *no virtual DOM*, making it [faster than Svelte](https://rawgit.com/krausest/js-framework-benchmark/master/webdriver-ts-results/table.html) in some cases (with improvements on the horizon to get *even faster*), and Perseus builds on these foundations to create a framework designed to make your life easier by minimizing boilerplate.
+
+Assuming you're familiar with a few terms from the usual JS jargon about frameworks, let's run through Perseus' features. It supports static site generation (building your app to HTML before it's even running), server-side rendering (building pages at request-time based on user details, like cookies), client-side rendering (fetching data in the browser to render components), using SSG and SSR *on the same page* (which, to our knowledge, no other framework in the world supports), revalidation (allowing you to rebuild a page that was built originally at build-time, based on either timing or custom logic), incremental generation (rendering a page at request-time the first time it's requested, and then caching it for future use so it can be returned instantly next time), and [*capsules*](:capsules/intro).
+
+This is all based around *state*, because that's the focus of Perseus. Unashamedly, Perseus focuses on supporting highly complex apps with many moving parts and interconnected components. Of course, if you want to build a static blog, that's a piece of cake.
+
+Fundamentally, Perseus boils down to a state framework, and, really, the whole idea of actually displaying content to a user is secondary. As far as Perseus is concerned, your state is generated in almost any way conceivable, it gets to the user, it's made *reactive* of its own accord (meaning, if you're coming from React, that any state you generate on the server comes to you already in a `useState()` hook), and then you can work with it however you like to display it to users (that's [Sycamore](https://github.com/sycamore-rs/sycamore)'s job). If your site isn't interactive (like a static blog), you can use unreactive state instead, no problem.
+
+Based on this, Perseus' rendering model comes down to *templates*, which are like stencils for creating pages. For example, you might have a blog post template at the `post` URL, which would have the basic structure that all blog posts share. When you plug in the data of an individual blog post called `foo`, you get out that template, filled in with that state, to produce `post/foo`, a page.
+
+In essence, **template + state = page**, that's the fundamental equation of Perseus.
+
+But, we went further than this. If you're familiar with [Astro](https://astro.build), then you'll have heard of the *islands architecture*, where you split your app into components that can individually render, hydrate, etc. Now, things are a bit different over here in Wasm-world, because things are so fast here that we don't really have to care about delaying hydration, or things like that, because it all happens just about instantly. Instead, our main concern is minimizing the amount of *stuff* (i.e. HTML and Wasm) that needs to be sent to the user's browser, because that's the real bottleneck for us. So, if you split out a complex ecommerce page into, say, a *widget* (Perseus' term for islands) for each product on your home page, then your home page can load as a simple skeleton waiting for some content. It's kind of like a template waiting for state, but the pieces that need to be filled in are actual mini-pages themselves. In fact, unlike any other framework ever created, Perseus has the unique concept that **capsule + state = widget**. That's right, as a template creates pages, a capsule creates widgets, meaning you can have a `product` capsule that incrementally generates product widgets as they're requested. You can use every single rendering strategy that works for pages on widgets, and you can control exactly when they're rendered too. If you want, say, the first row of products on your website's landing page to be instantly rendered, and then the rest to be lazy-loaded in parallel, you can do that by chaning `.widget()` to `.delayed_widget()`. It's that simple. Oh, and *everything* is cached by Perseus at the application-level, taking single-page routing into the world of caching and ensuring that users can literally *instantly* navigate back to any pages they've visited in the past.
+
+Naturally, Perseus also comes with the usual stew of extra framework features, like internationalization out of the box that just works (translator APIs etc. are all available for you, and you can pick a really powerful one using [Fluent](https://projectfluent.org) or a really tiny one using JSON, with more to come), and one-command deployment to a `pkg/` folder that you put literally anywhere that runs executables. And if you want a static site, you just run `perseus export`, and you're set.
+
+As for the Lighthouse scores, Perseus achieves 100 on desktop without even trying, and consistently above 90 on mobile. The reason for the dropoff in mobile performance is mostly because of the way mobile browsers still have to go in optimizing Wasm, but this will improve with time, and any user on a modern smartphone will see a snappy and responsive site practically instantly. That whole idea of render-then-hydrate is baked into Perseus: your users see content straight away, and it becomes reactive a moment later.
+
+Unfortunately, the idea of *resumability*, as pioneered by [Qwik](https://qwik.builder.io), isn't really possible with Wasm yet, because you actually can't split a Wasm bundle into smaller pieces, you just send the whole thing to the user. While that does mean that Perseus apps are *insanely* fast when going between pages, it can mean slightly slower load times when a user first comes to your site. That said, it's still 100 on Lighthouse, so it can't be *that* bad. Even so, we're sure you've had that bad experience of loading a site and trying to press buttons that don't work, and knowing (as a developer) that it's because the site hasn't hydrated yet. Now, with Perseus, your users really won't be waiting too long for those buttons to be working, but you can enable a feature flag that holds user interactions in stasis until your app is hydrated, before automatically re-sending them, leading to a much better overall user experience. And, if you don't like it, you can just turn it off.
+
+The other really cool thing about Perseus is *error handling*. A lot of JS frameworks have this concept of *error boundaries*, but still more leave all the error management to you. If JS blows up (as it frequently does), you're left to clean up on your own. In Rust, errors have to be propagated explicitly with a type called `Result`, which can either be `Ok` or `Err`. Unless a function `panic!`s, it can't rip the floor out from under you and cause everything to fail. That means Perseus can handle nearly all errors gracefully: for example, if a single widget can't render its contents properly, it will automatically render an error instead. If Perseus can't start up your app, but it knows the user can already see some content, it will show a popup error message instead of replacing the perfectly good static content. And, if your whole app panics, crashing and burning to the ground, Perseus gives you the opportunity to run arbitrary code (like crash analytics) as well as display a nice error message to the user. And, because Rust is strongly-typed, if you forget to explicitly handle (or not handle) a particular type of error, your app just won't compile, and you'll get a lovely error message from the compiler. Basically, it would take an alignment of cosmic rays flipping dozens of bits in your computer simultaneously, or a total browser crash, to make Perseus fail without producing an error message of some kind. We don't crash and burn a lot, but when we do, we do it in style.
+
+*Note: if you're completely new to Rust, you might want to check out [the Rust book](https://doc.rust-lang.org/stable/book/) before starting with Perseus.*
+
+## You're new to web development and Rust, welcome!
+
+Usually, people build websites with three languages: HTML (HyperText Markup Language), CSS (Cascading Style Sheets), and JS (JavaScript). If you imagine building a bed in real life with these languages, HTML would be responsible for declaring that what you're building is a ``, while you would use CSS to set how rounded the corners are, what color the whole thing is, what shape, what size, etc. Finally, you would use JS to make the bed, perhaps, start playing music at a certain time in the morning to wake you up.
+
+However, these languages are all *interpreted*, meaning the browser tries to figure out what your code does as it gets it. So, if you were to, say, make a typo in some code that you put on your website, you wouldn't necessarily know until the code just doesn't run for your users, and some part of your site breaks. Although there are ways of working around these types of errors, usually with extensions to JS like [TypeScript](https://typescriptlang.org), they effectively bring the power of *compiled* and *typed* languages (like [Rust](https://rust-lang.org)) to the web, except they're just extensions, which means they don't solve a lot the underlying problems (and they aren't any faster).
+
+For example, let's say we have a variable `x` in JavaScript, which we set to be `5`. If we then change this to say the string `foo`, that's perfectly fine according to JS, but think about it: how many units of memory does it take to represent `5`? And how many to represent `foo`? The fact that these are different, and that this sort of thing is permissible in the language, means that JS has to do a whole lot of overhead work making everything function. Sure, it can be nice to be able to set any variable to anything, and that sort of freedom can certainly be useful for rapid prototyping (one of the great appeals of conceptually similar languages, like Python), but it doesn't make for very fast (or very safe) code.
+
+If, instead, you were to build your site in another programming language that's *typed* (meaning, once you set `x = 5`, it can't be anything other than a number, because the language knows exactly how much memory to allocate) and *compiled* (meaning there's a stage before code execution where your code is parsed, checked for errors, and automatically optimized, being translated from human-readable code to machine-readable instructions), it could be, at a minimum, over 30% faster than one built with JS. Also, you get much more performant continuity between platforms. For example, you can happily build your site in Rust, and your server. If you were to do that with JavaScript, then both would be *quite slow*. And, when we're talking about corporate applications, even a second slower loads can do [meaningful harm](https://www.cloudflare.com/learning/performance/more/website-performance-conversion-rates/) to customer conversion.
+
+Perseus is a framework for building complex websites and webapps in Rust, which consistently outperforms almost every other JS framework under the sun in benchmarks. It's based on [Sycamore](https://github.com/sycamore-rs/sycamore), which provides underlying *reactivity* (which lets you do cool things like say "show the value of variable `x` here and update the view whenever that variable updates"), and is [faster than Svelte](https://rawgit.com/krausest/js-framework-benchmark/master/webdriver-ts-results/table.html), one of the fastest JS frameworks, in several benchmarks. On its own, Perseus will take your code, compile it, and then add an extra stage of *building* your app, in which it looks at your code, figures out the earliest pages can be prepared for users, and prepares them. So, if you have an *about us* page that's the same for every user, and that doesn't depend on users, say, being logged in, then Perseus will automatically render that page when you build your app, meaning your users will see it more quickly when they want it.
+
+If you're completely new to web development and Rust, explaining the rest of Perseus' features will probably not be the best thing, so we'd recommend taking a look at the [MDN](https://developer.mozilla.org) documentation for information about web dev generally, and you should read [the Rust book](https://doc.rust-lang.org/stable/book) (it's not too long) to get a feel for Rust. Once you've got the basics down, you should be ready to dive straight into Perseus! And, if you need some help, don't hesitate to ask on [our Discord](https://discord.com/invite/GNqWYWNTdp)! Best of luck!
+
+## Summary
+
+If all that was way too long, here's a quick summary of what Perseus does and why it's useful!
+
+- JS is slow and 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!)
+- Perseus also handles errors very efficiently and safely
+- Perseus supports a cool thing called *capsules* that let you write some really powerful and fast code
diff --git a/docs/manifest.json b/docs/manifest.json
index 1b9cf24d73..126af36e51 100644
--- a/docs/manifest.json
+++ b/docs/manifest.json
@@ -20,8 +20,13 @@
"docs_rs": "0.3"
},
"0.4.x": {
+ "state": "outdated",
+ "git": "v0.4.2",
+ "docs_rs": "0.4"
+ },
+ "0.5.x": {
"state": "stable",
"git": "HEAD",
- "docs_rs": "0.4"
+ "docs_rs": "0.5"
}
}
diff --git a/examples/.base/Cargo.toml b/examples/.base/Cargo.toml
index 7987fe67f2..f7fcf3fdc7 100644
--- a/examples/.base/Cargo.toml
+++ b/examples/.base/Cargo.toml
@@ -1,24 +1,24 @@
[package]
name = "perseus-example-base"
-version = "0.4.3"
+version = "0.5.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
-perseus = { path = "../../../packages/perseus", features = [ "hydrate" ] }
-sycamore = "^0.8.1"
+perseus = { path = "../../../packages/perseus", features = ["hydrate"] }
+sycamore = "^0.9.2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
[target.'cfg(engine)'.dev-dependencies]
-fantoccini = "0.17"
+fantoccini = "0.22"
[target.'cfg(engine)'.dependencies]
-tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] }
+tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread"] }
## **WARNING!** Before running this example outside the Perseus repo, replace the below line with
## the one commented out below it (changing the path dependency to the version you want to use)
-perseus-warp = { package = "perseus-integration", path = "../../../packages/perseus-integration", default-features = false }
+perseus-warp = { package = "perseus-integration", path = "../../../packages/perseus-integration", default-features = false, features = ["warp"] }
# perseus-warp = { path = "../../../packages/perseus-warp", features = [ "dflt-server" ] }
[target.'cfg(client)'.dependencies]
diff --git a/examples/.base/src/main.rs b/examples/.base/src/main.rs
index 36895e1bb6..9fc7586352 100644
--- a/examples/.base/src/main.rs
+++ b/examples/.base/src/main.rs
@@ -3,7 +3,7 @@ mod templates;
use perseus::prelude::*;
#[perseus::main(perseus_warp::dflt_server)]
-pub fn main() -> PerseusApp {
+pub fn main() -> PerseusApp {
PerseusApp::new()
.template(crate::templates::index::get_template())
.error_views(ErrorViews::unlocalized_development_default())
diff --git a/examples/.base/src/templates/index.rs b/examples/.base/src/templates/index.rs
index b6cf33245a..8999e211a2 100644
--- a/examples/.base/src/templates/index.rs
+++ b/examples/.base/src/templates/index.rs
@@ -1,21 +1,21 @@
use perseus::prelude::*;
use sycamore::prelude::*;
-fn index_page(cx: Scope) -> View {
- view! { cx,
+fn index_page() -> View {
+ view! {
p { "Hello World!" }
}
}
#[engine_only_fn]
-fn head(cx: Scope) -> View {
- view! { cx,
+fn head() -> View {
+ view! {
title { "Index Page" }
}
}
-pub fn get_template() -> Template {
- Template::new("index")
+pub fn get_template() -> Template {
+ Template::build("index")
.view(index_page)
.head(head)
.build()
diff --git a/examples/comprehensive/tiny/Cargo.toml b/examples/comprehensive/tiny/Cargo.toml
index ee2b057e9f..1fdc830618 100644
--- a/examples/comprehensive/tiny/Cargo.toml
+++ b/examples/comprehensive/tiny/Cargo.toml
@@ -1,19 +1,19 @@
[package]
name = "perseus-example-tiny"
-version = "0.4.3"
+version = "0.5.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
-perseus = { path = "../../../packages/perseus" }
-sycamore = "^0.8.1"
+perseus = { path = "../../../packages/perseus", features = ["hydrate"] }
+sycamore = "^0.9.2"
[target.'cfg(engine)'.dependencies]
-tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] }
+tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread"] }
## **WARNING!** Before running this example outside the Perseus repo, replace the below line with
## the one commented out below it (changing the path dependency to the version you want to use)
-perseus-axum = { package = "perseus-integration", path = "../../../packages/perseus-integration", default-features = false }
+perseus-axum = { package = "perseus-integration", path = "../../../packages/perseus-integration", default-features = false, features = ["axum"] }
# perseus-axum = { path = "../../../packages/perseus-axum", features = [ "dflt-server" ] }
[target.'cfg(client)'.dependencies]
diff --git a/examples/comprehensive/tiny/src/main.rs b/examples/comprehensive/tiny/src/main.rs
index acd8b33d30..6438388e9b 100644
--- a/examples/comprehensive/tiny/src/main.rs
+++ b/examples/comprehensive/tiny/src/main.rs
@@ -1,18 +1,26 @@
use perseus::prelude::*;
use sycamore::prelude::*;
+fn index_view() -> View {
+ view! {
+ div {
+ h1 { "Hello World!" }
+ p { "This is a tiny Perseus example." }
+ }
+ }
+}
+
+#[engine_only_fn]
+fn head() -> View {
+ view! {
+ title { "Tiny Perseus Example" }
+ }
+}
+
#[perseus::main(perseus_axum::dflt_server)]
-pub fn main() -> PerseusApp {
+pub fn main() -> PerseusApp {
PerseusApp::new()
- .template(
- Template::build("index")
- .view(|cx| {
- view! { cx,
- p { "Hello World!" }
- }
- })
- .build(),
- )
+ .template(Template::build("index").view(index_view).head(head).build())
// This forces Perseus to use the development defaults in production, which just
// lets you easily deploy this app. In a real app, you should always provide your own
// error pages!
diff --git a/examples/core/basic/Cargo.toml b/examples/core/basic/Cargo.toml
index c661d03384..04d4cfaefc 100644
--- a/examples/core/basic/Cargo.toml
+++ b/examples/core/basic/Cargo.toml
@@ -1,24 +1,28 @@
[package]
name = "perseus-example-basic"
-version = "0.4.3"
+version = "0.5.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
-perseus = { path = "../../../packages/perseus", features = [ "hydrate" ] }
-sycamore = "^0.8.1"
+perseus = { path = "../../../packages/perseus", features = ["hydrate"] }
+sycamore = "^0.9.2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
[target.'cfg(engine)'.dev-dependencies]
-fantoccini = "0.19"
+fantoccini = "0.22"
[target.'cfg(engine)'.dependencies]
-tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] }
+tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread"] }
## **WARNING!** Before running this example outside the Perseus repo, replace the below line with
## the one commented out below it (changing the path dependency to the version you want to use)
-perseus-axum = { package = "perseus-integration", path = "../../../packages/perseus-integration", default-features = false }
-# perseus-axum = { path = "../../../packages/perseus-axum", features = [ "dflt-server" ] }
+## perseus-integration = { path = "../../../packages/perseus-integration", features = [
+## "axum",
+##] }
+perseus-axum = { path = "../../../packages/perseus-axum", features = [
+ "dflt-server",
+] }
[target.'cfg(client)'.dependencies]
diff --git a/examples/core/basic/src/error_views.rs b/examples/core/basic/src/error_views.rs
index bf523a1833..d867fb15d4 100644
--- a/examples/core/basic/src/error_views.rs
+++ b/examples/core/basic/src/error_views.rs
@@ -2,58 +2,58 @@ use perseus::errors::ClientError;
use perseus::prelude::*;
use sycamore::prelude::*;
-pub fn get_error_views() -> ErrorViews {
- ErrorViews::new(|cx, err, _err_info, _err_pos| {
+pub fn get_error_views() -> ErrorViews {
+ ErrorViews::new(|err, _err_info, _err_pos| {
match err {
ClientError::ServerError { status, message: _ } => match status {
404 => (
- view! { cx,
+ view! {
title { "Page not found" }
},
- view! { cx,
+ view! {
p { "Sorry, that page doesn't seem to exist." }
},
),
// 4xx is a client error
_ if (400..500).contains(&status) => (
- view! { cx,
+ view! {
title { "Error" }
},
- view! { cx,
+ view! {
p { "There was something wrong with the last request, please try reloading the page." }
},
),
// 5xx is a server error
_ => (
- view! { cx,
+ view! {
title { "Error" }
},
- view! { cx,
+ view! {
p { "Sorry, our server experienced an internal error. Please try reloading the page." }
},
),
},
ClientError::Panic(_) => (
- view! { cx,
+ view! {
title { "Critical error" }
},
- view! { cx,
+ view! {
p { "Sorry, but a critical internal error has occurred. This has been automatically reported to our team, who'll get on it as soon as possible. In the mean time, please try reloading the page." }
},
),
ClientError::FetchError(_) => (
- view! { cx,
+ view! {
title { "Error" }
},
- view! { cx,
+ view! {
p { "A network error occurred, do you have an internet connection? (If you do, try reloading the page.)" }
},
),
_ => (
- view! { cx,
+ view! {
title { "Error" }
},
- view! { cx,
+ view! {
p { (format!("An internal error has occurred: '{}'.", err)) }
},
),
diff --git a/examples/core/basic/src/main.rs b/examples/core/basic/src/main.rs
index e38c97de4c..69c57e58f1 100644
--- a/examples/core/basic/src/main.rs
+++ b/examples/core/basic/src/main.rs
@@ -4,7 +4,7 @@ mod templates;
use perseus::prelude::*;
#[perseus::main(perseus_axum::dflt_server)]
-pub fn main() -> PerseusApp {
+pub fn main() -> PerseusApp {
PerseusApp::new()
.template(crate::templates::index::get_template())
.template(crate::templates::about::get_template())
diff --git a/examples/core/basic/src/templates/about.rs b/examples/core/basic/src/templates/about.rs
index 12989c60c7..76a27f5ee1 100644
--- a/examples/core/basic/src/templates/about.rs
+++ b/examples/core/basic/src/templates/about.rs
@@ -1,19 +1,19 @@
use perseus::prelude::*;
use sycamore::prelude::*;
-fn about_page(cx: Scope) -> View {
- view! { cx,
+fn about_page() -> View {
+ view! {
p { "About." }
}
}
#[engine_only_fn]
-fn head(cx: Scope) -> View {
- view! { cx,
+fn head() -> View {
+ view! {
title { "About Page | Perseus Example – Basic" }
}
}
-pub fn get_template() -> Template {
+pub fn get_template() -> Template {
Template::build("about").view(about_page).head(head).build()
}
diff --git a/examples/core/basic/src/templates/index.rs b/examples/core/basic/src/templates/index.rs
index 09ef5e05f7..568825ae99 100644
--- a/examples/core/basic/src/templates/index.rs
+++ b/examples/core/basic/src/templates/index.rs
@@ -9,16 +9,16 @@ struct IndexPageState {
}
#[auto_scope]
-fn index_page(cx: Scope, state: &IndexPageStateRx) -> View {
- view! { cx,
- p { (state.greeting.get()) }
- a(href = "about", id = "about-link") { "About!" }
+fn index_page(state: IndexPageStateRx) -> View {
+ view! {
+ p { (state.greeting.get_clone()) }
+ Link(to = "/about", id = "about-link") { "About!" }
}
}
#[engine_only_fn]
-fn head(cx: Scope, _props: IndexPageState) -> View {
- view! { cx,
+fn head(_props: IndexPageState) -> View {
+ view! {
title { "Index Page | Perseus Example – Basic" }
}
}
@@ -30,7 +30,7 @@ async fn get_build_state(_info: StateGeneratorInfo<()>) -> IndexPageState {
}
}
-pub fn get_template() -> Template {
+pub fn get_template() -> Template {
Template::build("index")
.build_state_fn(get_build_state)
.view_with_state(index_page)
diff --git a/examples/core/capsules/Cargo.toml b/examples/core/capsules/Cargo.toml
index f3af761660..67b267a4a2 100644
--- a/examples/core/capsules/Cargo.toml
+++ b/examples/core/capsules/Cargo.toml
@@ -1,25 +1,29 @@
[package]
name = "perseus-example-capsules"
-version = "0.4.3"
+version = "0.5.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
-perseus = { path = "../../../packages/perseus", features = [ "hydrate" ] }
-sycamore = "^0.8.1"
+perseus = { path = "../../../packages/perseus", features = ["hydrate"] }
+sycamore = "^0.9.2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
lazy_static = "1"
[target.'cfg(engine)'.dev-dependencies]
-fantoccini = "0.19"
+fantoccini = "0.22"
[target.'cfg(engine)'.dependencies]
-tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] }
+tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread"] }
## **WARNING!** Before running this example outside the Perseus repo, replace the below line with
## the one commented out below it (changing the path dependency to the version you want to use)
-perseus-axum = { package = "perseus-integration", path = "../../../packages/perseus-integration", default-features = false }
-# perseus-axum = { path = "../../../packages/perseus-axum", features = [ "dflt-server" ] }
+# perseus-axum = { package = "perseus-integration", path = "../../../packages/perseus-integration", default-features = false, features = [
+# "axum",
+#] }
+perseus-axum = { path = "../../../packages/perseus-axum", features = [
+ "dflt-server",
+] }
[target.'cfg(client)'.dependencies]
diff --git a/examples/core/capsules/src/capsules/greeting.rs b/examples/core/capsules/src/capsules/greeting.rs
index dc94e633e8..4ebc7b1079 100644
--- a/examples/core/capsules/src/capsules/greeting.rs
+++ b/examples/core/capsules/src/capsules/greeting.rs
@@ -8,13 +8,13 @@ lazy_static! {
// This `PerseusNodeType` alias will resolve to `SsrNode`/`DomNode`/`HydrateNode` automatically
// as needed. This is needed because `lazy_static!` doesn't support generics, like `G: Html`.
// Perseus can bridge the gap internally with type coercions, so this "just works"!
- pub static ref GREETING: Capsule = get_capsule();
+ pub static ref GREETING: Capsule = get_capsule();
}
#[auto_scope]
-fn greeting_capsule(cx: Scope, state: &GreetingStateRx, props: GreetingProps) -> View {
- view! { cx,
- p(id = "greeting", style = format!("color: {};", props.color)) { (state.greeting.get()) }
+fn greeting_capsule(state: GreetingStateRx, props: GreetingProps) -> View {
+ view! {
+ p(id = "greeting", style = format!("color: {};", props.color)) { (state.greeting.get_clone()) }
}
}
@@ -30,7 +30,7 @@ pub struct GreetingProps {
pub color: String,
}
-pub fn get_capsule() -> Capsule {
+pub fn get_capsule() -> Capsule {
// Template properties, to do with state generation, are set on a template
// that's passed to the capsule. Note that we don't call `.build()` on the
// template, because we want a capsule, not a template (we're using the
diff --git a/examples/core/capsules/src/capsules/ip.rs b/examples/core/capsules/src/capsules/ip.rs
index 2f4d18908f..31f9347449 100644
--- a/examples/core/capsules/src/capsules/ip.rs
+++ b/examples/core/capsules/src/capsules/ip.rs
@@ -4,13 +4,13 @@ use serde::{Deserialize, Serialize};
use sycamore::prelude::*;
lazy_static! {
- pub static ref IP: Capsule = get_capsule();
+ pub static ref IP: Capsule<()> = get_capsule();
}
// Note the use of props as `()`, indicating that this capsule doesn't take any
// properties
-fn ip_capsule(cx: Scope, state: IpState, _props: ()) -> View {
- view! { cx,
+fn ip_capsule(state: IpState, _props: ()) -> View {
+ view! {
p(id = "ip") { (state.ip) }
}
}
@@ -21,7 +21,7 @@ struct IpState {
ip: String,
}
-pub fn get_capsule() -> Capsule {
+pub fn get_capsule() -> Capsule<()> {
Capsule::build(Template::build("ip").request_state_fn(get_request_state))
.empty_fallback()
// Very importantly, we declare our views on the capsule, **not** the template!
diff --git a/examples/core/capsules/src/capsules/links.rs b/examples/core/capsules/src/capsules/links.rs
index 9ff6b0794f..224fd0dde2 100644
--- a/examples/core/capsules/src/capsules/links.rs
+++ b/examples/core/capsules/src/capsules/links.rs
@@ -7,26 +7,26 @@ use sycamore::prelude::*;
// and that would probably make more sense, but this is a capsules example!)
lazy_static! {
- pub static ref LINKS: Capsule = get_capsule();
+ pub static ref LINKS: Capsule<()> = get_capsule();
}
-fn links_capsule(cx: Scope, _: ()) -> View {
- view! { cx,
+fn links_capsule(_: ()) -> View {
+ view! {
div(id = "links", style = "margin-top: 1rem;") {
- a(id = "index-link", href = "") { "Index" }
+ Link(to = "/", id = "index-link") { "Index" }
br {}
- a(id = "about-link", href = "about") { "About" }
+ Link(to = "/about", id = "about-link") { "About" }
br {}
- a(id = "clock-link", href = "clock") { "Clock" }
+ Link(to = "/clock", id = "clock-link") { "Clock" }
br {}
- a(id = "four-link", href = "four") { "4" }
+ Link(to = "/four", id = "four-link") { "4" }
br {}
- a(id = "calc-link", href = "calc") { "Calc" }
+ Link(to = "/calc", id = "calc-link") { "Calc" }
}
}
}
-pub fn get_capsule() -> Capsule {
+pub fn get_capsule() -> Capsule<()> {
Capsule::build(Template::build("links"))
.empty_fallback()
.view(links_capsule)
diff --git a/examples/core/capsules/src/capsules/number.rs b/examples/core/capsules/src/capsules/number.rs
index b988c6f44f..d21731bc60 100644
--- a/examples/core/capsules/src/capsules/number.rs
+++ b/examples/core/capsules/src/capsules/number.rs
@@ -10,13 +10,13 @@ use sycamore::prelude::*;
// work in capsules (by passing through a non-number).
lazy_static! {
- pub static ref NUMBER: Capsule = get_capsule();
+ pub static ref NUMBER: Capsule<()> = get_capsule();
}
// Note the use of props as `()`, indicating that this capsule doesn't take any
// properties
-fn time_capsule(cx: Scope, state: Number, _props: ()) -> View {
- view! { cx,
+fn time_capsule(state: Number, _props: ()) -> View {
+ view! {
span {
(state.number)
@@ -24,9 +24,9 @@ fn time_capsule(cx: Scope, state: Number, _props: ()) -> View {
// a particular incremental path that has incremental dependencies
// itself. Perseus resolves this without problems.
(if state.number == 5 {
- view! { cx, (NUMBER.widget(cx, "/6", ())) }
+ view! { (NUMBER.widget("/6", ())) }
} else {
- View::empty()
+ View::new()
})
}
}
@@ -37,7 +37,7 @@ struct Number {
number: u16,
}
-pub fn get_capsule() -> Capsule {
+pub fn get_capsule() -> Capsule<()> {
Capsule::build(
Template::build("number")
.build_paths_fn(get_build_paths)
diff --git a/examples/core/capsules/src/capsules/time.rs b/examples/core/capsules/src/capsules/time.rs
index f35d920b57..ef215f0127 100644
--- a/examples/core/capsules/src/capsules/time.rs
+++ b/examples/core/capsules/src/capsules/time.rs
@@ -4,16 +4,16 @@ use serde::{Deserialize, Serialize};
use sycamore::prelude::*;
lazy_static! {
- pub static ref TIME: Capsule = get_capsule();
+ pub static ref TIME: Capsule<()> = get_capsule();
}
// Note the use of props as `()`, indicating that this capsule doesn't take any
// properties
#[auto_scope]
-fn time_capsule(cx: Scope, state: &TimeStateRx, _props: ()) -> View {
- view! { cx,
+fn time_capsule(state: TimeStateRx, _props: ()) -> View {
+ view! {
// We'll put this inside a `p`, so we'll use a `span`
- span(id = "time") { (state.time.get()) }
+ span(id = "time") { (state.time.get_clone()) }
}
}
@@ -23,7 +23,7 @@ struct TimeState {
time: String,
}
-pub fn get_capsule() -> Capsule {
+pub fn get_capsule() -> Capsule<()> {
Capsule::build(
Template::build("time")
.build_state_fn(get_build_state)
diff --git a/examples/core/capsules/src/capsules/wrapper.rs b/examples/core/capsules/src/capsules/wrapper.rs
index 59c52d0a2d..5f991b1977 100644
--- a/examples/core/capsules/src/capsules/wrapper.rs
+++ b/examples/core/capsules/src/capsules/wrapper.rs
@@ -5,18 +5,18 @@ use sycamore::prelude::*;
use super::greeting::{GreetingProps, GREETING};
lazy_static! {
- pub static ref WRAPPER: Capsule = get_capsule();
+ pub static ref WRAPPER: Capsule = get_capsule();
}
// A simple wrapper capsule to show how capsules can use capsules
-fn wrapper_capsule(cx: Scope, props: GreetingProps) -> View {
- view! { cx,
+fn wrapper_capsule(props: GreetingProps) -> View {
+ view! {
// Because `props` is an owned variable, it has to be cloned here
- (GREETING.widget(cx, "", props.clone()))
+ (GREETING.widget( "", props.clone()))
}
}
-pub fn get_capsule() -> Capsule {
+pub fn get_capsule() -> Capsule {
Capsule::build(Template::build("wrapper"))
.empty_fallback()
.view(wrapper_capsule)
diff --git a/examples/core/capsules/src/main.rs b/examples/core/capsules/src/main.rs
index 798bd0c7f2..04bb474f76 100644
--- a/examples/core/capsules/src/main.rs
+++ b/examples/core/capsules/src/main.rs
@@ -4,7 +4,7 @@ mod templates;
use perseus::prelude::*;
#[perseus::main(perseus_axum::dflt_server)]
-pub fn main() -> PerseusApp {
+pub fn main() -> PerseusApp {
PerseusApp::new()
.template(crate::templates::index::get_template())
.template(crate::templates::about::get_template())
diff --git a/examples/core/capsules/src/templates/about.rs b/examples/core/capsules/src/templates/about.rs
index 202aadec20..d1470d6db2 100644
--- a/examples/core/capsules/src/templates/about.rs
+++ b/examples/core/capsules/src/templates/about.rs
@@ -3,18 +3,18 @@ use crate::capsules::links::LINKS;
use perseus::prelude::*;
use sycamore::prelude::*;
-fn about_page(cx: Scope) -> View {
- view! { cx,
+fn about_page() -> View {
+ view! {
// This will display the user's IP address using a delayed widget,
// meaning it will take a moment to load, even on initial loads. This can
// be useful for reducing the amount of content that needs to be served
// to users initially (sort of like the Perseus version of HTML streaming).
- (IP.delayed_widget(cx, "", ()))
- (LINKS.widget(cx, "", ()))
+ (IP.delayed_widget("", ()))
+ (LINKS.widget("", ()))
}
}
-pub fn get_template() -> Template {
+pub fn get_template() -> Template {
Template::build("about")
.view(about_page)
// This is extremely important. Notice that this template doesn't have any state of its own?
diff --git a/examples/core/capsules/src/templates/calc.rs b/examples/core/capsules/src/templates/calc.rs
index 258642c8c6..173de15b07 100644
--- a/examples/core/capsules/src/templates/calc.rs
+++ b/examples/core/capsules/src/templates/calc.rs
@@ -5,8 +5,8 @@ use serde::{Deserialize, Serialize};
use sycamore::prelude::*;
#[auto_scope]
-fn calc_page(cx: Scope, state: &CalcStateRx) -> View {
- view! { cx,
+fn calc_page(state: CalcStateRx) -> View {
+ view! {
// This was *not* built at build-time in `number`, so we're incrementally
// generating it. Importantly, Perseus can figure out that this should just
// be added to the build paths list of the `number` widget, so we don't need
@@ -14,7 +14,7 @@ fn calc_page(cx: Scope, state: &CalcStateRx) -> View {
p(id = "fifty-six") {
"The number fifty-six: "
// See `number.rs` for why this yields `56`
- (NUMBER.widget(cx, "/5", ()))
+ (NUMBER.widget("/5", ()))
"."
}
// Now, let me be clear. Using a widget as an addition function is a woeful abuse
@@ -31,19 +31,19 @@ fn calc_page(cx: Scope, state: &CalcStateRx) -> View {
p(id = "sum") {
"The sum of the state numbers: "
(NUMBER.widget(
- cx,
+
// We're using this widget as a glorified addition function
&format!(
"/{}/{}",
// We need to make them strings first
state
.numbers
- .get()
+ .get_clone()
.iter()
.map(|n| n.to_string())
.collect::>()
.join("/"),
- state.user_number.get()
+ state.user_number.get_clone()
),
()
))
@@ -51,7 +51,7 @@ fn calc_page(cx: Scope, state: &CalcStateRx) -> View {
}
p { "Type your number below..." }
input(bind:value = state.user_number) {}
- (LINKS.widget(cx, "", ()))
+ (LINKS.widget("", ()))
}
}
@@ -63,7 +63,7 @@ struct CalcState {
user_number: String,
}
-pub fn get_template() -> Template {
+pub fn get_template() -> Template {
Template::build("calc")
.view_with_state(calc_page)
.build_state_fn(get_build_state)
diff --git a/examples/core/capsules/src/templates/clock.rs b/examples/core/capsules/src/templates/clock.rs
index a6e1dea410..7e4c9d7d26 100644
--- a/examples/core/capsules/src/templates/clock.rs
+++ b/examples/core/capsules/src/templates/clock.rs
@@ -3,22 +3,22 @@ use crate::capsules::time::TIME;
use perseus::prelude::*;
use sycamore::prelude::*;
-fn clock_page(cx: Scope) -> View {
+fn clock_page() -> View {
// Nothing's wrong with preparing a widget in advance, especially if you want to
// use the same one in a few places (this will avoid unnecessary fetches in
// some cases, see the book for details)
- let time = TIME.widget(cx, "", ());
+ let time = TIME.widget("", ());
- view! { cx,
+ view! {
p {
"The most recent update to the time puts it at "
(time)
}
- (LINKS.widget(cx, "", ()))
+ (LINKS.widget("", ()))
}
}
-pub fn get_template() -> Template {
+pub fn get_template() -> Template {
Template::build("clock")
.view(clock_page)
// See `about.rs` for an explanation of this
diff --git a/examples/core/capsules/src/templates/four.rs b/examples/core/capsules/src/templates/four.rs
index c00a62024f..91dc3f351c 100644
--- a/examples/core/capsules/src/templates/four.rs
+++ b/examples/core/capsules/src/templates/four.rs
@@ -3,19 +3,19 @@ use crate::capsules::number::NUMBER;
use perseus::prelude::*;
use sycamore::prelude::*;
-fn four_page(cx: Scope) -> View {
- view! { cx,
+fn four_page() -> View {
+ view! {
p(id = "four") {
"The number four: "
// We're using the second argument to provide a *widget path* within the capsule
- (NUMBER.widget(cx, "/4", ()))
+ (NUMBER.widget( "/4", ()))
"."
}
- (LINKS.widget(cx, "", ()))
+ (LINKS.widget("", ()))
}
}
-pub fn get_template() -> Template {
+pub fn get_template() -> Template {
// Notice that this doesn't need to have rescheduling, because the widget it
// uses was built at build-time as part of `number`'s `get_build_paths`
// function.
diff --git a/examples/core/capsules/src/templates/index.rs b/examples/core/capsules/src/templates/index.rs
index c35777788e..706b58fd49 100644
--- a/examples/core/capsules/src/templates/index.rs
+++ b/examples/core/capsules/src/templates/index.rs
@@ -4,27 +4,27 @@ use crate::capsules::wrapper::WRAPPER;
use perseus::prelude::*;
use sycamore::prelude::*;
-fn index_page(cx: Scope) -> View {
- view! { cx,
+fn index_page() -> View {
+ view! {
p { "Hello World!" }
// This capsule wraps another capsule
- (WRAPPER.widget(cx, "", GreetingProps { color: "red".to_string() }))
+ (WRAPPER.widget( "", GreetingProps { color: "red".to_string() }))
// This is not the prettiest function call, deliberately, to encourage you
// to make this sort of thing part of the template it's used in, or to use
// a Sycamore component instead (which, for a navbar, we should, this is
// just an example)
- (LINKS.widget(cx, "", ()))
+ (LINKS.widget( "", ()))
}
}
#[engine_only_fn]
-fn head(cx: Scope) -> View {
- view! { cx,
+fn head() -> View {
+ view! {
title { "Index Page" }
}
}
-pub fn get_template() -> Template {
+pub fn get_template() -> Template {
Template::build("index").view(index_page).head(head).build()
}
diff --git a/examples/core/capsules/tests/main.rs b/examples/core/capsules/tests/main.rs
index faf40aa2a0..23713612f0 100644
--- a/examples/core/capsules/tests/main.rs
+++ b/examples/core/capsules/tests/main.rs
@@ -20,11 +20,13 @@ async fn index(c: &mut Client) -> Result<(), fantoccini::error::CmdError> {
Ok(())
}
- // Subsequent...
+ // Subsequent load (client-side navigation)
goto_page_from_links(c, "index-link").await?;
+ wait_for_checkpoint!("page_interactive", 1, c);
test(c).await?;
- // ...and initial
+ // Initial load (full page reload resets checkpoint counter)
c.refresh().await?;
+ wait_for_checkpoint!("page_interactive", 0, c);
test(c).await?;
Ok(())
@@ -46,11 +48,13 @@ async fn about(c: &mut Client) -> Result<(), fantoccini::error::CmdError> {
Ok(())
}
- // Subsequent...
+ // Subsequent load (client-side navigation)
goto_page_from_links(c, "about-link").await?;
+ wait_for_checkpoint!("page_interactive", 1, c);
test(c).await?;
- // ...and initial
+ // Initial load (full page reload resets checkpoint counter)
c.refresh().await?;
+ wait_for_checkpoint!("page_interactive", 0, c);
test(c).await?;
Ok(())
@@ -75,11 +79,13 @@ async fn clock(c: &mut Client) -> Result<(), fantoccini::error::CmdError> {
Ok(())
}
- // Subsequent...
+ // Subsequent load (client-side navigation)
goto_page_from_links(c, "clock-link").await?;
+ wait_for_checkpoint!("page_interactive", 1, c);
test(c).await?;
- // ...and initial
+ // Initial load (full page reload resets checkpoint counter)
c.refresh().await?;
+ wait_for_checkpoint!("page_interactive", 0, c);
test(c).await?;
Ok(())
@@ -98,11 +104,13 @@ async fn four(c: &mut Client) -> Result<(), fantoccini::error::CmdError> {
Ok(())
}
- // Subsequent...
+ // Subsequent load (client-side navigation)
goto_page_from_links(c, "four-link").await?;
+ wait_for_checkpoint!("page_interactive", 1, c);
test(c).await?;
- // ...and initial
+ // Initial load (full page reload resets checkpoint counter)
c.refresh().await?;
+ wait_for_checkpoint!("page_interactive", 0, c);
test(c).await?;
Ok(())
@@ -152,10 +160,11 @@ async fn calc(c: &mut Client) -> Result<(), fantoccini::error::CmdError> {
Ok(())
}
- // Subsequent...
+ // Subsequent load (client-side navigation)
goto_page_from_links(c, "calc-link").await?;
+ wait_for_checkpoint!("page_interactive", 1, c);
test(c).await?;
- // ...and initial
+ // Initial load (full page reload resets checkpoint counter)
c.refresh().await?;
wait_for_checkpoint!("page_interactive", 0, c);
test(c).await?;
diff --git a/examples/core/custom_server/Cargo.toml b/examples/core/custom_server/Cargo.toml
index 54da474215..c454761052 100644
--- a/examples/core/custom_server/Cargo.toml
+++ b/examples/core/custom_server/Cargo.toml
@@ -1,22 +1,24 @@
[package]
name = "perseus-example-custom-server"
-version = "0.4.3"
+version = "0.5.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
-perseus = { path = "../../../packages/perseus", features = [ "hydrate" ] }
-sycamore = "^0.8.1"
+perseus = { path = "../../../packages/perseus", features = ["hydrate"] }
+sycamore = "^0.9.2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
[target.'cfg(engine)'.dev-dependencies]
-fantoccini = "0.19"
+fantoccini = "0.22"
[target.'cfg(engine)'.dependencies]
-tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] }
-perseus-warp = { path = "../../../packages/perseus-warp", features = [ "dflt-server" ] }
+tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread"] }
+perseus-warp = { path = "../../../packages/perseus-warp", features = [
+ "dflt-server",
+] }
warp = { package = "warp-fix-171", version = "0.3" } # Temporary until Warp #171 is resolved
[target.'cfg(client)'.dependencies]
diff --git a/examples/core/custom_server/src/main.rs b/examples/core/custom_server/src/main.rs
index d0438ec005..571a811ded 100644
--- a/examples/core/custom_server/src/main.rs
+++ b/examples/core/custom_server/src/main.rs
@@ -53,7 +53,7 @@ pub async fn dflt_server<
}
#[perseus::main(dflt_server)]
-pub fn main() -> PerseusApp {
+pub fn main() -> PerseusApp {
PerseusApp::new()
.template(crate::templates::index::get_template())
.template(crate::templates::about::get_template())
diff --git a/examples/core/custom_server/src/templates/about.rs b/examples/core/custom_server/src/templates/about.rs
index cbebcd1bb9..933ec553c7 100644
--- a/examples/core/custom_server/src/templates/about.rs
+++ b/examples/core/custom_server/src/templates/about.rs
@@ -1,12 +1,12 @@
use perseus::prelude::*;
use sycamore::prelude::*;
-fn about_page(cx: Scope) -> View {
- view! { cx,
+fn about_page() -> View {
+ view! {
p { "About." }
}
}
-pub fn get_template() -> Template {
+pub fn get_template() -> Template {
Template::build("about").view(about_page).build()
}
diff --git a/examples/core/custom_server/src/templates/index.rs b/examples/core/custom_server/src/templates/index.rs
index 664d2c48bc..00e34a6294 100644
--- a/examples/core/custom_server/src/templates/index.rs
+++ b/examples/core/custom_server/src/templates/index.rs
@@ -1,13 +1,13 @@
use perseus::prelude::*;
use sycamore::prelude::*;
-fn index_page(cx: Scope) -> View {
- view! { cx,
+fn index_page() -> View {
+ view! {
p { "Hello World!" }
- a(href = "about", id = "about-link") { "About!" }
+ Link(to = "/about", id = "about-link") { "About!" }
}
}
-pub fn get_template() -> Template {
+pub fn get_template() -> Template {
Template::build("index").view(index_page).build()
}
diff --git a/examples/core/custom_server_rocket/Cargo.toml b/examples/core/custom_server_rocket/Cargo.toml
index 16e185e061..25d96d753f 100644
--- a/examples/core/custom_server_rocket/Cargo.toml
+++ b/examples/core/custom_server_rocket/Cargo.toml
@@ -1,22 +1,24 @@
[package]
name = "perseus-example-custom-server-rocket"
-version = "0.4.3"
+version = "0.5.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
perseus = { path = "../../../packages/perseus", features = ["hydrate"] }
-sycamore = "^0.8.1"
+sycamore = "^0.9.2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
[target.'cfg(engine)'.dev-dependencies]
-fantoccini = "0.17"
+fantoccini = "0.22"
[target.'cfg(engine)'.dependencies]
tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread"] }
-perseus-rocket = { path = "../../../packages/perseus-rocket", features = [ "dflt-server" ] }
-rocket = "0.5.0-rc.2"
+perseus-rocket = { path = "../../../packages/perseus-rocket", features = [
+ "dflt-server",
+] }
+rocket = "0.5.1"
[target.'cfg(client)'.dependencies]
diff --git a/examples/core/custom_server_rocket/src/main.rs b/examples/core/custom_server_rocket/src/main.rs
index 818c110d5b..cf01f4ca1e 100644
--- a/examples/core/custom_server_rocket/src/main.rs
+++ b/examples/core/custom_server_rocket/src/main.rs
@@ -42,7 +42,7 @@ pub async fn dflt_server<
}
#[perseus::main(dflt_server)]
-pub fn main() -> PerseusApp {
+pub fn main() -> PerseusApp {
PerseusApp::new()
.template(crate::templates::index::get_template())
.template(crate::templates::about::get_template())
diff --git a/examples/core/custom_server_rocket/src/templates/about.rs b/examples/core/custom_server_rocket/src/templates/about.rs
index 12989c60c7..76a27f5ee1 100644
--- a/examples/core/custom_server_rocket/src/templates/about.rs
+++ b/examples/core/custom_server_rocket/src/templates/about.rs
@@ -1,19 +1,19 @@
use perseus::prelude::*;
use sycamore::prelude::*;
-fn about_page(cx: Scope) -> View {
- view! { cx,
+fn about_page() -> View {
+ view! {
p { "About." }
}
}
#[engine_only_fn]
-fn head(cx: Scope) -> View {
- view! { cx,
+fn head() -> View {
+ view! {
title { "About Page | Perseus Example – Basic" }
}
}
-pub fn get_template() -> Template {
+pub fn get_template() -> Template {
Template::build("about").view(about_page).head(head).build()
}
diff --git a/examples/core/custom_server_rocket/src/templates/index.rs b/examples/core/custom_server_rocket/src/templates/index.rs
index 09ef5e05f7..568825ae99 100644
--- a/examples/core/custom_server_rocket/src/templates/index.rs
+++ b/examples/core/custom_server_rocket/src/templates/index.rs
@@ -9,16 +9,16 @@ struct IndexPageState {
}
#[auto_scope]
-fn index_page(cx: Scope, state: &IndexPageStateRx) -> View {
- view! { cx,
- p { (state.greeting.get()) }
- a(href = "about", id = "about-link") { "About!" }
+fn index_page(state: IndexPageStateRx) -> View {
+ view! {
+ p { (state.greeting.get_clone()) }
+ Link(to = "/about", id = "about-link") { "About!" }
}
}
#[engine_only_fn]
-fn head(cx: Scope, _props: IndexPageState) -> View {
- view! { cx,
+fn head(_props: IndexPageState) -> View {
+ view! {
title { "Index Page | Perseus Example – Basic" }
}
}
@@ -30,7 +30,7 @@ async fn get_build_state(_info: StateGeneratorInfo<()>) -> IndexPageState {
}
}
-pub fn get_template() -> Template {
+pub fn get_template() -> Template {
Template::build("index")
.build_state_fn(get_build_state)
.view_with_state(index_page)
diff --git a/examples/core/error_views/Cargo.toml b/examples/core/error_views/Cargo.toml
index dacf38504f..5e2ed547c0 100644
--- a/examples/core/error_views/Cargo.toml
+++ b/examples/core/error_views/Cargo.toml
@@ -1,24 +1,24 @@
[package]
name = "perseus-example-base"
-version = "0.4.3"
+version = "0.5.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
-perseus = { path = "../../../packages/perseus", features = [ "hydrate" ] }
-sycamore = "^0.8.1"
+perseus = { path = "../../../packages/perseus", features = ["hydrate"] }
+sycamore = "^0.9.2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
[target.'cfg(engine)'.dev-dependencies]
-fantoccini = "0.19"
+fantoccini = "0.22"
[target.'cfg(engine)'.dependencies]
-tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] }
+tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread"] }
## **WARNING!** Before running this example outside the Perseus repo, replace the below line with
## the one commented out below it (changing the path dependency to the version you want to use)
-perseus-axum = { package = "perseus-integration", path = "../../../packages/perseus-integration", default-features = false }
+perseus-axum = { package = "perseus-integration", path = "../../../packages/perseus-integration", default-features = false, features = ["axum"] }
# perseus-axum = { path = "../../../packages/perseus-axum", features = [ "dflt-server" ] }
[target.'cfg(client)'.dependencies]
diff --git a/examples/core/error_views/src/error_views.rs b/examples/core/error_views/src/error_views.rs
index 2d5c3fb9c9..4b29966e38 100644
--- a/examples/core/error_views/src/error_views.rs
+++ b/examples/core/error_views/src/error_views.rs
@@ -4,7 +4,7 @@ use sycamore::prelude::*;
// Like templates, error views are generic over `G`, so that they can be
// rendered ahead of time on the engine-side when an error occurs
-pub fn get_error_views() -> ErrorViews {
+pub fn get_error_views() -> ErrorViews {
// Creating a set of error views is a matter of creating a single handler
// function that can respond to any error. This handler takes a Sycamore scope,
// the actual error (`perseus::errors::ClientError`), some information about
@@ -41,7 +41,7 @@ pub fn get_error_views() -> ErrorViews {
// load extra material like new stylesheets on an error, as it might be a
// network error), and the second one for the body (to be displayed in
// `err_pos`).
- ErrorViews::new(|cx, err, _err_info, _err_pos| {
+ ErrorViews::new(|err, _err_info, _err_pos| {
match err {
// Errors from the server, like 404s; these are best displayed over the whole
// page
@@ -52,28 +52,28 @@ pub fn get_error_views() -> ErrorViews {
} => match status {
// This one is usually handled separately
404 => (
- view! { cx,
+ view! {
title { "Page not found" }
},
- view! { cx,
+ view! {
p { "Sorry, that page doesn't seem to exist." }
}
),
// If the status is 4xx, it's a client-side problem (which is weird, and might indicate tampering)
_ if (400..500).contains(&status) => (
- view! { cx,
+ view! {
title { "Error" }
},
- view! { cx,
+ view! {
p { "There was something wrong with the last request, please try reloading the page." }
}
),
// 5xx is a server error
_ => (
- view! { cx,
+ view! {
title { "Error" }
},
- view! { cx,
+ view! {
p { "Sorry, our server experienced an internal error. Please try reloading the page." }
}
)
@@ -83,19 +83,19 @@ pub fn get_error_views() -> ErrorViews {
//
// The argument here is the formatted panic message.
ClientError::Panic(_) => (
- view! { cx,
+ view! {
title { "Critical error" }
},
- view! { cx,
+ view! {
p { "Sorry, but a critical internal error has occurred. This has been automatically reported to our team, who'll get on it as soon as possible. In the mean time, please try reloading the page." }
}
),
// Network errors (but these could be caused by unexpected server rejections)
ClientError::FetchError(_) => (
- view! { cx,
+ view! {
title { "Error" }
},
- view! { cx,
+ view! {
p { "A network error occurred, do you have an internet connection? (If you do, try reloading the page.)" }
}
),
@@ -117,10 +117,10 @@ pub fn get_error_views() -> ErrorViews {
// caught at the time of the function's execution, but sometimes
// you'll just want to leave them to a popup error)
ClientError::PreloadError(_) => (
- view! { cx,
+ view! {
title { "Error" }
},
- view! { cx,
+ view! {
p { (format!("An internal error has occurred: '{}'.", err)) }
}
)
diff --git a/examples/core/error_views/src/main.rs b/examples/core/error_views/src/main.rs
index a751c7d6eb..2ae0d215a1 100644
--- a/examples/core/error_views/src/main.rs
+++ b/examples/core/error_views/src/main.rs
@@ -4,7 +4,7 @@ mod templates;
use perseus::prelude::*;
#[perseus::main(perseus_axum::dflt_server)]
-pub fn main() -> PerseusApp {
+pub fn main() -> PerseusApp {
PerseusApp::new()
.template(crate::templates::index::get_template())
// The same convention of a function to return the needed `struct` is
diff --git a/examples/core/error_views/src/templates/index.rs b/examples/core/error_views/src/templates/index.rs
index 9d79a79bcf..9e290b8d28 100644
--- a/examples/core/error_views/src/templates/index.rs
+++ b/examples/core/error_views/src/templates/index.rs
@@ -1,26 +1,26 @@
use perseus::prelude::*;
use sycamore::prelude::*;
-fn index_page(cx: Scope) -> View {
+fn index_page() -> View {
// Deliberate panic to show how panic handling works (in an `on_mount` so we
// still reach the right checkpoints for testing)
#[cfg(client)]
- on_mount(cx, || {
+ on_mount(|| {
panic!();
});
- view! { cx,
+ view! {
p { "Hello World!" }
}
}
#[engine_only_fn]
-fn head(cx: Scope) -> View {
- view! { cx,
+fn head() -> View {
+ view! {
title { "Index Page" }
}
}
-pub fn get_template() -> Template {
+pub fn get_template() -> Template {
Template::build("index").view(index_page).head(head).build()
}
diff --git a/examples/core/freezing_and_thawing/Cargo.toml b/examples/core/freezing_and_thawing/Cargo.toml
index 466bb2b453..2e094fa7ae 100644
--- a/examples/core/freezing_and_thawing/Cargo.toml
+++ b/examples/core/freezing_and_thawing/Cargo.toml
@@ -6,19 +6,19 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
-perseus = { path = "../../../packages/perseus", features = [ "hydrate" ] }
-sycamore = "^0.8.1"
+perseus = { path = "../../../packages/perseus", features = ["hydrate"] }
+sycamore = "^0.9.2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
[dev-dependencies]
-fantoccini = "0.19"
+fantoccini = "0.22"
[target.'cfg(engine)'.dependencies]
-tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] }
+tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread"] }
## **WARNING!** Before running this example outside the Perseus repo, replace the below line with
## the one commented out below it (changing the path dependency to the version you want to use)
-perseus-axum = { package = "perseus-integration", path = "../../../packages/perseus-integration", default-features = false }
+perseus-axum = { package = "perseus-integration", path = "../../../packages/perseus-integration", default-features = false, features = ["axum"] }
# perseus-axum = { path = "../../../packages/perseus-axum", features = [ "dflt-server" ] }
[target.'cfg(client)'.dependencies]
diff --git a/examples/core/freezing_and_thawing/src/main.rs b/examples/core/freezing_and_thawing/src/main.rs
index 0c7e36be7f..da68882c0c 100644
--- a/examples/core/freezing_and_thawing/src/main.rs
+++ b/examples/core/freezing_and_thawing/src/main.rs
@@ -4,7 +4,7 @@ mod templates;
use perseus::prelude::*;
#[perseus::main(perseus_axum::dflt_server)]
-pub fn main() -> PerseusApp {
+pub fn main() -> PerseusApp {
PerseusApp::new()
.template(crate::templates::index::get_template())
.template(crate::templates::about::get_template())
diff --git a/examples/core/freezing_and_thawing/src/templates/about.rs b/examples/core/freezing_and_thawing/src/templates/about.rs
index f4b1c84d5c..7aacfc53fd 100644
--- a/examples/core/freezing_and_thawing/src/templates/about.rs
+++ b/examples/core/freezing_and_thawing/src/templates/about.rs
@@ -3,33 +3,37 @@ use sycamore::prelude::*;
use crate::global_state::AppStateRx;
-fn about_page(cx: Scope) -> View {
+fn about_page() -> View {
// This is not part of our data model, we do NOT want the frozen app
// synchronized as part of our page's state, it should be separate
- let frozen_app = create_signal(cx, String::new());
- let render_ctx = Reactor::::from_cx(cx);
+ let frozen_app = create_signal(String::new());
+ let render_ctx = Reactor::from_cx();
- let global_state = render_ctx.get_global_state::(cx);
+ let global_state = render_ctx.get_global_state::();
- view! { cx,
- p(id = "global_state") { (global_state.test.get()) }
+ // Clone for the closure (required for 'static lifetime in Sycamore 0.9.2)
+ let frozen_app_clone = frozen_app.clone();
+ let render_ctx_clone = render_ctx.clone();
+
+ view! {
+ p(id = "global_state") { (global_state.test.get_clone()) }
// When the user visits this and then comes back, they'll still be able to see their username (the previous state will be retrieved from the global state automatically)
- a(href = "", id = "index-link") { "Index" }
+ Link(to = "/", id = "index-link") { "Index" }
br()
// We'll let the user freeze from here to demonstrate that the frozen state also navigates back to the last route
- button(id = "freeze_button", on:click = |_| {
+ button(id = "freeze_button", on:click = move |_| {
#[cfg(client)]
{
use perseus::state::Freeze;
- frozen_app.set(render_ctx.freeze());
+ frozen_app_clone.set(render_ctx_clone.freeze());
}
}) { "Freeze!" }
- p(id = "frozen_app") { (frozen_app.get()) }
+ p(id = "frozen_app") { (frozen_app.get_clone()) }
}
}
-pub fn get_template() -> Template {
+pub fn get_template() -> Template {
Template::build("about").view(about_page).build()
}
diff --git a/examples/core/freezing_and_thawing/src/templates/index.rs b/examples/core/freezing_and_thawing/src/templates/index.rs
index bd3a573ae8..896636f96f 100644
--- a/examples/core/freezing_and_thawing/src/templates/index.rs
+++ b/examples/core/freezing_and_thawing/src/templates/index.rs
@@ -9,38 +9,44 @@ struct IndexPageState {
username: String,
}
-fn index_page<'a, G: Html>(cx: BoundedScope<'_, 'a>, state: &'a IndexPageStateRx) -> View {
+fn index_page(state: IndexPageStateRx) -> View {
// This is not part of our data model, we do NOT want the frozen app
// synchronized as part of our page's state, it should be separate
- let frozen_app = create_signal(cx, String::new());
- let reactor = Reactor::::from_cx(cx);
+ let frozen_app = create_signal(String::new());
+ let reactor = Reactor::from_cx();
- let global_state = reactor.get_global_state::(cx);
+ let global_state = reactor.get_global_state::();
- view! { cx,
+ // Clone for closures (required for 'static lifetime in Sycamore 0.9.2)
+ let frozen_app_freeze = frozen_app.clone();
+ let reactor_freeze = reactor.clone();
+ let frozen_app_thaw = frozen_app.clone();
+ let reactor_thaw = reactor.clone();
+
+ view! {
// For demonstration, we'll let the user modify the page's state and the global state arbitrarily
- p(id = "page_state") { (format!("Greetings, {}!", state.username.get())) }
+ p(id = "page_state") { (format!("Greetings, {}!", state.username.get_clone())) }
input(id = "set_page_state", bind:value = state.username, placeholder = "Username")
- p(id = "global_state") { (global_state.test.get()) }
+ p(id = "global_state") { (global_state.test.get_clone()) }
input(id = "set_global_state", bind:value = global_state.test, placeholder = "Global state")
// When the user visits this and then comes back, they'll still be able to see their username (the previous state will be retrieved from the global state automatically)
- a(href = "about", id = "about-link") { "About" }
+ Link(to = "/about", id = "about-link") { "About" }
br()
- button(id = "freeze_button", on:click = |_| {
+ button(id = "freeze_button", on:click = move |_| {
#[cfg(client)]
{
use perseus::state::Freeze;
- frozen_app.set(reactor.freeze());
+ frozen_app_freeze.set(reactor_freeze.freeze());
}
}) { "Freeze!" }
- p(id = "frozen_app") { (frozen_app.get()) }
+ p(id = "frozen_app") { (frozen_app.get_clone()) }
input(id = "thaw_input", bind:value = frozen_app, placeholder = "Frozen state")
- button(id = "thaw_button", on:click = |_| {
+ button(id = "thaw_button", on:click = move |_| {
#[cfg(client)]
- reactor.thaw(&frozen_app.get(), perseus::state::ThawPrefs {
+ reactor_thaw.thaw(&frozen_app_thaw.get_clone(), perseus::state::ThawPrefs {
page: perseus::state::PageThawPrefs::IncludeAll,
global_prefer_frozen: true
}).unwrap();
@@ -48,7 +54,7 @@ fn index_page<'a, G: Html>(cx: BoundedScope<'_, 'a>, state: &'a IndexPageStateRx
}
}
-pub fn get_template() -> Template {
+pub fn get_template() -> Template {
Template::build("index")
.build_state_fn(get_build_state)
.view_with_state(index_page)
diff --git a/examples/core/freezing_and_thawing/tests/main.rs b/examples/core/freezing_and_thawing/tests/main.rs
index fafa8e3844..f565d01dc8 100644
--- a/examples/core/freezing_and_thawing/tests/main.rs
+++ b/examples/core/freezing_and_thawing/tests/main.rs
@@ -58,12 +58,24 @@ async fn main(c: &mut Client) -> Result<(), fantoccini::error::CmdError> {
.send_keys(&frozen_app)
.await?;
c.find(Locator::Id("thaw_button")).await?.click().await?;
- // We should now be back on the about page, with the global state restored there
+ // Wait for navigation to complete by polling the URL until it changes
+ for _ in 0..50 {
+ if c.current_url()
+ .await?
+ .as_ref()
+ .starts_with("http://localhost:8080/about")
+ {
+ break;
+ }
+ tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
+ }
+ // Now verify we're on the about page
assert!(c
.current_url()
.await?
.as_ref()
.starts_with("http://localhost:8080/about"));
+ // And verify the global state was restored
assert_eq!(
c.find(Locator::Id("global_state")).await?.text().await?,
"Hello World! Extra text."
diff --git a/examples/core/global_state/Cargo.toml b/examples/core/global_state/Cargo.toml
index 24b1331b06..edfd1587b9 100644
--- a/examples/core/global_state/Cargo.toml
+++ b/examples/core/global_state/Cargo.toml
@@ -6,19 +6,19 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
-perseus = { path = "../../../packages/perseus", features = [ "hydrate" ] }
-sycamore = "^0.8.1"
+perseus = { path = "../../../packages/perseus", features = ["hydrate"] }
+sycamore = "^0.9.2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
[dev-dependencies]
-fantoccini = "0.19"
+fantoccini = "0.22"
[target.'cfg(engine)'.dependencies]
-tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] }
+tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread"] }
## **WARNING!** Before running this example outside the Perseus repo, replace the below line with
## the one commented out below it (changing the path dependency to the version you want to use)
-perseus-axum = { package = "perseus-integration", path = "../../../packages/perseus-integration", default-features = false }
+perseus-axum = { package = "perseus-integration", path = "../../../packages/perseus-integration", default-features = false, features = ["axum"] }
# perseus-axum = { path = "../../../packages/perseus-axum", features = [ "dflt-server" ] }
[target.'cfg(client)'.dependencies]
diff --git a/examples/core/global_state/src/main.rs b/examples/core/global_state/src/main.rs
index 0c7e36be7f..da68882c0c 100644
--- a/examples/core/global_state/src/main.rs
+++ b/examples/core/global_state/src/main.rs
@@ -4,7 +4,7 @@ mod templates;
use perseus::prelude::*;
#[perseus::main(perseus_axum::dflt_server)]
-pub fn main() -> PerseusApp {
+pub fn main() -> PerseusApp {
PerseusApp::new()
.template(crate::templates::index::get_template())
.template(crate::templates::about::get_template())
diff --git a/examples/core/global_state/src/templates/about.rs b/examples/core/global_state/src/templates/about.rs
index 6b819d8b2c..f50e76e4d0 100644
--- a/examples/core/global_state/src/templates/about.rs
+++ b/examples/core/global_state/src/templates/about.rs
@@ -3,25 +3,25 @@ use sycamore::prelude::*;
use crate::global_state::AppStateRx;
-fn about_page(cx: Scope) -> View {
- let global_state = Reactor::::from_cx(cx).get_global_state::(cx);
+fn about_page() -> View {
+ let global_state = Reactor::from_cx().get_global_state::();
- view! { cx,
+ view! {
// The user can change the global state through an input, and the changes they make will be reflected throughout the app
- p { (global_state.test.get()) }
+ p { (global_state.test.get_clone()) }
input(bind:value = global_state.test)
- a(href = "") { "Index" }
+ Link(to = "/") { "Index" }
}
}
#[engine_only_fn]
-fn head(cx: Scope) -> View {
- view! { cx,
+fn head() -> View {
+ view! {
title { "About Page" }
}
}
-pub fn get_template() -> Template {
+pub fn get_template() -> Template {
Template::build("about").view(about_page).head(head).build()
}
diff --git a/examples/core/global_state/src/templates/index.rs b/examples/core/global_state/src/templates/index.rs
index 3ac3568c62..bd23f353dd 100644
--- a/examples/core/global_state/src/templates/index.rs
+++ b/examples/core/global_state/src/templates/index.rs
@@ -4,27 +4,27 @@ use sycamore::prelude::*;
// Note that this template takes no state of its own in this example, but it
// certainly could
-fn index_page(cx: Scope) -> View {
+fn index_page() -> View {
// We access the global state through the render context, extracted from
// Sycamore's context system
- let global_state = Reactor::::from_cx(cx).get_global_state::(cx);
+ let global_state = Reactor::from_cx().get_global_state::();
- view! { cx,
+ view! {
// The user can change the global state through an input, and the changes they make will be reflected throughout the app
- p { (global_state.test.get()) }
+ p { (global_state.test.get_clone()) }
input(bind:value = global_state.test)
- a(href = "about", id = "about-link") { "About" }
+ Link(to = "/about", id = "about-link") { "About" }
}
}
#[engine_only_fn]
-fn head(cx: Scope) -> View {
- view! { cx,
+fn head() -> View {
+ view! {
title { "Index Page" }
}
}
-pub fn get_template() -> Template {
+pub fn get_template() -> Template {
Template::build("index").view(index_page).head(head).build()
}
diff --git a/examples/core/helper_build_state/Cargo.toml b/examples/core/helper_build_state/Cargo.toml
index b5e5ba6e19..6f330097ec 100644
--- a/examples/core/helper_build_state/Cargo.toml
+++ b/examples/core/helper_build_state/Cargo.toml
@@ -1,24 +1,24 @@
[package]
name = "perseus-example-helper-build-state"
-version = "0.4.3"
+version = "0.5.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
-perseus = { path = "../../../packages/perseus", features = [ "hydrate" ] }
-sycamore = "^0.8.1"
+perseus = { path = "../../../packages/perseus", features = ["hydrate"] }
+sycamore = "^0.9.2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
[target.'cfg(engine)'.dev-dependencies]
-fantoccini = "0.19"
+fantoccini = "0.22"
[target.'cfg(engine)'.dependencies]
-tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] }
+tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread"] }
## **WARNING!** Before running this example outside the Perseus repo, replace the below line with
## the one commented out below it (changing the path dependency to the version you want to use)
-perseus-axum = { package = "perseus-integration", path = "../../../packages/perseus-integration", default-features = false }
+perseus-axum = { package = "perseus-integration", path = "../../../packages/perseus-integration", default-features = false, features = ["axum"] }
# perseus-axum = { path = "../../../packages/perseus-axum", features = [ "dflt-server" ] }
[target.'cfg(client)'.dependencies]
diff --git a/examples/core/helper_build_state/src/main.rs b/examples/core/helper_build_state/src/main.rs
index 804b51e5f4..893f6302fe 100644
--- a/examples/core/helper_build_state/src/main.rs
+++ b/examples/core/helper_build_state/src/main.rs
@@ -3,7 +3,7 @@ mod templates;
use perseus::prelude::*;
#[perseus::main(perseus_axum::dflt_server)]
-pub fn main() -> PerseusApp {
+pub fn main() -> PerseusApp {
PerseusApp::new()
.template(crate::templates::index::get_template())
.error_views(ErrorViews::unlocalized_development_default())
diff --git a/examples/core/helper_build_state/src/templates/index.rs b/examples/core/helper_build_state/src/templates/index.rs
index 1e71f68bc2..a3cbc14555 100644
--- a/examples/core/helper_build_state/src/templates/index.rs
+++ b/examples/core/helper_build_state/src/templates/index.rs
@@ -2,13 +2,13 @@ use perseus::prelude::*;
use serde::{Deserialize, Serialize};
use sycamore::prelude::*;
-fn index_page<'a, G: Html>(cx: BoundedScope<'_, 'a>, state: &'a PageStateRx) -> View {
- view! { cx,
+fn index_page(state: PageStateRx) -> View {
+ view! {
h1 {
- (state.title.get())
+ (state.title.get_clone())
}
p {
- (state.content.get())
+ (state.content.get_clone())
}
}
}
@@ -58,7 +58,7 @@ async fn get_build_paths() -> BuildPaths {
}
}
-pub fn get_template