Crystal +
Svelte = :zap:
Celestite allows you to use the full power of Svelte reactive components in your Crystal web apps. It's a drop-in replacement for your view layer -- no more need for intermediate .ecr templates. With celestite, you write your backend server code in Crystal, your frontend client code in JavaScript & HTML, and everything works together seamlessly...and fast.
Read the full introductory blog post here.
This is not much more than a proof-of-concept at the moment, but it does work! Standard warnings apply - it will likely break/crash in spectacular and ill-timed glory, so don't poke it, feed it past midnight, or use it for anything mission-critical (yet).
Celestite has been developed / tested with Kemal, but there's no reason it won't work with Amber, Lucky, Athena, etc. (but no work integrating with those has been done yet.) The steps below assume you'll be working with Kemal.
shard.yml and run shards installdependencies:
celestite:
github: noahlh/celestite
version: ~> 0.2.0
The postinstall hook will automatically install JavaScript dependencies via Bun.
For Kemal:
require "celestite"
include Celestite::Adapter::Kemal
Create an initializer file (e.g., /config/initializers/celestite.cr):
require "celestite"
Celestite.initialize(
engine: Celestite::Engine::Svelte,
component_dir: "#{Dir.current}/src/views/",
build_dir: "#{Dir.current}/public/celestite/",
port: 4000,
vite_port: 5173,
)
See example config for more options.
For Kemal:
# myapp.cr
add_handler Kemal::StaticFileHandler.new("./public/celestite")
.svelte files and start building!Name your root component index.svelte (all lowercase).
celestite_render(component : String?, context : Celestite::Context?, layout : String?)
Call this where you'd normally call render in your controllers.
component - The Svelte component to render (without .svelte extension)context - A Celestite::Context hash with data to pass to your componentlayout - Optional HTML layout file from your layout_dir<!-- src/views/layouts/layout.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<!-- CELESTITE HEAD -->
<!-- The above comment is actually needed - Celestite looks for it and injects optional svelte:head content -->
</head>
<body>
<div id="celestite-app">
<!-- CELESTITE BODY -->
<!-- The above comment is also actually needed - Celestite looks for it and injects the server-side rendered component -->
</div>
<!-- CELESTITE CLIENT -->
<!-- The above comment is also also actually needed - Celestite looks for it and injects the client-side bundle -->
</body>
</html>
# myapp.cr
get "/test" do
context = Celestite::Context{ data: "Hello from Crystal!" }
celestite_render(component:"Home.svelte", context: context, layout: "layout.html")
end
<script>
let { context } = $props();
</script>
<h1>Result: {context.data}</h1>
Your .svelte components are automatically rendered server-side before being sent to the client, then hydrated on the client for interactivity.
Code that relies on browser-specific APIs (like document or window) must be wrapped in Svelte's onMount() or otherwise guarded.
<script>
import { onMount } from 'svelte';
onMount(() => {
// Browser-only code here
console.log(window.location);
});
</script>
or
<script>
let isBrowser = false;
if (typeof window !== 'undefined') {
isBrowser = true;
}
</script>
{#if isBrowser}
<!-- Browser-specific content -->
{/if}
Celestite supports running the Vite dev server over HTTPS, useful for tunneled connections (ngrok, localtunnel, etc.).
Install mkcert:
brew install mkcert # macOS
Install the local CA:
sudo mkcert -install
Generate certificates:
mkcert -key-file dev.key -cert-file dev.crt localhost 127.0.0.1 ::1
Enable in configuration:
Celestite.initialize(
dev_secure: true,
# ... other config
)
For production, Svelte components must be pre-built using Vite.
From your app's root directory:
# Build client bundles
COMPONENT_DIR=/path/to/views BUILD_DIR=/path/to/public/celestite \
bunx --bun vite build --config /path/to/lib/celestite/vite.config.js
# Build SSR bundles
COMPONENT_DIR=/path/to/views BUILD_DIR=/path/to/public/celestite \
bunx --bun vite build --config /path/to/lib/celestite/vite.config.js --ssr
Or use the Makefile target:
cd /path/to/lib/celestite/src/svelte-scripts
make build COMPONENT_DIR=/path/to/views BUILD_DIR=/path/to/public/celestite
BUILD_DIR/client/ - Client-side JS/CSS with content hashesBUILD_DIR/client/.vite/manifest.json - Asset manifest for hydrationBUILD_DIR/server/ - SSR modules for server-side renderingNODE_ENV=production NODE_PORT=4000 \
COMPONENT_DIR=/path/to/views \
LAYOUT_DIR=/path/to/views/layouts \
BUILD_DIR=/path/to/public/celestite \
bun run /path/to/lib/celestite/src/svelte-scripts/vite-render-server.js
| Option | Default | Description |
|---|---|---|
engine |
Svelte |
Rendering engine (currently only Svelte) |
component_dir |
- | Path to your Svelte components |
layout_dir |
- | Path to HTML layout templates |
build_dir |
- | Output directory for production builds |
port |
4000 |
Bun SSR server port |
vite_port |
5173 |
Vite dev server port (development only) |
dev_secure |
false |
Enable HTTPS for dev server |
disable_a11y_warnings |
false |
Suppress Svelte accessibility warnings |
Contributions are welcome! This is an open source project and feedback, bug reports, and PRs are appreciated.
git checkout -b my-feature)git commit -am 'Add feature')git push origin my-feature)