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.
- Crystal 1.0.0+
- Bun 1.0+ (for the SSR render server)
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.
dependencies:
celestite:
github: noahlh/celestite
version: ~> 0.2.0The postinstall hook will automatically install JavaScript dependencies via Bun.
For Kemal:
require "celestite"
include Celestite::Adapter::KemalCreate 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")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.svelteextension)context- ACelestite::Contexthash 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 --ssrOr use the Makefile target:
cd /path/to/lib/celestite/src/svelte-scripts
make build COMPONENT_DIR=/path/to/views BUILD_DIR=/path/to/public/celestiteBUILD_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 rendering
NODE_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 |
- Svelte 5 support with Vite
- Hot Module Reloading (HMR)
- Production builds with content hashing
- Example/demo project
Contributions are welcome! This is an open source project and feedback, bug reports, and PRs are appreciated.
- Fork it (https://github.com/noahlh/celestite/fork)
- Create your feature branch (
git checkout -b my-feature) - Write tests!
- Commit your changes (
git commit -am 'Add feature') - Push to the branch (
git push origin my-feature) - Create a Pull Request
- Noah Lehmann-Haupt (nlh@nlh.me / noahlh) - creator, maintainer