keen_phoenix_svelte
π Live demo & docs: keen-phoenix-svelte.keenmate.dev Β· Hex Β· HexDocs
Auto-mount compiled Svelte apps into Phoenix β on both LiveView and plain controller-rendered pages β as self-contained islands.
Write this in a template:
<.app name="like" id={"like-#{@id}"} props={%{id: @id, liked: @liked}} />
β¦and the compiled Svelte app in assets/apps/like/ is mounted into that element,
kept in sync with server state, given a context/api/channel/live bridge to
the server (plus a bus for island-to-island messaging), and torn down on
navigation β no manual <script>/<link> wiring. A configurable placeholder
shows while the bundle loads (server-wide default, per-app <:placeholder>
override), so there's no flash of empty container.
Svelte is the first-class, tooled path, but the mount boundary is
framework-neutral β an island's entry just default-exports
(target, opts) => { setProps, destroy }. Because every island mounts through
that one contract, there's a single component, <.app> (an optional
framework="β¦" attribute just tags data-framework for debugging). The demo
mounts Svelte, Lit, React and vanilla-JS islands through it.
New here, or comparing this to
live_svelte? Start with Philosophy & comparison β the autonomous-island model, what this deliberately doesn't do, and how it differs fromlive_svelte.
What's New in v1.0.0-rc.4
- Base-path proxy β proxy a whole multi-file bundle β A registered app can name an upstream directory with
base:(+ optionalentry:); every sub-path re-serves same-origin, so a vendor bundle's JS entry plus its stylesheet, fonts, and images all proxy through one registration. - Local
dir:source β serve a content-hashed bundle from disk β Point an app at a local directory (a mounted volume another process rebuilds) with anentry:glob; the newest match wins, sobundle.a1b2c3.jsresolves without knowing the hash. It reuses the proxy cache β the file's mtime is the validator,:ttlthe re-scan cadence. - Unified app manifest β local and registered apps in one map β
Apps.manifest/0now merges folder-detected local apps with registered ones (setotp_app:so the library can locate them). One authoritative map for the client,preload, and tooling; a registered entry wins on a name clash. <.runtime preload={β¦}>β fetch island bundles during initial HTML parse β Emit<link rel="modulepreload">for on-page islands so the browser downloads them in parallel instead of waiting for the LiveView hook's lazyimport(). Cross-origin bundles getcrossoriginso the preload is actually reused.- One island component,
<.app>β The<.svelte>alias is removed; every island (Svelte, React, Lit, vanilla) mounts through the single framework-neutral<.app>. In yourLayoutsmodule (which defines its ownapp/1), call it fully-qualified as<KeenPhoenixSvelte.app>. - Docs β one authoring guide, a packaging litmus, and diagrams β "Authoring apps" now covers building an island in any framework (Vite library mode + React/Lit/Vue/vanilla recipes); a new "Island-able vs page-owning apps" guide draws the line on what can be an island; and the guides render Mermaid diagrams of how apps resolve and load.
What's New in v1.0.0-rc.3
- Proxy cache with revalidation β
:proxymode now works for unversioned upstreams β Registered proxied bundles are cached in ETS and revalidated rather than pinned forever: a stale entry triggers a single-flight conditionalGET(If-None-Match/If-Modified-Since), so a304keeps the bytes and a200swaps them, with concurrent requests collapsed into one upstream fetch. Freshness follows the upstream'sCache-Control/ETag, falling back to a:ttl(default 5 min) β so a barecdn/app.jswith no version in its URL is re-checked on a cadence instead of cached for a year. - End-to-end conditional-request chain β The proxy forwards an
ETag(synthesizing a weak one when the origin ships none) and answers the browser'sIf-None-Matchwith a304, so browser β Phoenix β origin all revalidate cheaply. This replaces the old fixed 1-yearimmutableheader; a per-appimmutable: trueopts truly-versioned URLs back into it. - Bounded proxy memory β Cache entries upsert by URL (refreshes never grow the table) and a periodic sweep evicts URLs no longer in the registry, so a rotating DB-driven app registry stays bounded to its working set.
- Renamed to speak "app", not "island" β
KeenPhoenixSvelte.IslandProxyβKeenPhoenixSvelte.Apps.Proxy, config:island_providerβ:app_provider(which now also accepts a 2-arity conditional-fetch form), and the proxy-path default/keen-islandsβ/apps(it shares thebase_pathprefix). Update your routerforward. - Package metadata / links β corrected the
:source_urlcasing, addedhomepage_url, and aWebsitelink to the live demo.
How it works
Two cooperating halves:
- Elixir β
<.app>renders a hook-bound<div>(phx-update="ignore",data-app, JSONdata-props);<KeenPhoenixSvelte.runtime>emits the page context. - JS β the
KeenSveltehook mounts the island inside a LiveView andAppsManagerlazilyimport()s/apps/<name>/main.mjs(or a registered URL);mountStatic()mounts islands on plain pages. Only the bundles on a page are fetched β and any framework's bundle mounts through the same(target, opts) => { setProps, destroy }contract.
phx-update="ignore" keeps LiveView out of the Svelte-owned subtree; the hook
drives mount / prop-update / teardown across live navigation.
Unlike live_svelte, this is the island model β no ~V sigil, no
server-rendered slots, no SSR Node runtime.
Install
{:keen_phoenix_svelte, "~> 1.0"}
Plus the npm package @keenmate/phoenix_svelte and a Svelte/Vite toolchain. Full
steps in Installation & setup.
Documentation
- Installation & setup β wire the library into your app
- Authoring apps β folder layout, the mount contract, Svelte 5 & 4
- Server communication β runtime context,
live,api,channel,bus - External apps (CDN, direct vs proxy) β load islands from elsewhere, and the
:direct/:proxydelivery modes
A complete, runnable demo (the like app on a LiveView route and a plain route,
plus a channel) lives in the
example/ app.
Versioning
keen_phoenix_svelte is a dual package: the Hex library keen_phoenix_svelte
and the npm package @keenmate/phoenix_svelte are released in lockstep at the
same version β install matching versions of both. The Elixir side renders the
component + hook wiring; the npm side supplies the client runtime (the KeenSvelte
hook, AppsManager, the api/channel helpers, and the Vite build helper).
License
MIT.