Wiring keen_phoenix_svelte into an existing Phoenix (1.8+) app.
1. Elixir
Add the dependency (mix.exs):
{:keen_phoenix_svelte, "~> 1.0"}Installing a pre-release (
rc)? SemVer ranges like~> 1.0(Hex) and^1.0(npm) do not match pre-releases, so during thercphase you must pin the exact version on both sides — and keep them in lockstep:{:keen_phoenix_svelte, "1.0.0-rc.7"}inmix.exs, and"@keenmate/phoenix_svelte": "1.0.0-rc07"inassets/package.json(note the npm rc form is zero-padded, no dot). Once1.0.0is out,~> 1.0/^1.0are correct.
Import the components wherever you render HTML — typically html_helpers/0 in
your *_web.ex, so <.app> and <KeenPhoenixSvelte.runtime> are available
everywhere:
import KeenPhoenixSvelteNote — the
app/1name collides with yourLayouts.app/1. Phoenix's generatedLayoutsmodule defines its ownapp/1(the app-shell layout), so a bare<.app …>inside that one module is ambiguous with the imported island component. Everywhere else (LiveViews, controllers, other components)<.app>works unqualified; inLayouts— the rare case you mount an island in the layout itself — call it fully-qualified as<KeenPhoenixSvelte.app …>.
Serve the built app bundles by adding apps to your static paths (*_web.ex):
def static_paths, do: ~w(assets apps fonts images favicon.ico robots.txt)Compiled apps are served from priv/static/apps/<name>/main.mjs.
Only needed for local apps (bundles you build into
priv/static/apps/). Apps loaded from a CDN —:direct(browser → CDN) or:proxy(served by the routerforward, see External apps) — aren't served byPlug.Static, so you can skip this if you use no local apps.
2. JavaScript
Add the npm package plus your Svelte toolchain (assets/package.json):
{
"dependencies": { "@keenmate/phoenix_svelte": "^1.0" },
"devDependencies": {
"svelte": "^5",
"@sveltejs/vite-plugin-svelte": "^4",
"vite": "^5",
"cross-env": "^7",
"yargs": "^17"
}
}Register the hook in your existing assets/js/app.js — the one mix phx.new
generated. You don't rewrite the file; you add just three things (marked
// ← add below). Everything else here is already in the generated file:
import {Socket} from "phoenix" // already there
import {LiveSocket} from "phoenix_live_view" // already there
import { getHooks, mountStatic } from "@keenmate/phoenix_svelte" // ← add
// already there — Phoenix reads the token from the <meta name="csrf-token">
// tag in root.html.heex. (This is the socket's CSRF; the island `api` helper's
// token is context.csrf_token, emitted separately by <.runtime>.)
const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
const liveSocket = new LiveSocket("/live", Socket, { // already there
params: {_csrf_token: csrfToken}, // already there
hooks: { ...getHooks() }, // ← add ...getHooks() to your existing hooks
})
liveSocket.connect() // already there
// ← add — mount islands on plain (non-LiveView) pages. Harmless on LiveView
// pages: elements inside a LiveView are skipped and mounted by the hook instead.
mountStatic()To summarize, the three additions are: the import line, spreading ...getHooks()
into your existing hooks: {}, and a mountStatic() call after connect(). (If
your app already has hooks — e.g. hooks: {...colocatedHooks} — just add
...getHooks() alongside them: hooks: {...colocatedHooks, ...getHooks()}.)
3. Build the Svelte apps
Each app is built into priv/static/apps/<name>/main.mjs as a self-contained ES
module (CSS injected by the JS — no separate stylesheet). Use Vite in library
mode. A config factory is exported as @keenmate/phoenix_svelte/vite:
// assets/apps.vite.config.mjs
import { defineConfig } from "vite"
import { appConfig } from "@keenmate/phoenix_svelte/vite"
export default defineConfig(({ mode }) =>
appConfig({ appName: process.env.SVELTE_APP, mode }))Local
file:dependency? It is symlinked, so Node resolves the Svelte plugin from the library's realpath rather than your app'snode_modules. In that case inline the config (importsvelte/vitePreprocessdirectly). Seeexample/assets/apps.vite.config.mjs. Published/hoisted installs use the helper.
A small builder that discovers every folder under assets/apps/ is in the
example (example/assets/builder.js); copy it, then add a dev watcher so apps
rebuild on change (config/dev.exs):
watchers: [
# ...esbuild, tailwind...
node: ["builder.js", "-m", "dev", "-w", cd: Path.expand("../assets", __DIR__)]
]builder.js is a plain CommonJS script (it uses require), so keep
assets/package.json without "type": "module" — the .mjs Vite config is
ESM regardless of that field, so both coexist under the default (CommonJS) package.
Wire the build into mix (one-shot + production)
The watcher only covers mix phx.server. For one-shot builds, mix setup, and
production (mix assets.deploy), add two npm scripts and call them from your
mix asset aliases — otherwise a release ships with no island bundles.
assets/package.json:
"scripts": {
"dev": "node builder.js -m dev -w",
"prod": "node builder.js -m prod"
}mix.exs aliases (install the npm deps in setup; build the islands in both
assets.build and assets.deploy):
"assets.setup": [
"tailwind.install --if-missing",
"esbuild.install --if-missing",
"cmd --cd assets npm install"
],
"assets.build": ["compile", "tailwind my_app", "esbuild my_app", "cmd --cd assets npm run prod"],
"assets.deploy": [
"tailwind my_app --minify",
"esbuild my_app --minify",
"cmd --cd assets npm run prod",
"phx.digest"
]For
esbuildto bundle@keenmate/phoenix_svelteinto yourapp.js, the npm package must be installed underassets/node_modules— that's what thenpm installinassets.setup(and your initialnpm install) is for.
4. (Optional) runtime context
To give apps a user context, a REST helper, and/or channels, render the context once in your root layout — see Server communication.
That's it. Add an app under assets/apps/ (see Authoring apps)
and drop a <.app name="..." id="..." /> into any template.