Static Sites

There are several ways to generate static sites with Bonito. The main one is:

Bonito.export_staticFunction
export_static(html_file::Union{IO, String}, app::App)
export_static(folder::String, routes::Routes)

Exports the app defined by app with all its assets a single HTML file. Or exports all routes defined by routes to folder.

source

The simplest one, which also allows an interactive Revise based workflow is enabled by interactive_server:

Bonito.interactive_serverFunction
interactive_server(f, paths, modules=[]; url="127.0.0.1", port=8081, all=true)

Revise base server that will serve a static side based on Bonito and will update on any code change!

Usage:

using Revise, Website
using Website.Bonito

# Start the interactive server and develop your website!
routes, task, server = interactive_server(Website.asset_paths()) do
    return Routes(
        "/" => App(index, title="Makie"),
        "/team" => App(team, title="Team"),
        "/contact" => App(contact, title="Contact"),
        "/support" => App(support, title="Support")
    )
end

# Once everything looks good, export the static site
dir = joinpath(@__DIR__, "docs")
# only delete the bonito generated files
rm(joinpath(dir, "bonito"); recursive=true, force=true)
Bonito.export_static(dir, routes)

For the complete code, visit the Makie website repository which is using Bonito: MakieOrg/Website

source

When exporting interactions defined within Julia not using Javascript, one can use, to cache all interactions:

Bonito.record_statesFunction
record_states(session::Session, dom::Hyperscript.Node)

Records widget states and their UI updates for offline/static HTML export. This function captures how the UI changes in response to widget interactions, allowing exported HTML to remain interactive without a Julia backend.

How it works

Each widget's states are recorded independently:

  • The function finds all widgets in the DOM that implement the widget interface
  • For each widget, it records the UI updates triggered by each possible state
  • The resulting state map is embedded in the exported HTML

Widget Interface

To make a widget recordable, implement these methods:

is_widget(::YourWidget) = true                    # Marks the type as a recordable widget
value_range(w::YourWidget) = [...]                 # Returns all possible states
to_watch(w::YourWidget) = w.observable             # Returns the observable to monitor

Limitations

Experimental Feature
  • Large file sizes: Recording all states can significantly increase HTML size
  • Independent states only: Widgets are recorded independently. Computed observables that depend on multiple widgets won't update correctly in the exported HTML
  • Performance: Not optimized for large numbers of widgets or states

Example

# This will work - independent widgets
s = Slider(1:10)
c = Checkbox(true)
record_states(session, DOM.div(s, c))

# This won't fully work - dependent computed observable
combined = map((s,c) -> "Slider: $s, Checkbox: $c", s.value, c.value)
record_states(session, DOM.div(s, c, combined))  # combined won't update
source