Islands API

Islands API

Server-side island rendering for partial hydration.

Create an island element using a type-safe ComponentRef. Factory functions are auto-generated by sol generate from client Props types.

// Auto-generated: app/__gen__/types/types.mbt
pub struct CounterProps { initial_count : Int } derive(ToJson, FromJson)
pub fn counter(props : CounterProps, trigger~ : @luna.Trigger) -> @luna.ComponentRef[CounterProps]
// Server-side usage
let counter_props : @types.CounterProps = { initial_count: 42 }

@sol.island(
  @types.counter(counter_props),
  [div([button([text("Count: 42")])])],
)

Signature

fn[T : ToJson, E] island(
  cref : @luna.ComponentRef[T],
  children : Array[@luna.Node[E, String]],
) -> @luna.Node[E, String]

Parameters

ParameterTypeDescription
crefComponentRef[T]Type-safe component reference (from generated factory)
childrenArray[Node]Server-rendered fallback content

island_with

Variant that accepts a render function instead of children array:

@sol.island_with(
  @types.counter(counter_props),
  fn() { div([button([text("Count: 42")])]) },
)

Alternative: @server_dom.client()

Equivalent API provided by Luna's static DOM package:

@server_dom.client(
  @types.counter(counter_props),
  [div([text("Loading...")])],
)

island_raw (string-based, low-level)

Create an island element with raw string parameters. Use island() with ComponentRef for type safety.

@sol.island_raw(
  "counter",
  "/components/counter.js",
  initial.to_string(),
  [@element.div([@element.button([@element.text("Count: \{initial}")])])],
  trigger=@luna.Load,
)

Signature

fn island_raw(
  id : String,
  url : String,
  state : String,
  children : Array[@luna.Node],
  trigger~ : Trigger = Load,
) -> @luna.Node

Parameters

ParameterTypeDescription
idStringComponent identifier (matches luna:id)
urlStringJavaScript module URL
stateStringSerialized props (JSON)
childrenArray[Node]Server-rendered content
triggerTriggerWhen to hydrate (default: Load)

HTML Output

<div
  luna:id="counter"
  luna:url="/components/counter.js"
  luna:state="0"
  luna:client-trigger="load"
>
  <div><button>Count: 0</button></div>
</div>

Example with Visible Trigger

// Lazy-loaded island - hydrates when scrolled into view
@sol.island(
  @types.lazy({}, trigger=@luna.TriggerType::Visible),
  [@element.text("Lazy content")],
)
// Output: luna:client-trigger="visible"

Trigger

Enum for hydration timing.

enum Trigger {
  Load      // Immediately on page load
  Idle      // When browser is idle
  Visible   // When element enters viewport
  Media(String)  // When media query matches
  None      // Manual trigger only
}

Values

ValueHTML OutputDescription
@luna.LoadloadImmediate hydration
@luna.IdleidlerequestIdleCallback
@luna.VisiblevisibleIntersectionObserver
@luna.Media(query)media:(query)Media query match
@luna.NonenoneManual via __LUNA_HYDRATE__

Examples

// Immediate (default)
trigger=@luna.Load

// When browser is idle
trigger=@luna.Idle

// When scrolled into view
trigger=@luna.Visible

// Desktop only
trigger=@luna.Media("(min-width: 768px)")

// Manual trigger
trigger=@luna.None

renderwithpreloads

Render and collect island URLs for preloading.

let node = @element.div([
  @sol.island(@types.component_a({}), [@element.text("A")]),
  @sol.island(@types.component_b({}), [@element.text("B")]),
])

let result = render_with_preloads(node)
// result.html contains the rendered HTML
// result.preload_urls = ["/static/component_a.js", "/static/component_b.js"]
// Generate preload links for all islands
let preload_links = result.preload_urls.map(fn(url) {
  @element.link(rel="modulepreload", href=url)
})

Web Components Island

For Web Components islands, use island() with a WC-prefixed ComponentRef (auto-generated with wc: true):

// Auto-generated: WcCounterProps โ†’ wc_counter() factory with wc=true
@sol.island(
  @types.wc_counter(wc_counter_props),
  [@element.button([@element.text("Count: 0")])],
)

Low-level: wcislandraw

For direct string-based Web Component islands, use @luna.wc_island or @sol.wc_island_raw:

@luna.wc_island(
  name="wc-counter",
  url="/static/wc-counter.js",
  state=initial.to_string(),
  trigger=@luna.Load,
  styles=":host { display: block; }",
  children=[
    @element.button([@element.text("Count: \{initial}")])
  ],
)

Parameters (@luna.wc_island)

ParameterTypeDescription
nameStringCustom element tag name
urlStringJavaScript module URL
stateStringSerialized props (JSON)
triggerTriggerWhen to hydrate
stylesStringScoped CSS for Shadow DOM
childrenArray[Node]Server-rendered content

HTML Output

<wc-counter
  luna:wc-url="/static/wc-counter.js"
  luna:wc-state="0"
  luna:wc-trigger="load"
>
  <template shadowrootmode="open">
    <style>:host { display: block; }</style>
    <button>Count: 0</button>
  </template>
</wc-counter>

slot_

Create a slot element for Web Components.

@luna.wc_island(
  name="wc-card",
  children=[
    @element.slot_(),                    // Default slot
    @element.slot_(name="header"),       // Named slot
    @element.slot_(name="footer"),       // Named slot
  ],
)

HTML Output

<slot></slot>
<slot name="header"></slot>
<slot name="footer"></slot>

Serializing State

Use derive(ToJson) for automatic serialization. With ComponentRef, serialization is handled automatically:

pub(all) struct CounterProps {
  initial : Int
  max : Int
} derive(ToJson, FromJson)

// ComponentRef-based (recommended) - serialization is automatic
@sol.island(
  @types.counter({ initial: 0, max: 100 }),
  [@element.div([@element.text("Loading...")])],
)

// String-based (low-level) - manual serialization
let state : CounterProps = { initial: 0, max: 100 }
@sol.island_raw(
  "counter",
  "/static/counter.js",
  state.to_json().stringify(),
  [@element.div([@element.text("Loading...")])],
)

Client-Side Hydration

The island loader (@luna_ui/luna-loader) handles hydration on the client:

<!-- Add to your page -->
<script type="module">
import { setupHydration } from '@luna_ui/luna-loader';
setupHydration();
</script>

Hydration Process

  1. Loader scans for elements with luna:id or luna:wc-* attributes

  2. Based on trigger, it:

    • load: Immediately imports the module

    • idle: Uses requestIdleCallback

    • visible: Uses IntersectionObserver

    • media: Uses matchMedia

  3. Module's default export receives the element and parsed state

Island Module Structure

// /components/counter.js
export default function hydrate(element, state) {
  // state is parsed from luna:state
  const count = state.initial || 0;

  // Set up reactivity
  element.querySelector('button').onclick = () => {
    // Update logic
  };
}

API Summary

FunctionDescription
@sol.island(cref, children)Create island from ComponentRef (recommended)
@sol.island_with(cref, render)Create island with render function
@sol.island_raw(id, url, state, children, trigger~)Create island from strings (low-level)
@server_dom.client(cref, children)Equivalent to @sol.island
@luna.wc_island(...)Create Web Component island (low-level)
@sol.wc_island_raw(...)Sol wrapper for WC island (low-level)
@element.slot_(name~)Create slot element
render_with_preloads(node)Render and collect preload URLs
TriggerWhen
@luna.LoadPage load (default)
@luna.IdleBrowser idle
@luna.VisibleIn viewport
@luna.Media(query)Media query matches
@luna.NoneManual