[ANN] Snapshot.jl — interactive Pluto exports without a Julia server

I (alongside AI agents, of course) have been working on Snapshot.jl, a package that exports a Pluto notebook as a static page while keeping supported @bind interactions live via wasm.

The interactive cells are compiled to WebAssembly with WasmTarget.jl. When somebody moves a slider, the affected Julia code runs in their browser. The exported page does not need a Julia server, a kernel, or precomputed slider responses. Because WasmTarget.jl itself is brittle, Snapshot.jl is of course still brittle for now too. That being said, I am having a ton of fun with this already:

Open the live notebook gallery →

A small example

Install Snapshot.jl from the General registry:

import Pkg
Pkg.add("Snapshot")

using Snapshot

export_notebook("notebook.jl")

This runs the notebook and writes a lean, static Therapy-format page: cell output rendered to HTML at export time plus a WebAssembly island for each reactive group that compiled and passed verification. Pass fragment=true when the same output should also be emitted as an embeddable fragment.

The classic Pluto-style HTML export is also available:

export_notebook("notebook.jl"; therapy=false)

Both outputs are static files and can be served by an ordinary static host. The current lean presentation loads pinned DaisyUI, KaTeX and syntax-highlighting resources from public CDNs, so it is static-hostable but not an offline-contained bundle.

What happens during export?

Snapshot does four things:

  1. Runs the notebook once in Pluto.
  2. Uses Pluto’s dependency information to find cells affected by each @bind value.
  3. Lifts those reactive groups into Julia functions and compiles their typed IR to WasmGC through WasmTarget.jl.
  4. Runs the compiled result through the verification runtime and checks it against the real notebook output before including the island.

If a group does not compile or does not reproduce the notebook result, it does not quietly ship a different answer. The original output stays on the page and the export explains that the interaction is static. A notebook can therefore be partly interactive without being treated as a failed export.

Some notebooks

The documentation currently contains 16 notebooks: 14 lightly adapted Pluto featured notebooks and two Snapshot demos. It reports coverage per notebook and links back to the .jl source.

The lean collection shell includes a theme picker rather than a single light/dark switch. The screenshots below use different choices from the same exported pages; switching presentation does not rebuild the notebooks.

In the current gallery build, this chemistry notebook has all three reactive cells compiled, so the titration curve is recalculated and plotted in the browser as the inputs change.

Open Simulating titrations →

The output is still a notebook: Markdown, equations, Julia source, controls and figures remain in document order. The lean export removes the Pluto editor and session state rather than redesigning the notebook as an application.

The examples include larger reactive graphs too. In the current gallery build, this fractals notebook has 15 of 15 interactive cells compiled.

Open Fractals and Fractal Art →

Canvas output uses WasmMakie.jl, which is itself still a half baked package. The visible canvas remains mounted while a complete new frame is drawn and presented, so rapid slider changes do not replace the figure with an intermediate blank canvas.

Snapshot.jl is an exporter, not a Julia interpreter in the browser. Its interactive coverage is the subset that WasmTarget.jl can compile faithfully from type-inferred Julia IR.

That subset already includes ordinary control flow, closures, arrays, strings, dictionaries, exceptions, a growing part of Base and several numerical packages. It is not all of Julia. Dynamic dispatch, runtime facilities, ccall-dependent paths and unsupported library internals can still stop an island from compiling.

This boundary is visible on purpose. The gallery reports how many cells are interactive, and failed groups keep their build-time output. As WasmTarget.jl matures and Snapshot updates its pinned compiler dependency, rebuilding may compile more interactions without changing the notebook format or hosting model.

Status and trust boundary

  • Snapshot.jl is pre-1.0 and available from the General registry with Pkg.add("Snapshot").
  • It currently targets Julia 1.12 because it works directly with Julia compiler IR.
  • The package is MIT licensed. WasmTarget.jl is Apache-2.0 licensed.
  • Exporting executes the notebook with the permissions of the Julia process. It is not a sandbox; only export notebooks and environments you trust just like you would with any other Pluto notebook.
  • The generated page contains static HTML, JavaScript glue and WebAssembly. It does not contain an always-running Julia process.

The package source, tests and current limitations are in the Snapshot.jl repository. I would be especially interested in notebooks that export correctly but leave an interaction static, because those tend to be useful, concrete WasmTarget test cases.

One small preview

I am also testing snapshot.show, a separate hosted workflow that builds Snapshot.jl exports from GitHub repositories. I will write about that service separately after the package has had some time on its own.

Thanks for putting this package forward. It is extremely interesting to those who use Pluto frequently.

I tested your fractals notebook, and the export_notebook function worked very smoothly. However, the fractal plots do not react to the changes in the sliders. The sliders respond to changes, but those changes are not reflected in the plots themselves. I am on a Windows 11 machine and Julia 1.12.6. I tested your original fractal Julia file, and it worked immaculately on the same Julia version and Pluto 1.0.3.

I tested your package on one of my notebooks, and it produced a version that looked OK, but the sliders were not responding to changes, probably because there were Unicode characters in the sliders’ definitions.

Do you know what version of Snapshot.jl you used for this test? Is it v0.2.0?

Yes, it is Snapshot v0.2.0.

Can you share your entire Pkg.status() log. I will try to investigate this! I can reproduce that issue in Snapshot v0.1.1 but so far I am unable in v0.2.0

I have your Snapshot package installed as the single package in one specific env.

Do you need the entire toml file?

Oh wait, I have one more question: are you opening the HTML directly from the filesystem by double-clicking it? If so, that could be the issue.

When an export is opened as a file:// URL, the sliders themselves move, but the browser blocks the adjacent WebAssembly files from loading, which prevents the plots and dependent outputs from updating.

Currently, Snapshot exports must be served as a static directory. On Windows, open a terminal in the output directory and run:

py -m http.server 8000

Then open:

http://localhost:8000/fractals.html

No Julia process or application server is required. This small HTTP server only allows the browser to load the neighboring .wasm and JSON assets. Could you let me know whether this resolves the issue for both notebooks? If this is the issue, I could see how the documentation/announcement does NOT do a good job describing this

Yes, I was double-clicking on the HTML file.

I implemented your two-step output directory approach, and the output was exactly the same as before: sliders react to changes, but plots do not react to sliders.

Just one sideline question. If double-clicking on an HTML file will not work directly, how can I post a Pluto notebook exported by Snapshot on a website or on an educational platform (e.g., Moodle)?

Give me like an hour or two. I will push a fix to Snapshot. I think I see what the issue might be, plus I will answer how to deliver a static site using Snapshot/Therapy for easy hosting

Another question, is it just the fractals.jl notebook that wont work for you or is it all of the notebooks?

As far as your notebooks are concerned, I only tested your fractals notebook.

I did it because initially I had tested one of my notebooks, and Snapshot spat out some errors during the compilation process. To check whether the problem was confined to my notebook, I ran your notebook using Pluto 1.0.3, and the output came out immaculate on my machine. When I compiled that output with Snapshot, I got the issues that I have described.

Ok thank you. If you have the time, could you try this out. And if you get any errors during this process could you share them here:

In a fresh terminal, launch Julia and set up your temporary environment:

import Pkg
Pkg.activate(; temp=true)

Pkg.add(url="https://github.com/GroupTherapyOrg/Snapshot.jl",
        rev="fix/static-preview")
Pkg.add("PlutoUI")

using Snapshot

Next, create a minimal Pluto notebook programmatically:

dir = mktempdir()
notebook = joinpath(dir, "preview_test.jl")

write(notebook, raw"""
### A Pluto.jl notebook ###
# v0.20.28

using Markdown
using InteractiveUtils

# ╔═╡ 11111111-1111-4111-8111-111111111111
using PlutoUI

# ╔═╡ 22222222-2222-4222-8222-222222222222
@bind x Slider(1:10; default=3, show_value=true)

# ╔═╡ 33333333-3333-4333-8333-333333333333
x^2

# ╔═╡ 44444444-4444-4444-8444-444444444444
Markdown.parse("The square of **$(x)** is **$(x^2)**.")

# ╔═╡ Cell order:
# ╠═11111111-1111-4111-8111-111111111111
# ╠═22222222-2222-4222-8222-222222222222
# ╠═33333333-3333-4333-8333-333333333333
# ╟─44444444-4444-4444-8444-444444444444
""")

Now, export and preview it:

html = Snapshot.export_notebook(notebook)
Snapshot.preview(html)

Your browser should open automatically. Move the slider and confirm the square updates. Markdown output should show a compile error flag. Press Ctrl-C in the terminal to stop previewing.

To test the classic Pluto export as well, run:

classic = Snapshot.export_notebook(
    notebook;
    output_dir=joinpath(dir, "classic"),
    therapy=false,
)

Snapshot.preview(classic; port=8001)

If this works you can also try to do the same for the fractals.jl notebook.

import Downloads

fractals = joinpath(dir, "fractals.jl")

Downloads.download(
    "https://raw.githubusercontent.com/GroupTherapyOrg/Snapshot.jl/main/test/notebooks/featured/fractals.jl",
    fractals,
)

fractals_html = Snapshot.export_notebook(fractals)
Snapshot.preview(fractals_html)

OK. Give me a couple of hours. I am in the middle of another task that I have to finish today.

I followed your instructions, and the notebook was correctly created. However, the Snapshot HTML export does not work correctly. Please see the attached image.

Thanks for that – can you also check the fractals example I shared above too? I am on mac so having more windows examples is helpful as that might be potentially different

The same occurs in the fractals notebook: sliders respond correctly, the plot is stuck. See image below.

I will guess that Snapshot should work in a Linux machine as well. Right? If so I can test it tomorrow.