Islands Architecture
Taxus implements the Islands Architecture: Tera templates render the static "sea" of HTML, while Yew components are pre-rendered server-side into "islands" and then hydrated by the WASM client in the browser.
How It Works
BUILD TIME (generator) BROWSER
───────────────────── ───────
1. Tera renders page shell 4. HTML is immediately visible (no JS needed)
2. island() calls Yew SSR 5. WASM loads asynchronously
3. SSR HTML + props JSON emitted 6. Yew hydrates mount points → interactive
Build-Time: Server-Side Rendering
When you use {{ island(component="Counter", initial=5) | safe }} in a template:
- The
island()Tera function is called during template rendering - Yew SSR renders the component to HTML
- The output is wrapped in a mount point div with serialized props:
<div data-island="Counter" data-props='{"initial":5}'>
<!-- Pre-rendered by Yew SSR: -->
<div class="counter"><span>5</span><button>+</button></div>
</div>
Browser-Time: Hydration
When the page loads:
- The pre-rendered HTML is immediately visible (no JavaScript required)
- The WASM bundle loads asynchronously
- The client finds all
[data-island]elements in the DOM - For each island: deserialize
data-props, callyew::Renderer::hydrate() - The component becomes interactive without re-rendering
Two-Tier Interactivity
taxus supports two layers of interactivity:
| Tier | Technology | Use For |
|---|---|---|
| 1 — General | static/scripts.js (vanilla JS) | DOM manipulation, toggles, analytics, lightweight events |
| 2 — Performance | Yew WASM island | Heavy computation, complex reactive state, data-intensive UI |
Use vanilla JS for simple interactions; reserve Yew islands for components that benefit from the Yew component model.
Using Islands in Templates
The island() Tera Function
Use the island() function in any .html template:
{% extends "base.html" %}
{% block content %}
{{ page.content | safe }}
<h2>Interactive Counter</h2>
{{ island(component="Counter", initial=5) | safe }}
{% endblock %}
Important: Always use | safe after island() to prevent HTML escaping.
Passing Props
Props are passed as keyword arguments to island():
{{ island(component="Counter", initial=5) | safe }}
{{ island(component="MyWidget", label="Hello", count=3) | safe }}
The props are serialized to JSON and stored in data-props.
The class Prop
All island components accept an optional class prop that appends custom CSS classes to the component's outer <div>:
{{ island(component="SearchBox", class="docs-search") | safe }}
This renders as:
<div data-island="SearchBox" data-props='{"placeholder":"Search...","max_results":5,"class":"docs-search"}' class="search-box docs-search">
<!-- component content -->
</div>
This enables template authors to pass CSS styling hooks for targeting descendant elements without modifying component source.
Writing an Island Component
Island components live in common/src/components/. Their props must implement Serialize and Deserialize:
#![allow(unused)] fn main() { // common/src/components/counter.rs use serde::{Deserialize, Serialize}; use yew::prelude::*; #[derive(Properties, PartialEq, Clone, Serialize, Deserialize)] pub struct CounterProps { #[prop_or_default] pub initial: i32, #[prop_or_default] pub class: String, } #[function_component(Counter)] pub fn counter(props: &CounterProps) -> Html { let count = use_state(|| props.initial); let on_click = { let count = count.clone(); Callback::from(move |_| count.set(*count + 1)) }; html! { <div class="counter"> <span>{ *count }</span> <button onclick={on_click}>{ "+" }</button> </div> } } }
Export the Component
Add the component module to common/src/components/mod.rs:
#![allow(unused)] fn main() { pub mod counter; }
Registering a New Island
Two registries must be updated in sync:
1. Generator SSR Registry
In generator/src/templates/renderer.rs, add a match arm to the island() Tera function:
#![allow(unused)] fn main() { #[cfg(feature = "islands")] tera.register_function("island", |args: &HashMap<String, tera::Value>| { use tera::Value; let component = args.get("component").and_then(Value::as_str).unwrap_or(""); let html = match component { "Counter" => { use crate::build::pipeline::render_island_counter; use common::components::counter::CounterProps; let initial = args.get("initial").and_then(Value::as_i64).unwrap_or(0) as i32; render_island_counter(CounterProps { initial }) } "SearchBox" => { use crate::build::pipeline::render_search_box; use common::components::search_box::SearchBoxProps; let placeholder = args.get("placeholder") .and_then(Value::as_str) .unwrap_or("Search...") .to_string(); let max_results = args.get("max_results") .and_then(Value::as_i64) .unwrap_or(5) as usize; render_search_box(SearchBoxProps { placeholder, max_results }) } "MyWidget" => { // Add your component here use crate::build::pipeline::render_island_generic; use common::components::my_widget::{MyWidget, MyWidgetProps}; let label = args.get("label") .and_then(Value::as_str) .unwrap_or("") .to_string(); let count = args.get("count") .and_then(Value::as_i64) .unwrap_or(0) as i32; render_island_generic::<MyWidget>( MyWidgetProps { label, count }, "MyWidget" ) } other => format!("<!-- unknown island: {other} -->"), }; Ok(Value::String(html)) }); }
2. Client Hydration Registry
In client/src/main.rs, add a match arm to the hydration function:
#![allow(unused)] fn main() { fn hydrate_island(name: &str, el: HtmlElement, props_json: &str) { match name { "Counter" => { let props: CounterProps = serde_json::from_str(props_json) .unwrap_or(CounterProps { initial: 0 }); yew::Renderer::<Counter>::with_root_and_props(el.into(), props).hydrate(); } "SearchBox" => { let props: SearchBoxProps = serde_json::from_str(props_json) .unwrap_or(SearchBoxProps { placeholder: String::new(), max_results: 5, }); yew::Renderer::<SearchBox>::with_root_and_props(el.into(), props).hydrate(); } "MyWidget" => { let props: MyWidgetProps = serde_json::from_str(props_json) .unwrap_or(MyWidgetProps { label: String::new(), count: 0 }); yew::Renderer::<MyWidget>::with_root_and_props(el.into(), props).hydrate(); } _ => { /* ignore unknown islands */ } } } }
Built-in Islands
Taxus includes two built-in island components:
Counter
A simple counter with increment button. This is an example component demonstrating the islands architecture — useful for testing and learning, but not intended for production use.
SearchBox
A production-ready search component with debounced input and async results. See Search for full documentation.
{{ island(component="SearchBox", placeholder="Search...", max_results=10, class="my-search") | safe }}
Initializing a Site with Islands
Use the --islands flag when creating a new site:
cargo run -- init my-site --islands
This includes the WASM hydration script in the generated templates/base.html:
<script type="module">
import init from '/wasm/client.js';
init();
</script>
Building the WASM Client
The WASM client is compiled automatically when building with the islands feature. A Cargo build script (taxus-generator/build.rs) compiles the taxus-client crate to wasm32-unknown-unknown, runs wasm-bindgen to generate JS bindings, and embeds the resulting client.js and client_bg.wasm into the generator binary via include_bytes!. At site build time, these embedded files are written to dist/wasm/.
No separate build step or external tooling (such as Trunk) is required.
Development Workflow
1. Create a Site with Islands
cargo run -- init my-site --islands --name "My Site" --base-url "https://example.com"
2. Build the Static Site (includes WASM client)
cargo run --features islands -- build --dir my-site --verbose
The WASM client is compiled as part of the Cargo build and embedded in the binary. During the taxus build pipeline, the embedded client.js and client_bg.wasm are written to dist/wasm/ automatically — no separate build step needed.
3. Serve and Test
cargo run -- serve --dir my-site --open
The page should:
- Render immediately from the pre-rendered HTML
- Show the counter with the initial value
- After WASM loads (~1s), the button becomes interactive
Feature Flag
The islands Cargo feature controls whether Yew SSR and the WASM client are compiled in:
# Plain SSG — no Yew, fastest compile, smallest binary
cargo run -- build --dir my-site
# Islands SSG — Yew SSR pre-renders components at build time;
# WASM client is compiled and embedded in the binary automatically
cargo run --features islands -- build --dir my-site
Without the feature:
island()function returns empty string (no error)- No Yew or WASM dependencies compiled
- Templates that use
{{ island(...) | safe }}still render without output
With the feature:
- The WASM client (
taxus-client) is compiled towasm32-unknown-unknownbytaxus-generator/build.rs wasm-bindgenJS bindings are generated at Cargo build time- The resulting
client.jsandclient_bg.wasmare embedded in the generator binary viainclude_bytes! - At site build time, these files are written to
dist/wasm/automatically
Search
The islands feature also enables search index generation. See Search for details.